PSPublishModule.psm1

$LibraryName = 'PSPublishModule'
$Library = "$LibraryName.dll"
$Class = "$LibraryName.Initialize"

$AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue

$Default = $false
$Core = $false
$Standard = $false
foreach ($A in $AssemblyFolders.Name) {
    if ($A -eq 'Default') {
        $Default = $true
    }
    elseif ($A -eq 'Core') {
        $Core = $true
    }
    elseif ($A -eq 'Standard') {
        $Standard = $true
    }
}
if ($Standard -and $Core -and $Default) {
    $FrameworkNet = 'Default'
    $Framework = 'Standard'
}
elseif ($Standard -and $Core) {
    $Framework = 'Standard'
    $FrameworkNet = 'Standard'
}
elseif ($Core -and $Default) {
    $Framework = 'Core'
    $FrameworkNet = 'Default'
}
elseif ($Standard -and $Default) {
    $Framework = 'Standard'
    $FrameworkNet = 'Default'
}
elseif ($Standard) {
    $Framework = 'Standard'
    $FrameworkNet = 'Standard'
}
elseif ($Core) {
    $Framework = 'Core'
    $FrameworkNet = ''
}
elseif ($Default) {
    $Framework = ''
    $FrameworkNet = 'Default'
}
else {
    Write-Error -Message 'No assemblies found'
}
if ($PSEdition -eq 'Core') {
    $LibFolder = $Framework
}
else {
    $LibFolder = $FrameworkNet
}

try {
    $ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core

    if (-not ($Class -as [type])) {
        & $ImportModule ([IO.Path]::Combine($PSScriptRoot, 'Lib', $LibFolder, $Library)) -ErrorAction Stop
    }
    else {
        $Type = "$Class" -as [Type]
        & $importModule -Force -Assembly ($Type.Assembly)
    }
}
catch {
    if ($ErrorActionPreference -eq 'Stop') {
        throw
    }
    else {
        Write-Warning -Message "Importing module $Library failed. Fix errors before continuing. Error: $($_.Exception.Message)"
    }
}

. $PSScriptRoot\PSPublishModule.Libraries.ps1

function Add-Artefact {
    [CmdletBinding()]
    param(
        [string] $ModuleName,
        [string] $ModuleVersion,
        [string] $ArtefactName,
        [alias('IncludeTagName')][nullable[bool]] $IncludeTag,
        [nullable[bool]] $LegacyName,
        [nullable[bool]] $CopyMainModule,
        [nullable[bool]] $CopyRequiredModules,
        [string] $ProjectPath,
        [string] $Destination,
        [string] $DestinationMainModule,
        [string] $DestinationRequiredModules,
        [nullable[bool]] $DestinationFilesRelative,
        [alias('DestinationDirectoriesRelative')][nullable[bool]] $DestinationFoldersRelative,
        [alias('FilesOutput')][System.Collections.IDictionary] $Files,
        [alias('DirectoryOutput')][System.Collections.IDictionary] $Folders,
        [array] $RequiredModules,
        # [string] $TagName,
        #[string] $FileName,
        [nullable[bool]] $ZipIt,
        [string] $DestinationZip,
        [bool] $ConvertToScript,
        [string] $ScriptName,
        [string] $PreScriptMerge,
        [string] $PostScriptMerge,
        [System.Collections.IDictionary] $Configuration,
        [string] $ID,
        [switch] $DoNotClear
    )

    $DestinationMainModule = Initialize-ReplacePath -ReplacementPath $DestinationMainModule -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
    $DestinationRequiredModules = Initialize-ReplacePath -ReplacementPath $DestinationRequiredModules -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
    $DestinationZip = Initialize-ReplacePath -ReplacementPath $DestinationZip -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
    $Destination = Initialize-ReplacePath -ReplacementPath $Destination -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration

    $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination)
    if (-not $DoNotClear) {
        if (Test-Path -LiteralPath $ResolvedDestination) {
            Write-TextWithTime -Text "Removing files/folders from $ResolvedDestination before copying artefacts" -SpacesBefore ' ' -PreAppend Minus {
                Remove-ItemAlternative -Path $ResolvedDestination -SkipFolder -Exclude '*.zip' -ErrorAction Stop
            } -ColorBefore Yellow -ColorTime Green -ColorError Red -Color Yellow
        }
    }
    if ($ConvertToScript) {
        Write-TextWithTime -Text "Converting merged release to script" -PreAppend Plus -SpacesBefore ' ' {
            $convertToScriptSplat = @{
                Enabled         = $true
                IncludeTagName  = $IncludeTag
                ModuleName      = $ModuleName
                Destination     = $DestinationMainModule
                PreScriptMerge  = $PreScriptMerge
                PostScriptMerge = $PostScriptMerge
                ScriptName      = $ScriptName
                Configuration   = $Configuration
                ModuleVersion   = $ModuleVersion
            }
            Remove-EmptyValue -Hashtable $convertToScriptSplat
            Copy-ArtefactToScript @convertToScriptSplat

            $copyRequiredModuleSplat = @{
                Enabled         = $CopyRequiredModules
                RequiredModules = $RequiredModules
                Destination     = $DestinationRequiredModules
            }
            Copy-ArtefactRequiredModule @copyRequiredModuleSplat

            $copyArtefactRequiredFoldersSplat = @{
                FoldersInput        = $Folders
                ProjectPath         = $ProjectPath
                Destination         = $Destination
                DestinationRelative = $DestinationFoldersRelative
            }
            Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat

            $copyArtefactRequiredFilesSplat = @{
                FilesInput          = $Files
                ProjectPath         = $ProjectPath
                Destination         = $Destination
                DestinationRelative = $DestinationFilesRelative
            }
            Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat
        }
    }
    else {
        Write-TextWithTime -Text "Copying merged release to $ResolvedDestination" -PreAppend Addition -SpacesBefore ' ' {
            $copyMainModuleSplat = @{
                Enabled        = $true
                IncludeTagName = $IncludeTag
                ModuleName     = $ModuleName
                Destination    = $DestinationMainModule
            }
            Copy-ArtefactMainModule @copyMainModuleSplat

            $copyRequiredModuleSplat = @{
                Enabled         = $CopyRequiredModules
                RequiredModules = $RequiredModules
                Destination     = $DestinationRequiredModules
            }
            Copy-ArtefactRequiredModule @copyRequiredModuleSplat

            $copyArtefactRequiredFoldersSplat = @{
                FoldersInput        = $Folders
                ProjectPath         = $ProjectPath
                Destination         = $Destination
                DestinationRelative = $DestinationFoldersRelative
            }
            Copy-ArtefactRequiredFolders @copyArtefactRequiredFoldersSplat

            $copyArtefactRequiredFilesSplat = @{
                FilesInput          = $Files
                ProjectPath         = $ProjectPath
                Destination         = $Destination
                DestinationRelative = $DestinationFilesRelative
            }
            Copy-ArtefactRequiredFiles @copyArtefactRequiredFilesSplat
        }
    }
    if ($ZipIt -and $DestinationZip) {
        $ResolvedDestinationZip = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationZip)
        Write-TextWithTime -Text "Zipping merged release to $ResolvedDestinationZip" -PreAppend Information -SpacesBefore ' ' {
            $zipSplat = @{
                Source        = $ResolvedDestination
                Destination   = $ResolvedDestinationZip

                Configuration = $Configuration
                LegacyName    = if ($Configuration.Steps.BuildModule.Releases -is [bool]) {
                    $true 
                }
                else {
                    $false 
                }
                ModuleName    = $ModuleName
                ModuleVersion = $ModuleVersion
                IncludeTag    = $IncludeTag
                ArtefactName  = $ArtefactName
                ID            = $ID
            }
            Compress-Artefact @zipSplat
        }
        Write-TextWithTime -Text "Removing temporary files from $ResolvedDestination" -SpacesBefore ' ' -PreAppend Minus {
            Remove-ItemAlternative -Path $ResolvedDestination -SkipFolder -Exclude '*.zip' -ErrorAction Stop
        } -ColorBefore Yellow -ColorTime Green -ColorError Red -Color Yellow
    }
}
function Add-BinaryImportModule {
    <#
    .SYNOPSIS
    Add code into PSM1 that will import binary modules based on the edition
 
    .DESCRIPTION
    Add code into PSM1 that will import binary modules based on the edition
 
    .PARAMETER LibrariesStandard
    Parameter description
 
    .PARAMETER LibrariesCore
    Parameter description
 
    .PARAMETER LibrariesDefault
    Parameter description
 
    .PARAMETER Configuration
    Parameter description
 
    .EXAMPLE
     Add-BinaryImportModule -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault
 
    .NOTES
 
    .OUTPUT
    # adds code into PSM1 file similar to this one
    if ($PSEdition -eq 'Core') {
        Import-Module -Name "$PSScriptRoot\Lib\Standard\PSEventViewer.PowerShell.dll" -Force -ErrorAction Stop
    } else {
        Import-Module -Name "$PSScriptRoot\Lib\Default\PSEventViewer.PowerShell.dll" -Force -ErrorAction Stop
    }
 
    #>

    [CmdletBinding()]
    param(
        [string[]] $LibrariesStandard,
        [string[]] $LibrariesCore,
        [string[]] $LibrariesDefault,
        [System.Collections.IDictionary] $Configuration
    )

    if ($null -ne $Configuration.Steps.BuildLibraries.BinaryModule) {
        foreach ($BinaryModule in $Configuration.Steps.BuildLibraries.BinaryModule) {
            if ($LibrariesStandard.Count -gt 0) {
                foreach ($Library in $LibrariesStandard) {
                    if ($Library -like "*\$BinaryModule") {
                        "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop"
                    }
                }
            }
            elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) {
                'if ($PSEdition -eq ''Core'') {'
                if ($LibrariesCore.Count -gt 0) {
                    foreach ($Library in $LibrariesCore) {
                        if ($Library -like "*\$BinaryModule") {
                            "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop"
                        }
                    }
                }
                '} else {'
                if ($LibrariesDefault.Count -gt 0) {
                    foreach ($Library in $LibrariesDefault) {
                        if ($Library -like "*\$BinaryModule") {
                            "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop"
                        }
                    }
                }
                '}'
            }
            else {
                if ($LibrariesCore.Count -gt 0) {
                    if ($LibrariesCore.Count -gt 0) {
                        foreach ($Library in $LibrariesCore) {
                            if ($Library -like "*\$BinaryModule") {
                                "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop"
                            }
                        }
                    }
                }
                if ($LibrariesDefault.Count -gt 0) {
                    foreach ($Library in $LibrariesDefault) {
                        if ($Library -like "*\$BinaryModule") {
                            "Import-Module -Name `"`$PSScriptRoot\$Library`" -Force -ErrorAction Stop"
                        }
                    }
                }
            }
        }
    }
}
function Add-Directory {
    [CmdletBinding()]
    param(
        [string] $Directory
    )
    $exists = Test-Path -Path $Directory
    if ($exists -eq $false) {
        $null = New-Item -Path $Directory -ItemType Directory -Force
    }
}
function Approve-RequiredModules {
    [CmdletBinding()]
    param(
        [Array] $ApprovedModules,
        [Array] $ModulesToCheck,
        [Array] $RequiredModules,
        [Array] $DependantRequiredModules,
        [System.Collections.IDictionary] $MissingFunctions,
        [System.Collections.IDictionary] $Configuration,
        [Array] $CommandsWithoutModule
    )

    $TerminateEarly = $false

    Write-TextWithTime -Text "Pre-Verification of approved modules" {
        foreach ($ApprovedModule in $ApprovedModules) {
            $ApprovedModuleStatus = Get-Module -Name $ApprovedModule -ListAvailable
            if ($ApprovedModuleStatus) {
                Write-Text " [>] Approved module $ApprovedModule exists - can be used for merging." -Color Green
            }
            else {
                Write-Text " [>] Approved module $ApprovedModule doesn't exists. Potentially issue with merging." -Color Red
            }
        }
    } -PreAppend Plus

    Write-TextWithTime -Text "Analyze required, approved modules" {
        foreach ($Module in $ModulesToCheck.Source | Sort-Object) {
            if ($Module -in $RequiredModules -and $Module -in $ApprovedModules) {
                Write-Text " [+] Module $Module is in required modules with ability to merge." -Color DarkYellow
                $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                foreach ($F in $MyFunctions) {
                    if ($F.IsPrivate) {
                        Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color Magenta
                    }
                    else {
                        Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsPrivate: $($F.IsPrivate))" -Color DarkYellow
                    }
                }
            }
            elseif ($Module -in $DependantRequiredModules -and $Module -in $ApprovedModules) {
                Write-Text " [+] Module $Module is in dependant required module within required modules with ability to merge." -Color DarkYellow
                $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                foreach ($F in $MyFunctions) {
                    Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkYellow
                }
            }
            elseif ($Module -in $DependantRequiredModules) {
                Write-Text " [+] Module $Module is in dependant required module within required modules." -Color DarkGray
                $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                foreach ($F in $MyFunctions) {
                    Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color DarkGray
                }
            }
            elseif ($Module -in $RequiredModules) {
                Write-Text " [+] Module $Module is in required modules." -Color Green
                $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                foreach ($F in $MyFunctions) {
                    Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Green
                }
            }
            elseif ($Module -notin $RequiredModules -and $Module -in $ApprovedModules) {
                Write-Text " [+] Module $Module is missing in required module, but it's in approved modules." -Color Magenta
                $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                foreach ($F in $MyFunctions) {
                    Write-Text " [>] Command used $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Magenta
                }
            }
            else {
                [Array] $MyFunctions = ($MissingFunctions.Summary | Where-Object { $_.Source -eq $Module })
                if ($Configuration.Options.Merge.ModuleSkip.Force -eq $true) {
                    Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (force used)." -Color Gray
                    foreach ($F in $MyFunctions) {
                        Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray
                    }
                }
                else {
                    if ($Module -in $Configuration.Options.Merge.ModuleSkip.IgnoreModuleName) {
                        Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped module)." -Color Gray
                        foreach ($F in $MyFunctions) {
                            Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray
                        }
                    }
                    else {
                        $FoundProblem = $false
                        foreach ($F in $MyFunctions) {
                            if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                                $FoundProblem = $true
                            }
                        }
                        if (-not $FoundProblem) {
                            Write-Text " [-] Module $Module is missing in required modules. Non-critical issue as per configuration (skipped functions)." -Color Gray
                            foreach ($F in $MyFunctions) {
                                if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                                    Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray
                                }
                                else {
                                    Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red
                                }
                            }
                        }
                        else {
                            $TerminateEarly = $true
                            Write-Text " [-] Module $Module is missing in required modules. Potential issue. Fix configuration required." -Color Red
                            foreach ($F in $MyFunctions) {
                                if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                                    Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate)). Ignored by configuration." -Color Gray
                                }
                                else {
                                    Write-Text " [>] Command affected $($F.Name) (Command Type: $($F.CommandType) / IsAlias: $($F.IsAlias)) / IsAlias: $($F.IsPrivate))" -Color Red
                                }
                            }
                        }
                    }
                }
            }
        }

        if ($CommandsWithoutModule.Count -gt 0) {
            $FoundProblem = $false
            foreach ($F in $CommandsWithoutModule) {
                if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                    $FoundProblem = $true
                }
            }
            if ($FoundProblem) {
                Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Potential issue." -Color Red
                foreach ($F in $CommandsWithoutModule) {
                    if ($F.Name -notin $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                        $TerminateEarly = $true
                        Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red
                    }
                    else {
                        Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray
                    }
                }
            }
            else {
                Write-Text " [-] Some commands couldn't be resolved to functions (private function maybe?). Non-critical issue as per configuration (skipped functions)." -Color Gray
                foreach ($F in $CommandsWithoutModule) {
                    if ($F.Name -in $Configuration.Options.Merge.ModuleSkip.IgnoreFunctionName) {
                        Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias)). Ignored by configuration." -Color Gray
                    }
                    else {

                        Write-Text " [>] Command affected $($F.Name) (Command Type: Unknown / IsAlias: $($F.IsAlias))" -Color Red
                    }
                }
            }
        }
        if ($TerminateEarly) {
            Write-Text " [-] Some commands are missing in required modules. Fix this issue or use New-ConfigurationModuleSkip to skip verification." -Color Red
            return $false
        }
    } -PreAppend Plus
}
function Compress-Artefact {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $Source,
        [string] $Destination,
        [string] $ModuleName,
        [string] $ModuleVersion,
        [nullable[bool]] $IncludeTag,
        [nullable[bool]] $LegacyName,
        [string] $ArtefactName,
        [string] $ID
    )

    if ($Configuration.CurrentSettings.PreRelease) {
        $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
        $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)"
    }
    else {
        $ModuleVersionWithPreRelease = $ModuleVersion
        $TagModuleVersionWithPreRelease = "v$($ModuleVersion)"
    }
    if ($LegacyName) {

        $FileName = -join ("v$($ModuleVersion)", '.zip')
    }
    elseif ($ArtefactName) {
        $TagName = "v$($ModuleVersion)"
        $FileName = $ArtefactName
        $FileName = $FileName.Replace('{ModuleName}', $ModuleName)
        $FileName = $FileName.Replace('<ModuleName>', $ModuleName)
        $FileName = $FileName.Replace('{ModuleVersion}', $ModuleVersion)
        $FileName = $FileName.Replace('<ModuleVersion>', $ModuleVersion)
        $FileName = $FileName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease)
        $FileName = $FileName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease)
        $FileName = $FileName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease)
        $FileName = $FileName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease)
        $FileName = $FileName.Replace('{TagName}', $TagName)
        $FileName = $FileName.Replace('<TagName>', $TagName)

        $FileName = if ($FileName.EndsWith(".zip")) {
            $FileName 
        }
        else {
            -join ($FileName, '.zip') 
        }
    }
    else {
        if ($IncludeTag) {
            $TagName = "v$($ModuleVersion)"
        }
        else {
            $TagName = ''
        }
        if ($TagName) {
            $FileName = -join ($ModuleName, ".$TagName", '.zip')
        }
        else {
            $FileName = -join ($ModuleName, '.zip')
        }
    }
    $ZipPath = [System.IO.Path]::Combine($Destination, $FileName)
    $ZipPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ZipPath)

    $Configuration.CurrentSettings.ArtefactZipName = $FileName
    $Configuration.CurrentSettings.ArtefactZipPath = $ZipPath

    if ($ID) {

        $ZipPackage = [ordered] @{
            'Id'      = $ID
            'ZipName' = $FileName
            'ZipPath' = $ZipPath
        }
        $Configuration.CurrentSettings['Artefact'] += $ZipPackage
    }
    else {
        if (-not $Configuration.CurrentSettings['ArtefactDefault']) {
            $Configuration.CurrentSettings['ArtefactDefault'] = [ordered] @{
                'Id'      = 'Default'
                'ZipName' = $FileName
                'ZipPath' = $ZipPath
            }
        }
    }

    $Success = Write-TextWithTime -Text "Compressing final merged release $ZipPath" {
        $null = New-Item -ItemType Directory -Path $Destination -Force

        [Array] $DirectoryToCompress = Get-ChildItem -Path $Source -Directory -ErrorAction SilentlyContinue
        $FilesToCompressSource = [System.IO.Path]::Combine($Source, "*")
        [Array] $FilesToCompress = Get-ChildItem -Path $FilesToCompressSource -File -Exclude '*.zip' -ErrorAction SilentlyContinue
        if ($DirectoryToCompress.Count -gt 0 -and $FilesToCompress.Count -gt 0) {
            Compress-Archive -Path @(
                $DirectoryToCompress.FullName
                $FilesToCompress.FullName
            ) -DestinationPath $ZipPath -Force -ErrorAction Stop
        }
        elseif ($DirectoryToCompress.Count -gt 0) {
            Compress-Archive -Path $DirectoryToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop
        }
        elseif ($FilesToCompress.Count -gt 0) {
            Compress-Archive -Path $FilesToCompress.FullName -DestinationPath $ZipPath -Force -ErrorAction Stop
        }
    } -PreAppend Addition -SpacesBefore ' ' -Color Yellow -ColorTime Green -ColorBefore Yellow -ColorError Red

    if ($Success -eq $false) {
        return $false
    }
}
function Convert-FileEncoding {
    <#
    .SYNOPSIS
    Converts files from one encoding to another.
 
    .DESCRIPTION
    Reads a single file or all files within a directory and rewrites them using a new encoding.
    Useful for converting files from UTF8 with BOM to UTF8 without BOM or any other supported encoding.
    Files are only converted when their detected encoding matches the provided SourceEncoding unless -Force is used.
    If a file already uses the target encoding it is skipped.
    After conversion the content is verified to ensure it matches the original string.
    If the content differs the change is rolled back by default unless -NoRollbackOnMismatch is specified.
    Supports -WhatIf for previewing changes.
 
    .PARAMETER Path
    Specifies the file or directory to process.
 
    .PARAMETER Filter
    Filters which files are processed when Path is a directory. Wildcards are supported.
 
    .PARAMETER SourceEncoding
    Encoding used when reading files. The default is UTF8BOM.
 
    .PARAMETER TargetEncoding
    Encoding used when writing files. The default is UTF8.
 
    .PARAMETER Recurse
    When Path is a directory, process files in all subdirectories as well.
 
    .PARAMETER Force
    Convert files even when their detected encoding does not match SourceEncoding.
 
    .PARAMETER NoRollbackOnMismatch
    Skip rolling back files when the verification step detects that content changed during conversion.
 
    .EXAMPLE
    Convert-FileEncoding -Path 'C:\Scripts' -Filter '*.ps1' -SourceEncoding UTF8BOM -TargetEncoding UTF8
 
    Converts all PowerShell scripts under C:\Scripts from UTF8 with BOM to UTF8.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [string] $Filter = '*.*',

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $SourceEncoding = 'UTF8BOM',

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $TargetEncoding = 'UTF8',

        [switch] $Recurse,
        [switch] $Force,
        [switch] $NoRollbackOnMismatch
    )

    $source = Resolve-Encoding -Name $SourceEncoding
    $target = Resolve-Encoding -Name $TargetEncoding

    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        $files = Get-Item -LiteralPath $Path
    }
    elseif (Test-Path -LiteralPath $Path -PathType Container) {
        $gciParams = @{ LiteralPath = $Path; File = $true; Filter = $Filter }
        if ($Recurse) {
            $gciParams.Recurse = $true 
        }
        $files = Get-ChildItem @gciParams
    }
    else {
        throw "Path $Path not found"
    }

    foreach ($file in $files) {
        $result = Convert-FileEncodingSingle -FilePath $file.FullName -SourceEncoding $source -TargetEncoding $target -Force:$Force -NoRollbackOnMismatch:$NoRollbackOnMismatch -WhatIf:$WhatIfPreference

        if ($result) {
            Write-Verbose "File: $($result.FilePath) - Status: $($result.Status) - Reason: $($result.Reason)"
            if ($result.Warning) {
                Write-Warning $result.Warning
            }
        }
    }
}

function Convert-FileEncodingSingle {
    <#
    .SYNOPSIS
    Converts a single file from one encoding to another with validation and rollback protection.
 
    .DESCRIPTION
    Internal helper function that converts a single file's encoding with comprehensive validation.
    Includes content verification and automatic rollback on mismatch to prevent data corruption.
 
    .PARAMETER FilePath
    Full path to the file to convert.
 
    .PARAMETER SourceEncoding
    Expected source encoding of the file.
 
    .PARAMETER TargetEncoding
    Target encoding to convert the file to.
 
    .PARAMETER Force
    Convert file even if detected encoding doesn't match SourceEncoding.
 
    .PARAMETER NoRollbackOnMismatch
    Skip rolling back changes when content verification fails.
 
    .PARAMETER CreateBackup
    Create a backup file before conversion for additional safety.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $FilePath,

        [Parameter(Mandatory)]
        [System.Text.Encoding] $SourceEncoding,

        [Parameter(Mandatory)]
        [System.Text.Encoding] $TargetEncoding,

        [switch] $Force,
        [switch] $NoRollbackOnMismatch,
        [switch] $CreateBackup
    )

    $bytesBefore = $null
    $backupPath = $null

    try {

        $detectedObj = Get-FileEncoding -Path $FilePath -AsObject
        $detected = $detectedObj.Encoding
        $detectedName = $detectedObj.EncodingName

        $sourceExpected = if ($SourceEncoding -is [System.Text.UTF8Encoding] -and $SourceEncoding.GetPreamble().Length -eq 3) {
            'UTF8BOM' 
        }
        elseif ($SourceEncoding -is [System.Text.UTF8Encoding]) {
            'UTF8' 
        }
        elseif ($SourceEncoding -is [System.Text.UnicodeEncoding]) {
            'Unicode' 
        }
        elseif ($SourceEncoding -is [System.Text.UTF7Encoding]) {
            'UTF7' 
        }
        elseif ($SourceEncoding -is [System.Text.UTF32Encoding]) {
            'UTF32' 
        }
        elseif ($SourceEncoding -is [System.Text.ASCIIEncoding]) {
            'ASCII' 
        }
        elseif ($SourceEncoding -is [System.Text.BigEndianUnicodeEncoding]) {
            'BigEndianUnicode' 
        }
        else {
            $SourceEncoding.WebName 
        }

        if ($detectedName -ne $sourceExpected -and -not $Force) {
            Write-Verbose "Skipping $FilePath because detected encoding '$detectedName' does not match expected '$sourceExpected'."
            return @{
                FilePath         = $FilePath
                Status           = 'Skipped'
                Reason           = "Encoding mismatch: detected '$detectedName', expected '$sourceExpected'"
                DetectedEncoding = $detectedName
            }
        }

        $targetExpected = if ($TargetEncoding -is [System.Text.UTF8Encoding] -and $TargetEncoding.GetPreamble().Length -eq 3) {
            'UTF8BOM' 
        }
        elseif ($TargetEncoding -is [System.Text.UTF8Encoding]) {
            'UTF8' 
        }
        elseif ($TargetEncoding -is [System.Text.UnicodeEncoding]) {
            'Unicode' 
        }
        elseif ($TargetEncoding -is [System.Text.UTF7Encoding]) {
            'UTF7' 
        }
        elseif ($TargetEncoding -is [System.Text.UTF32Encoding]) {
            'UTF32' 
        }
        elseif ($TargetEncoding -is [System.Text.ASCIIEncoding]) {
            'ASCII' 
        }
        elseif ($TargetEncoding -is [System.Text.BigEndianUnicodeEncoding]) {
            'BigEndianUnicode' 
        }
        else {
            $TargetEncoding.WebName 
        }

        if ($detectedName -eq $targetExpected) {
            Write-Verbose "Skipping $FilePath because encoding is already '$targetExpected'."
            return @{
                FilePath         = $FilePath
                Status           = 'Skipped'
                Reason           = "Already target encoding '$targetExpected'"
                DetectedEncoding = $detectedName
            }
        }

        if ($PSCmdlet.ShouldProcess($FilePath, "Convert from '$detectedName' to '$targetExpected'")) {

            $content = [System.IO.File]::ReadAllText($FilePath, $detected)
            $bytesBefore = [System.IO.File]::ReadAllBytes($FilePath)

            if ($CreateBackup) {
                $backupPath = "$FilePath.backup"
                $counter = 1
                while (Test-Path $backupPath) {
                    $backupPath = "$FilePath.backup$counter"
                    $counter++
                }
                [System.IO.File]::WriteAllBytes($backupPath, $bytesBefore)
                Write-Verbose "Created backup at: $backupPath"
            }

            [System.IO.File]::WriteAllText($FilePath, $content, $TargetEncoding)

            $convertedContent = [System.IO.File]::ReadAllText($FilePath, $TargetEncoding)
            if ($convertedContent -ne $content) {
                $warningMsg = "Content verification failed for $FilePath - characters may have been lost during conversion"
                Write-Warning $warningMsg

                if (-not $NoRollbackOnMismatch) {
                    [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore)
                    Write-Warning "Reverted changes to $FilePath due to content mismatch"

                    return @{
                        FilePath         = $FilePath
                        Status           = 'Failed'
                        Reason           = 'Content verification failed - reverted'
                        DetectedEncoding = $detectedName
                        BackupPath       = $backupPath
                    }
                }
                else {
                    return @{
                        FilePath         = $FilePath
                        Status           = 'Converted'
                        Reason           = 'Content verification failed but conversion kept'
                        DetectedEncoding = $detectedName
                        TargetEncoding   = $targetExpected
                        BackupPath       = $backupPath
                        Warning          = $warningMsg
                    }
                }
            }

            Write-Verbose "Successfully converted $FilePath from '$detectedName' to '$targetExpected'"
            return @{
                FilePath         = $FilePath
                Status           = 'Converted'
                Reason           = 'Successfully converted'
                DetectedEncoding = $detectedName
                TargetEncoding   = $targetExpected
                BackupPath       = $backupPath
            }
        }
    }
    catch {
        $errorMsg = "Failed to convert ${FilePath}: $_"
        Write-Warning $errorMsg

        if (-not $NoRollbackOnMismatch -and $bytesBefore) {
            try {
                [System.IO.File]::WriteAllBytes($FilePath, $bytesBefore)
                Write-Verbose "Rolled back $FilePath due to conversion error"
            }
            catch {
                Write-Warning "Failed to rollback $FilePath after error: $_"
            }
        }

        return @{
            FilePath         = $FilePath
            Status           = 'Error'
            Reason           = $errorMsg
            DetectedEncoding = $detectedName
            BackupPath       = $backupPath
        }
    }
}

function Convert-FolderEncoding {
    <#
    .SYNOPSIS
    Converts files in folders to a target encoding based on file extensions.
 
    .DESCRIPTION
    A user-friendly wrapper around Convert-FileEncoding that makes it easy to target specific file types
    by their extensions across one or more folders. This function is ideal for scenarios where you want to convert encoding for
    specific file types across directories without needing to know filter syntax.
 
    The function supports both single and multiple file extensions, with smart defaults for PowerShell
    compatibility. It provides comprehensive feedback and safety features including WhatIf support
    and backup creation.
 
    .PARAMETER Path
    The directory path to search for files. Can be a single directory or an array of directories.
    Use '.' for the current directory.
 
    .PARAMETER Extensions
    File extensions to target for conversion. Can be specified with or without the leading dot.
    Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb')
 
    Common presets available via -FileType parameter for convenience.
 
    .PARAMETER FileType
    Predefined file type groups for common scenarios:
    - PowerShell: .ps1, .psm1, .psd1, .ps1xml
    - CSharp: .cs, .csx
    - Web: .html, .css, .js, .json, .xml
    - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd
    - Text: .txt, .md, .log, .config
    - All: Processes all common text file types
 
    .PARAMETER SourceEncoding
    Expected source encoding of files. Default is 'UTF8BOM'.
 
    .PARAMETER TargetEncoding
    Target encoding for conversion.
    Default is 'UTF8BOM' for PowerShell compatibility (prevents PS 5.1 ASCII misinterpretation).
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from processing (e.g., '.git', 'bin', 'obj', 'node_modules').
    Default excludes common build and version control directories.
 
    .PARAMETER Recurse
    Process files in subdirectories recursively. Default is $true.
 
    .PARAMETER CreateBackups
    Create backup files before conversion for additional safety.
    Backups are created with .bak extension in the same directory.
 
    .PARAMETER Force
    Convert files even when their detected encoding doesn't match SourceEncoding.
 
    .PARAMETER NoRollbackOnMismatch
    Skip rolling back files when verification detects content changes during conversion.
 
    .PARAMETER MaxDepth
    Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited.
 
    .PARAMETER PassThru
    Return conversion results for further processing.
 
    .EXAMPLE
    Convert-FolderEncoding -Path . -Extensions 'ps1' -WhatIf
 
    Preview what PowerShell files in the current directory would be converted.
 
    .EXAMPLE
    Convert-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -CreateBackups
 
    Convert all PowerShell files in Scripts and Modules directories to UTF8BOM with backups.
 
    .EXAMPLE
    Convert-FolderEncoding -Path . -Extensions @('cs', 'vb') -TargetEncoding UTF8 -Recurse
 
    Convert all C# and VB.NET files recursively to UTF8 (without BOM).
 
    .EXAMPLE
    Convert-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist') -Force
 
    Convert web files, excluding build directories, forcing conversion regardless of detected encoding.
 
    .NOTES
    Author: PowerShell Encoding Tools
 
    This function provides a more user-friendly interface than Convert-FileEncoding for common scenarios.
    For complex filtering requirements, use Convert-FileEncoding directly.
 
    PowerShell Encoding Notes:
    - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility
    - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII
    - Always test with -WhatIf first and consider using -CreateBackups
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Extensions')]
    param(
        [Parameter(Mandatory)]
        [Alias('Directory', 'Folder')]
        [string[]] $Path,

        [Parameter(ParameterSetName = 'Extensions', Mandatory)]
        [string[]] $Extensions,

        [Parameter(ParameterSetName = 'FileType', Mandatory)]
        [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')]
        [string] $FileType,

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $SourceEncoding = 'UTF8BOM',

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $TargetEncoding = 'UTF8BOM',

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'),

        [bool] $Recurse = $true,

        [switch] $CreateBackups,

        [switch] $Force,

        [switch] $NoRollbackOnMismatch,

        [int] $MaxDepth,

        [switch] $PassThru
    )

    foreach ($singlePath in $Path) {
        if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) {
            throw "Directory path '$singlePath' not found or is not a directory"
        }
    }

    $fileTypeMappings = @{
        'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml')
        'CSharp'     = @('.cs', '.csx', '.csproj')
        'Web'        = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt')
        'Scripts'    = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd')
        'Text'       = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml')
        'All'        = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml')
    }

    if ($PSCmdlet.ParameterSetName -eq 'FileType') {
        $targetExtensions = $fileTypeMappings[$FileType]
        Write-Verbose "Using $FileType file type: $($targetExtensions -join ', ')"
    }
    else {

        $targetExtensions = $Extensions | ForEach-Object {
            if ($_.StartsWith('.')) {
                $_ 
            }
            else {
                ".$_" 
            }
        }
        Write-Verbose "Target extensions: $($targetExtensions -join ', ')"
    }

    $allFiles = @()
    $summary = @{
        TotalDirectories     = $Path.Count
        ProcessedDirectories = 0
        TotalFiles           = 0
        ProcessedFiles       = 0
        ConvertedFiles       = 0
        SkippedFiles         = 0
        ErrorFiles           = 0
        StartTime            = Get-Date
    }

    Write-Verbose "Scanning directories for files..."

    foreach ($singlePath in $Path) {
        Write-Verbose "Processing directory: $singlePath"
        $summary.ProcessedDirectories++

        try {
            $gciParams = @{
                LiteralPath = $singlePath
                File        = $true
            }

            if ($Recurse) {
                $gciParams.Recurse = $true
                if ($MaxDepth) {
                    $gciParams.Depth = $MaxDepth 
                }
            }

            $directoryFiles = Get-ChildItem @gciParams | Where-Object {

                $extension = $_.Extension.ToLower()
                $extensionMatch = $targetExtensions -contains $extension

                $directoryExcluded = $false
                if ($ExcludeDirectories -and $_.DirectoryName) {
                    $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/')
                    $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" }
                }

                return $extensionMatch -and -not $directoryExcluded
            }

            $allFiles += $directoryFiles
            $summary.TotalFiles += $directoryFiles.Count

            Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath"
        }
        catch {
            Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)"
            continue
        }
    }

    if ($allFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria."
        Write-Verbose "Extensions searched: $($targetExtensions -join ', ')"
        Write-Verbose "Paths searched: $($Path -join ', ')"
        return
    }

    Write-Verbose "Found $($allFiles.Count) files across $($summary.ProcessedDirectories) directories"
    Write-Verbose "Target extensions: $($targetExtensions -join ', ')"
    Write-Verbose "Converting: $SourceEncoding → $TargetEncoding"

    if ($PSCmdlet.ShouldProcess("$($allFiles.Count) files", "Convert encoding from $SourceEncoding to $TargetEncoding")) {

        $results = @()
        $progressCounter = 0

        foreach ($file in $allFiles) {
            $progressCounter++
            $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1)

            Write-Progress -Activity "Converting file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete

            try {

                if ($CreateBackups) {
                    $backupPath = "$($file.FullName).bak"
                    Copy-Item -LiteralPath $file.FullName -Destination $backupPath -Force
                    Write-Verbose "Created backup: $backupPath"
                }

                $convertParams = @{
                    Path                 = $file.FullName
                    SourceEncoding       = $SourceEncoding
                    TargetEncoding       = $TargetEncoding
                    Force                = $Force
                    NoRollbackOnMismatch = $NoRollbackOnMismatch
                    WhatIf               = $WhatIfPreference
                }

                Convert-FileEncoding @convertParams

                $summary.ProcessedFiles++
                $summary.ConvertedFiles++

                if ($PassThru) {
                    $results += [PSCustomObject]@{
                        FilePath      = $file.FullName
                        Extension     = $file.Extension
                        Status        = 'Converted'
                        BackupCreated = $CreateBackups
                    }
                }
            }
            catch {
                $summary.ErrorFiles++
                Write-Error "Error converting '$($file.FullName)': $($_.Exception.Message)"

                if ($PassThru) {
                    $results += [PSCustomObject]@{
                        FilePath  = $file.FullName
                        Extension = $file.Extension
                        Status    = 'Error'
                        Error     = $_.Exception.Message
                    }
                }
            }
        }

        Write-Progress -Activity "Converting file encodings" -Completed
    }

    $summary.EndTime = Get-Date
    $summary.Duration = $summary.EndTime - $summary.StartTime

    Write-Verbose "Conversion Summary:"
    Write-Verbose " Directories processed: $($summary.ProcessedDirectories)"
    Write-Verbose " Files found: $($summary.TotalFiles)"
    Write-Verbose " Files processed: $($summary.ProcessedFiles)"
    Write-Verbose " Files converted: $($summary.ConvertedFiles)"
    Write-Verbose " Files with errors: $($summary.ErrorFiles)"
    Write-Verbose " Duration: $($summary.Duration.TotalSeconds.ToString('F2')) seconds"

    if ($CreateBackups -and $summary.ConvertedFiles -gt 0) {
        Write-Verbose " Backups created with .bak extension"
    }

    if ($PassThru) {
        return $results
    }
}

function Convert-HashTableToNicelyFormattedString {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $hashTable
    )
    [string] $nicelyFormattedString = $hashTable.Keys | ForEach-Object `
    {
        $key = $_
        $value = $hashTable.$key
        " $key = $value$NewLine"
    }
    return $nicelyFormattedString
}

function Convert-LineEndingSingle {
    param(
        [string] $FilePath,
        [string] $TargetLineEnding,
        [hashtable] $CurrentInfo,
        [bool] $CreateBackup,
        [bool] $EnsureFinalNewline
    )

    try {

        $content = [System.IO.File]::ReadAllText($FilePath)

        if ([string]::IsNullOrEmpty($content)) {
            return @{
                Status = 'Skipped'
                Reason = 'Empty file'
            }
        }

        $backupPath = $null
        if ($CreateBackup) {
            $backupPath = "$FilePath.backup"
            $counter = 1
            while (Test-Path $backupPath) {
                $backupPath = "$FilePath.backup$counter"
                $counter++
            }
            $originalBytes = [System.IO.File]::ReadAllBytes($FilePath)
            [System.IO.File]::WriteAllBytes($backupPath, $originalBytes)
        }

        $normalizedContent = $content -replace "`r`n", "`n" -replace "`r", "`n"

        $convertedContent = if ($TargetLineEnding -eq 'CRLF') {
            $normalizedContent -replace "`n", "`r`n"
        }
        else {
            $normalizedContent
        }

        if ($EnsureFinalNewline -and -not [string]::IsNullOrEmpty($convertedContent)) {
            $targetNewline = if ($TargetLineEnding -eq 'CRLF') {
                "`r`n" 
            }
            else {
                "`n" 
            }
            if (-not $convertedContent.EndsWith($targetNewline)) {
                $convertedContent += $targetNewline
            }
        }

        $encoding = Get-FileEncoding -Path $FilePath -AsObject
        [System.IO.File]::WriteAllText($FilePath, $convertedContent, $encoding.Encoding)

        $changesMade = @()
        if ($CurrentInfo.LineEnding -ne $TargetLineEnding -and $CurrentInfo.LineEnding -ne 'None') {
            $changesMade += "line endings ($($CurrentInfo.LineEnding) → $TargetLineEnding)"
        }
        if ($EnsureFinalNewline -and -not $CurrentInfo.HasFinalNewline) {
            $changesMade += "added final newline"
        }

        return @{
            Status     = 'Converted'
            Reason     = "Converted: $($changesMade -join ', ')"
            BackupPath = $backupPath
        }
    }
    catch {
        return @{
            Status     = 'Error'
            Reason     = "Failed to convert: $_"
            BackupPath = $backupPath
        }
    }
}
function Convert-RequiredModules {
    <#
    .SYNOPSIS
    Converts the RequiredModules section of the manifest to the correct format
 
    .DESCRIPTION
    Converts the RequiredModules section of the manifest to the correct format
    Fixes the ModuleVersion and Guid if set to 'Latest' or 'Auto'
 
    .PARAMETER Configuration
    The configuration object of the module
 
    .EXAMPLE
    Convert-RequiredModules -Configuration $Configuration
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration
    )
    $Manifest = $Configuration.Information.Manifest

    $Failures = $false
    if ($Manifest.Contains('RequiredModules')) {
        foreach ($SubModule in $Manifest.RequiredModules) {
            if ($SubModule -is [string]) {
            }
            else {
                [Array] $AvailableModule = Get-Module -ListAvailable $SubModule.ModuleName -Verbose:$false
                if ($SubModule.ModuleVersion -in 'Latest', 'Auto') {
                    if ($AvailableModule) {
                        $SubModule.ModuleVersion = $AvailableModule[0].Version.ToString()
                    }
                    else {
                        Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available (Version), but defined as required with last version. Terminating." -Color Red
                        $Failures = $true
                    }
                }
                if ($SubModule.Guid -in 'Latest', 'Auto') {
                    if ($AvailableModule) {
                        $SubModule.Guid = $AvailableModule[0].Guid.ToString()
                    }
                    else {
                        Write-Text -Text "[-] Module $($SubModule.ModuleName) is not available (GUID), but defined as required with last version. Terminating." -Color Red
                        $Failures = $true
                    }
                }
            }
        }
    }
    if ($Failures -eq $true) {
        $false
    }
}
function Copy-ArtefactMainModule {
    [CmdletBinding()]
    param(
        [switch] $Enabled,
        [nullable[bool]] $IncludeTagName,
        [string] $ModuleName,
        [string] $Destination
    )
    if (-not $Enabled) {
        return
    }
    if ($IncludeTagName) {
        $NameOfDestination = [io.path]::Combine($Destination, $ModuleName, $TagName)
    }
    else {
        $NameOfDestination = [io.path]::Combine($Destination, $ModuleName)
    }
    $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination)
    Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow {
        if (Test-Path -Path $NameOfDestination) {
            Remove-ItemAlternative -LiteralPath $NameOfDestination -ErrorAction Stop
        }
        $null = New-Item -ItemType Directory -Path $Destination -Force

        if ($DestinationPaths.Desktop) {
            Copy-Item -LiteralPath $DestinationPaths.Desktop -Recurse -Destination $ResolvedDestination -Force
        }
        elseif ($DestinationPaths.Core) {
            Copy-Item -LiteralPath $DestinationPaths.Core -Recurse -Destination $ResolvedDestination -Force
        }
    } -SpacesBefore ' '
}
function Copy-ArtefactRequiredFiles {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $FilesInput,
        [string] $ProjectPath,
        [string] $Destination,
        [nullable[bool]] $DestinationRelative
    )

    foreach ($File in $FilesInput.Keys) {
        if ($FilesInput[$File] -is [string]) {
            $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $File)
            if (Test-Path -Path $FullFilePath) {
                if ($DestinationRelative) {
                    $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File])
                }
                else {
                    $DestinationPath = $FilesInput[$File]
                }
                $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath)
                Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" {
                    $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination)
                    $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath
                    Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop
                } -PreAppend Addition -SpacesBefore ' ' -Color Yellow
            }
            else {
                Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red
                return $false
            }
        }
        elseif ($FilesInput[$File] -is [System.Collections.IDictionary]) {
            if ($FilesInput[$File].Enabled -eq $true) {
                if ($FilesInput[$File].Source) {
                    $FullFilePath = [System.IO.Path]::Combine($ProjectPath, $FilesInput[$File].Source)
                    if (Test-Path -Path $FullFilePath) {
                        if ($FilesInput[$File].DestinationRelative) {
                            $DestinationPath = [System.IO.Path]::Combine($Destination, $FilesInput[$File].Destination)
                        }
                        else {
                            $DestinationPath = $FilesInput[$File].Destination
                        }
                        $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DestinationPath)
                        Write-TextWithTime -Text "Copying file $FullFilePath to $ResolvedDestination" {
                            $DirectoryPath = [Io.Path]::GetDirectoryName($ResolvedDestination)
                            $null = New-Item -ItemType Directory -Force -ErrorAction Stop -Path $DirectoryPath
                            Copy-Item -LiteralPath $FullFilePath -Destination $ResolvedDestination -Force -ErrorAction Stop
                        } -PreAppend Addition -SpacesBefore ' ' -Color Yellow
                    }
                    else {
                        Write-TextWithTime -Text "File $FullFilePath does not exist" -PreAppend Plus -SpacesBefore ' ' -Color Red -ColorTime Red -ColorBefore Red
                        return $false
                    }
                }
            }
        }
    }
}
function Copy-ArtefactRequiredFolders {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $FoldersInput,
        [string] $ProjectPath,
        [string] $Destination,
        [nullable[bool]] $DestinationRelative
    )
}
function Copy-ArtefactRequiredModule {
    [CmdletBinding()]
    param(
        [switch] $Enabled,
        [Array] $RequiredModules,
        [string] $Destination
    )
    if (-not $Enabled) {
        return
    }
    if (-not (Test-Path -LiteralPath $Destination)) {
        New-Item -ItemType Directory -Path $Destination -Force
    }
    foreach ($Module in $RequiredModules) {
        if ($Module.ModuleName) {
            Write-TextWithTime -PreAppend Addition -Text "Copying required module $($Module.ModuleName)" -Color Yellow {
                $ModulesFound = Get-Module -ListAvailable -Name $Module.ModuleName
                if ($ModulesFound.Count -gt 0) {
                    $PathToPSD1 = if ($Module.ModuleVersion -eq 'Latest') {
                        $ModulesFound[0].Path
                    }
                    else {
                        $FoundModule = foreach ($M in $ModulesFound) {
                            if ($M.Version -eq $Module.ModuleVersion) {
                                $M.Path
                                break
                            }
                        }
                        if (-not $FoundModule) {

                            $ModulesFound[0].Path
                        }
                        else {
                            $FoundModule
                        }
                    }
                    $FolderToCopy = [System.IO.Path]::GetDirectoryName($PathToPSD1)
                    $ItemInformation = Get-Item -LiteralPath $FolderToCopy

                    if ($ItemInformation.Name -ne $Module.ModuleName) {
                        $NewPath = [io.path]::Combine($Destination, $Module.ModuleName)
                        if (Test-Path -LiteralPath $NewPath) {
                            Remove-Item -LiteralPath $NewPath -Recurse -Force -ErrorAction Stop
                        }
                        Copy-Item -LiteralPath $FolderToCopy -Destination $NewPath -Recurse -Force -ErrorAction Stop
                    }
                    else {
                        Copy-Item -LiteralPath $FolderToCopy -Destination $Destination -Recurse -Force
                    }
                }
            } -SpacesBefore ' '
        }
    }
}
function Copy-ArtefactToScript {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $ModuleVersion,
        [switch] $Enabled,
        [nullable[bool]] $IncludeTagName,
        [string] $ModuleName,
        [string] $Destination,
        [string] $PreScriptMerge,
        [string] $PostScriptMerge,
        [string] $ScriptName
    )
    if (-not $Enabled) {
        return
    }

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($IncludeTagName) {
        $NameOfDestination = [io.path]::Combine($Destination, $TagName)
    }
    else {
        $NameOfDestination = [io.path]::Combine($Destination)
    }
    $ResolvedDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($NameOfDestination)
    Write-TextWithTime -PreAppend Addition -Text "Copying main module to $ResolvedDestination" -Color Yellow {
        if (Test-Path -Path $NameOfDestination) {
            Remove-ItemAlternative -LiteralPath $NameOfDestination -ErrorAction Stop
        }
        $null = New-Item -ItemType Directory -Path $Destination -Force

        if ($DestinationPaths.Desktop) {
            $DestinationToUse = [System.IO.Path]::Combine($DestinationPaths.Desktop, "*")
            Copy-Item -Path $DestinationToUse -Recurse -Destination "$ResolvedDestination" -Force
        }
        elseif ($DestinationPaths.Core) {
            $DestinationToUse = [System.IO.Path]::Combine($DestinationPaths.Core, "*")
            Copy-Item -Path $DestinationToUse -Recurse -Destination "$ResolvedDestination" -Force
        }
    } -SpacesBefore ' '
    Write-TextWithTime -PreAppend Addition -Text "Cleaning up main module" -Color Yellow {
        $PSD1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psd1")
        Remove-Item -LiteralPath $PSD1 -Force -ErrorAction Stop
        $PSM1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.psm1")
        if ($ScriptName) {

            $TagName = "v$($ModuleVersion)"
            if ($Configuration.CurrentSettings.PreRelease) {
                $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
                $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)"
            }
            else {
                $ModuleVersionWithPreRelease = $ModuleVersion
                $TagModuleVersionWithPreRelease = "v$($ModuleVersion)"
            }

            $ScriptName = $ScriptName.Replace('{ModuleName}', $ModuleName)
            $ScriptName = $ScriptName.Replace('<ModuleName>', $ModuleName)
            $ScriptName = $ScriptName.Replace('{ModuleVersion}', $ModuleVersion)
            $ScriptName = $ScriptName.Replace('<ModuleVersion>', $ModuleVersion)
            $ScriptName = $ScriptName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease)
            $ScriptName = $ScriptName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease)
            $ScriptName = $ScriptName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease)
            $ScriptName = $ScriptName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease)
            $ScriptName = $ScriptName.Replace('{TagName}', $TagName)
            $ScriptName = $ScriptName.Replace('<TagName>', $TagName)

            if ($ScriptName.EndsWith(".ps1")) {
                $PS1 = [io.path]::Combine($ResolvedDestination, "$ScriptName")
                Rename-Item -LiteralPath $PSM1 -NewName "$ScriptName" -Force -ErrorAction Stop
            }
            else {
                $PS1 = [io.path]::Combine($ResolvedDestination, "$ScriptName.ps1")
                Rename-Item -LiteralPath $PSM1 -NewName "$ScriptName.ps1" -Force -ErrorAction Stop
            }
        }
        else {
            $PS1 = [io.path]::Combine($ResolvedDestination, "$ModuleName.ps1")

            Rename-Item -LiteralPath $PSM1 -NewName "$ModuleName.ps1" -Force -ErrorAction Stop
        }
        $Content = Get-Content -LiteralPath $PS1 -ErrorAction Stop -Encoding UTF8

        if ($Content -contains "# Export functions and aliases as required") {
            $Index = ($Content | Select-String -Pattern "# Export functions and aliases as required" -SimpleMatch | Select-Object -Last 1).LineNumber
            $Content = $Content | Select-Object -First ($Index - 1)
        }
        else {
            $index = ($Content | Select-String -Pattern "Export-ModuleMember " -SimpleMatch | Select-Object -Last 1).LineNumber

            $Content = $Content | Select-Object -First ($Index - 1)
        }

        if ($PreScriptMerge) {
            $Content = @(
                $PreScriptMerge.Trim()
                $Content
            )
        }
        if ($PostScriptMerge) {
            $Content = @(
                $Content
                $PostScriptMerge.Trim()
            )
        }

        Set-Content -LiteralPath $PS1 -Value $Content -Force -ErrorAction Stop -Encoding $Encoding
    } -SpacesBefore ' '
}
function Copy-DictionaryManual {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $clone = @{}
    foreach ($Key in $Dictionary.Keys) {
        $value = $Dictionary.$Key

        $clonedValue = switch ($Dictionary.$Key) {
            { $null -eq $_ } {
                $null
                continue
            }
            { $_ -is [System.Collections.IDictionary] } {
                Copy-DictionaryManual -Dictionary $_
                continue
            }
            {
                $type = $_.GetType()
                $type.IsPrimitive -or $type.IsValueType -or $_ -is [string]
            } {
                $_
                continue
            }
            default {
                $_ | Select-Object -Property *
            }
        }

        if ($value -is [System.Collections.IList]) {
            $clone[$Key] = @($clonedValue)
        }
        else {
            $clone[$Key] = $clonedValue
        }
    }

    $clone
}
function Copy-InternalDictionary {
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $ms = [System.IO.MemoryStream]::new()
    $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new()
    $bf.Serialize($ms, $Dictionary)
    $ms.Position = 0
    $clone = $bf.Deserialize($ms)
    $ms.Close()
    $clone
}
function Copy-InternalFiles {
    [CmdletBinding()]
    param(
        [string[]] $LinkFiles,
        [string] $FullModulePath,
        [string] $FullProjectPath,
        [switch] $Delete
    )

    foreach ($File in $LinkFiles) {
        [string] $Path = [System.IO.Path]::Combine($FullModulePath, $File)
        [string] $Path2 = [System.IO.Path]::Combine($FullProjectPath, $File)

        if ($Delete) {
            if (Test-ReparsePoint -path $Path) {

                Remove-Item $Path -Confirm:$false
            }
        }

        Copy-Item -Path $Path2 -Destination $Path -Force -Recurse -Confirm:$false
    }
}
function Export-PSData {
    <#
    .Synopsis
        Exports property bags into a data file
    .Description
        Exports property bags and the first level of any other object into a ps data file (.psd1)
    .Link
        https://github.com/StartAutomating/Pipeworks
        Import-PSData
    .Example
        Get-Web -Url http://www.youtube.com/watch?v=xPRC3EDR_GU -AsMicrodata -ItemType http://schema.org/VideoObject |
            Export-PSData .\PipeworksQuickstart.video.psd1
    #>

    [OutputType([IO.FileInfo])]
    [cmdletbinding()]
    param(
        # The data that will be exported
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)][PSObject[]]$InputObject,
        # The path to the data file
        [Parameter(Mandatory = $true, Position = 0)][string] $DataFile,
        [switch] $Sort
    )
    begin {
        $AllObjects = [System.Collections.Generic.List[object]]::new()
    }
    process {
        $AllObjects.AddRange($InputObject)
    }
    end {
        if ($PSVersionTable.PSVersion.Major -gt 5) {
            $Encoding = 'UTF8BOM'
        }
        else {
            $Encoding = 'UTF8'
        }

        $Text = $AllObjects | Write-PowerShellHashtable -Sort:$Sort.IsPresent
        $Text | Out-File -FilePath $DataFile -Encoding $Encoding
    }
}
function Find-NetFramework {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER RequireVersion
    Parameter description
 
    .EXAMPLE
    Find-NetFramework -RequireVersion 4.8
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [string] $RequireVersion
    )
    if ($RequireVersion) {
        $Framework = [ordered] @{
            '4.5'   = 378389
            '4.5.1'    = 378675
            '4.5.2'    = 379893
            '4.6'   = 393295
            '4.6.1'    = 394254
            '4.6.2'    = 394802
            '4.7'   = 460798
            '4.7.1'    = 461308
            '4.7.2'    = 461808
            '4.8'   = 528040
        }
        $DetectVersion = $Framework[$RequireVersion]

        "if (`$PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty `"HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full`").Release -lt $DetectVersion) { Write-Warning `"This module requires .NET Framework $RequireVersion or later.`"; return } "
    }
}
function Find-RequiredModules {
    [cmdletbinding()]
    param(
        [string] $Name
    )
    $Module = Get-Module -ListAvailable $Name -ErrorAction SilentlyContinue -Verbose:$false
    $AllModules = if ($Module) {
        [Array] $RequiredModules = $Module.RequiredModules.Name
        if ($null -ne $RequiredModules) {
            $null
        }
        $RequiredModules
        foreach ($_ in $RequiredModules) {
            Find-RequiredModules -Name $_
        }
    }

    [Array] $ListModules = $AllModules | Where-Object { $null -ne $_ }
    if ($null -ne $ListModules) {
        [array]::Reverse($ListModules)
    }
    $CleanedModules = [System.Collections.Generic.List[string]]::new()
    foreach ($_ in $ListModules) {
        if ($CleanedModules -notcontains $_) {
            $CleanedModules.Add($_)
        }
    }
    $CleanedModules
}
function Format-Code {
    [cmdletbinding()]
    param(
        [string] $FilePath,
        [System.Collections.IDictionary] $FormatCode
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($FormatCode.Enabled) {
        if ($FormatCode.RemoveComments -or $FormatCode.RemoveCommentsInParamBlock -or $FormatCode.RemoveCommentsBeforeParamBlock) {
            $Output = Write-TextWithTime -Text "[+] Removing Comments - $FilePath" {
                $removeCommentsSplat = @{
                    SourceFilePath                 = $FilePath
                    RemoveCommentsInParamBlock     = $FormatCode.RemoveCommentsInParamBlock
                    RemoveCommentsBeforeParamBlock = $FormatCode.RemoveCommentsBeforeParamBlock
                    RemoveAllEmptyLines            = $FormatCode.RemoveAllEmptyLines
                    RemoveEmptyLines               = $FormatCode.RemoveEmptyLines
                }
                Remove-Comments @removeCommentsSplat
            }
        }
        elseif ($FormatCode.RemoveAllEmptyLines -or $FormatCode.RemoveEmptyLines) {
            $Output = Write-TextWithTime -Text "[+] Removing Empty Lines - $FilePath" {
                $removeEmptyLinesSplat = @{
                    SourceFilePath      = $FilePath
                    RemoveAllEmptyLines = $FormatCode.RemoveAllEmptyLines
                    RemoveEmptyLines    = $FormatCode.RemoveEmptyLines
                }
                Remove-EmptyLines @removeEmptyLinesSplat
            }
        }
        else {
            $Output = Write-TextWithTime -Text "Reading file content - $FilePath" {
                Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 -ErrorAction Stop
            } -PreAppend Plus -SpacesBefore ' '
        }
        if ($Output -eq $false) {
            return $false
        }
        if ($null -eq $FormatCode.FormatterSettings) {
            $FormatCode.FormatterSettings = $Script:FormatterSettings
        }
        $Data = Write-TextWithTime -Text "Formatting file - $FilePath" {
            try {
                Invoke-Formatter -ScriptDefinition $Output -Settings $FormatCode.FormatterSettings -ErrorAction Stop
            }
            catch {
                $ErrorMessage = $_.Exception.Message

                Write-Host 
                Write-Text " [-] Format-Code - Formatting on file $FilePath failed." -Color Red
                Write-Text " [-] Format-Code - Error: $ErrorMessage" -Color Red
                Write-Text " [-] Format-Code - This is most likely related to a bug in PSScriptAnalyzer running inside VSCode. Please try running outside of VSCode when using formatting." -Color Red
                return $false
            }
        } -PreAppend Plus -SpacesBefore ' '
        if ($Data -eq $false) {
            return $false
        }
        Write-TextWithTime -Text "Saving file - $FilePath" {

            $Final = foreach ($O in $Data) {
                if ($O.Trim() -ne '') {
                    $O.Trim()
                }
            }
            try {
                $Final | Out-File -LiteralPath $FilePath -NoNewline -Encoding $Encoding -ErrorAction Stop
            }
            catch {
                $ErrorMessage = $_.Exception.Message

                Write-Host 
                Write-Text "[-] Format-Code - Resaving file $FilePath failed. Error: $ErrorMessage" -Color Red
                return $false
            }
        } -PreAppend Plus -SpacesBefore ' '
    }
}

function Format-UsingNamespace {
    [CmdletBinding()]
    param(
        [string] $FilePath,
        [string] $FilePathSave,
        [string] $FilePathUsing
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($FilePathSave -eq '') {
        $FilePathSave = $FilePath
    }
    if ($FilePath -ne '' -and (Test-Path -Path $FilePath) -and (Get-Item -LiteralPath $FilePath).Length -gt 0kb) {
        $FileStream = New-Object -TypeName IO.FileStream -ArgumentList ($FilePath), ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read), ([System.IO.FileShare]::ReadWrite);
        $ReadFile = New-Object -TypeName System.IO.StreamReader -ArgumentList ($FileStream, [System.Text.Encoding]::UTF8, $true);

        $UsingNamespaces = [System.Collections.Generic.List[string]]::new()

        $Content = while (!$ReadFile.EndOfStream) {
            $Line = $ReadFile.ReadLine()
            if ($Line -like 'using namespace*') {
                $UsingNamespaces.Add($Line)
            }
            else {
                $Line
            }
        }
        $ReadFile.Close()

        $null = New-Item -Path $FilePathSave -ItemType file -Force
        if ($UsingNamespaces) {

            $null = New-Item -Path $FilePathUsing -ItemType file -Force
            $UsingNamespaces = $UsingNamespaces.Trim() | Sort-Object -Unique
            $UsingNamespaces | Out-File -Append -LiteralPath $FilePathUsing -Encoding $Encoding

            $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding $Encoding
            return $true
        }
        else {
            $Content | Out-File -Append -LiteralPath $FilePathSave -Encoding $Encoding
            return $False
        }
    }
}
function Get-AstTokens {
    [cmdletBinding()]
    param(
        [System.Management.Automation.Language.Token[]] $ASTTokens,
        [System.Collections.Generic.List[Object]] $Commands,
        [System.Management.Automation.Language.Ast] $FileAst
    )

    $ListOfFuncionsAst = $FileAst.FindAll( {
            param([System.Management.Automation.Language.Ast] $ast)
            end {
                if ($ast -isnot [System.Management.Automation.Language.CommandAst]) {
                    return $false
                }

                for ($node = $ast.Parent; $null -ne $node; $node = $node.Parent) {
                    if ($node -isnot [System.Management.Automation.Language.CommandAst]) {
                        continue
                    }

                    if ($node.GetCommandName() -in 'Get-ADComputer', 'Get-ADUser', 'Get-ADObject', 'Get-ADDomainController', 'Get-ADReplicationSubnet') {
                        return $false
                    }
                }

                return $true
            }
        }, $true
    )

    $List = foreach ($Function in $ListOfFuncionsAst) {
        $Line = $Function.CommandElements[0]
        if ($Line.Value) {
            $Line.Value
        }
    }
    $List
}
function Get-CurrentLineEnding {
    param([string] $FilePath)

    try {
        $bytes = [System.IO.File]::ReadAllBytes($FilePath)
        if ($bytes.Length -eq 0) {
            return @{
                LineEnding      = 'None'
                HasFinalNewline = $true
            }
        }

        $crlfCount = 0
        $lfOnlyCount = 0
        $crOnlyCount = 0
        $hasFinalNewline = $false

        $lastByte = $bytes[$bytes.Length - 1]
        if ($lastByte -eq 10) {
            $hasFinalNewline = $true
        }
        elseif ($lastByte -eq 13) {
            $hasFinalNewline = $true
        }

        for ($i = 0; $i -lt $bytes.Length - 1; $i++) {
            if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) {
                $crlfCount++
                $i++
            }
            elseif ($bytes[$i] -eq 10) {
                $lfOnlyCount++
            }
            elseif ($bytes[$i] -eq 13) {
                if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) {
                    $crOnlyCount++
                }
            }
        }

        if ($bytes.Length -gt 0) {
            $lastByte = $bytes[$bytes.Length - 1]
            if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) {
                $lfOnlyCount++
            }
            elseif ($lastByte -eq 13) {
                $crOnlyCount++
            }
        }

        $typesFound = @()
        if ($crlfCount -gt 0) {
            $typesFound += 'CRLF' 
        }
        if ($lfOnlyCount -gt 0) {
            $typesFound += 'LF' 
        }
        if ($crOnlyCount -gt 0) {
            $typesFound += 'CR' 
        }

        $lineEndingType = if ($typesFound.Count -eq 0) {
            'None'
        }
        elseif ($typesFound.Count -eq 1) {
            $typesFound[0]
        }
        else {
            'Mixed'
        }

        return @{
            LineEnding      = $lineEndingType
            HasFinalNewline = $hasFinalNewline
        }
    }
    catch {
        return @{
            LineEnding      = 'Error'
            HasFinalNewline = $false
        }
    }
}
function Get-CurrentVersionFromBuildScript {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ScriptFile
    )

    if (!(Test-Path -Path $ScriptFile)) {
        Write-Warning "Build script file not found: $ScriptFile"
        return $null
    }

    try {
        $content = Get-Content -Path $ScriptFile -Raw
        if ($content -match 'ModuleVersion\s*=\s*[''"\"]?([\d\.]+)[''"\"]?') {
            return $matches[1]
        }
        return $null
    }
    catch {
        Write-Warning "Error reading build script $ScriptFile`: $_"
        return $null
    }
}
function Get-CurrentVersionFromCsProj {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ProjectFile
    )

    if (!(Test-Path -Path $ProjectFile)) {
        Write-Warning "Project file not found: $ProjectFile"
        return $null
    }

    try {
        $content = Get-Content -Path $ProjectFile -Raw
        if ($content -match '<VersionPrefix>([\d\.]+)<\/VersionPrefix>') {
            return $matches[1]
        }
        return $null
    }
    catch {
        Write-Warning "Error reading project file $ProjectFile`: $_"
        return $null
    }
}
function Get-CurrentVersionFromPsd1 {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ManifestFile
    )

    if (!(Test-Path -Path $ManifestFile)) {
        Write-Warning "Module manifest file not found: $ManifestFile"
        return $null
    }

    try {
        $manifest = Import-PowerShellDataFile -Path $ManifestFile
        return $manifest.ModuleVersion
    }
    catch {
        Write-Warning "Error reading module manifest $ManifestFile`: $_"
        return $null
    }
}
function Get-FileEncoding {
    <#
    .SYNOPSIS
    Get the encoding of a file (ASCII, UTF8, UTF8BOM, Unicode, BigEndianUnicode, UTF7, UTF32).
 
    .DESCRIPTION
    Detects the encoding of a file using its byte order mark or by scanning for non‑ASCII characters.
    Encoding is determined by the first few bytes of the file (BOM) or by the presence of non-ASCII characters.
    Returns a string with the encoding name or a custom object when -AsObject is used.
 
    .PARAMETER Path
    Path to the file to check. Supports pipeline input and can accept FullName property from Get-ChildItem.
 
    .PARAMETER AsObject
    Returns a custom object with Path, Encoding, and EncodingName properties instead of just the encoding name string.
 
    .EXAMPLE
    Get-FileEncoding -Path 'C:\temp\test.txt'
    Returns the encoding name as a string (e.g., 'UTF8BOM', 'ASCII', 'Unicode')
 
    .EXAMPLE
    Get-FileEncoding -Path 'C:\temp\test.txt' -AsObject
    Returns a custom object with detailed encoding information
 
    .EXAMPLE
    Get-ChildItem -Path 'C:\temp\*.txt' | Get-FileEncoding
    Gets encoding for all text files in the directory via pipeline
 
    .NOTES
    Supported encodings: ASCII, UTF8, UTF8BOM, Unicode (UTF-16LE), BigEndianUnicode (UTF-16BE), UTF7, UTF32
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [Alias('FullName')]
        [string] $Path,

        [switch] $AsObject
    )
    process {
        if (-not (Test-Path -LiteralPath $Path)) {
            $msg = "Get-FileEncoding - File not found: $Path"
            if ($ErrorActionPreference -eq 'Stop') {
                throw $msg 
            }
            Write-Warning $msg
            return
        }

        $fs = [System.IO.FileStream]::new($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
        try {
            $bom = [byte[]]::new(4)
            $null = $fs.Read($bom, 0, 4)
            $enc = [System.Text.Encoding]::ASCII

            if ($bom[0] -eq 0x00 -and $bom[1] -eq 0x00 -and $bom[2] -eq 0xfe -and $bom[3] -eq 0xff) {
                $enc = [System.Text.Encoding]::UTF32
            }
            elseif ($bom[0] -eq 0xef -and $bom[1] -eq 0xbb -and $bom[2] -eq 0xbf) {
                $enc = [System.Text.UTF8Encoding]::new($true)
            }
            elseif ($bom[0] -eq 0xff -and $bom[1] -eq 0xfe) {
                $enc = [System.Text.Encoding]::Unicode
            }
            elseif ($bom[0] -eq 0xfe -and $bom[1] -eq 0xff) {
                $enc = [System.Text.Encoding]::BigEndianUnicode
            }
            elseif ($bom[0] -eq 0x2b -and $bom[1] -eq 0x2f -and $bom[2] -eq 0x76) {
                $enc = [System.Text.Encoding]::UTF7
            }
            else {

                $fs.Position = 0
                $byte = [byte[]]::new(1)
                while ($fs.Read($byte, 0, 1) -gt 0) {
                    if ($byte[0] -gt 0x7F) {
                        $enc = [System.Text.UTF8Encoding]::new($false)
                        break
                    }
                }
            }
        }
        finally {
            $fs.Close()
            $fs.Dispose()
        }

        $encName = if ($enc -is [System.Text.UTF8Encoding] -and $enc.GetPreamble().Length -eq 3) {
            'UTF8BOM'
        }
        elseif ($enc -is [System.Text.UTF8Encoding]) {
            'UTF8'
        }
        elseif ($enc -is [System.Text.UnicodeEncoding]) {
            'Unicode'
        }
        elseif ($enc -is [System.Text.UTF7Encoding]) {
            'UTF7'
        }
        elseif ($enc -is [System.Text.UTF32Encoding]) {
            'UTF32'
        }
        elseif ($enc -is [System.Text.ASCIIEncoding]) {
            'ASCII'
        }
        elseif ($enc -is [System.Text.BigEndianUnicodeEncoding]) {
            'BigEndianUnicode'
        }
        else {
            $enc.WebName
        }

        if ($AsObject) {
            [PSCustomObject]@{
                Path         = $Path
                Encoding     = $enc
                EncodingName = $encName
            }
        }
        else {
            $encName
        }
    }
}

function Get-FilteredScriptCommands {
    [CmdletBinding()]
    param(
        [Array] $Commands,
        [switch] $NotCmdlet,
        [switch] $NotUnknown,
        [switch] $NotApplication,
        [string[]] $Functions,
        [string] $FilePath,
        [string[]] $ApprovedModules
    )
    if ($Functions.Count -eq 0) {
        $Functions = Get-FunctionNames -Path $FilePath
    }
    $Commands = $Commands | Where-Object { $_ -notin $Functions }
    $Commands = $Commands | Sort-Object -Unique
    $Scan = foreach ($Command in $Commands) {
        try {
            $IsAlias = $false
            $Data = Get-Command -Name $Command -ErrorAction Stop
            if ($Data.Source -eq 'PSPublishModule') {

                if ($Data.Source -notin $ApprovedModules) {
                    throw
                }
            }
            if ($Data.CommandType -eq 'Alias') {
                $Data = Get-Command -Name $Data.Definition
                $IsAlias = $true
            }
            [PSCustomObject] @{
                Name        = $Data.Name
                Source      = $Data.Source
                CommandType = $Data.CommandType
                IsAlias     = $IsAlias
                IsPrivate   = $false
                Error       = ''
                ScriptBlock = $Data.ScriptBlock
            }
        }
        catch {
            $CurrentOutput = [PSCustomObject] @{
                Name        = $Command
                Source      = ''
                CommandType = ''
                IsAlias     = $IsAlias
                IsPrivate   = $false
                Error       = $_.Exception.Message
                ScriptBlock = ''
            }

            foreach ($ApprovedModule in $ApprovedModules) {
                try {
                    $ImportModuleWithPrivateCommands = Import-Module -PassThru -Name $ApprovedModule -ErrorAction Stop -Verbose:$false
                    $Data = & $ImportModuleWithPrivateCommands { param($command); Get-Command $command -Verbose:$false -ErrorAction Stop } $command
                    $CurrentOutput = [PSCustomObject] @{
                        Name        = $Data.Name
                        Source      = $Data.Source
                        CommandType = $Data.CommandType
                        IsAlias     = $IsAlias
                        IsPrivate   = $true
                        Error       = ''
                        ScriptBlock = $Data.ScriptBlock
                    }
                    break
                }
                catch {
                    $CurrentOutput = [PSCustomObject] @{
                        Name        = $Command
                        Source      = ''
                        CommandType = ''
                        IsAlias     = $IsAlias
                        IsPrivate   = $false
                        Error       = $_.Exception.Message
                        ScriptBlock = ''
                    }
                }
            }
            $CurrentOutput
        }
    }
    $Filtered = foreach ($Command in $Scan) {
        if ($Command.Source -eq 'Microsoft.PowerShell.Core') {

            continue
        }
        if ($NotCmdlet -and $NotUnknown -and $NotApplication) {
            if ($Command.CommandType -ne 'Cmdlet' -and $Command.Source -ne '' -and $Command.CommandType -ne 'Application') {
                $Command
            }
        }
        elseif ($NotCmdlet -and $NotUnknown) {
            if ($Command.CommandType -ne 'Cmdlet' -and $Command.Source -ne '') {
                $Command
            }
        }
        elseif ($NotCmdlet) {
            if ($Command.CommandType -ne 'Cmdlet') {
                $Command
            }
        }
        elseif ($NotUnknown) {
            if ($Command.Source -ne '') {
                $Command
            }
        }
        elseif ($NotApplication) {
            if ($Command.CommandType -ne 'Application') {
                $Command
            }
        }
        else {
            $Command
        }
    }
    $Filtered
}

function Get-FolderEncoding {
    <#
    .SYNOPSIS
    Analyzes file encodings in folders based on file extensions.
 
    .DESCRIPTION
    A user-friendly wrapper that analyzes encoding distribution across files in one or more folders,
    filtered by file extensions. This function is ideal for understanding the current encoding state
    of specific file types before performing conversions.
 
    The function supports both single and multiple file extensions, with predefined file type groups
    for common scenarios. It provides comprehensive analysis including encoding distribution,
    inconsistencies, and recommendations.
 
    .PARAMETER Path
    The directory path to analyze. Can be a single directory or an array of directories.
    Use '.' for the current directory.
 
    .PARAMETER Extensions
    File extensions to analyze. Can be specified with or without the leading dot.
    Examples: 'ps1', '.ps1', @('ps1', 'psm1'), @('.cs', '.vb')
 
    Common presets available via -FileType parameter for convenience.
 
    .PARAMETER FileType
    Predefined file type groups for common scenarios:
    - PowerShell: .ps1, .psm1, .psd1, .ps1xml
    - CSharp: .cs, .csx
    - Web: .html, .css, .js, .json, .xml
    - Scripts: .ps1, .py, .rb, .sh, .bat, .cmd
    - Text: .txt, .md, .log, .config
    - All: Analyzes all common text file types
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj', 'node_modules').
    Default excludes common build and version control directories.
 
    .PARAMETER Recurse
    Process files in subdirectories recursively. Default is $true.
 
    .PARAMETER MaxDepth
    Maximum directory depth to recurse when -Recurse is enabled. Default is unlimited.
 
    .PARAMETER GroupByExtension
    Group results by file extension to show encoding distribution per file type.
 
    .PARAMETER ShowFiles
    Include individual file details in the output. Default is $true.
    Use -ShowFiles:$false to disable if you only want summary statistics.
 
    .PARAMETER RecommendTarget
    Provide encoding recommendations based on file types (e.g., UTF8BOM for PowerShell files).
    Default is $true. Use -RecommendTarget:$false to disable recommendations.
 
    .EXAMPLE
    Get-FolderEncoding -Path . -Extensions 'ps1'
 
    Analyze PowerShell files in the current directory with file details and recommendations (default behavior).
 
    .EXAMPLE
    Get-FolderEncoding -Path @('.\Scripts', '.\Modules') -FileType PowerShell -GroupByExtension
 
    Analyze all PowerShell files in Scripts and Modules directories, grouped by extension.
 
    .EXAMPLE
    Get-FolderEncoding -Path . -Extensions @('cs', 'vb') -ShowFiles:$false
 
    Analyze C# and VB.NET files showing only summary statistics, no individual file details.
 
    .EXAMPLE
    Get-FolderEncoding -Path .\Source -FileType Web -ExcludeDirectories @('node_modules', 'dist')
 
    Analyze web files, excluding build directories, with full details and recommendations.
 
    .OUTPUTS
    PSCustomObject with the following properties:
    - Summary: Overall statistics and recommendations
    - EncodingDistribution: Hashtable of encoding counts
    - ExtensionAnalysis: Analysis grouped by file extension (if -GroupByExtension)
    - Files: Individual file details (default: included)
    - Recommendations: Encoding recommendations (default: included)
 
    .NOTES
    Author: PowerShell Encoding Tools
 
    This function provides analysis capabilities to complement Convert-FolderEncoding.
    Use this to understand your current encoding state before performing conversions.
 
    PowerShell Encoding Notes:
    - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility
    - Mixed encodings within a project can cause issues
    - ASCII files are compatible with UTF8 but may need BOM for PowerShell
    #>

    [CmdletBinding(DefaultParameterSetName = 'Extensions')]
    param(
        [Parameter(Mandatory)]
        [Alias('Directory', 'Folder')]
        [string[]] $Path,

        [Parameter(ParameterSetName = 'Extensions', Mandatory)]
        [string[]] $Extensions,

        [Parameter(ParameterSetName = 'FileType', Mandatory)]
        [ValidateSet('PowerShell', 'CSharp', 'Web', 'Scripts', 'Text', 'All')]
        [string] $FileType,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode', 'dist', 'build'),

        [bool] $Recurse = $true,

        [int] $MaxDepth,

        [switch] $GroupByExtension,

        [bool] $ShowFiles = $true,

        [bool] $RecommendTarget = $true
    )

    foreach ($singlePath in $Path) {
        if (-not (Test-Path -LiteralPath $singlePath -PathType Container)) {
            throw "Directory path '$singlePath' not found or is not a directory"
        }
    }

    $fileTypeMappings = @{
        'PowerShell' = @('.ps1', '.psm1', '.psd1', '.ps1xml')
        'CSharp'     = @('.cs', '.csx', '.csproj')
        'Web'        = @('.html', '.htm', '.css', '.js', '.json', '.xml', '.xsl', '.xslt')
        'Scripts'    = @('.ps1', '.py', '.rb', '.sh', '.bash', '.bat', '.cmd')
        'Text'       = @('.txt', '.md', '.log', '.config', '.ini', '.conf', '.yaml', '.yml')
        'All'        = @('.ps1', '.psm1', '.psd1', '.ps1xml', '.cs', '.csx', '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.md', '.py', '.rb', '.sh', '.bat', '.cmd', '.config', '.ini', '.yaml', '.yml')
    }

    $encodingRecommendations = @{
        '.ps1'    = 'UTF8BOM'
        '.psm1'   = 'UTF8BOM'
        '.psd1'   = 'UTF8BOM'
        '.ps1xml' = 'UTF8BOM'
        '.cs'     = 'UTF8'
        '.csx'    = 'UTF8'
        '.html'   = 'UTF8'
        '.htm'    = 'UTF8'
        '.css'    = 'UTF8'
        '.js'     = 'UTF8'
        '.json'   = 'UTF8'
        '.xml'    = 'UTF8'
        '.txt'    = 'UTF8'
        '.md'     = 'UTF8'
        '.py'     = 'UTF8'
        '.rb'     = 'UTF8'
        '.sh'     = 'UTF8'
        '.bat'    = 'UTF8'
        '.cmd'    = 'UTF8'
        '.config' = 'UTF8'
        '.ini'    = 'UTF8'
        '.yaml'   = 'UTF8'
        '.yml'    = 'UTF8'
    }

    if ($PSCmdlet.ParameterSetName -eq 'FileType') {
        $targetExtensions = $fileTypeMappings[$FileType]
        Write-Verbose "Analyzing $FileType file type: $($targetExtensions -join ', ')"
    }
    else {

        $targetExtensions = $Extensions | ForEach-Object {
            if ($_.StartsWith('.')) {
                $_ 
            }
            else {
                ".$_" 
            }
        }
        Write-Verbose "Target extensions: $($targetExtensions -join ', ')"
    }

    if (-not (Get-Command Get-FileEncoding -ErrorAction SilentlyContinue)) {
        throw "Get-FileEncoding function not found. Please ensure the encoding detection module is loaded."
    }

    $allFiles = @()
    $summary = @{
        TotalDirectories     = $Path.Count
        ProcessedDirectories = 0
        TotalFiles           = 0
        StartTime            = Get-Date
    }

    Write-Verbose "Scanning directories for files..."

    foreach ($singlePath in $Path) {
        Write-Verbose "Processing directory: $singlePath"
        $summary.ProcessedDirectories++

        try {
            $gciParams = @{
                LiteralPath = $singlePath
                File        = $true
            }

            if ($Recurse) {
                $gciParams.Recurse = $true
                if ($MaxDepth) {
                    $gciParams.Depth = $MaxDepth 
                }
            }

            $directoryFiles = Get-ChildItem @gciParams | Where-Object {

                $extension = $_.Extension.ToLower()
                $extensionMatch = $targetExtensions -contains $extension

                $directoryExcluded = $false
                if ($ExcludeDirectories -and $_.DirectoryName) {
                    $relativePath = $_.DirectoryName.Replace($singlePath, '').TrimStart('\', '/')
                    $directoryExcluded = $ExcludeDirectories | Where-Object { $relativePath -like "*$_*" }
                }

                return $extensionMatch -and -not $directoryExcluded
            }

            $allFiles += $directoryFiles
            $summary.TotalFiles += $directoryFiles.Count

            Write-Verbose "Found $($directoryFiles.Count) matching files in $singlePath"
        }
        catch {
            Write-Error "Error processing directory '$singlePath': $($_.Exception.Message)"
            continue
        }
    }

    if ($allFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria."
        Write-Verbose "Extensions searched: $($targetExtensions -join ', ')"
        Write-Verbose "Paths searched: $($Path -join ', ')"
        return
    }

    Write-Verbose "Analyzing $($allFiles.Count) files across $($summary.ProcessedDirectories) directories"

    $encodingDistribution = @{}
    $extensionAnalysis = @{}
    $fileDetails = @()
    $inconsistentExtensions = @()

    $progressCounter = 0
    foreach ($file in $allFiles) {
        $progressCounter++
        $percentComplete = [math]::Round(($progressCounter / $allFiles.Count) * 100, 1)

        Write-Progress -Activity "Analyzing file encodings" -Status "Processing $($file.Name) ($progressCounter of $($allFiles.Count))" -PercentComplete $percentComplete

        try {
            $encoding = Get-FileEncoding -Path $file.FullName
            $extension = $file.Extension.ToLower()

            if ($encodingDistribution.ContainsKey($encoding)) {
                $encodingDistribution[$encoding]++
            }
            else {
                $encodingDistribution[$encoding] = 1
            }

            if (-not $extensionAnalysis.ContainsKey($extension)) {
                $extensionAnalysis[$extension] = @{}
            }
            if ($extensionAnalysis[$extension].ContainsKey($encoding)) {
                $extensionAnalysis[$extension][$encoding]++
            }
            else {
                $extensionAnalysis[$extension][$encoding] = 1
            }

            if ($ShowFiles) {
                $recommendedEncoding = if ($RecommendTarget -and $encodingRecommendations.ContainsKey($extension)) {
                    $encodingRecommendations[$extension]
                }
                else {
                    $null
                }

                $fileDetails += [PSCustomObject]@{
                    FullPath            = $file.FullName
                    RelativePath        = $file.FullName.Replace((Get-Location).Path, '.').TrimStart('\', '/')
                    Extension           = $extension
                    CurrentEncoding     = $encoding
                    RecommendedEncoding = $recommendedEncoding
                    NeedsConversion     = $recommendedEncoding -and ($encoding -ne $recommendedEncoding)
                    Size                = $file.Length
                    LastModified        = $file.LastWriteTime
                }
            }
        }
        catch {
            Write-Warning "Could not analyze encoding for '$($file.FullName)': $($_.Exception.Message)"
            continue
        }
    }

    Write-Progress -Activity "Analyzing file encodings" -Completed

    foreach ($ext in $extensionAnalysis.Keys) {
        if ($extensionAnalysis[$ext].Keys.Count -gt 1) {
            $inconsistentExtensions += $ext
        }
    }

    $recommendations = @()
    if ($RecommendTarget) {
        foreach ($ext in $targetExtensions) {
            $recommendedEncoding = $encodingRecommendations[$ext]
            if ($recommendedEncoding -and $extensionAnalysis.ContainsKey($ext)) {
                $currentEncodings = $extensionAnalysis[$ext]
                $needsConversion = $currentEncodings.Keys | Where-Object { $_ -ne $recommendedEncoding }

                if ($needsConversion) {
                    $totalFiles = ($currentEncodings.Values | Measure-Object -Sum).Sum
                    $nonCompliantFiles = $needsConversion | ForEach-Object { $currentEncodings[$_] } | Measure-Object -Sum | Select-Object -ExpandProperty Sum

                    $recommendations += [PSCustomObject]@{
                        Extension           = $ext
                        RecommendedEncoding = $recommendedEncoding
                        TotalFiles          = $totalFiles
                        CompliantFiles      = $totalFiles - $nonCompliantFiles
                        NonCompliantFiles   = $nonCompliantFiles
                        CurrentEncodings    = $currentEncodings
                    }
                }
            }
        }
    }

    $summary.EndTime = Get-Date
    $summary.Duration = $summary.EndTime - $summary.StartTime
    $summary.UniqueEncodings = $encodingDistribution.Keys.Count
    $summary.MostCommonEncoding = ($encodingDistribution.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key
    $summary.HasMixedEncodings = $inconsistentExtensions.Count -gt 0
    $summary.InconsistentExtensions = $inconsistentExtensions

    Write-Verbose "Analysis completed: $($summary.TotalFiles) files, $($summary.UniqueEncodings) unique encodings"
    Write-Verbose "Most common encoding: $($summary.MostCommonEncoding)"
    if ($summary.HasMixedEncodings) {
        Write-Verbose "Extensions with mixed encodings: $($inconsistentExtensions -join ', ')"
    }

    $result = [PSCustomObject]@{
        Summary              = [PSCustomObject]@{
            TotalDirectories       = $summary.TotalDirectories
            ProcessedDirectories   = $summary.ProcessedDirectories
            TotalFiles             = $summary.TotalFiles
            UniqueEncodings        = $summary.UniqueEncodings
            MostCommonEncoding     = $summary.MostCommonEncoding
            HasMixedEncodings      = $summary.HasMixedEncodings
            InconsistentExtensions = $summary.InconsistentExtensions
            Duration               = $summary.Duration
            StartTime              = $summary.StartTime
            EndTime                = $summary.EndTime
        }
        EncodingDistribution = $encodingDistribution
        Files                = if ($ShowFiles) {
            $fileDetails 
        }
        else {
            $null 
        }
        Recommendations      = if ($RecommendTarget) {
            $recommendations 
        }
        else {
            $null 
        }
    }

    if ($GroupByExtension) {
        $result | Add-Member -NotePropertyName 'ExtensionAnalysis' -NotePropertyValue $extensionAnalysis
    }

    $result | Add-Member -MemberType ScriptMethod -Name 'DisplaySummary' -Value {
        Write-Host ""
        Write-Host "📊 Folder Encoding Analysis Summary" -ForegroundColor Green
        Write-Host "=================================="
        Write-Host "📁 Directories analyzed: $($this.Summary.ProcessedDirectories)" -ForegroundColor Cyan
        Write-Host "📄 Total files found: $($this.Summary.TotalFiles)" -ForegroundColor Cyan
        Write-Host "🔤 Unique encodings: $($this.Summary.UniqueEncodings)" -ForegroundColor Cyan
        Write-Host "⭐ Most common encoding: $($this.Summary.MostCommonEncoding)" -ForegroundColor Green
        Write-Host "⚠️ Mixed encodings: $($this.Summary.HasMixedEncodings)" -ForegroundColor $(if ($this.Summary.HasMixedEncodings) {
                'Yellow' 
            }
            else {
                'Green' 
            })

        if ($this.Summary.HasMixedEncodings) {
            Write-Host "📝 Inconsistent extensions: $($this.Summary.InconsistentExtensions -join ', ')" -ForegroundColor Yellow
        }

        Write-Host ""
        Write-Host "📈 Encoding Distribution:" -ForegroundColor Blue
        $this.EncodingDistribution.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object {
            $percentage = [math]::Round(($_.Value / $this.Summary.TotalFiles) * 100, 1)
            Write-Host " $($_.Key): $($_.Value) files ($percentage%)" -ForegroundColor White
        }

        if ($this.Recommendations -and $this.Recommendations.Count -gt 0) {
            Write-Host ""
            Write-Host "💡 Recommendations:" -ForegroundColor Magenta
            foreach ($rec in $this.Recommendations) {
                Write-Host " $($rec.Extension) files:" -ForegroundColor White
                Write-Host " Target encoding: $($rec.RecommendedEncoding)" -ForegroundColor Green
                Write-Host " Files needing conversion: $($rec.NonCompliantFiles)/$($rec.TotalFiles)" -ForegroundColor Yellow
            }
        }

        Write-Host ""
        Write-Host "⏱️ Analysis duration: $($this.Summary.Duration.TotalSeconds.ToString('F2')) seconds" -ForegroundColor Gray
    }

    $result | Add-Member -MemberType ScriptMethod -Name 'ToString' -Value {
        return "Folder Encoding Analysis: $($this.Summary.TotalFiles) files, $($this.Summary.UniqueEncodings) encodings, Most common: $($this.Summary.MostCommonEncoding)"
    } -Force

    return $result
}

function Get-FunctionAliases {
    [cmdletbinding()]
    param (
        [Alias('PSPath', 'FullName')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)][string[]]$Path,
        [string] $Content,
        [switch] $RecurseFunctionNames,
        [switch] $AsHashtable
    )
    process {
        if ($Content) {
            $ProcessData = $Content
            $Code = $true
        }
        else {
            $ProcessData = $Path
            $Code = $false
        }
        foreach ($File in $ProcessData) {
            $Ast = $null
            if ($Code) {
                $FileAst = [System.Management.Automation.Language.Parser]::ParseInput($File, [ref]$null, [ref]$null)
            }
            else {
                $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($File , [ref]$null, [ref]$null)
            }

            $ListOfFuncionsAst = $FileAst.FindAll( {
                    param ($ast)
                    $ast -is [System.Management.Automation.Language.FunctionDefinitionAst]
                }, $RecurseFunctionNames
            )

            if ($AsHashtable) {

                $OutputList = [ordered] @{}
                foreach ($Function in $ListOfFuncionsAst) {
                    $AliasDefinitions = $Function.FindAll( {
                            param ( $ast )
                            $ast -is [System.Management.Automation.Language.AttributeAst] -and
                            $ast.TypeName.Name -eq 'Alias' -and
                            $ast.Parent -is [System.Management.Automation.Language.ParamBlockAst]
                        }, $true)

                    $AliasTarget = @(
                        $AliasDefinitions.PositionalArguments.Value
                        foreach ($_ in  $AliasDefinitions.Parent.CommandElements) {
                            if ($_.StringConstantType -eq 'BareWord' -and $null -ne $_.Value -and $_.Value -notin ('New-Alias', 'Set-Alias', $Function.Name)) {
                                $_.Value
                            }
                        }
                    )
                    $OutputList[$Function.Name] = $AliasTarget
                }
                $OutputList
            }
            else {

                $Ast = $Null
                $AliasDefinitions = $FileAst.FindAll( {
                        param ( $ast )
                        $ast -is [System.Management.Automation.Language.AttributeAst] -and
                        $ast.TypeName.Name -eq 'Alias' -and
                        $ast.Parent -is [System.Management.Automation.Language.ParamBlockAst]
                    }, $true)

                $AliasTarget = @(
                    $AliasDefinitions.PositionalArguments.Value
                    foreach ($_ in  $AliasDefinitions.Parent.CommandElements) {
                        if ($_.StringConstantType -eq 'BareWord' -and $null -ne $_.Value -and $_.Value -notin ('New-Alias', 'Set-Alias', $FunctionName)) {
                            $_.Value
                        }
                    }
                )
                [PsCustomObject]@{
                    Name  = $ListOfFuncionsAst.Name
                    Alias = $AliasTarget
                }
            }
        }
    }
}
function Get-FunctionAliasesFromFolder {
    [cmdletbinding()]
    param(
        [string] $FullProjectPath,
        [string[]] $Folder,
        [Array] $Files,
        [string] $FunctionsToExport,
        [string] $AliasesToExport
    )
    [Array] $FilesPS1 = foreach ($File in $Files) {
        if ($FunctionsToExport) {
            $PathFunctions = [io.path]::Combine($FullProjectPath, $FunctionsToExport, '*')
            if ($File.FullName -like $PathFunctions) {
                if ($File.Extension -eq '.ps1' -or $File.Extension -eq '*.psm1') {
                    $File
                }
            }
        }
        if ($AliasesToExport -and $AliasesToExport -ne $FunctionsToExport) {
            $PathAliases = [io.path]::Combine($FullProjectPath, $AliasesToExport, '*')
            if ($File.FullName -like $PathAliases) {
                if ($File.Extension -eq '.ps1' -or $File.Extension -eq '*.psm1') {
                    $File
                }
            }
        }
    }
    [Array] $Content = foreach ($File in $FilesPS1 | Sort-Object -Unique) {
        ''
        Get-Content -LiteralPath $File.FullName -Raw -Encoding UTF8
    }
    $Code = $Content -join [System.Environment]::NewLine

    $OutputAliasesToExport = Get-FunctionAliases -Content $Code -AsHashtable
    $OutputAliasesToExport
}
function Get-FunctionNames {
    [cmdletbinding()]
    param(
        [string] $Path,
        [switch] $Recurse
    )
    if ($Path -ne '' -and (Test-Path -LiteralPath $Path)) {
        $FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
        [System.Management.Automation.Language.Parser]::ParseFile(($FilePath),
            [ref]$null,
            [ref]$null).FindAll(
            { param($c)$c -is [Management.Automation.Language.FunctionDefinitionAst] }, $Recurse).Name
    }
}
function Get-GitLog {
    # Source https://gist.github.com/thedavecarroll/3245449f5ff893e51907f7aa13e33ebe
    # Author: thedavecarroll/Get-GitLog.ps1
    [CmdLetBinding(DefaultParameterSetName = 'Default')]
    param (

        [Parameter(ParameterSetName = 'Default', Mandatory)]
        [Parameter(ParameterSetName = 'SourceTarget', Mandatory)]
        [ValidateScript({ Resolve-Path -Path $_ | Test-Path })]
        [string]$GitFolder,

        [Parameter(ParameterSetName = 'SourceTarget', Mandatory)]
        [string]$StartCommitId,
        [Parameter(ParameterSetName = 'SourceTarget')]
        [string]$EndCommitId = 'HEAD'
    )

    Push-Location
    try {
        Set-Location -Path $GitFolder
        $GitCommand = Get-Command -Name git -ErrorAction Stop
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    if ($StartCommitId) {
        $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" {1}...{2} 2>&1' -f $GitCommand.Source, $StartCommitId, $EndCommitId
    }
    else {
        $GitLogCommand = '"{0}" log --oneline --format="%H`t%h`t%ai`t%an`t%ae`t%ci`t%cn`t%ce`t%s`t%f" 2>&1' -f $GitCommand.Source
    }

    Write-Verbose -Message $GitLogCommand
    $GitLog = Invoke-Expression -Command "& $GitLogCommand" -ErrorAction SilentlyContinue
    Pop-Location

    if ($GitLog[0] -notmatch 'fatal:') {
        $GitLog | ConvertFrom-Csv -Delimiter "`t" -Header 'CommitId', 'ShortCommitId', 'AuthorDate', 'AuthorName', 'AuthorEmail', 'CommitterDate', 'CommitterName', 'ComitterEmail', 'CommitMessage', 'SafeCommitMessage'
    }
    else {
        if ($GitLog[0] -like "fatal: ambiguous argument '*...*'*") {
            Write-Warning -Message 'Unknown revision. Please check the values for StartCommitId or EndCommitId; omit the parameters to retrieve the entire log.'
        }
        else {
            Write-Error -Category InvalidArgument -Message ($GitLog -join "`n")
        }
    }
}
function Get-LineEndingType {
    param([string] $FilePath)

    try {
        $bytes = [System.IO.File]::ReadAllBytes($FilePath)
        if ($bytes.Length -eq 0) {
            return @{
                LineEnding      = 'None'
                HasFinalNewline = $true  
                FileSize        = 0
            }
        }

        $crlfCount = 0
        $lfOnlyCount = 0
        $crOnlyCount = 0
        $hasFinalNewline = $false

        $lastByte = $bytes[$bytes.Length - 1]
        if ($lastByte -eq 10) {

            $hasFinalNewline = $true
            if ($bytes.Length -gt 1 -and $bytes[$bytes.Length - 2] -eq 13) {
            }
        }
        elseif ($lastByte -eq 13) {

            $hasFinalNewline = $true
        }

        for ($i = 0; $i -lt $bytes.Length - 1; $i++) {
            if ($bytes[$i] -eq 13 -and $bytes[$i + 1] -eq 10) {

                $crlfCount++
                $i++ 
            }
            elseif ($bytes[$i] -eq 10) {

                $lfOnlyCount++
            }
            elseif ($bytes[$i] -eq 13) {

                if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -ne 10) {
                    $crOnlyCount++
                }
            }
        }

        if ($bytes.Length -gt 0) {
            $lastByte = $bytes[$bytes.Length - 1]
            if ($lastByte -eq 10 -and ($bytes.Length -eq 1 -or $bytes[$bytes.Length - 2] -ne 13)) {
                $lfOnlyCount++
            }
            elseif ($lastByte -eq 13) {
                $crOnlyCount++
            }
        }

        $typesFound = @()
        if ($crlfCount -gt 0) {
            $typesFound += 'CRLF' 
        }
        if ($lfOnlyCount -gt 0) {
            $typesFound += 'LF' 
        }
        if ($crOnlyCount -gt 0) {
            $typesFound += 'CR' 
        }

        $lineEndingType = if ($typesFound.Count -eq 0) {
            'None'
        }
        elseif ($typesFound.Count -eq 1) {
            $typesFound[0]
        }
        else {
            'Mixed'
        }

        return @{
            LineEnding      = $lineEndingType
            HasFinalNewline = $hasFinalNewline
            FileSize        = $bytes.Length
        }
    }
    catch {
        return @{
            LineEnding      = 'Error'
            HasFinalNewline = $false
            FileSize        = 0
        }
    }
}
function Get-RelativePath {
    <#
    .SYNOPSIS
        Gets the relative path from one path to another, compatible with PowerShell 5.1.
 
    .DESCRIPTION
        Provides PowerShell 5.1 compatible relative path calculation that works like
        [System.IO.Path]::GetRelativePath() which is only available in .NET Core 2.0+.
 
    .PARAMETER From
        The base path to calculate the relative path from.
 
    .PARAMETER To
        The target path to calculate the relative path to.
 
    .EXAMPLE
        Get-RelativePath -From 'C:\Projects' -To 'C:\Projects\MyProject\file.txt'
        Returns: MyProject\file.txt
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $From,

        [Parameter(Mandatory)]
        [string] $To
    )

    if ([System.IO.Path].GetMethods() | Where-Object { $_.Name -eq 'GetRelativePath' -and $_.IsStatic }) {
        return [System.IO.Path]::GetRelativePath($From, $To)
    }

    try {

        $fromPath = [System.IO.Path]::GetFullPath($From)
        if (-not $fromPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
            $fromPath += [System.IO.Path]::DirectorySeparatorChar
        }
        $fromUri = New-Object System.Uri $fromPath
        $toUri = New-Object System.Uri ([System.IO.Path]::GetFullPath($To))

        $relativeUri = $fromUri.MakeRelativeUri($toUri)
        $relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())

        if ([System.IO.Path]::DirectorySeparatorChar -eq '\') {
            $relativePath = $relativePath.Replace('/', '\')
        }

        return $relativePath
    }
    catch {

        return [System.IO.Path]::GetFileName($To)
    }
}

function Get-RequiredModule {
    [cmdletbinding()]
    param(
        [string] $Path,
        [string] $Name
    )
    $LiteralPath = [System.IO.Path]::Combine($Path, $Name)
    $PrimaryModule = Get-ChildItem -LiteralPath $LiteralPath -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1
    if ($PrimaryModule) {
        $Module = Get-Module -ListAvailable $PrimaryModule.FullName -ErrorAction SilentlyContinue -Verbose:$false
        if ($Module) {
            [Array] $RequiredModules = $Module.RequiredModules.Name
            if ($null -ne $RequiredModules) {
                $null
            }
            $RequiredModules
            foreach ($_ in $RequiredModules) {
                Get-RequiredModule -Path $Path -Name $_
            }
        }
    }
    else {
        Write-Warning "Initialize-ModulePortable - Modules to load not found in $Path"
    }
}
function Get-RestMethodExceptionDetailsOrNull {
    [CmdletBinding()]
    param(
        [Exception] $restMethodException
    )
    try {
        $responseDetails = @{
            ResponseUri       = $exception.Response.ResponseUri
            StatusCode        = $exception.Response.StatusCode
            StatusDescription = $exception.Response.StatusDescription
            ErrorMessage      = $exception.Message
        }
        [string] $responseDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $responseDetails

        [string] $errorInfo = "Request Details:" + $NewLine + $requestDetailsAsNicelyFormattedString
        $errorInfo += $NewLine
        $errorInfo += "Response Details:" + $NewLine + $responseDetailsAsNicelyFormattedString
        return $errorInfo
    }
    catch {
        return $null
    }
}

function Get-ScriptCommands {
    [CmdletBinding()]
    param(
        [string] $FilePath,
        [alias('ScriptBlock')][scriptblock] $Code,
        [switch] $CommandsOnly
    )
    $astTokens = $null
    $astErr = $null

    if ($FilePath) {
        $FileAst = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$astTokens, [ref]$astErr)
    }
    else {
        $FileAst = [System.Management.Automation.Language.Parser]::ParseInput($Code, [ref]$astTokens, [ref]$astErr)
    }

    $Commands = Get-AstTokens -ASTTokens $astTokens -Commands $Commands -FileAst $FileAst

    $Commands | Sort-Object -Unique
}
function Get-ScriptsContentAndTryReplace {
    <#
    .SYNOPSIS
    Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\
 
    .DESCRIPTION
    Gets script content and replaces $PSScriptRoot\..\..\ with $PSScriptRoot\
 
    .PARAMETER Files
    Parameter description
 
    .PARAMETER OutputPath
    Parameter description
 
    .EXAMPLE
    Get-ScriptsContentAndTryReplace -Files 'C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos.ps1' -OutputPath "C:\Support\GitHub\PSWriteHTML\Private\Get-HTMLLogos1.ps1"
 
    .NOTES
    Often in code people would use relative paths to get to the root of the module.
    This is all great but the path changes during merge.
    So we fix this by replacing $PSScriptRoot\..\..\ with $PSScriptRoot\
    While in best case they should always use $MyInvocation.MyCommand.Module.ModuleBase
    It's not always possible. So this is a workaround.
    Very bad workaround, but it works, but may have unintended consequences.
 
    $Content = @(
        '$PSScriptRoot\..\..\Build\Manage-PSWriteHTML.ps1'
        '$PSScriptRoot\..\Build\Manage-PSWriteHTML.ps1'
        '$PSScriptRoot\Build\Manage-PSWriteHTML.ps1'
        "[IO.Path]::Combine(`$PSScriptRoot, '..', 'Images')"
        "[IO.Path]::Combine(`$PSScriptRoot,'..','Images')"
    )
    $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\' -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\'
    $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,' -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,'
    $Content
 
    #>

    [cmdletbinding()]
    param(
        [string[]] $Files,
        [string] $OutputPath,
        [switch] $DoNotAttemptToFixRelativePaths
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($DoNotAttemptToFixRelativePaths) {
        Write-TextWithTime -Text "Without expanding variables (`$PSScriptRoot\..\.. etc.)" {
            foreach ($FilePath in $Files) {
                $Content = Get-Content -Path $FilePath -Raw -Encoding utf8
                if ($Content.Count -gt 0) {
                    try {
                        $Content | Out-File -Append -LiteralPath $OutputPath -Encoding $Encoding
                    }
                    catch {
                        $ErrorMessage = $_.Exception.Message
                        Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red
                        return $false
                    }
                }
            }
        } -PreAppend Plus -Color Green -SpacesBefore " " -ColorTime Green
    }
    else {
        Write-TextWithTime -Text "Replacing expandable variables (`$PSScriptRoot\..\.. etc.)" {
            foreach ($FilePath in $Files) {
                $Content = Get-Content -Path $FilePath -Raw -Encoding utf8
                if ($Content.Count -gt 0) {

                    if (-not $FilePath.EndsWith('Get-ScriptsContentAndTryReplace.ps1')) {
                        $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\..\'), '$PSScriptRoot\'
                        $Content = $Content -replace [regex]::Escape('$PSScriptRoot\..\'), '$PSScriptRoot\'
                        $Content = $Content -replace [regex]::Escape("`$PSScriptRoot, '..',"), '$PSScriptRoot,'
                        $Content = $Content -replace [regex]::Escape("`$PSScriptRoot,'..',"), '$PSScriptRoot,'
                    }
                }
                try {
                    $Content | Out-File -Append -LiteralPath $OutputPath -Encoding $Encoding
                }
                catch {
                    $ErrorMessage = $_.Exception.Message
                    Write-Text "[-] Get-ScriptsContentAndTryReplace - Merge on file $FilePath failed. Error: $ErrorMessage" -Color Red
                    return $false
                }
            }
        } -PreAppend Information -Color Magenta -SpacesBefore " " -ColorBefore Magenta -ColorTime Magenta
    }
}

function Import-ValidCertificate {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    param(
        [parameter(Mandatory, ParameterSetName = 'FilePath')][string] $FilePath,
        [parameter(Mandatory, ParameterSetName = 'Base64')][string] $CertificateAsBase64,
        [parameter(Mandatory)][string] $PfxPassword
    )
    if ($FilePath -and (Test-Path -LiteralPath $FilePath)) {
        $TemporaryFile = $FilePath
    }
    elseif ($CertificateAsBase64) {
        $TemporaryFile = [io.path]::GetTempFileName()
        if ($PSVersionTable.PSEdition -eq 'Core') {
            Set-Content -AsByteStream -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -ErrorAction Stop
        }
        else {
            Set-Content -Value $([System.Convert]::FromBase64String($CertificateAsBase64)) -Path $TemporaryFile -Encoding Byte -ErrorAction Stop
        }
    }
    else {
        return $false
    }
}
function Initialize-InternalTests {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $Type
    )

    if ($Configuration.Options.$Type.TestsPath -and (Test-Path -LiteralPath $Configuration.Options.$Type.TestsPath)) {
        Write-TextWithTime -PreAppend Plus -Text "Running tests ($Type)" {
            $TestsResult = Invoke-Pester -Script $Configuration.Options.$Type.TestsPath -Verbose -PassThru
            Write-Host
            if (-not $TestsResult) {
                if ($Configuration.Options.$Type.Force) {
                    Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red
                }
                else {
                    Write-Text "[e] Tests ($Type) failed. Terminating." -Color Red
                    return $false
                }
            }
            elseif ($TestsResult.FailedCount -gt 0) {
                if ($Configuration.Options.$Type.Force) {
                    Write-Text "[e] Tests ($Type) failed, but Force was used to skip failed tests. Continuing" -Color Red
                }
                else {
                    Write-Text "[e] Tests ($Type) failed (failedCount $($TestsResult.FailedCount)). Terminating." -Color Red
                    return $false
                }
            }
        } -Color Blue
    }
    else {
        Write-Text "[e] Tests ($Type) are enabled, but the path to tests doesn't exits. Terminating." -Color Red
        return $false
    }
}
function Initialize-ReplacePath {
    [CmdletBinding()]
    param(
        [string] $ReplacementPath,
        [string] $ModuleName,
        [string] $ModuleVersion,
        [System.Collections.IDictionary] $Configuration
    )

    $TagName = "v$($ModuleVersion)"
    if ($Configuration.CurrentSettings.PreRelease) {
        $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
        $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)"
    }
    else {
        $ModuleVersionWithPreRelease = $ModuleVersion
        $TagModuleVersionWithPreRelease = "v$($ModuleVersion)"
    }

    $ReplacementPath = $ReplacementPath.Replace('<TagName>', $TagName)
    $ReplacementPath = $ReplacementPath.Replace('{TagName}', $TagName)
    $ReplacementPath = $ReplacementPath.Replace('<ModuleVersion>', $ModuleVersion)
    $ReplacementPath = $ReplacementPath.Replace('{ModuleVersion}', $ModuleVersion)
    $ReplacementPath = $ReplacementPath.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease)
    $ReplacementPath = $ReplacementPath.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease)
    $ReplacementPath = $ReplacementPath.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease)
    $ReplacementPath = $ReplacementPath.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease)
    $ReplacementPath = $ReplacementPath.Replace('<ModuleName>', $ModuleName)
    $ReplacementPath = $ReplacementPath.Replace('{ModuleName}', $ModuleName)
    $ReplacementPath
}
function Invoke-RestMethodAndThrowDescriptiveErrorOnFailure {
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $requestParametersHashTable
    )
    $requestDetailsAsNicelyFormattedString = Convert-HashTableToNicelyFormattedString $requestParametersHashTable
    Write-Verbose "Making web request with the following parameters:$NewLine$requestDetailsAsNicelyFormattedString"

    try {
        $webRequestResult = Invoke-RestMethod @requestParametersHashTable
    }
    catch {
        [Exception] $exception = $_.Exception

        [string] $errorMessage = Get-RestMethodExceptionDetailsOrNull -restMethodException $exception
        if ([string]::IsNullOrWhiteSpace($errorMessage)) {
            $errorMessage = $exception.ToString()
        }

        throw "An unexpected error occurred while making web request:$NewLine$errorMessage"
    }

    Write-Verbose "Web request returned the following result:$NewLine$webRequestResult"
    return $webRequestResult
}

function Merge-Module {
    [CmdletBinding()]
    param (
        [string] $ModuleName,
        [string] $ModulePathSource,
        [string] $ModulePathTarget,
        [Parameter(Mandatory = $false, ValueFromPipeline = $false)]
        [ValidateSet("ASC", "DESC", "NONE", '')]
        [string] $Sort = 'NONE',
        [string[]] $FunctionsToExport,
        [string[]] $AliasesToExport,
        [System.Collections.IDictionary] $AliasesAndFunctions,
        [System.Collections.IDictionary] $CmdletsAliases,
        [Array] $LibrariesStandard,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        [System.Collections.IDictionary] $FormatCodePSM1,
        [System.Collections.IDictionary] $FormatCodePSD1,
        [System.Collections.IDictionary] $Configuration,
        [string[]] $DirectoriesWithPS1,
        [string[]] $ClassesPS1,
        [System.Collections.IDictionary] $IncludeAsArray

    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew()
    Write-Text "[+] Merging files into PSM1" -Color Blue

    $PSM1FilePath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.psm1")
    $PSD1FilePath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.psd1")

    [Array] $ArrayIncludes = foreach ($VariableName in $IncludeAsArray.Keys) {
        $FilePathVariables = [System.IO.Path]::Combine($ModulePathSource, $IncludeAsArray[$VariableName], "*.ps1")

        [Array] $FilesInternal = if ($PSEdition -eq 'Core') {
            Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse -FollowSymlink
        }
        else {
            Get-ChildItem -Path $FilePathVariables -ErrorAction SilentlyContinue -Recurse
        }
        "$VariableName = @("
        foreach ($Internal in $FilesInternal) {
            Get-Content -Path $Internal.FullName -Raw -Encoding utf8
        }
        ")"
    }

    if ($Configuration.Steps.BuildModule.ClassesDotSource) {
        [Array] $ListDirectoriesPS1 = foreach ($Dir in $DirectoriesWithPS1) {
            if ($Dir -ne $ClassesPS1) {
                $Dir
            }
        }
    }
    else {
        [Array] $ListDirectoriesPS1 = $DirectoriesWithPS1
    }

    [Array] $ScriptFunctions = foreach ($Directory in $ListDirectoriesPS1) {
        $PathToFiles = [System.IO.Path]::Combine($ModulePathSource, $Directory, "*.ps1")
        if ($PSEdition -eq 'Core') {
            Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse -FollowSymlink
        }
        else {
            Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse
        }
    }
    [Array] $ClassesFunctions = foreach ($Directory in $ClassesPS1) {
        $PathToFiles = [System.IO.Path]::Combine($ModulePathSource, $Directory, "*.ps1")
        if ($PSEdition -eq 'Core') {
            Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse -FollowSymlink
        }
        else {
            Get-ChildItem -Path $PathToFiles -ErrorAction SilentlyContinue -Recurse
        }
    }
    if ($Sort -eq 'ASC') {
        $ScriptFunctions = $ScriptFunctions | Sort-Object -Property Name
        $ClassesFunctions = $ClassesFunctions | Sort-Object -Property Name
    }
    elseif ($Sort -eq 'DESC') {
        $ScriptFunctions = $ScriptFunctions | Sort-Object -Descending -Property Name
        $ClassesFunctions = $ClassesFunctions | Sort-Object -Descending -Property Name
    }

    if ($ArrayIncludes.Count -gt 0) {
        $ArrayIncludes | Out-File -Append -LiteralPath $PSM1FilePath -Encoding $Encoding
    }
    $Success = Get-ScriptsContentAndTryReplace -Files $ScriptFunctions -OutputPath $PSM1FilePath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths
    if ($Success -eq $false) {
        return $false
    }

    $FilePathUsing = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Usings.ps1")

    $UsingInPlace = Format-UsingNamespace -FilePath $PSM1FilePath -FilePathUsing $FilePathUsing
    if ($UsingInPlace) {
        $Success = Format-Code -FilePath $FilePathUsing -FormatCode $FormatCodePSM1
        if ($Success -eq $false) {
            return $false
        }
        $Configuration.UsingInPlace = "$ModuleName.Usings.ps1"
    }

    $TimeToExecute.Stop()
    Write-Text "[+] Merging files into PSM1 [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue

    $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew()
    Write-Text "[+] Detecting required modules" -Color Blue

    $RequiredModules = @(
        if ($Configuration.Information.Manifest.RequiredModules.Count -gt 0) {
            if ($Configuration.Information.Manifest.RequiredModules[0] -is [System.Collections.IDictionary]) {
                $Configuration.Information.Manifest.RequiredModules.ModuleName
            }
            else {
                $Configuration.Information.Manifest.RequiredModules
            }
        }
        if ($Configuration.Information.Manifest.ExternalModuleDependencies.Count -gt 0) {
            $Configuration.Information.Manifest.ExternalModuleDependencies
        }
    )

    [Array] $DuplicateModules = $RequiredModules | Group-Object | Where-Object { $_.Count -gt 1 } | Select-Object -ExpandProperty Name
    if ($DuplicateModules.Count -gt 0) {
        Write-Text " [!] Duplicate modules detected in required modules configuration. Please fix your configuration." -Color Red
        foreach ($DuplicateModule in $DuplicateModules) {
            Write-Text " [>] Duplicate module $DuplicateModule" -Color Red
        }
        return $false
    }

    [Array] $ApprovedModules = $Configuration.Options.Merge.Integrate.ApprovedModules | Sort-Object -Unique

    $ModulesThatWillMissBecauseOfIntegrating = [System.Collections.Generic.List[string]]::new()
    [Array] $DependantRequiredModules = foreach ($Module in $RequiredModules) {
        [Array] $TemporaryDependant = Find-RequiredModules -Name $Module
        if ($TemporaryDependant.Count -gt 0) {
            if ($Module -in $ApprovedModules) {

                foreach ($ModulesTemp in $TemporaryDependant) {
                    $ModulesThatWillMissBecauseOfIntegrating.Add($ModulesTemp)
                }
            }
            else {
                $TemporaryDependant
            }
        }
    }
    $DependantRequiredModules = $DependantRequiredModules | Sort-Object -Unique

    $TimeToExecute.Stop()
    Write-Text "[+] Detecting required modules [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue

    $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew()
    Write-Text "[+] Searching for missing functions" -Color Blue

    $MissingFunctions = Get-MissingFunctions -FilePath $PSM1FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules

    $TimeToExecute.Stop()
    Write-Text "[+] Searching for missing functions [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue

    $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew()
    Write-Text "[+] Detecting commands used" -Color Blue

    [Array] $ApplicationsCheck = $MissingFunctions.Summary | Where-Object { $_.CommandType -eq 'Application' } | Sort-Object -Unique -Property 'Source'
    [Array] $ModulesToCheck = $MissingFunctions.Summary | Where-Object { $_.CommandType -ne 'Application' -and $_.CommandType -ne '' } | Sort-Object -Unique -Property 'Source'
    [Array] $CommandsWithoutModule = $MissingFunctions.Summary | Where-Object { $_.CommandType -eq '' } 

    if ($ApplicationsCheck.Source) {
        Write-Text "[i] Applications used by this module. Make sure those are present on destination system. " -Color Yellow
        foreach ($Application in $ApplicationsCheck.Source) {
            Write-Text " [>] Application $Application " -Color Yellow
        }
    }
    $TimeToExecute.Stop()
    Write-Text "[+] Detecting commands used [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue

    $approveRequiredModulesSplat = @{
        ApprovedModules          = $ApprovedModules
        ModulesToCheck           = $ModulesToCheck
        RequiredModules          = $RequiredModules
        DependantRequiredModules = $DependantRequiredModules
        MissingFunctions         = $MissingFunctions
        Configuration            = $Configuration
        CommandsWithoutModule    = $CommandsWithoutModule
    }

    $Success = Approve-RequiredModules @approveRequiredModulesSplat
    if ($Success -eq $false) {
        return $false
    }

    if ($Configuration.Steps.BuildModule.MergeMissing -eq $true) {
        if (Test-Path -LiteralPath $PSM1FilePath) {
            $TimeToExecute = [System.Diagnostics.Stopwatch]::StartNew()
            Write-Text "[+] Merge mergable commands" -Color Blue

            $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding $Encoding
            $IntegrateContent = @(
                $MissingFunctions.Functions
                $PSM1Content
            )
            $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding $Encoding

            $NewRequiredModules = foreach ($_ in $Configuration.Information.Manifest.RequiredModules) {
                if ($_ -is [System.Collections.IDictionary]) {
                    if ($_.ModuleName -notin $ApprovedModules) {
                        $_
                    }
                }
                else {
                    if ($_ -notin $ApprovedModules) {
                        $_
                    }
                }
            }
            $Configuration.Information.Manifest.RequiredModules = $NewRequiredModules
            $TimeToExecute.Stop()
            Write-Text "[+] Merge mergable commands [Time: $($($TimeToExecute.Elapsed).Tostring())]" -Color Blue
        }
    }

    $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew()
    Write-Text "[+] Finalizing PSM1/PSD1" -Color Blue

    if ($null -eq $Configuration.Steps.BuildModule.DebugDLL) {
        $Configuration.Steps.BuildModule.DebugDLL = $false
    }

    if ($Configuration.Steps.BuildLibraries.NETLineByLineAddType) {
        $DoNotOptimizeLoading = $Configuration.Steps.BuildLibraries.NETLineByLineAddType
    }
    else {
        $DoNotOptimizeLoading = $false
    }

    [Array] $LibraryContent = New-LibraryContent -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault -OptimizedLoading:(-not $DoNotOptimizeLoading)

    if ($LibraryContent.Count -gt 0) {
        if ($Configuration.Steps.BuildModule.LibrarySeparateFile -eq $true) {
            $LibariesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Libraries.ps1")
            $ScriptsToProcessLibrary = "$ModuleName.Libraries.ps1"
        }
        if ($Configuration.Steps.BuildModule.LibraryDotSource -eq $true) {
            $LibariesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Libraries.ps1")
            $DotSourcePath = ". `$PSScriptRoot\$ModuleName.Libraries.ps1"
        }
        if ($LibariesPath) {
            $LibraryContent | Out-File -Append -LiteralPath $LibariesPath -Encoding $Encoding
        }
    }

    if ($ClassesFunctions.Count -gt 0) {
        $ClassesPath = [System.IO.Path]::Combine($ModulePathTarget, "$ModuleName.Classes.ps1")
        $DotSourceClassPath = ". `$PSScriptRoot\$ModuleName.Classes.ps1"
        $Success = Get-ScriptsContentAndTryReplace -Files $ClassesFunctions -OutputPath $ClassesPath -DoNotAttemptToFixRelativePaths:$Configuration.Steps.BuildModule.DoNotAttemptToFixRelativePaths
        if ($Success -eq $false) {
            return $false
        }
    }

    if ($LibariesPath -gt 0 -or $ClassesPath -gt 0 -or $Configuration.Steps.BuildModule.ResolveBinaryConflicts) {
        if (Test-Path -LiteralPath $PSM1FilePath) {
            $PSM1Content = Get-Content -LiteralPath $PSM1FilePath -Raw -Encoding UTF8
        }
        else {
            Write-Text "[+] PSM1 file doesn't exists. Creating empty content" -Color Blue
            $PSM1Content = ''
        }
        $IntegrateContent = @(

            if ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -is [System.Collections.IDictionary]) {
                New-DLLResolveConflict -ProjectName $Configuration.Steps.BuildModule.ResolveBinaryConflicts.ProjectName -LibraryConfiguration $Configuration.Steps.BuildLibraries
            }
            elseif ($Configuration.Steps.BuildModule.ResolveBinaryConflicts -eq $true) {
                New-DLLResolveConflict -LibraryConfiguration $Configuration.Steps.BuildLibraries
            }

            Add-BinaryImportModule -Configuration $Configuration -LibrariesStandard $LibrariesStandard -LibrariesCore $LibrariesCore -LibrariesDefault $LibrariesDefault

            if ($LibraryContent.Count -gt 0) {
                if ($DotSourcePath) {
                    "# Dot source all libraries by loading external file"
                    $DotSourcePath
                    ""
                }
                if (-not $LibariesPath) {
                    "# Load all types"
                    $LibraryContent
                    ""
                }
            }
            if ($ClassesPath) {
                "# Dot source all classes by loading external file"
                $DotSourceClassPath
                ""
            }
            $PSM1Content
        )
        $IntegrateContent | Set-Content -LiteralPath $PSM1FilePath -Encoding $Encoding
    }

    if ($Configuration.Information.Manifest.DotNetFrameworkVersion) {
        Find-NetFramework -RequireVersion $Configuration.Information.Manifest.DotNetFrameworkVersion | Out-File -Append -LiteralPath $PSM1FilePath -Encoding $Encoding
    }

    $newPSMFileSplat = @{
        Path                       = $PSM1FilePath
        FunctionNames              = $FunctionsToExport
        FunctionAliaes             = $AliasesToExport
        AliasesAndFunctions        = $AliasesAndFunctions
        CmdletsAliases             = $CmdletsAliases
        LibrariesStandard          = $LibrariesStandard
        LibrariesCore              = $LibrariesCore
        LibrariesDefault           = $LibrariesDefault
        ModuleName                 = $ModuleName

        LibariesPath               = $LibariesPath
        InternalModuleDependencies = $Configuration.Information.Manifest.InternalModuleDependencies
        CommandModuleDependencies  = $Configuration.Information.Manifest.CommandModuleDependencies

        BinaryModule               = $Configuration.Steps.BuildLibraries.BinaryModule
        Configuration              = $Configuration
    }

    $Success = New-PSMFile @newPSMFileSplat
    if ($Success -eq $false) {
        return $false
    }

    $Success = Repair-CustomPlaceHolders -Path $PSM1FilePath -Configuration $Configuration
    if ($Success -eq $false) {
        return $false
    }

    $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $FormatCodePSM1
    if ($Success -eq $false) {
        return $false
    }

    if ($LibariesPath) {
        $Success = Format-Code -FilePath $LibariesPath -FormatCode $FormatCodePSM1
        if ($Success -eq $false) {
            return $false
        }
    }

    $newPersonalManifestSplat = @{
        Configuration           = $Configuration
        ManifestPath            = $PSD1FilePath
        AddUsingsToProcess      = $true
        ScriptsToProcessLibrary = $ScriptsToProcessLibrary
        OnMerge                 = $true
    }

    if ($Configuration.Steps.BuildLibraries.BinaryModule) {
        $newPersonalManifestSplat.BinaryModule = $Configuration.Steps.BuildLibraries.BinaryModule
    }

    New-PersonalManifest @newPersonalManifestSplat

    $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $FormatCodePSD1
    if ($Success -eq $false) {
        return $false
    }

    Get-ChildItem $ModulePathTarget -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | `
            Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | `
                Remove-Item 

    $TimeToExecuteSign.Stop()
    Write-Text "[+] Finalizing PSM1/PSD1 [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue
}
function New-DLLCodeOutput {
    [CmdletBinding()]
    param(
        [string[]] $File,
        [bool] $DebugDLL,
        [alias('NETHandleAssemblyWithSameName')][bool] $HandleAssemblyWithSameName
    )
    if ($File.Count -gt 1) {
        if ($DebugDLL) {
            @(
                "`$LibrariesToLoad = @("
                foreach ($F in $File) {
                    " '$F'"
                }
                ")"
                "`$FoundErrors = @("
                " foreach (`$L in `$LibrariesToLoad) {"
                " try {"
                " Add-Type -Path `$PSScriptRoot\`$L -ErrorAction Stop"
                " } catch [System.Reflection.ReflectionTypeLoadException] {"
                " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`""
                " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique",
                " foreach (`$E in `$LoaderExceptions) {",
                " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`""
                " }"
                " `$true"
                " } catch {"
                if ($HandleAssemblyWithSameName) {
                    " if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') {"
                    " Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$L'.`""
                    " } else {"
                    " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`"",
                    " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique",
                    " foreach (`$E in `$LoaderExceptions) {"
                    " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`""
                    " }"
                    " `$true"
                    " }"
                }
                else {
                    " Write-Warning `"Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)`"",
                    " `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique",
                    " foreach (`$E in `$LoaderExceptions) {"
                    " Write-Warning `"Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)`""
                    " }"
                    " `$true"
                }
                " }"
                " }"
                ")"
                "if (`$FoundErrors.Count -gt 0) {"
                " Write-Warning `"Importing module failed. Fix errors before continuing.`""
                " break"
                "}"
            )
        }
        else {
            if ($HandleAssemblyWithSameName) {
                $Output = @(
                    "`$LibrariesToLoad = @("
                    foreach ($F in $File) {
                        " '$F'"
                    }
                    ")"
                    "foreach (`$L in `$LibrariesToLoad) {"
                    ' try {'
                    ' Add-Type -Path $PSScriptRoot\$L -ErrorAction Stop'
                    ' } catch {'
                    " if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') {"
                    " Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$L'.`""
                    ' } else {'
                    ' throw $_'
                    ' }'
                    ' }'
                    "}"
                )
            }
            else {
                $Output = @(
                    "`$LibrariesToLoad = @("
                    foreach ($F in $File) {
                        " '$F'"
                    }
                    ")"
                    "foreach (`$L in `$LibrariesToLoad) {"
                    " Add-Type -Path `$PSScriptRoot\`$L"
                    "}"
                )
            }
        }
        $Output
    }
    else {
        if ($DebugDLL) {
            $Output = @"
    `$FoundErrors = @(
        try {
            `$ImportName = "`$PSScriptRoot\$File"
            Add-Type -Path `$ImportName -ErrorAction Stop
        } catch [System.Reflection.ReflectionTypeLoadException] {
            Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)"
            `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique
            foreach (`$E in `$LoaderExceptions) {
                Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)"
            }
           `$true
        } catch {
            Write-Warning "Processing `$(`$ImportName) Exception: `$(`$_.Exception.Message)"
            `$LoaderExceptions = `$(`$_.Exception.LoaderExceptions) | Sort-Object -Unique
            foreach (`$E in `$LoaderExceptions) {
                Write-Warning "Processing `$(`$ImportName) LoaderExceptions: `$(`$E.Message)"
            }
            `$true
        }
    )
    if (`$FoundErrors.Count -gt 0) {
        Write-Warning "Importing module failed. Fix errors before continuing."
        break
    }
"@

        }
        else {
            if ($HandleAssemblyWithSameName) {
                $Output = @"
            try {
                Add-Type -Path `$PSScriptRoot\$File -ErrorAction Stop
            } catch {
                if (`$_.Exception.Message -like '*Assembly with same name is already loaded*') {
                    Write-Warning -Message `"Assembly with same name is already loaded. Ignoring '`$(`$_.InvocationInfo.Statement)'."
                } else {
                    throw `$_
                }
            }
"@

            }
            else {
                $Output = 'Add-Type -Path $PSScriptRoot\' + $File
            }
        }
        $Output
    }
}
function New-DLLHandleRuntime {
    [CmdletBinding()]
    param(
        [alias('NETHandleRuntimes')][bool] $HandleRuntimes
    )
    if ($HandleRuntimes) {
        $DataHandleRuntimes = @"
        if (`$IsWindows) {
            `$Arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture
            `$ArchFolder = switch (`$Arch) {
                'X64' { 'win-x64' }
                'X86' { 'win-x86' }
                'Arm64' { 'win-arm64' }
                'Arm' { 'win-arm' }
                Default { 'win-x64' }
            }
 
            `$LibFolder = if (`$PSEdition -eq 'Core') { 'Core' } else { 'Default' }
            `$NativePath = Join-Path -Path `$PSScriptRoot -ChildPath "Lib\`$LibFolder\runtimes\`$ArchFolder\native"
 
            if ((Test-Path `$NativePath) -and (`$env:PATH -notlike "*`$NativePath*") ) {
                # Write-Warning -Message "Adding `$NativePath to PATH"
                `$env:PATH = "`$NativePath;`$env:PATH"
            }
        }
"@

        $DataHandleRuntimes
    }
}

function New-DLLResolveConflict {
    [CmdletBinding()]
    param(
        [string] $ProjectName,
        [System.Collections.IDictionary] $LibraryConfiguration
    )

    if ($LibraryConfiguration.SearchClass) {
        $SearchClass = $LibraryConfiguration.SearchClass
    }
    else {
        $SearchClass = "`$LibraryName.Initialize"
    }

    if ($ProjectName) {
        $StandardName = "'$ProjectName'"
    }
    else {
        $StandardName = '$myInvocation.MyCommand.Name.Replace(".psm1", "")'
    }
    $Output = @"
 
    # Get library name, from the PSM1 file name
    `$LibraryName = $StandardName
    `$Library = "`$LibraryName.dll"
    `$Class = "$SearchClass"
 
    `$AssemblyFolders = Get-ChildItem -Path `$PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue
 
    # Lets find which libraries we need to load
    `$Default = `$false
    `$Core = `$false
    `$Standard = `$false
    foreach (`$A in `$AssemblyFolders.Name) {
        if (`$A -eq 'Default') {
            `$Default = `$true
        } elseif (`$A -eq 'Core') {
            `$Core = `$true
        } elseif (`$A -eq 'Standard') {
            `$Standard = `$true
        }
    }
    if (`$Standard -and `$Core -and `$Default) {
        `$FrameworkNet = 'Default'
        `$Framework = 'Standard'
    } elseif (`$Standard -and `$Core) {
        `$Framework = 'Standard'
        `$FrameworkNet = 'Standard'
    } elseif (`$Core -and `$Default) {
        `$Framework = 'Core'
        `$FrameworkNet = 'Default'
    } elseif (`$Standard -and `$Default) {
        `$Framework = 'Standard'
        `$FrameworkNet = 'Default'
    } elseif (`$Standard) {
        `$Framework = 'Standard'
        `$FrameworkNet = 'Standard'
    } elseif (`$Core) {
        `$Framework = 'Core'
        `$FrameworkNet = ''
    } elseif (`$Default) {
        `$Framework = ''
        `$FrameworkNet = 'Default'
    } else {
        Write-Error -Message 'No assemblies found'
    }
    if (`$PSEdition -eq 'Core') {
        `$LibFolder = `$Framework
    } else {
        `$LibFolder = `$FrameworkNet
    }
 
    try {
        `$ImportModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core
 
        if (-not (`$Class -as [type])) {
            & `$ImportModule ([IO.Path]::Combine(`$PSScriptRoot, 'Lib', `$LibFolder, `$Library)) -ErrorAction Stop
        } else {
            `$Type = "`$Class" -as [Type]
            & `$importModule -Force -Assembly (`$Type.Assembly)
        }
    } catch {
        if (`$ErrorActionPreference -eq 'Stop') {
            throw
        } else {
            Write-Warning -Message "Importing module `$Library failed. Fix errors before continuing. Error: `$(`$_.Exception.Message)"
            # we will continue, but it's not a good idea to do so
            # return
        }
    }
"@

    $Output
}
function New-LibraryContent {
    [CmdletBinding()]
    param(
        [string[]] $LibrariesStandard,
        [string[]] $LibrariesCore,
        [string[]] $LibrariesDefault,
        [System.Collections.IDictionary] $Configuration,
        [switch] $OptimizedLoading
    )
    if ($Configuration.Steps.BuildLibraries.HandleAssemblyWithSameName) {
        $Handle = $Configuration.Steps.BuildLibraries.HandleAssemblyWithSameName
    }
    else {
        $Handle = $false
    }

    if ($Configuration.Steps.BuildLibraries.HandleRuntimes) {
        $HandleRuntimes = $Configuration.Steps.BuildLibraries.HandleRuntimes
    }
    else {
        $HandleRuntimes = $false
    }

    $LibrariesDefault = foreach ($Library in $LibrariesDefault) {

        $SlashCount = ($Library -split '\\').Count
        if ($SlashCount -gt 3) {
            continue
        }
        $Library
    }
    $LibrariesCore = foreach ($Library in $LibrariesCore) {

        $SlashCount = ($Library -split '\\').Count
        if ($SlashCount -gt 3) {
            continue
        }
        $Library
    }
    $LibrariesStandard = foreach ($Library in $LibrariesStandard) {

        $SlashCount = ($Library -split '\\').Count
        if ($SlashCount -gt 3) {
            continue
        }
        $Library
    }

    if ($OptimizedLoading) {
        $LibraryContent = @(
            if ($LibrariesStandard.Count -gt 0) {
                $Files = :nextFile foreach ($File in $LibrariesStandard) {
                    $Extension = $File.Substring($File.Length - 4, 4)
                    if ($Extension -eq '.dll') {
                        foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                            if ($File -like "*\$IgnoredFile") {
                                continue nextFile
                            }
                        }
                        $File
                    }
                }
                $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle
                $Output
            }
            elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) {
                'if ($PSEdition -eq ''Core'') {'
                if ($LibrariesCore.Count -gt 0) {
                    $Files = :nextFile foreach ($File in $LibrariesCore) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $File
                        }
                    }
                    $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle
                    $Output
                }
                '} else {'
                if ($LibrariesDefault.Count -gt 0) {
                    $Files = :nextFile foreach ($File in $LibrariesDefault) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $File
                        }
                    }
                    $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle
                    $Output
                }
                '}'
            }
            else {
                if ($LibrariesCore.Count -gt 0) {
                    $Files = :nextFile foreach ($File in $LibrariesCore) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $File
                        }
                    }
                    $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle
                    $Output
                }
                if ($LibrariesDefault.Count -gt 0) {
                    $Files = :nextFile foreach ($File in $LibrariesDefault) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $File
                        }
                    }
                    $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $Files -HandleAssemblyWithSameName $Handle
                    $Output
                }
            }
        )
    }
    else {

        $LibraryContent = @(
            if ($LibrariesStandard.Count -gt 0) {
                :nextFile foreach ($File in $LibrariesStandard) {
                    $Extension = $File.Substring($File.Length - 4, 4)
                    if ($Extension -eq '.dll') {
                        foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                            if ($File -like "*\$IgnoredFile") {
                                continue nextFile
                            }
                        }
                        $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle
                        $Output
                    }
                }
            }
            elseif ($LibrariesCore.Count -gt 0 -and $LibrariesDefault.Count -gt 0) {
                'if ($PSEdition -eq ''Core'') {'
                if ($LibrariesCore.Count -gt 0) {
                    :nextFile foreach ($File in $LibrariesCore) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle
                            $Output
                        }
                    }
                }
                '} else {'
                if ($LibrariesDefault.Count -gt 0) {
                    :nextFile foreach ($File in $LibrariesDefault) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle
                            $Output
                        }
                    }
                }
                '}'
            }
            else {
                if ($LibrariesCore.Count -gt 0) {
                    :nextFile foreach ($File in $LibrariesCore) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle
                            $Output
                        }
                    }
                }
                if ($LibrariesDefault.Count -gt 0) {
                    :nextFile foreach ($File in $LibrariesDefault) {
                        $Extension = $File.Substring($File.Length - 4, 4)
                        if ($Extension -eq '.dll') {
                            foreach ($IgnoredFile in $Configuration.Steps.BuildLibraries.IgnoreLibraryOnLoad) {
                                if ($File -like "*\$IgnoredFile") {
                                    continue nextFile
                                }
                            }
                            $Output = New-DLLCodeOutput -DebugDLL $Configuration.Steps.BuildModule.DebugDLL -File $File -HandleAssemblyWithSameName $Handle
                            $Output
                        }
                    }
                }
            }
        )
    }
    if ($HandleRuntimes) {
        $LibraryContent = @(
            New-DLLHandleRuntime -HandleRuntimes $HandleRuntimes
            $LibraryContent
        )
    }
    $LibraryContent
}
function New-PersonalManifest {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $ManifestPath,
        [switch] $AddScriptsToProcess,
        [switch] $AddUsingsToProcess,
        [string] $ScriptsToProcessLibrary,
        [switch] $UseWildcardForFunctions,
        [switch] $OnMerge,
        [string[]] $BinaryModule
    )

    $TemporaryManifest = [ordered] @{ }
    $Manifest = $Configuration.Information.Manifest

    if ($UseWildcardForFunctions) {
        $Manifest.FunctionsToExport = @("*")
        $Manifest.AliasesToExport = @("*")
    }

    if ($BinaryModule.Count -gt 0) {

        if ($null -eq $Manifest.CmdletsToExport) {
            $Data.CmdletsToExport = @("*")
        }
    }

    $Manifest.Path = $ManifestPath

    if (-not $AddScriptsToProcess) {
        $Manifest.ScriptsToProcess = @()
    }
    if ($AddUsingsToProcess -and $Configuration.UsingInPlace -and -not $ScriptsToProcessLibrary) {
        $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace)
    }
    elseif ($AddUsingsToProcess -and $Configuration.UsingInPlace -and $ScriptsToProcessLibrary) {
        $Manifest.ScriptsToProcess = @($Configuration.UsingInPlace, $ScriptsToProcessLibrary)
    }
    elseif ($ScriptsToProcessLibrary) {
        $Manifest.ScriptsToProcess = @($ScriptsToProcessLibrary)
    }
    if ($Manifest.Contains('ExternalModuleDependencies')) {
        $TemporaryManifest.ExternalModuleDependencies = $Manifest.ExternalModuleDependencies
        $Manifest.Remove('ExternalModuleDependencies')
    }
    if ($Manifest.Contains('InternalModuleDependencies')) {
        $TemporaryManifest.InternalModuleDependencies = $Manifest.InternalModuleDependencies
        $Manifest.Remove('InternalModuleDependencies')
    }
    if ($Manifest.Contains('CommandModuleDependencies')) {
        $TemporaryManifest.CommandModuleDependencies = $Manifest.CommandModuleDependencies
        $Manifest.Remove('CommandModuleDependencies')
    }
    if ($Manifest.PreRelease) {
        $Configuration.CurrentSettings.PreRelease = $Manifest.PreRelease
    }
    if ($OnMerge) {
        if ($Configuration.Options.Merge.Style.PSD1) {
            $PSD1Style = $Configuration.Options.Merge.Style.PSD1
        }
    }
    else {
        if ($Configuration.Options.Standard.Style.PSD1) {
            $PSD1Style = $Configuration.Options.Standard.Style.PSD1
        }
    }
    if (-not $PSD1Style) {
        if ($Configuration.Options.Style.PSD1) {
            $PSD1Style = $Configuration.Options.Style.PSD1
        }
        else {
            $PSD1Style = 'Minimal'
        }
    }

    if ($PSD1Style -eq 'Native' -and $Configuration.Steps.PublishModule.Prerelease -eq '' -and (-not $TemporaryManifest.ExternalModuleDependencies)) {
        if ($Manifest.ModuleVersion) {
            New-ModuleManifest @Manifest
        }
        else {
            Write-Text -Text '[-] Module version is not available. Terminating.' -Color Red
            return $false
        }
        Write-TextWithTime -Text "[i] Converting $($ManifestPath) UTF8 without BOM" {
            (Get-Content -Path $ManifestPath -Raw -Encoding utf8) | Out-FileUtf8NoBom $ManifestPath
        }
    }
    else {
        if ($PSD1Style -eq 'Native') {
            Write-Text -Text '[-] Native PSD1 style is not available when using PreRelease or ExternalModuleDependencies. Switching to Minimal.' -Color Yellow
        }

        $Data = $Manifest
        $Data.PrivateData = @{
            PSData = [ordered]@{}
        }
        if ($Data.Path) {
            $Data.Remove('Path')
        }
        $ValidateEntriesPrivateData = @('Tags', 'LicenseUri', 'ProjectURI', 'IconUri', 'ReleaseNotes', 'Prerelease', 'RequireLicenseAcceptance', 'ExternalModuleDependencies')
        foreach ($Entry in [string[]] $Data.Keys) {
            if ($Entry -in $ValidateEntriesPrivateData) {
                $Data.PrivateData.PSData.$Entry = $Data.$Entry
                $Data.Remove($Entry)
            }
        }
        $ValidDataEntries = @('ModuleToProcess', 'NestedModules', 'GUID', 'Author', 'CompanyName', 'Copyright', 'ModuleVersion', 'Description', 'PowerShellVersion', 'PowerShellHostName', 'PowerShellHostVersion', 'CLRVersion', 'DotNetFrameworkVersion', 'ProcessorArchitecture', 'RequiredModules', 'TypesToProcess', 'FormatsToProcess', 'ScriptsToProcess', 'PrivateData', 'RequiredAssemblies', 'ModuleList', 'FileList', 'FunctionsToExport', 'VariablesToExport', 'AliasesToExport', 'CmdletsToExport', 'DscResourcesToExport', 'CompatiblePSEditions', 'HelpInfoURI', 'RootModule', 'DefaultCommandPrefix')
        foreach ($Entry in [string[]] $Data.Keys) {
            if ($Entry -notin $ValidDataEntries) {
                Write-Text -Text "[-] Removing wrong entries from PSD1 - $Entry" -Color Red
                $Data.Remove($Entry)
            }
        }
        foreach ($Entry in [string[]] $Data.PrivateData.PSData.Keys) {
            if ($Entry -notin $ValidateEntriesPrivateData) {
                Write-Text -Text "[-] Removing wrong entries from PSD1 Private Data - $Entry" -Color Red
                $Data.PrivateData.PSData.Remove($Entry)
            }
        }

        if ($Configuration.Steps.PublishModule.Prerelease) {
            $Data.PrivateData.PSData.Prerelease = $Configuration.Steps.PublishModule.Prerelease
        }
        if ($TemporaryManifest.ExternalModuleDependencies) {

            $Data.PrivateData.PSData.ExternalModuleDependencies = $TemporaryManifest.ExternalModuleDependencies

            $Data.RequiredModules = @(
                foreach ($Module in $Manifest.RequiredModules) {
                    if ($Module -is [System.Collections.IDictionary]) {

                        $Module = [ordered] @{
                            ModuleName    = $Module.ModuleName
                            ModuleVersion = $Module.ModuleVersion
                            Guid          = $Module.Guid
                        }
                        Remove-EmptyValue -Hashtable $Module
                        $Module
                    }
                    else {
                        $Module
                    }
                }
                foreach ($Module in $TemporaryManifest.ExternalModuleDependencies) {
                    if ($Module -is [System.Collections.IDictionary]) {

                        $Module = [ordered] @{
                            ModuleName    = $Module.ModuleName
                            ModuleVersion = $Module.ModuleVersion
                            Guid          = $Module.Guid
                        }
                        Remove-EmptyValue -Hashtable $Module
                        $Module
                    }
                    else {
                        $Module
                    }
                }
            )
        }
        if (-not $Data.RequiredModules) {
            $Data.Remove('RequiredModules')
        }
        $Data | Export-PSData -DataFile $ManifestPath -Sort
    }
}
function New-PrepareManifest {
    [CmdletBinding()]
    param(
        [string] $ProjectName,
        [string] $ModulePath,
        [string] $ProjectPath,
        $FunctionToExport,
        [string] $ProjectUrl
    )
    $Location = [System.IO.Path]::Combine($projectPath, $ProjectName)
    Set-Location -Path $Location
    $manifest = @{
        Path              = ".\$ProjectName.psd1"
        RootModule        = "$ProjectName.psm1"
        Author            = 'Przemyslaw Klys'
        CompanyName       = 'Evotec'
        Copyright         = 'Evotec (c) 2011-2022. All rights reserved.'
        Description       = "Simple project"
        FunctionsToExport = $functionToExport
        CmdletsToExport   = ''
        VariablesToExport = ''
        AliasesToExport   = ''
        FileList          = "$ProjectName.psm1", "$ProjectName.psd1"
        HelpInfoURI       = $projectUrl
        ProjectUri        = $projectUrl
    }
    New-ModuleManifest @manifest
}
function New-PrepareStructure {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary]$Configuration,
        [scriptblock] $Settings,
        [string] $PathToProject,
        [string] $ModuleName,
        [string] $FunctionsToExportFolder,
        [string] $AliasesToExportFolder,
        [string[]] $ExcludeFromPackage,
        [string[]] $IncludeRoot,
        [string[]] $IncludePS1,
        [string[]] $IncludeAll,
        [scriptblock] $IncludeCustomCode,
        [System.Collections.IDictionary] $IncludeToArray,
        [string] $LibrariesCore,
        [string] $LibrariesDefault,
        [string] $LibrariesStandard
    )

    if (-not $Configuration) {
        $Configuration = [ordered] @{}
    }
    if (-not $Configuration.Information) {
        $Configuration.Information = [ordered] @{}
    }
    if (-not $Configuration.Information.Manifest) {

        $PathToPSD1 = [io.path]::Combine($PathToProject, $ModuleName + '.psd1')
        if (Test-Path -LiteralPath $PathToPSD1) {
            try {
                $Configuration.Information.Manifest = Import-PowerShellDataFile -Path $PathToPSD1 -ErrorAction Stop

                $Configuration.Information.Manifest.RequiredModules = $null
                $Configuration.Information.Manifest.FunctionsToExport = $null
                $Configuration.Information.Manifest.AliasesToExport = $null
                $Configuration.Information.Manifest.CmdletsToExport = $null
            }
            catch {
                Write-Text "[-] Reading $PathToPSD1 failed. Error: $($_.Exception.Message)" -Color Red
                Write-Text "[+] Building $PathToPSD1 from scratch." -Color Yellow
                $Configuration.Information.Manifest = [ordered] @{}
            }
        }
        else {
            $Configuration.Information.Manifest = [ordered] @{}
        }
    }

    if (-not $Configuration.Information.DirectoryModulesCore) {
        if ($IsWindows) {
            $PathCore = [io.path]::Combine($([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)), "PowerShell", 'Modules')
        }
        else {

            $PathCore = [io.path]::Combine($env:HOME, ".local", "share", "powershell", "Modules")
        }
        $Configuration.Information.DirectoryModulesCore = $PathCore
    }

    if (-not $Configuration.Information.DirectoryModules) {
        if ($IsWindows) {
            $PathStandard = [io.path]::Combine($([Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)), "WindowsPowerShell", 'Modules')
        }
        else {

            $PathStandard = $Configuration.Information.DirectoryModulesCore
        }
        $Configuration.Information.DirectoryModules = $PathStandard
    }

    if (-not $Configuration.CurrentSettings) {
        $Configuration.CurrentSettings = [ordered] @{}
    }
    if (-not $Configuration.CurrentSettings['Artefact']) {
        $Configuration.CurrentSettings['Artefact'] = @()
    }
    if ($ModuleName) {
        $Configuration.Information.ModuleName = $ModuleName
    }
    if ($PSBoundParameters.ContainsKey('ExcludeFromPackage')) {
        $Configuration.Information.Exclude = $ExcludeFromPackage
    }
    if ($PSBoundParameters.ContainsKey('IncludeRoot')) {
        $Configuration.Information.IncludeRoot = $IncludeRoot
    }
    if ($PSBoundParameters.ContainsKey('IncludePS1')) {
        $Configuration.Information.IncludePS1 = $IncludePS1
    }
    if ($PSBoundParameters.ContainsKey('IncludeAll')) {
        $Configuration.Information.IncludeAll = $IncludeAll
    }
    if ($PSBoundParameters.ContainsKey('IncludeCustomCode')) {
        $Configuration.Information.IncludeCustomCode = $IncludeCustomCode
    }
    if ($PSBoundParameters.ContainsKey('IncludeToArray')) {
        $Configuration.Information.IncludeToArray = $IncludeToArray
    }
    if ($PSBoundParameters.ContainsKey('LibrariesCore')) {
        $Configuration.Information.LibrariesCore = $LibrariesCore
    }
    if ($PSBoundParameters.ContainsKey('LibrariesDefault')) {
        $Configuration.Information.LibrariesDefault = $LibrariesDefault
    }
    if ($PSBoundParameters.ContainsKey('LibrariesStandard')) {
        $Configuration.Information.LibrariesStandard = $LibrariesStandard
    }
    if ($FunctionsToExportFolder) {
        $Configuration.Information.FunctionsToExport = $FunctionsToExportFolder
    }
    if ($AliasesToExportFolder) {
        $Configuration.Information.AliasesToExport = $AliasesToExportFolder
    }
    if (-not $Configuration.Options) {
        $Configuration.Options = [ordered] @{}
    }
    if (-not $Configuration.Options.Merge) {
        $Configuration.Options.Merge = [ordered] @{}
    }
    if (-not $Configuration.Options.Merge.Integrate) {
        $Configuration.Options.Merge.Integrate = [ordered] @{}
    }
    if (-not $Configuration.Options.Standard) {
        $Configuration.Options.Standard = [ordered] @{}
    }
    if (-not $Configuration.Options.Signing) {
        $Configuration.Options.Signing = [ordered] @{}
    }
    if (-not $Configuration.Steps) {
        $Configuration.Steps = [ordered] @{}
    }
    if (-not $Configuration.Steps.PublishModule) {
        $Configuration.Steps.PublishModule = [ordered] @{}
    }
    if (-not $Configuration.Steps.ImportModules) {
        $Configuration.Steps.ImportModules = [ordered] @{}
    }
    if (-not $Configuration.Steps.BuildModule) {
        $Configuration.Steps.BuildModule = [ordered] @{}
    }
    if (-not $Configuration.Steps.BuildModule.Releases) {
        $Configuration.Steps.BuildModule.Releases = [ordered] @{}
    }
    if (-not $Configuration.Steps.BuildModule.ReleasesUnpacked) {
        $Configuration.Steps.BuildModule.ReleasesUnpacked = [ordered] @{}
    }
    if (-not $Configuration.Steps.BuildLibraries) {
        $Configuration.Steps.BuildLibraries = [ordered] @{}
    }
    if (-not $Configuration.Information.Manifest.CommandModuleDependencies) {
        $Configuration.Information.Manifest.CommandModuleDependencies = [ordered] @{}
    }
    if (-not $Configuration.Steps.BuildModule.Artefacts) {
        $Configuration.Steps.BuildModule.Artefacts = [System.Collections.Generic.List[System.Collections.IDictionary]]::new()
    }
    if (-not $Configuration.Steps.BuildModule.GitHubNugets) {
        $Configuration.Steps.BuildModule.GitHubNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new()
    }
    if (-not $Configuration.Steps.BuildModule.GalleryNugets) {
        $Configuration.Steps.BuildModule.GalleryNugets = [System.Collections.Generic.List[System.Collections.IDictionary]]::new()
    }

    $Configuration.Information.Manifest.RootModule = "$($ModuleName).psm1"

    if (-not $Configuration.PlaceHolder) {
        $Configuration.PlaceHolder = [System.Collections.Generic.List[System.Collections.IDictionary]]::new()
    }
    if (-not $Configuration.PlaceHolderOption) {
        $Configuration.PlaceHolderOption = [ordered] @{}
    }

    Write-TextWithTime -Text "Reading configuration" {
        if ($Settings) {
            $ExecutedSettings = & $Settings
            foreach ($Setting in $ExecutedSettings) {
                if ($Setting.Type -eq 'RequiredModule') {
                    if ($Configuration.Information.Manifest.RequiredModules -isnot [System.Collections.Generic.List[System.Object]]) {
                        $Configuration.Information.Manifest.RequiredModules = [System.Collections.Generic.List[System.Object]]::new()
                    }
                    $Configuration.Information.Manifest.RequiredModules.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'ExternalModule') {
                    if ($Configuration.Information.Manifest.ExternalModuleDependencies -isnot [System.Collections.Generic.List[System.Object]]) {
                        $Configuration.Information.Manifest.ExternalModuleDependencies = [System.Collections.Generic.List[System.Object]]::new()
                    }
                    $Configuration.Information.Manifest.ExternalModuleDependencies.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'ApprovedModule') {
                    if ($Configuration.Options.Merge.Integrate.ApprovedModules -isnot [System.Collections.Generic.List[System.Object]]) {
                        $Configuration.Options.Merge.Integrate.ApprovedModules = [System.Collections.Generic.List[System.Object]]::new()
                    }
                    $Configuration.Options.Merge.Integrate.ApprovedModules.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'ModuleSkip') {
                    $Configuration.Options.Merge.ModuleSkip = $Setting.Configuration
                }
                elseif ($Setting.Type -eq 'Manifest') {
                    foreach ($Key in $Setting.Configuration.Keys) {
                        $Configuration.Information.Manifest[$Key] = $Setting.Configuration[$Key]
                    }
                }
                elseif ($Setting.Type -eq 'Information') {
                    foreach ($Key in $Setting.Configuration.Keys) {
                        $Configuration.Information[$Key] = $Setting.Configuration[$Key]
                    }
                }
                elseif ($Setting.Type -eq 'Formatting') {
                    foreach ($Key in $Setting.Options.Keys) {
                        if (-not $Configuration.Options[$Key]) {
                            $Configuration.Options[$Key] = [ordered] @{}
                        }
                        foreach ($Entry in $Setting.Options[$Key].Keys) {
                            $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry]
                        }
                    }
                }
                elseif ($Setting.Type -eq 'Command') {
                    $Configuration.Information.Manifest.CommandModuleDependencies[$Setting.Configuration.ModuleName] = @($Setting.Configuration.CommandName)
                }
                elseif ($Setting.Type -eq 'Documentation') {
                    $Configuration.Options.Documentation = $Setting.Configuration
                }
                elseif ($Setting.Type -eq 'BuildDocumentation') {
                    $Configuration.Steps.BuildDocumentation = $Setting.Configuration
                }
                elseif ($Setting.Type -eq 'TestsBeforeMerge') {
                    $Configuration.Options.TestsBeforeMerge = $Setting.Configuration
                }
                elseif ($Setting.Type -eq 'TestsAfterMerge') {
                    $Configuration.Options.TestsAfterMerge = $Setting.Configuration
                }
                elseif ($Setting.Type -eq 'GitHubPublishing') {
                    $Configuration.Steps.BuildModule.Nugets.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'ImportModules') {
                    foreach ($Key in $Setting.ImportModules.Keys) {
                        $Configuration.Steps.ImportModules[$Key] = $Setting.ImportModules[$Key]
                    }
                }
                elseif ($Setting.Type -in 'GalleryNuget') {
                    $Configuration.Steps.BuildModule.GalleryNugets.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -in 'GitHubNuget') {
                    $Configuration.Steps.BuildModule.GitHubNugets.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -in 'Unpacked', 'Packed', 'Script', 'ScriptPacked') {
                    $Configuration.Steps.BuildModule.Artefacts.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'Build') {
                    foreach ($Key in $Setting.BuildModule.Keys) {
                        $Configuration.Steps.BuildModule[$Key] = $Setting.BuildModule[$Key]
                    }
                }
                elseif ($Setting.Type -eq 'BuildLibraries') {
                    foreach ($Key in $Setting.BuildLibraries.Keys) {
                        $Configuration.Steps.BuildLibraries[$Key] = $Setting.BuildLibraries[$Key]
                    }
                }
                elseif ($Setting.Type -eq 'Options') {
                    foreach ($Key in $Setting.Options.Keys) {
                        if (-not $Configuration.Options[$Key]) {
                            $Configuration.Options[$Key] = [ordered] @{}
                        }
                        foreach ($Entry in $Setting.Options[$Key].Keys) {
                            $Configuration.Options[$Key][$Entry] = $Setting.Options[$Key][$Entry]
                        }
                    }
                }
                elseif ($Setting.Type -eq 'PlaceHolder') {
                    $Configuration.PlaceHolder.Add($Setting.Configuration)
                }
                elseif ($Setting.Type -eq 'PlaceHolderOption') {
                    foreach ($Key in $Setting.PlaceHolderOption.Keys) {
                        $Configuration.PlaceHolderOption[$Key] = $Setting.PlaceHolderOption[$Key]
                    }
                }
            }
        }
    } -PreAppend Information

    if (-not $Configuration.Options.Merge.Sort) {
        $Configuration.Options.Merge.Sort = 'None'
    }
    if (-not $Configuration.Options.Standard.Sort) {
        $Configuration.Options.Standard.Sort = 'None'
    }

    $Success = Start-ModuleBuilding -Configuration $Configuration -PathToProject $PathToProject
    if ($Success -contains $false) {
        return $false
    }
}
function New-PSMFile {
    [cmdletbinding()]
    param(
        [string] $Path,
        [string[]] $FunctionNames,
        [string[]] $FunctionAliaes,
        [System.Collections.IDictionary] $AliasesAndFunctions,
        [System.Collections.IDictionary] $CmdletsAliases,
        [Array] $LibrariesStandard,
        [Array] $LibrariesCore,
        [Array] $LibrariesDefault,
        [string] $ModuleName,
        # [switch] $UsingNamespaces,
        [string] $LibariesPath,
        [Array] $InternalModuleDependencies,
        [System.Collections.IDictionary] $CommandModuleDependencies,
        [string[]] $BinaryModule,
        [System.Collections.IDictionary] $Configuration
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    Write-TextWithTime -Text "Adding alises/functions to load in PSM1 file - $Path" -PreAppend Plus {
        if ($FunctionNames.Count -gt 0) {
            $Functions = ($FunctionNames | Sort-Object -Unique) -join "','"
            $Functions = "'$Functions'"
        }
        else {
            $Functions = @()
        }

        if ($FunctionAliaes.Count -gt 0) {
            $Aliases = ($FunctionAliaes | Sort-Object -Unique) -join "','"
            $Aliases = "'$Aliases'"
        }
        else {
            $Aliases = @()
        }

        "" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop

        if ($InternalModuleDependencies.Count -gt 0) {
            @(
                "# Added internal module loading to cater for special cases "
                ""
            ) | Out-File -Append -LiteralPath $Path -Encoding $Encoding
            $ModulesText = "'$($InternalModuleDependencies -join "','")'"
            @"
            `$ModulesOptional = $ModulesText
            foreach (`$Module in `$ModulesOptional) {
                Import-Module -Name `$Module -ErrorAction SilentlyContinue
            }
"@
 | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop
        }

        if ($Configuration.Information.Manifest.CmdletsToExport) {
            $Cmdlet = ($Configuration.Information.Manifest.CmdletsToExport | Sort-Object -Unique) -join "','"
            $Cmdlet = "'$Cmdlet'"
        }
        else {
            if ($BinaryModule.Count -gt 0) {
                $Cmdlet = '"*"'
            }
        }

        if ($CommandModuleDependencies -and $CommandModuleDependencies.Keys.Count -gt 0) {
            @(
                "`$ModuleFunctions = @{"
                foreach ($Module in $CommandModuleDependencies.Keys) {
                    "$Module = @{"
                    foreach ($Command in $($CommandModuleDependencies[$Module])) {
                        $Alias = "'$($AliasesAndFunctions[$Command] -join "','")'"
                        " '$Command' = $Alias"
                    }
                    "}"
                }
                "}"

                @"
                [Array] `$FunctionsAll = $Functions
                [Array] `$AliasesAll = $Aliases
                `$AliasesToRemove = [System.Collections.Generic.List[string]]::new()
                `$FunctionsToRemove = [System.Collections.Generic.List[string]]::new()
                foreach (`$Module in `$ModuleFunctions.Keys) {
                    try {
                        Import-Module -Name `$Module -ErrorAction Stop
                    } catch {
                        foreach (`$Function in `$ModuleFunctions[`$Module].Keys) {
                            `$FunctionsToRemove.Add(`$Function)
                            `$ModuleFunctions[`$Module][`$Function] | ForEach-Object {
                                if (`$_) {
                                    `$AliasesToRemove.Add(`$_)
                                }
                            }
                        }
                    }
                }
                `$FunctionsToLoad = foreach (`$Function in `$FunctionsAll) {
                    if (`$Function -notin `$FunctionsToRemove) {
                        `$Function
                    }
                }
                `$AliasesToLoad = foreach (`$Alias in `$AliasesAll) {
                    if (`$Alias -notin `$AliasesToRemove) {
                        `$Alias
                    }
                }
                # Export functions and aliases as required
"@

            ) | Out-File -Append -LiteralPath $Path -Encoding $Encoding

            if ($Cmdlet) {
                "Export-ModuleMember -Function @(`$FunctionsToLoad) -Alias @(`$AliasesToLoad) -Cmdlet $Cmdlet" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop
            }
            else {
                "Export-ModuleMember -Function @(`$FunctionsToLoad) -Alias @(`$AliasesToLoad)" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop
            }
        }
        else {

            "# Export functions and aliases as required" | Out-File -Append -LiteralPath $Path -Encoding $Encoding
            if ($Cmdlet) {

                "Export-ModuleMember -Function @($Functions) -Alias @($Aliases) -Cmdlet $Cmdlet" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop
            }
            else {
                "Export-ModuleMember -Function @($Functions) -Alias @($Aliases)" | Out-File -Append -LiteralPath $Path -Encoding $Encoding -ErrorAction Stop
            }
        }
    } -SpacesBefore ' '
}
function Out-FileUtf8NoBom {
    <#
    .SYNOPSIS
      Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).
 
    .DESCRIPTION
      Mimics the most important aspects of Out-File:
      * Input objects are sent to Out-String first.
      * -Append allows you to append to an existing file, -NoClobber prevents
        overwriting of an existing file.
      * -Width allows you to specify the line width for the text representations
        of input objects that aren't strings.
      However, it is not a complete implementation of all Out-String parameters:
      * Only a literal output path is supported, and only as a parameter.
      * -Force is not supported.
 
      Caveat: *All* pipeline input is buffered before writing output starts,
              but the string representations are generated and written to the target
              file one by one.
 
    .NOTES
      The raison d'être for this advanced function is that, as of PowerShell v5,
      Out-File still lacks the ability to write UTF-8 files without a BOM:
      using -Encoding UTF8 invariably prepends a BOM.
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)] [string] $LiteralPath,
        [switch] $Append,
        [switch] $NoClobber,
        [AllowNull()] [int] $Width,
        [Parameter(ValueFromPipeline)] $InputObject
    )

    [System.IO.Directory]::SetCurrentDirectory($PWD) 
    $LiteralPath = [IO.Path]::GetFullPath($LiteralPath)

    if ($NoClobber -and (Test-Path $LiteralPath)) {
        throw [IO.IOException] "The file '$LiteralPath' already exists."
    }

    $sw = New-Object IO.StreamWriter $LiteralPath, $Append

    $htOutStringArgs = @{}
    if ($Width) {
        $htOutStringArgs += @{ Width = $Width }
    }

    try {
        $Input | Out-String -Stream @htOutStringArgs | ForEach-Object { $sw.WriteLine($_) }
    }
    finally {
        $sw.Dispose()
    }
}
function Register-DataForInitialModule {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $FilePath,
        [Parameter(Mandatory)][string] $ModuleName,
        [Parameter(Mandatory)][string] $Guid
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }
    try {
        $BuildModule = Get-Content -Path $FilePath -Raw -ErrorAction Stop
    }
    catch {
        Write-Text -Text "[-] Couldn't read $FilePath, error: $($_.Exception.Message)" -Color Red
        return $false
    }
    $BuildModule = $BuildModule -replace "\`$GUID", $Guid
    $BuildModule = $BuildModule -replace "\`$ModuleName", $ModuleName
    try {
        Set-Content -Path $FilePath -Value $BuildModule -Encoding $Encoding -ErrorAction Stop
    }
    catch {
        Write-Text -Text "[-] Couldn't save $FilePath, error: $($_.Exception.Message)" -Color Red
        return $false
    }
}
function Remove-Directory {
    [CmdletBinding()]
    param (
        [string] $Directory,
        [string] $SpacesBefore
    )
    if ($Directory) {
        $Exists = Test-Path -LiteralPath $Directory
        if ($Exists) {
            try {
                Remove-Item -Path $Directory -ErrorAction Stop -Force -Recurse -Confirm:$false
            }
            catch {
                $ErrorMessage = $_.Exception.Message
                Write-Text "Can't delete folder $Directory. Fix error before continuing: $ErrorMessage" -PreAppend Error -SpacesBefore $SpacesBefore
                return $false
            }
        }
    }
}
function Remove-EmptyLines {
    [CmdletBinding(DefaultParameterSetName = 'FilePath')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'FilePath')]
        [alias('FilePath', 'Path', 'LiteralPath')][string] $SourceFilePath,

        [Parameter(Mandatory, ParameterSetName = 'Content')][string] $Content,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [alias('Destination')][string] $DestinationFilePath,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveAllEmptyLines,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveEmptyLines
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($SourceFilePath) {
        $Fullpath = Resolve-Path -LiteralPath $SourceFilePath
        $Content = [IO.File]::ReadAllText($FullPath)
    }
    if ($RemoveEmptyLines) {

        $Content = $Content -replace '(?m)^\s*$', ''
    }
    if ($RemoveAllEmptyLines) {

        $Content = $Content -replace '(?m)^\s*$(\r?\n)?', ''
    }
    if ($Content) {
        $Content = $Content.Trim()
    }
    if ($DestinationFilePath) {
        $Content | Set-Content -Path $DestinationFilePath -Encoding $Encoding
    }
    else {
        $Content
    }
}
function Remove-EmptyValue {
    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    }
                    else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                }
                else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    }
                    elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    }
                    elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            }
            else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                }
                elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                }
                elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Remove-ItemAlternative {
    <#
    .SYNOPSIS
    Removes all files and folders within given path
 
    .DESCRIPTION
    Removes all files and folders within given path. Workaround for Access to the cloud file is denied issue
 
    .PARAMETER Path
    Path to file/folder
 
    .PARAMETER SkipFolder
    Do not delete top level folder
 
    .PARAMETER Exclude
    Skip files/folders matching given pattern
 
    .EXAMPLE
    Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs"
 
    .EXAMPLE
    Remove-ItemAlternative -Path "C:\Support\GitHub\GpoZaurr\Docs"
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    param(
        [alias('LiteralPath')][string] $Path,
        [switch] $SkipFolder,
        [string[]] $Exclude
    )
    if ($Path -and (Test-Path -LiteralPath $Path)) {
        $getChildItemSplat = @{
            Path    = $Path
            Recurse = $true
            Force   = $true
            File    = $true
            Exclude = $Exclude
        }
        Remove-EmptyValue -Hashtable $getChildItemSplat
        $Items = Get-ChildItem @getChildItemSplat
        foreach ($Item in $Items) {
            try {
                $Item.Delete()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
                else {
                    Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
            }
        }
        $getChildItemSplat = @{
            Path    = $Path
            Recurse = $true
            Force   = $true
            Exclude = $Exclude
        }
        Remove-EmptyValue -Hashtable $getChildItemSplat
        $Items = Get-ChildItem @getChildItemSplat | Sort-Object -Descending -Property 'FullName'
        foreach ($Item in $Items) {
            try {
                $Item.Delete()
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
                else {
                    Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
            }
        }
        if (-not $SkipFolder.IsPresent) {
            $Item = Get-Item -LiteralPath $Path
            try {
                $Item.Delete($true)
            }
            catch {
                if ($ErrorActionPreference -eq 'Stop') {
                    throw "Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
                else {
                    Write-Warning "Remove-ItemAlternative - Couldn't delete $($Item.FullName), error: $($_.Exception.Message)"
                }
            }
        }
    }
    else {
        if ($ErrorActionPreference -eq 'Stop') {
            throw "Remove-ItemAlternative - Path $Path doesn't exists. Skipping."
        }
        else {
            Write-Warning "Remove-ItemAlternative - Path $Path doesn't exists. Skipping."
        }
    }
}
function Repair-CustomPlaceHolders {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Path,
        [System.Collections.IDictionary] $Configuration
    )

    $ModuleName = $Configuration.Information.ModuleName
    $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion
    $TagName = "v$($ModuleVersion)"
    if ($Configuration.CurrentSettings.PreRelease) {
        $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
        $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)"
    }
    else {
        $ModuleVersionWithPreRelease = $ModuleVersion
        $TagModuleVersionWithPreRelease = "v$($ModuleVersion)"
    }

    $BuiltinPlaceHolders = @(
        @{ Find = '{ModuleName}'; Replace = $ModuleName }
        @{ Find = '<ModuleName>'; Replace = $ModuleName }
        @{ Find = '{ModuleVersion}'; Replace = $ModuleVersion }
        @{ Find = '<ModuleVersion>'; Replace = $ModuleVersion }
        @{ Find = '{ModuleVersionWithPreRelease}'; Replace = $ModuleVersionWithPreRelease }
        @{ Find = '<ModuleVersionWithPreRelease>'; Replace = $ModuleVersionWithPreRelease }
        @{ Find = '{TagModuleVersionWithPreRelease}'; Replace = $TagModuleVersionWithPreRelease }
        @{ Find = '<TagModuleVersionWithPreRelease>'; Replace = $TagModuleVersionWithPreRelease }
        @{ Find = '{TagName}'; Replace = $TagName }
        @{ Find = '<TagName>'; Replace = $TagName }
    )

    Write-TextWithTime -Text "Replacing built-in and custom placeholders" -Color Yellow {
        $PSM1Content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 -ErrorAction Stop

        if ($Configuration.PlaceHolderOption.SkipBuiltinReplacements -ne $true) {
            foreach ($PlaceHolder in $BuiltinPlaceHolders) {
                $PSM1Content = $PSM1Content.Replace($PlaceHolder.Find, $PlaceHolder.Replace)
            }
        }

        foreach ($PlaceHolder in $Configuration.PlaceHolders) {
            $PSM1Content = $PSM1Content.Replace($PlaceHolder.Find, $PlaceHolder.Replace)
        }

        Set-Content -LiteralPath $Path -Value $PSM1Content -Encoding UTF8 -ErrorAction Stop -Force
    } -PreAppend Plus -SpacesBefore ' '
}
function Resolve-Encoding {
    <#
    .SYNOPSIS
        Resolves encoding names to .NET System.Text.Encoding objects.
 
    .DESCRIPTION
        Converts encoding name strings to proper .NET encoding objects, with special handling
        for UTF8BOM (UTF8 with BOM) and OEM encodings that aren't directly available in
        System.Text.Encoding static properties.
 
    .PARAMETER Name
        The name of the encoding to resolve. Supports common encodings used in text file processing.
        'Any' is a special value that returns null - used for converting from any encoding.
 
    .EXAMPLE
        Resolve-Encoding -Name 'UTF8BOM'
        Returns a UTF8Encoding object configured to emit a BOM.
 
    .EXAMPLE
        Resolve-Encoding -Name 'ASCII'
        Returns the ASCII encoding object.
 
    .EXAMPLE
        Resolve-Encoding -Name 'Any'
        Returns null - used to indicate "any source encoding" in conversion functions.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM', 'Any')]
        [string] $Name
    )

    switch ($Name.ToUpperInvariant()) {
        'UTF8BOM' {
            return [System.Text.UTF8Encoding]::new($true)
        }
        'UTF8' {
            return [System.Text.UTF8Encoding]::new($false)
        }
        'OEM' {
            return [System.Text.Encoding]::GetEncoding([Console]::OutputEncoding.CodePage)
        }
        'ANY' {

            return $null
        }
        default {
            try {
                return [System.Text.Encoding]::$Name
            }
            catch {
                throw "Failed to resolve encoding '$Name': $($_.Exception.Message)"
            }
        }
    }
}

$Script:FormatterSettings = @{
    IncludeRules = @(
        'PSPlaceOpenBrace',
        'PSPlaceCloseBrace',
        'PSUseConsistentWhitespace',
        'PSUseConsistentIndentation',
        'PSAlignAssignmentStatement',
        'PSUseCorrectCasing'
    )

    Rules        = @{
        PSPlaceOpenBrace           = @{
            Enable             = $true
            OnSameLine         = $true
            NewLineAfter       = $true
            IgnoreOneLineBlock = $true
        }

        PSPlaceCloseBrace          = @{
            Enable             = $true
            NewLineAfter       = $false
            IgnoreOneLineBlock = $true
            NoEmptyLineBefore  = $false
        }

        PSUseConsistentIndentation = @{
            Enable              = $true
            Kind                = 'space'
            PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline'
            IndentationSize     = 4
        }

        PSUseConsistentWhitespace  = @{
            Enable          = $true
            CheckInnerBrace = $true
            CheckOpenBrace  = $true
            CheckOpenParen  = $true
            CheckOperator   = $true
            CheckPipe       = $true
            CheckSeparator  = $true
        }

        PSAlignAssignmentStatement = @{
            Enable         = $true
            CheckHashtable = $true
        }

        PSUseCorrectCasing         = @{
            Enable = $true
        }
    }
}
function Send-FilesToGitHubRelease {
    [cmdletbinding()]
    param(
        [string[]] $filePathsToUpload,
        [string] $urlToUploadFilesTo,
        $authHeader
    )
    [int] $numberOfFilesToUpload = $filePathsToUpload.Count
    [int] $numberOfFilesUploaded = 0

    $filePathsToUpload | ForEach-Object {
        $filePath = $_
        $fileName = Get-Item $filePath | Select-Object -ExpandProperty Name

        $uploadAssetWebRequestParameters =
        @{

            Uri         = $urlToUploadFilesTo + "?name=$fileName"
            Method      = 'POST'
            Headers     = $authHeader
            ContentType = 'application/zip'
            InFile      = $filePath
        }

        $numberOfFilesUploaded = $numberOfFilesUploaded + 1
        Write-Verbose "Uploading asset $numberOfFilesUploaded of $numberOfFilesToUpload, '$filePath'."
        Invoke-RestMethodAndThrowDescriptiveErrorOnFailure $uploadAssetWebRequestParameters > $null
    }
}

function Start-ArtefactsBuilding {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $ChosenArtefact,
        [System.Collections.IDictionary] $Configuration,
        [string] $FullProjectPath,
        [System.Collections.IDictionary] $DestinationPaths,
        [ValidateSet('ReleasesUnpacked', 'Releases')][string] $Type
    )

    if ($Artefact) {
        $Artefact = $ChosenArtefact
        $ChosenType = $Artefact.Type
        $ID = if ($ChosenArtefact.ID) {
            $ChosenArtefact.ID 
        }
        else {
            $null 
        }
    }
    elseif ($Type) {
        if ($Configuration.Steps.BuildModule.$Type) {
            $Artefact = $Configuration.Steps.BuildModule.$Type
            $ChosenType = $Type
        }
        else {
            $Artefact = $null
        }
        $ID = $null
    }
    else {
        $ID = $null
        $Artefact = $null
    }

    if ($ID) {
        $TextToDisplay = "Preparing Artefact of type '$ChosenType' (ID: $ID)"
    }
    else {
        $TextToDisplay = "Preparing Artefact of type '$ChosenType'"
    }

    if ($null -eq $Artefact -or $Artefact.Count -eq 0) {
        return
    }

    $ModuleName = $Configuration.Information.ModuleName
    $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion

    $FullProjectPath = Initialize-ReplacePath -ReplacementPath $FullProjectPath -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
    $Artefact.Path = Initialize-ReplacePath -ReplacementPath $Artefact.Path -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration

    Write-TextWithTime -Text $TextToDisplay -PreAppend Information -SpacesBefore ' ' {
        if ($Artefact -or $Artefact.Enabled) {
            if ($Artefact -is [System.Collections.IDictionary]) {
                if ($Artefact.Path) {
                    if ($Artefact.Relative -eq $false) {
                        $FolderPathReleases = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Artefact.Path)
                    }
                    else {
                        $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Artefact.Path)
                    }
                }
                else {
                    $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type)
                }
            }
            else {

                $FolderPathReleases = [System.IO.Path]::Combine($FullProjectPath, $Type)
            }
            if ($Artefact.RequiredModules.ModulesPath) {
                $DirectPathForPrimaryModule = $Artefact.RequiredModules.ModulesPath
            }
            elseif ($Artefact.RequiredModules.Path) {
                $DirectPathForPrimaryModule = $Artefact.RequiredModules.Path
            }
            elseif ($Artefact.Path) {
                $DirectPathForPrimaryModule = $Artefact.Path
            }
            else {
                $DirectPathForPrimaryModule = $FolderPathReleases
            }
            if ($Artefact.RequiredModules.Path) {
                $DirectPathForRequiredModules = $Artefact.RequiredModules.Path
            }
            elseif ($Artefact.RequiredModules.ModulesPath) {
                $DirectPathForRequiredModules = $Artefact.RequiredModules.ModulesPath
            }
            elseif ($Artefact.Path) {
                $DirectPathForRequiredModules = $Artefact.Path
            }
            else {
                $DirectPathForRequiredModules = $FolderPathReleases
            }
            if ($Artefact -eq $true -or $Artefact.Enabled) {
                if ($Artefact -is [System.Collections.IDictionary]) {
                    if ($DirectPathForPrimaryModule) {
                        if ($Artefact.Relative -eq $false) {
                            $CurrentModulePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForPrimaryModule)
                        }
                        else {
                            $DirectPathForPrimaryModule = Initialize-ReplacePath -ReplacementPath $DirectPathForPrimaryModule -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
                            $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForPrimaryModule)
                        }
                    }
                    else {
                        $CurrentModulePath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType)
                    }
                    if ($DirectPathForRequiredModules) {
                        if ($Artefact.Relative -eq $false) {
                            $RequiredModulesPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DirectPathForRequiredModules)
                        }
                        else {
                            $DirectPathForRequiredModules = Initialize-ReplacePath -ReplacementPath $DirectPathForRequiredModules -ModuleName $ModuleName -ModuleVersion $ModuleVersion -Configuration $Configuration
                            $RequiredModulesPath = [System.IO.Path]::Combine($FullProjectPath, $DirectPathForRequiredModules)
                        }
                    }
                    else {
                        $RequiredModulesPath = $ArtefactsPath
                    }
                    $ArtefactsPath = $Artefact.Path
                }
                else {

                    $ArtefactsPath = [System.IO.Path]::Combine($FullProjectPath, $ChosenType, $TagName)
                    $RequiredModulesPath = $ArtefactsPath
                    $CurrentModulePath = $ArtefactsPath
                }

                if ($null -eq $Artefact.DestinationFilesRelative) {
                    if ($null -ne $Artefact.Relative) {
                        $Artefact.DestinationFilesRelative = $Artefact.Relative
                    }
                }
                if ($null -eq $Artefact.DestinationDirectoriesRelative) {
                    if ($null -ne $Artefact.Relative) {
                        $Artefact.DestinationDirectoriesRelative = $Artefact.Relative
                    }
                }

                $SplatArtefact = @{
                    ModuleName                     = $Configuration.Information.ModuleName
                    ModuleVersion                  = $Configuration.Information.Manifest.ModuleVersion
                    LegacyName                     = if ($Artefact -is [bool]) {
                        $true 
                    }
                    else {
                        $false 
                    }
                    CopyMainModule                 = $true
                    CopyRequiredModules            = $Artefact.RequiredModules -eq $true -or $Artefact.RequiredModules.Enabled
                    ProjectPath                    = $FullProjectPath
                    Destination                    = $ArtefactsPath
                    DestinationMainModule          = $CurrentModulePath
                    DestinationRequiredModules     = $RequiredModulesPath
                    RequiredModules                = $Configuration.Information.Manifest.RequiredModules
                    Files                          = $Artefact.FilesOutput
                    Folders                        = $Artefact.DirectoryOutput
                    DestinationFilesRelative       = $Artefact.DestinationFilesRelative
                    DestinationDirectoriesRelative = $Artefact.DestinationDirectoriesRelative
                    Configuration                  = $Configuration
                    IncludeTag                     = $Artefact.IncludeTagName
                    ArtefactName                   = $Artefact.ArtefactName
                    ScriptName                     = $Artefact.ScriptName
                    ZipIt                          = if ($ChosenType -in 'Packed', 'Releases', 'ScriptPacked') {
                        $true 
                    }
                    else {
                        $false 
                    }
                    ConvertToScript                = if ($ChosenType -in 'ScriptPacked', 'Script') {
                        $true 
                    }
                    else {
                        $false 
                    }
                    DestinationZip                 = $ArtefactsPath
                    PreScriptMerge                 = $Artefact.PreScriptMerge
                    PostScriptMerge                = $Artefact.PostScriptMerge
                    DoNotClear                     = $Artefact.DoNotClear
                    ID                             = if ($ChosenArtefact.ID) {
                        $ChosenArtefact.ID 
                    }
                    else {
                        $null 
                    }
                }
                Remove-EmptyValue -Hashtable $SplatArtefact
                Add-Artefact @SplatArtefact
            }
        }
    } -ColorBefore Yellow -ColorTime Yellow -Color Yellow
}
function Start-DocumentationBuilding {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $FullProjectPath,
        [string] $ProjectName
    )

    if ($Configuration.Steps.BuildDocumentation -is [bool]) {
        $TemporaryBuildDocumentation = $Configuration.Steps.BuildDocumentation
        $Configuration.Steps.BuildDocumentation = @{
            Enable = $TemporaryBuildDocumentation
        }
    }

    if ($Configuration.Steps.BuildDocumentation -is [System.Collections.IDictionary]) {
        if ($Configuration.Steps.BuildDocumentation.Enable -eq $true) {
            $WarningVariablesMarkdown = @()
            $DocumentationPath = "$FullProjectPath\$($Configuration.Options.Documentation.Path)"
            $ReadMePath = "$FullProjectPath\$($Configuration.Options.Documentation.PathReadme)"
            Write-Text "[+] Generating documentation to $DocumentationPath with $ReadMePath" -Color Yellow

            if (-not (Test-Path -Path $DocumentationPath)) {
                $null = New-Item -Path "$FullProjectPath\Docs" -ItemType Directory -Force
            }
            if ($Configuration.Steps.BuildDocumentation.Tool -eq 'HelpOut') {
                try {
                    Save-MarkdownHelp -Module $ProjectName -OutputPath $DocumentationPath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -SkipCommandType Alias -ExcludeFile "*.svg"
                }
                catch {
                    Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow
                }
            }
            else {
                [Array] $Files = Get-ChildItem -Path $DocumentationPath
                if ($Files.Count -gt 0) {
                    if ($Configuration.Steps.BuildDocumentation.StartClean -ne $true) {
                        try {
                            $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow
                        }
                        catch {
                            Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow
                        }
                    }
                    else {
                        Remove-ItemAlternative -Path $DocumentationPath -SkipFolder
                        [Array] $Files = Get-ChildItem -Path $DocumentationPath
                    }
                }
                if ($Files.Count -eq 0) {
                    try {
                        $null = New-MarkdownHelp -Module $ProjectName -WithModulePage -OutputFolder $DocumentationPath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow
                    }
                    catch {
                        Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow
                    }
                    $null = Move-Item -Path "$DocumentationPath\$ProjectName.md" -Destination $ReadMePath -ErrorAction SilentlyContinue

                    if ($Configuration.Steps.BuildDocumentation.UpdateWhenNew) {
                        try {
                            $null = Update-MarkdownHelpModule $DocumentationPath -RefreshModulePage -ModulePagePath $ReadMePath -ErrorAction Stop -WarningVariable +WarningVariablesMarkdown -WarningAction SilentlyContinue -ExcludeDontShow
                        }
                        catch {
                            Write-Text "[-] Documentation warning: $($_.Exception.Message)" -Color Yellow
                        }
                    }
                }
            }
            foreach ($_ in $WarningVariablesMarkdown) {
                Write-Text "[-] Documentation warning: $_" -Color Yellow
            }
        }
    }
}
function Start-ImportingModules {
    [CmdletBinding()]
    param(
        [string] $ProjectName,
        [System.Collections.IDictionary] $Configuration
    )
    $TemporaryVerbosePreference = $VerbosePreference
    $TemporaryErrorPreference = $global:ErrorActionPreference
    $global:ErrorActionPreference = 'Stop'

    if ($null -ne $ImportModules.Verbose) {
        $VerbosePreference = $true
    }
    else {
        $VerbosePreference = $false
    }
    if ($Configuration.Steps.ImportModules.RequiredModules) {
        Write-TextWithTime -Text 'Importing modules (as defined in dependencies)' {
            foreach ($Module in $Configuration.Information.Manifest.RequiredModules) {
                if ($Module -is [System.Collections.IDictionary]) {
                    Write-Text " [>] Importing required module - $($Module.ModuleName)" -Color Yellow
                    if ($Module.ModuleVersion) {
                        Import-Module -Name $Module.ModuleName -MinimumVersion $Module.ModuleVersion -Force -ErrorAction Stop -Verbose:$VerbosePreference
                    }
                    elseif ($Module.ModuleName) {
                        Import-Module -Name $Module.ModuleName -Force -ErrorAction Stop -Verbose:$VerbosePreference
                    }
                }
                elseif ($Module -is [string]) {
                    Write-Text " [>] Importing required module - $($Module)" -Color Yellow
                    Import-Module -Name $Module -Force -ErrorAction Stop -Verbose:$VerbosePreference
                }
            }
        } -PreAppend 'Information'
    }
    if ($Configuration.Steps.ImportModules.Self) {
        Write-TextWithTime -Text 'Importing module - SELF' {

            Import-Module -Name $ProjectName -Force -ErrorAction Stop -Verbose:$VerbosePreference
        } -PreAppend 'Information'
    }
    $global:ErrorActionPreference = $TemporaryErrorPreference
    $VerbosePreference = $TemporaryVerbosePreference
}
function Start-LibraryBuilding {
    [CmdletBinding()]
    param(
        [string] $ModuleName,
        [string] $RootDirectory,
        [string] $Version,
        [System.Collections.IDictionary] $LibraryConfiguration,
        [System.Collections.IDictionary] $CmdletsAliases

    )
    if ($LibraryConfiguration.Count -eq 0) {
        return
    }
    if ($LibraryConfiguration.Enable -ne $true) {
        return
    }
    $TranslateFrameworks = [ordered] @{
        'net472'         = 'Default'
        'net48'          = 'Default'
        'net482'         = 'Default'
        'net470'         = 'Default'
        'net471'         = 'Default'
        'net452'         = 'Default'
        'net451'         = 'Default'
        'NetStandard2.0' = 'Standard'
        'netStandard2.1' = 'Standard'
        'netcoreapp2.1'  = 'Core'
        'netcoreapp3.1'  = 'Core'
        'net5.0'         = 'Core'
        'net6.0'         = 'Core'
        'net6.0-windows' = 'Core'
        'net7.0'         = 'Core'
        'net7.0-windows' = 'Core'
        'net8.0'         = 'Core'
    }

    if ($LibraryConfiguration.Configuration) {
        $Configuration = $LibraryConfiguration.Configuration
    }
    else {
        $Configuration = 'Release'
    }
    if ($LibraryConfiguration.ProjectName) {
        $ModuleName = $LibraryConfiguration.ProjectName
    }
    if ($LibraryConfiguration.NETProjectPath) {
        $ModuleProjectFile = [System.IO.Path]::Combine($LibraryConfiguration.NETProjectPath, "$($LibraryConfiguration.ProjectName).csproj")
        $SourceFolder = [System.IO.Path]::Combine($LibraryConfiguration.NETProjectPath)
    }
    else {
        $ModuleProjectFile = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName, "$ModuleName.csproj")
        $SourceFolder = [System.IO.Path]::Combine($RootDirectory, "Sources", $ModuleName)
    }
    $ModuleBinFolder = [System.IO.Path]::Combine($RootDirectory, "Lib")
    if (Test-Path -LiteralPath $ModuleBinFolder) {
        $Items = Get-ChildItem -LiteralPath $ModuleBinFolder -Recurse -Force
        $Items | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
    }
    $null = New-Item -Path $ModuleBinFolder -ItemType Directory -Force

    try {
        Push-Location -Path $SourceFolder -ErrorAction Stop
    }
    catch {
        Write-Text "[-] Couldn't switch to folder $SourceFolder. Error: $($_.Exception.Message)" -Color Red
        return $false
    }
    try {
        [xml] $ProjectInformation = Get-Content -Raw -LiteralPath $ModuleProjectFile -Encoding UTF8 -ErrorAction Stop
    }
    catch {
        Write-Text "[-] Can't read $ModuleProjectFile file. Error: $($_.Exception.Message)" -Color Red
        return $false
    }

    if ($IsLinux) {
        $OSVersion = 'Linux'
    }
    elseif ($IsMacOS) {
        $OSVersion = 'OSX'
    }
    else {
        $OSVersion = 'Windows'
    }

    $SupportedFrameworks = foreach ($PropertyGroup in $ProjectInformation.Project.PropertyGroup) {
        if ($PropertyGroup.TargetFrameworks) {
            if ($PropertyGroup.TargetFrameworks -is [array]) {
                foreach ($Target in $PropertyGroup.TargetFrameworks) {

                    if ($Target.Condition -and $Target.Condition -like "*$OSVersion*" -and $Target.'#text') {
                        $Target.'#text'.Trim() -split ";"
                    }
                    else {

                        $Target.'#text'.Trim() -split ";"
                    }
                }
            }
            else {

                $PropertyGroup.TargetFrameworks -split ";"
            }
        }
        elseif ($PropertyGroup.TargetFrameworkVersion) {
            throw "TargetFrameworkVersion is not supported. Please use TargetFrameworks/TargetFramework instead which may require different project profile."
        }
        elseif ($PropertyGroup.TargetFramework) {
            $PropertyGroup.TargetFramework
        }
    }

    $Count = 0
    foreach ($Framework in $TranslateFrameworks.Keys) {
        if ($SupportedFrameworks.Contains($Framework.ToLower()) -and $LibraryConfiguration.Framework.Contains($Framework.ToLower())) {
            Write-Text "[+] Building $Framework ($Configuration)"
            $null = dotnet publish --configuration $Configuration --verbosity q -nologo -p:Version=$Version --framework $Framework
            if ($LASTEXITCODE) {
                Write-Host 
                Write-Text "[-] Building $Framework - failed. Error: $LASTEXITCODE" -Color Red
                exit
            }
        }
        else {
            continue
        }

        $InitialFolder = [System.IO.Path]::Combine($SourceFolder, "bin", $Configuration, $Framework, "publish")
        $InitialFolder = (Resolve-Path -Path $InitialFolder).Path
        $PublishDirFolder = [System.IO.Path]::Combine($SourceFolder, "bin", $Configuration, $Framework, "publish", "*")
        $ModuleBinFrameworkFolder = [System.IO.Path]::Combine($ModuleBinFolder, $TranslateFrameworks[$Framework])

        $null = New-Item -Path $ModuleBinFrameworkFolder -ItemType Directory -ErrorAction SilentlyContinue

        try {
            $List = Get-ChildItem -Filter "*" -ErrorAction Stop -Path $PublishDirFolder -File -Recurse
            $List = @(
                foreach ($File in $List) {
                    if ($File.Extension -in '.dll', '.so', 'dylib') {
                        $File
                    }
                }
            )
        }
        catch {
            Write-Text "[-] Can't list files in $PublishDirFolder folder. Error: $($_.Exception.Message)" -Color Red
            return $false
        }
        $Errors = $false

        Write-Text -Text "[i] Preparing copying module files for $ModuleName from $InitialFolder" -Color DarkGray

        :fileLoop foreach ($File in $List) {
            if ($LibraryConfiguration.ExcludeMainLibrary -and $File.Name -eq "$ModuleName.dll") {
                continue
            }
            if ($LibraryConfiguration.ExcludeLibraryFilter) {
                foreach ($Library in $LibraryConfiguration.ExcludeLibraryFilter) {
                    if ($File.Name -like $Library) {
                        continue fileLoop
                    }
                }
            }

            if (-not $LibraryConfiguration.BinaryModuleCmdletScanDisabled) {

                $SkipAssembly = $false
                if ($PSVersionTable.PSEdition -eq 'Core') {
                    if ($TranslateFrameworks[$Framework] -eq 'Default') {
                        $SkipAssembly = $true
                    }
                }
                else {
                    if ($TranslateFrameworks[$Framework] -eq 'Core') {
                        $SkipAssembly = $true
                    }
                }

                if (-not $SkipAssembly) {

                    if ($InitialFolder -eq $File.DirectoryName) {
                        $CmdletsFound = Get-PowerShellAssemblyMetadata -Path $File.FullName
                        if ($CmdletsFound -eq $false) {
                            $Errors = $true
                        }
                        else {
                            if ($CmdletsFound.CmdletsToExport.Count -gt 0 -or $CmdletsFound.AliasesToExport.Count -gt 0) {
                                Write-Text -Text "Found $($CmdletsFound.CmdletsToExport.Count) cmdlets and $($CmdletsFound.AliasesToExport.Count) aliases in $File" -Color Yellow -PreAppend Information -SpacesBefore " "
                                if ($CmdletsFound.CmdletsToExport.Count -gt 0) {
                                    Write-Text -Text "Cmdlets: $($CmdletsFound.CmdletsToExport -join ', ')" -Color Yellow -PreAppend Plus -SpacesBefore " "
                                }
                                if ($CmdletsFound.AliasesToExport.Count -gt 0) {
                                    Write-Text -Text "Aliases: $($CmdletsFound.AliasesToExport -join ', ')" -Color Yellow -PreAppend Plus -SpacesBefore " "
                                }
                                $CmdletsAliases[$File.FullName] = $CmdletsFound
                            }
                        }
                    }
                    else {
                        Write-Text -Text " [-] Skipping $($File.FullName) as it's not in the same folder as the assembly" -Color Yellow
                    }
                }
            }
            try {
                if ($LibraryConfiguration.NETDoNotCopyLibrariesRecursively) {
                    Write-Text -Text " [+] Copying '$($File.FullName)' to folder '$ModuleBinFrameworkFolder'" -Color DarkGray
                    Copy-Item -Path $File.FullName -Destination $ModuleBinFrameworkFolder -ErrorAction Stop
                }
                else {

                    $relativePath = $File.FullName.Substring($InitialFolder.Length + 1)

                    $destinationFilePath = [System.IO.Path]::Combine($ModuleBinFrameworkFolder, $relativePath)

                    $destinationDirectory = [System.IO.Path]::GetDirectoryName($destinationFilePath)
                    if (-not (Test-Path -Path $destinationDirectory)) {
                        New-Item -ItemType Directory -Path $destinationDirectory -Force
                    }
                    Write-Text -Text " [+] Copying '$($File.FullName)' as file '$destinationFilePath'" -Color DarkGray

                    Copy-Item -Path $File.FullName -Destination $destinationFilePath -ErrorAction Stop
                }
            }
            catch {
                Write-Text " [-] Copying $File to $ModuleBinFrameworkFolder failed. Error: $($_.Exception.Message)" -Color Red
                $Errors = $true
            }
        }
        if ($Errors) {
            return $false
        }

        Write-Text -Text "[i] Preparing copying module files for $ModuleName from $($InitialFolder). Completed!" -Color DarkGray

        if ($LibraryConfiguration.NETBinaryModuleDocumentation) {
            $Errors = $false
            try {
                $List = Get-ChildItem -Filter "*.dll-Help.xml" -ErrorAction Stop -Path $PublishDirFolder -File
            }
            catch {
                Write-Text "[-] Can't list files in $PublishDirFolder folder. Error: $($_.Exception.Message)" -Color Red
                return $false
            }
            :fileLoop foreach ($File in $List) {
                if ($LibraryConfiguration.ExcludeMainLibrary -and $File.Name -eq "$ModuleName.dll") {
                    continue
                }

                $Culture = 'en-US'

                $TargetPathFolder = [System.IO.Path]::Combine($ModuleBinFrameworkFolder, $Culture)
                $TargetPath = [System.IO.Path]::Combine($TargetPathFolder, $File.Name)
                if (-not (Test-Path -Path $TargetPathFolder)) {
                    $null = New-Item -Path $TargetPathFolder -ItemType Directory -ErrorAction SilentlyContinue
                }
                try {
                    Copy-Item -Path $File.FullName -Destination $TargetPath -ErrorAction Stop
                }
                catch {
                    Write-Text "[-] Copying $File to $TargetPath failed. Error: $($_.Exception.Message)" -Color Red
                    $Errors = $true
                }
            }
            if ($Errors) {
                return $false
            }
        }
    }
    try {
        Pop-Location -ErrorAction Stop
    }
    catch {
        Write-Text "[-] Couldn't switch back to the root folder. Error: $($_.Exception.Message)" -Color Red
        return $false
    }
}
function Start-ModuleBuilding {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $PathToProject
    )
    $DestinationPaths = [ordered] @{ }
    if ($Configuration.Information.Manifest.CompatiblePSEditions) {
        if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Desktop') {
            $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName)
        }
        if ($Configuration.Information.Manifest.CompatiblePSEditions -contains 'Core') {
            $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName)
        }
    }
    else {

        $DestinationPaths.Desktop = [IO.path]::Combine($Configuration.Information.DirectoryModules, $Configuration.Information.ModuleName)
        $DestinationPaths.Core = [IO.path]::Combine($Configuration.Information.DirectoryModulesCore, $Configuration.Information.ModuleName)
    }

    [string] $Random = Get-Random 10000000000
    [string] $FullModuleTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName
    [string] $FullTemporaryPath = [IO.path]::GetTempPath() + '' + $Configuration.Information.ModuleName + "_TEMP_$Random"
    if ($Configuration.Information.DirectoryProjects) {
        [string] $FullProjectPath = [IO.Path]::Combine($Configuration.Information.DirectoryProjects, $Configuration.Information.ModuleName)
    }
    else {
        [string] $FullProjectPath = $PathToProject
    }
    [string] $ProjectName = $Configuration.Information.ModuleName

    $PSD1FilePath = [System.IO.Path]::Combine($FullProjectPath, "$ProjectName.psd1")
    $PSM1FilePath = [System.IO.Path]::Combine($FullProjectPath, "$ProjectName.psm1")

    if ($Configuration.Information.Manifest.ModuleVersion) {
        if ($Configuration.Steps.BuildModule.LocalVersion) {
            $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced -LocalPSD1 $PSD1FilePath
        }
        else {
            $Versioning = Step-Version -Module $Configuration.Information.ModuleName -ExpectedVersion $Configuration.Information.Manifest.ModuleVersion -Advanced
        }
        $Configuration.Information.Manifest.ModuleVersion = $Versioning.Version
    }
    else {

        $Configuration.Information.Manifest.ModuleVersion = 1.0.0
    }
    Write-Text '----------------------------------------------------'
    Write-Text "[i] Project/Module Name: $ProjectName" -Color Yellow
    if ($Configuration.Steps.BuildModule.LocalVersion) {
        Write-Text "[i] Current Local Version: $($Versioning.CurrentVersion)" -Color Yellow
    }
    else {
        Write-Text "[i] Current PSGallery Version: $($Versioning.CurrentVersion)" -Color Yellow
    }
    Write-Text "[i] Expected Version: $($Configuration.Information.Manifest.ModuleVersion)" -Color Yellow
    Write-Text "[i] Full module temporary path: $FullModuleTemporaryPath" -Color Yellow
    Write-Text "[i] Full project path: $FullProjectPath" -Color Yellow
    Write-Text "[i] Full temporary path: $FullTemporaryPath" -Color Yellow
    Write-Text "[i] PSScriptRoot: $PSScriptRoot" -Color Yellow
    Write-Text "[i] Current PSEdition: $PSEdition" -Color Yellow
    Write-Text "[i] Destination Desktop: $($DestinationPaths.Desktop)" -Color Yellow
    Write-Text "[i] Destination Core: $($DestinationPaths.Core)" -Color Yellow
    Write-Text '----------------------------------------------------'

    if (-not $Configuration.Steps.BuildModule) {
        Write-Text '[-] Section BuildModule is missing. Terminating.' -Color Red
        return $false
    }

    if (-not $Configuration.Information.ModuleName) {
        Write-Text '[-] Section Information.ModuleName is missing. Terminating.' -Color Red
        return $false
    }

    if (-not (Test-Path -Path $FullProjectPath)) {
        Write-Text "[-] Project path doesn't exists $FullProjectPath. Terminating" -Color Red
        return $false
    }

    $CmdletsAliases = [ordered] @{}

    $startLibraryBuildingSplat = @{
        RootDirectory        = $FullProjectPath
        Version              = $Configuration.Information.Manifest.ModuleVersion
        ModuleName           = $ProjectName
        LibraryConfiguration = $Configuration.Steps.BuildLibraries
        CmdletsAliases       = $CmdletsAliases
    }

    $Success = Start-LibraryBuilding @startLibraryBuildingSplat
    if ($Success -contains $false) {
        return $false
    }

    $Success = Convert-RequiredModules -Configuration $Configuration
    if ($Success -eq $false) {
        return $false
    }

    if ($Configuration.Steps.BuildModule.Enable -eq $true) {
        $CurrentLocation = (Get-Location).Path

        $Success = Start-PreparingStructure -Configuration $Configuration -FullProjectPath $FullProjectPath -FullTemporaryPath $FullTemporaryPath -FullModuleTemporaryPath $FullModuleTemporaryPath -DestinationPaths $DestinationPaths
        if ($Success -eq $false) {
            return $false
        }

        $Variables = Start-PreparingVariables -Configuration $Configuration -FullProjectPath $FullProjectPath
        if ($Variables -eq $false) {
            return $false
        }

        $LinkDirectories = $Variables.LinkDirectories
        $LinkFilesRoot = $Variables.LinkFilesRoot
        $LinkPrivatePublicFiles = $Variables.LinkPrivatePublicFiles
        $DirectoriesWithClasses = $Variables.DirectoriesWithClasses
        $DirectoriesWithPS1 = $Variables.DirectoriesWithPS1
        $Files = $Variables.Files

        $startPreparingFunctionsAndAliasesSplat = @{
            Configuration   = $Configuration
            FullProjectPath = $FullProjectPath
            Files           = $Files
            CmdletsAliases  = $CmdletsAliases
        }

        $AliasesAndFunctions = Start-PreparingFunctionsAndAliases @startPreparingFunctionsAndAliasesSplat
        if ($AliasesAndFunctions -contains $false) {
            return $false
        }

        $SaveConfiguration = Copy-DictionaryManual -Dictionary $Configuration

        $newPersonalManifestSplat = @{
            Configuration       = $Configuration
            ManifestPath        = $PSD1FilePath
            AddScriptsToProcess = $true
        }
        if ($Configuration.Steps.BuildModule.UseWildcardForFunctions) {
            $newPersonalManifestSplat.UseWildcardForFunctions = $Configuration.Steps.BuildModule.UseWildcardForFunctions
        }

        if ($Configuration.Steps.BuildLibraries.BinaryModule) {
            $newPersonalManifestSplat.BinaryModule = $Configuration.Steps.BuildLibraries.BinaryModule
        }
        $Success = New-PersonalManifest @newPersonalManifestSplat
        if ($Success -eq $false) {
            return $false
        }

        Write-TextWithTime -Text "Verifying created PSD1 is readable" -PreAppend Information {
            if (Test-Path -LiteralPath $PSD1FilePath) {
                try {
                    $null = Import-PowerShellDataFile -Path $PSD1FilePath -ErrorAction Stop
                }
                catch {
                    Write-Text "[-] PSD1 Reading $PSD1FilePath failed. Error: $($_.Exception.Message)" -Color Red
                    return $false
                }
            }
            else {
                Write-Text "[-] PSD1 Reading $PSD1FilePath failed. File not created..." -Color Red
                return $false
            }
        } -ColorBefore Yellow -ColorTime Yellow -Color Yellow

        $Configuration = $SaveConfiguration

        $Success = Format-Code -FilePath $PSD1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSD1
        if ($Success -eq $false) {
            return $false
        }
        $Success = Format-Code -FilePath $PSM1FilePath -FormatCode $Configuration.Options.Standard.FormatCodePSM1
        if ($Success -eq $false) {
            return $false
        }

        if ($Configuration.Steps.BuildModule.RefreshPSD1Only) {
            return
        }

        $startModuleMergingSplat = @{
            Configuration           = $Configuration
            ProjectName             = $ProjectName
            FullTemporaryPath       = $FullTemporaryPath
            FullModuleTemporaryPath = $FullModuleTemporaryPath
            FullProjectPath         = $FullProjectPath
            LinkDirectories         = $LinkDirectories
            LinkFilesRoot           = $LinkFilesRoot
            LinkPrivatePublicFiles  = $LinkPrivatePublicFiles
            DirectoriesWithPS1      = $DirectoriesWithPS1
            DirectoriesWithClasses  = $DirectoriesWithClasses
            AliasesAndFunction      = $AliasesAndFunctions
            CmdletsAliases          = $CmdletsAliases
        }

        $Success = Start-ModuleMerging @startModuleMergingSplat
        if ($Success -contains $false) {
            return $false
        }

        Set-Location -Path $CurrentLocation

        $Success = if ($Configuration.Steps.BuildModule.Enable) {
            if ($DestinationPaths.Desktop) {
                Write-TextWithTime -Text "Copy module to PowerShell 5 destination: $($DestinationPaths.Desktop)" {
                    $Success = Remove-Directory -Directory $DestinationPaths.Desktop
                    if ($Success -eq $false) {
                        return $false
                    }
                    Add-Directory -Directory $DestinationPaths.Desktop
                    Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Desktop -Recurse

                    Get-ChildItem $DestinationPaths.Desktop -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | `
                            Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | `
                                Remove-Item 
                } -PreAppend Plus
            }
            if ($DestinationPaths.Core) {
                Write-TextWithTime -Text "Copy module to PowerShell 6/7 destination: $($DestinationPaths.Core)" {
                    $Success = Remove-Directory -Directory $DestinationPaths.Core
                    if ($Success -eq $false) {
                        return $false
                    }
                    Add-Directory -Directory $DestinationPaths.Core
                    Get-ChildItem -LiteralPath $FullModuleTemporaryPath | Copy-Item -Destination $DestinationPaths.Core -Recurse

                    Get-ChildItem $DestinationPaths.Core -Recurse -Force -Directory | Sort-Object -Property FullName -Descending | `
                            Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } | `
                                Remove-Item 
                } -PreAppend Plus
            }
        }
        if ($Success -contains $false) {
            return $false
        }
        $Success = Write-TextWithTime -Text "Building artefacts" -PreAppend Information {

            $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'Releases'
            if ($Success -eq $false) {
                return $false
            }

            $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -Type 'ReleasesUnpacked'
            if ($Success -eq $false) {
                return $false
            }

            foreach ($Artefact in  $Configuration.Steps.BuildModule.Artefacts) {
                $Success = Start-ArtefactsBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -DestinationPaths $DestinationPaths -ChosenArtefact $Artefact
                if ($Success -contains $false) {
                    return $false
                }
            }
        } -ColorBefore Yellow -ColorTime Yellow -Color Yellow
        if ($Success -contains $false) {
            return $false
        }
    }

    if ($Configuration.Steps.ImportModules) {
        $ImportSuccess = Start-ImportingModules -Configuration $Configuration -ProjectName $ProjectName
        if ($ImportSuccess -contains $false) {
            return $false
        }
    }

    if ($Configuration.Options.TestsAfterMerge) {
        $TestsSuccess = Initialize-InternalTests -Configuration $Configuration -Type 'TestsAfterMerge'
        if ($TestsSuccess -eq $false) {
            return $false
        }
    }

    if ($Configuration.Steps.PublishModule.Enabled) {
        $Publishing = Start-PublishingGallery -Configuration $Configuration
        if ($Publishing -eq $false) {
            return $false
        }
    }

    if ($Configuration.Steps.PublishModule.GitHub) {
        $Publishing = Start-PublishingGitHub -Configuration $Configuration -ProjectName $ProjectName
        if ($Publishing -eq $false) {
            return $false
        }
    }

    foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GalleryNugets) {
        $Success = Start-PublishingGallery -Configuration $Configuration -ChosenNuget $ChosenNuget
        if ($Success -eq $false) {
            return $false
        }
    }

    foreach ($ChosenNuget in $Configuration.Steps.BuildModule.GitHubNugets) {
        $Success = Start-PublishingGitHub -Configuration $Configuration -ChosenNuget $ChosenNuget -ProjectName $ProjectName
        if ($Success -eq $false) {
            return $false
        }
    }

    if ($Configuration.Steps.BuildDocumentation) {
        Start-DocumentationBuilding -Configuration $Configuration -FullProjectPath $FullProjectPath -ProjectName $ProjectName
    }

    Write-Text "[+] Cleaning up directories created in TEMP directory" -Color Yellow
    $Success = Remove-Directory $FullModuleTemporaryPath
    if ($Success -eq $false) {
        return $false
    }
    $Success = Remove-Directory $FullTemporaryPath
    if ($Success -eq $false) {
        return $false
    }
}
function Start-ModuleMerging {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $ProjectName,
        [string] $FullTemporaryPath,
        [string] $FullModuleTemporaryPath,
        [string] $FullProjectPath,
        [Array] $LinkDirectories,
        [Array] $LinkFilesRoot,
        [Array] $LinkPrivatePublicFiles,
        [string[]] $DirectoriesWithPS1,
        [string[]] $DirectoriesWithClasses,
        [System.Collections.IDictionary] $AliasesAndFunctions,
        [System.Collections.IDictionary] $CmdletsAliases
    )
    if ($Configuration.Steps.BuildModule.Merge) {
        foreach ($Directory in $LinkDirectories) {
            $Dir = [System.IO.Path]::Combine($FullTemporaryPath, "$Directory")
            Add-Directory -Directory $Dir
        }

        [Array] $CompareWorkaround = foreach ($Directory in $DirectoriesWithPS1) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $Dir = -join ($Directory, "\")
            }
            else {
                $Dir = -join ($Directory, "/")
            }
        }

        $LinkDirectoriesWithSupportFiles = $LinkDirectories | Where-Object { $_ -notin $CompareWorkaround }
        foreach ($Directory in $LinkDirectoriesWithSupportFiles) {
            $Dir = [System.IO.Path]::Combine($FullModuleTemporaryPath, "$Directory")
            Add-Directory -Directory $Dir
        }

        $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start
        Copy-InternalFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath
        Copy-InternalFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullTemporaryPath -FullProjectPath $FullProjectPath
        Write-Text -End -Time $LinkingFilesTime

        $FilesToLink = $LinkPrivatePublicFiles | Where-Object { $_ -notlike '*.ps1' -and $_ -notlike '*.psd1' }
        Copy-InternalFiles -LinkFiles $FilesToLink -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath

        if ($Configuration.Information.LibrariesStandard) {
        }
        elseif ($Configuration.Information.LibrariesCore -and $Configuration.Information.LibrariesDefault) {
        }
        else {

            $Configuration.Information.LibrariesStandard = [System.IO.Path]::Combine("Lib", "Standard")
            $Configuration.Information.LibrariesCore = [System.IO.Path]::Combine("Lib", "Core")
            $Configuration.Information.LibrariesDefault = [System.IO.Path]::Combine("Lib", "Default")
        }

        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesCore)) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $StartsWithCore = -join ($Configuration.Information.LibrariesCore, "\")
            }
            else {
                $StartsWithCore = -join ($Configuration.Information.LibrariesCore, "/")
            }
        }
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesDefault)) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $StartsWithDefault = -join ($Configuration.Information.LibrariesDefault, "\")
            }
            else {
                $StartsWithDefault = -join ($Configuration.Information.LibrariesDefault, "/")
            }
        }
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.LibrariesStandard)) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $StartsWithStandard = -join ($Configuration.Information.LibrariesStandard, "\")
            }
            else {
                $StartsWithStandard = -join ($Configuration.Information.LibrariesStandard, "/")
            }
        }
        if ($null -ne $LinkPrivatePublicFiles) {
            $CoreFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithCore) }
            $DefaultFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithDefault) }
            $StandardFiles = $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithStandard) }
        }
        else {
            $CoreFiles = @()
            $DefaultFiles = @()
            $StandardFiles = @()
        }
        $Default = $false
        $Core = $false
        $Standard = $false
        if ($CoreFiles.Count -gt 0) {
            $Core = $true
        }
        if ($DefaultFiles.Count -gt 0) {
            $Default = $true
        }
        if ($StandardFiles.Count -gt 0) {
            $Standard = $true
        }
        if ($Standard -and $Core -and $Default) {
            $FrameworkNet = 'Default'
            $Framework = 'Standard'
        }
        elseif ($Standard -and $Core) {
            $Framework = 'Standard'
            $FrameworkNet = 'Standard'
        }
        elseif ($Core -and $Default) {
            $Framework = 'Core'
            $FrameworkNet = 'Default'
        }
        elseif ($Standard -and $Default) {
            $Framework = 'Standard'
            $FrameworkNet = 'Default'
        }
        elseif ($Standard) {
            $Framework = 'Standard'
            $FrameworkNet = 'Standard'
        }
        elseif ($Core) {
            $Framework = 'Core'
            $FrameworkNet = ''
        }
        elseif ($Default) {
            $Framework = ''
            $FrameworkNet = 'Default'
        }

        if ($Framework -eq 'Core') {
            $FilesLibrariesCore = $CoreFiles
        }
        elseif ($Framework -eq 'Standard') {
            $FilesLibrariesCore = $StandardFiles
        }
        if ($FrameworkNet -eq 'Default') {
            $FilesLibrariesDefault = $DefaultFiles
        }
        elseif ($FrameworkNet -eq 'Standard') {
            $FilesLibrariesDefault = $StandardFiles
        }
        if ($FrameworkNet -eq 'Standard' -and $Framework -eq 'Standard') {
            $FilesLibrariesStandard = $FilesLibrariesCore
        }

        $mergeModuleSplat = @{
            ModuleName          = $ProjectName
            ModulePathSource    = $FullTemporaryPath
            ModulePathTarget    = $FullModuleTemporaryPath
            Sort                = $Configuration.Options.Merge.Sort
            FunctionsToExport   = $Configuration.Information.Manifest.FunctionsToExport
            AliasesToExport     = $Configuration.Information.Manifest.AliasesToExport
            AliasesAndFunctions = $AliasesAndFunctions
            CmdletsAliases      = $CmdletsAliases
            LibrariesStandard   = $FilesLibrariesStandard
            LibrariesCore       = $FilesLibrariesCore
            LibrariesDefault    = $FilesLibrariesDefault
            FormatCodePSM1      = $Configuration.Options.Merge.FormatCodePSM1
            FormatCodePSD1      = $Configuration.Options.Merge.FormatCodePSD1
            Configuration       = $Configuration
            DirectoriesWithPS1  = $DirectoriesWithPS1
            ClassesPS1          = $DirectoriesWithClasses
            IncludeAsArray      = $Configuration.Information.IncludeAsArray
        }

        $Success = Merge-Module @mergeModuleSplat

        if ($Success -eq $false) {
            return $false
        }

        if ($Configuration.Steps.BuildModule.CreateFileCatalog) {

            $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew()
            Write-Text "[+] Creating file catalog" -Color Blue
            $TimeToExecuteSign = [System.Diagnostics.Stopwatch]::StartNew()
            $CategoryPaths = @(
                $FullModuleTemporaryPath
                $NotEmptyPaths = (Get-ChildItem -Directory -Path $FullModuleTemporaryPath -Recurse).FullName
                if ($NotEmptyPaths) {
                    $NotEmptyPaths
                }
            )
            foreach ($CatPath in $CategoryPaths) {
                $CatalogFile = [io.path]::Combine($CatPath, "$ProjectName.cat")
                $FileCreated = New-FileCatalog -Path $CatPath -CatalogFilePath $CatalogFile -CatalogVersion 2.0
                if ($FileCreated) {
                    Write-Text " [>] Catalog file covering $CatPath was created $($FileCreated.Name)" -Color Yellow
                }
            }
            $TimeToExecuteSign.Stop()
            Write-Text "[+] Creating file catalog [Time: $($($TimeToExecuteSign.Elapsed).Tostring())]" -Color Blue
        }
        $SuccessFullSigning = Start-ModuleSigning -Configuration $Configuration -FullModuleTemporaryPath $FullModuleTemporaryPath
        if ($SuccessFullSigning -eq $false) {
            return $false
        }
    }
    else {
        foreach ($Directory in $LinkDirectories) {
            $Dir = [System.IO.Path]::Combine($FullModuleTemporaryPath, "$Directory")
            Add-Directory -Directory $Dir
        }
        $LinkingFilesTime = Write-Text "[+] Linking files from root and sub directories" -Start
        Copy-InternalFiles -LinkFiles $LinkFilesRoot -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath
        Copy-InternalFiles -LinkFiles $LinkPrivatePublicFiles -FullModulePath $FullModuleTemporaryPath -FullProjectPath $FullProjectPath
        Write-Text -End -Time $LinkingFilesTime
    }
}
function Start-ModuleSigning {
    [CmdletBinding()]
    param(
        $Configuration,
        $FullModuleTemporaryPath
    )
    if ($Configuration.Steps.BuildModule.SignMerged) {
        Write-TextWithTime -Text 'Applying signature to files' {
            $registerCertificateSplat = @{
                WarningAction   = 'SilentlyContinue'
                WarningVariable = 'Warnings'
                LocalStore      = 'CurrentUser'
                Path            = $FullModuleTemporaryPath
                Include         = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat')
                TimeStampServer = 'http://timestamp.digicert.com'
            }

            if ($Configuration.Options.Signing) {
                if ($Configuration.Options.Signing.CertificatePFXBase64) {
                    $Success = Import-ValidCertificate -CertificateAsBase64 $Configuration.Options.Signing.CertificatePFXBase64 -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword
                    if (-not $Success) {
                        return $false
                    }
                    $registerCertificateSplat.Thumbprint = $Success.Thumbprint
                }
                elseif ($Configuration.Options.Signing.CertificatePFXPath) {
                    $Success = Import-ValidCertificate -FilePath $Configuration.Options.Signing.CertificatePFXPath -PfxPassword $Configuration.Options.Signing.CertificatePFXPassword
                    if (-not $Success) {
                        return $false
                    }

                    $registerCertificateSplat.Thumbprint = $Success.Thumbprint
                }
                else {
                    if ($Configuration.Options.Signing -and $Configuration.Options.Signing.Thumbprint) {
                        $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.Thumbprint
                    }
                    elseif ($Configuration.Options.Signing -and $Configuration.Options.Signing.CertificateThumbprint) {
                        $registerCertificateSplat.Thumbprint = $Configuration.Options.Signing.CertificateThumbprint
                    }
                }
                [Array] $SignedFiles = Register-Certificate @registerCertificateSplat
                if ($Warnings) {
                    foreach ($W in $Warnings) {
                        Write-Text -Text " [!] $($W.Message)" -Color Red
                    }
                }
                if ($SignedFiles.Count -eq 0) {
                    throw "Please configure certificate for use, or disable signing."
                    return $false
                }
                else {
                    if ($SignedFiles[0].Thumbprint) {
                        Write-Text -Text " [i] Multiple certificates found for signing:"
                        foreach ($Certificate in $SignedFiles) {
                            Write-Text " [>] Certificate $($Certificate.Thumbprint) with subject: $($Certificate.Subject)" -Color Yellow
                        }
                        throw "Please configure single certificate for use or disable signing."
                        return $false
                    }
                    else {
                        foreach ($File in $SignedFiles) {
                            Write-Text " [>] File $($File.Path) with status: $($File.StatusMessage)" -Color Yellow
                        }
                    }
                }
            }
        } -PreAppend Plus
    }
}
function Start-PreparingFunctionsAndAliases {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        $FullProjectPath,
        $Files,
        [System.Collections.IDictionary] $CmdletsAliases
    )
    if ($Configuration.Information.Manifest.FunctionsToExport -and $Configuration.Information.Manifest.AliasesToExport -and $Configuration.Information.Manifest.CmdletsToExport) {
        return $true
    }

    $AliasesAndFunctions = Write-TextWithTime -Text 'Preparing function and aliases names' {
        Get-FunctionAliasesFromFolder -FullProjectPath $FullProjectPath -Files $Files -FunctionsToExport $Configuration.Information.FunctionsToExport -AliasesToExport $Configuration.Information.AliasesToExport
    } -PreAppend Information

    Write-TextWithTime -Text "Checking for duplicates in funcions, aliases and cmdlets" {

        if ($null -eq $Configuration.Information.Manifest.FunctionsToExport) {
            $Configuration.Information.Manifest.FunctionsToExport = $AliasesAndFunctions.Keys | Where-Object { $_ }
            if (-not $Configuration.Information.Manifest.FunctionsToExport) {
                $Configuration.Information.Manifest.FunctionsToExport = @()
            }
        }

        if ($null -eq $Configuration.Information.Manifest.AliasesToExport) {

            $Configuration.Information.Manifest.AliasesToExport = @(
                $AliasesAndFunctions.Values | ForEach-Object { $_ } | Where-Object { $_ }

                $CmdletsAliases.Values.AliasesToExport | ForEach-Object { $_ } | Where-Object { $_ }
            )
            if (-not $Configuration.Information.Manifest.AliasesToExport) {
                $Configuration.Information.Manifest.AliasesToExport = @()
            }
        }

        if ($null -eq $Configuration.Information.Manifest.CmdletsToExport) {
            $Configuration.Information.Manifest.CmdletsToExport = @(
                $CmdletsAliases.Values.CmdletsToExport | ForEach-Object { $_ } | Where-Object { $_ }
            )
            if (-not $Configuration.Information.Manifest.CmdletsToExport) {
                $Configuration.Information.Manifest.CmdletsToExport = @()
            }
            else {
                $Configuration.Information.Manifest.CmdletsToExport = $Configuration.Information.Manifest.CmdletsToExport
            }
        }
        $FoundDuplicateAliases = $false
        if ($Configuration.Information.Manifest.AliasesToExport) {
            $UniqueAliases = $Configuration.Information.Manifest.AliasesToExport | Select-Object -Unique
            $DiffrenceAliases = Compare-Object -ReferenceObject $Configuration.Information.Manifest.AliasesToExport -DifferenceObject $UniqueAliases
            foreach ($Alias in $Configuration.Information.Manifest.AliasesToExport) {
                if ($Alias -in $Configuration.Information.Manifest.FunctionsToExport) {
                    Write-Text " [-] Alias $Alias is also used as function name. Fix it!" -Color Red
                    $FoundDuplicateAliases = $true
                }
                if ($Alias -in $Configuration.Information.Manifest.CmdletsToExport) {
                    Write-Text " [-] Alias $Alias is also used as cmdlet name. Fix it!" -Color Red
                    $FoundDuplicateAliases = $true
                }
            }
            foreach ($Alias in $DiffrenceAliases.InputObject) {
                Write-TextWithTime -Text " [-] Alias $Alias is used multiple times. Fix it!" -Color Red
                $FoundDuplicateAliases = $true
            }
            if ($FoundDuplicateAliases) {
                return $false
            }
        }
        $FoundDuplicateCmdlets = $false
        if ($Configuration.Information.Manifest.CmdletsToExport) {
            $UniqueCmdlets = $Configuration.Information.Manifest.CmdletsToExport | Select-Object -Unique
            $DiffrenceCmdlets = Compare-Object -ReferenceObject $Configuration.Information.Manifest.CmdletsToExport -DifferenceObject $UniqueCmdlets
            foreach ($Cmdlet in $Configuration.Information.Manifest.CmdletsToExport) {
                if ($Cmdlet -in $Configuration.Information.Manifest.FunctionsToExport) {
                    Write-Text " [-] Cmdlet $Cmdlet is also used as function name. Fix it!" -Color Red
                    $FoundDuplicateCmdlets = $true
                }
            }
            foreach ($Cmdlet in $DiffrenceCmdlets.InputObject) {
                Write-TextWithTime -Text " [-] Cmdlet $Cmdlet is used multiple times. Fix it!" -Color Red
                $FoundDuplicateCmdlets = $true
            }
            if ($FoundDuplicateCmdlets) {
                return $false
            }
        }
        if (-not [string]::IsNullOrWhiteSpace($Configuration.Information.ScriptsToProcess)) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)\"
            }
            else {
                $StartsWithEnums = "$($Configuration.Information.ScriptsToProcess)/"
            }
            $FilesEnums = @(
                $LinkPrivatePublicFiles | Where-Object { ($_).StartsWith($StartsWithEnums) }
            )

            if ($FilesEnums.Count -gt 0) {
                Write-TextWithTime -Text "ScriptsToProcess export $FilesEnums" -PreAppend Plus -SpacesBefore ' '
                $Configuration.Information.Manifest.ScriptsToProcess = $FilesEnums
            }
        }
    } -PreAppend Information
    $AliasesAndFunctions
}
function Start-PreparingStructure {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [System.Collections.IDictionary] $DestinationPaths,
        [string] $FullProjectPath,
        [string] $FullModuleTemporaryPath,
        [string] $FullTemporaryPath
    )
    Write-TextWithTime -Text "Preparing structure" -PreAppend Information {
        if ($Configuration.Steps.BuildModule.DeleteBefore -eq $true) {
            Write-TextWithTime -Text "Deleting old module (Desktop destination) $($DestinationPaths.Desktop)" {
                $Success = Remove-Directory -Directory $($DestinationPaths.Desktop) -ErrorAction Stop -SpacesBefore " "
                if ($Success -eq $false) {
                    return $false
                }
            } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow

            Write-TextWithTime -Text "Deleting old module (Core destination) $($DestinationPaths.Core)" {
                $Success = Remove-Directory -Directory $($DestinationPaths.Core) -SpacesBefore " "
                if ($Success -eq $false) {
                    return $false
                }
            } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow
        }

        Set-Location -Path $FullProjectPath

        Write-TextWithTime -Text "Cleaning up temporary path $($FullModuleTemporaryPath)" {
            $Success = Remove-Directory -Directory $FullModuleTemporaryPath -SpacesBefore " "
            if ($Success -eq $false) {
                return $false
            }
            Add-Directory -Directory $FullModuleTemporaryPath
        } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow
        Write-TextWithTime -Text "Cleaning up temporary path $($FullTemporaryPath)" {
            $Success = Remove-Directory -Directory $FullTemporaryPath -SpacesBefore " "
            if ($Success -eq $false) {
                return $false
            }
            Add-Directory -Directory $FullTemporaryPath
        } -PreAppend Minus -SpacesBefore " " -Color Blue -ColorError Red -ColorTime Green -ColorBefore Yellow
    }
}
function Start-PreparingVariables {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [string] $FullProjectPath
    )
    Write-TextWithTime -Text "Preparing files and folders variables" -PreAppend Plus {
        $LinkDirectories = @()
        $LinkPrivatePublicFiles = @()

        if ($null -ne $Configuration.Information.Exclude) {
            $Exclude = $Configuration.Information.Exclude
        }
        else {
            $Exclude = '.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'
        }
        if ($null -ne $Configuration.Information.IncludeRoot) {
            $IncludeFilesRoot = $Configuration.Information.IncludeRoot
        }
        else {
            $IncludeFilesRoot = '*.psm1', '*.psd1', 'License*'
        }
        if ($null -ne $Configuration.Information.IncludePS1) {
            $DirectoriesWithPS1 = $Configuration.Information.IncludePS1
        }
        else {
            $DirectoriesWithPS1 = 'Classes', 'Private', 'Public', 'Enums'
        }

        $DirectoriesWithArrays = $Configuration.Information.IncludeAsArray.Values

        if ($null -ne $Configuration.Information.IncludeClasses) {
            $DirectoriesWithClasses = $Configuration.Information.IncludeClasses
        }
        else {
            $DirectoriesWithClasses = 'Classes'
        }
        if ($null -ne $Configuration.Information.IncludeAll) {
            $DirectoriesWithAll = $Configuration.Information.IncludeAll | ForEach-Object {
                if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                    if ($_.EndsWith('\')) {
                        $_
                    }
                    else {
                        "$_\"
                    }
                }
                else {
                    if ($_.EndsWith('/')) {
                        $_
                    }
                    else {
                        "$_/"
                    }
                }
            }
        }
        else {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $DirectoriesWithAll = 'Images\', 'Resources\', 'Templates\', 'Bin\', 'Lib\', 'Data\'
            }
            else {
                $DirectoriesWithAll = 'Images/', 'Resources/', 'Templates/', 'Bin/', 'Lib/', 'Data/'
            }
        }

        $Path = [io.path]::Combine($FullProjectPath, '*')
        if ($PSEdition -eq 'core') {
            $Directories = @(
                $TempDirectories = Get-ChildItem -Path $FullProjectPath -Directory -Exclude $Exclude -FollowSymlink
                @(
                    $TempDirectories
                    $TempDirectories | Get-ChildItem -Directory -Recurse -FollowSymlink
                )
            )
            $Files = Get-ChildItem -Path $FullProjectPath -Exclude $Exclude -FollowSymlink | Get-ChildItem -File -Recurse -FollowSymlink
            $FilesRoot = Get-ChildItem -Path $Path -Include $IncludeFilesRoot -File -FollowSymlink
        }
        else {
            $Directories = @(
                $TempDirectories = Get-ChildItem -Path $FullProjectPath -Directory -Exclude $Exclude
                @(
                    $TempDirectories
                    $TempDirectories | Get-ChildItem -Directory -Recurse
                )
            )
            $Files = Get-ChildItem -Path $FullProjectPath -Exclude $Exclude | Get-ChildItem -File -Recurse
            $FilesRoot = Get-ChildItem -Path $Path -Include $IncludeFilesRoot -File
        }
        $LinkDirectories = @(
            foreach ($Directory in $Directories) {
                if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                    $RelativeDirectoryPath = (Resolve-Path -LiteralPath $Directory.FullName -Relative).Replace('.\', '')
                    $RelativeDirectoryPath = "$RelativeDirectoryPath\"
                }
                else {
                    $RelativeDirectoryPath = (Resolve-Path -LiteralPath $Directory.FullName -Relative).Replace('./', '')
                    $RelativeDirectoryPath = "$RelativeDirectoryPath/"
                }
                $RelativeDirectoryPath
            }
        )
        $AllFiles = foreach ($File in $Files) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            }
            else {
                $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('./', '')
            }
            $RelativeFilePath
        }
        $RootFiles = foreach ($File in $FilesRoot) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('.\', '')
            }
            else {
                $RelativeFilePath = (Resolve-Path -LiteralPath $File.FullName -Relative).Replace('./', '')
            }
            $RelativeFilePath
        }

        $LinkFilesRoot = @(
            foreach ($File in $RootFiles | Sort-Object -Unique) {
                switch -Wildcard ($file) {
                    '*.psd1' {
                        $File
                    }
                    '*.psm1' {
                        $File
                    }
                    'License*' {
                        $File
                    }
                }
            }
        )

        $LinkPrivatePublicFiles = @(
            foreach ($file in $AllFiles | Sort-Object -Unique) {
                switch -Wildcard ($file) {
                    '*.ps1' {
                        foreach ($dir in $DirectoriesWithPS1) {
                            if ($file -like "$dir*") {
                                $file
                            }
                        }
                        foreach ($dir in $DirectoriesWithArrays) {
                            if ($file -like "$dir*") {
                                $file
                            }
                        }
                        continue
                    }
                    '*.*' {
                        foreach ($dir in $DirectoriesWithAll) {
                            if ($file -like "$dir*") {
                                $file
                            }
                        }
                        continue
                    }
                }
            }
        )
        $LinkPrivatePublicFiles = $LinkPrivatePublicFiles | Select-Object -Unique

        [ordered] @{
            LinkDirectories        = $LinkDirectories
            LinkFilesRoot          = $LinkFilesRoot
            LinkPrivatePublicFiles = $LinkPrivatePublicFiles
            DirectoriesWithClasses = $DirectoriesWithClasses
            Files                  = $Files
            DirectoriesWithPS1     = $DirectoriesWithPS1
        }
    }
}
function Start-PublishingGallery {
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Configuration,
        [System.Collections.IDictionary] $ChosenNuget
    )

    if ($ChosenNuget) {
        $Repository = if ($ChosenNuget.RepositoryName) {
            $ChosenNuget.RepositoryName 
        }
        else {
            'PSGallery' 
        }
        Write-TextWithTime -Text "Publishing Module to Gallery ($Repository)" {
            if ($ChosenNuget.ApiKey) {
                $publishModuleSplat = @{
                    Name        = $Configuration.Information.ModuleName
                    Repository  = $Repository
                    NuGetApiKey = $ChosenNuget.ApiKey
                    Force       = $ChosenNuget.Force
                    Verbose     = $ChosenNuget.Verbose
                    ErrorAction = 'Stop'
                }
                Publish-Module @publishModuleSplat
            }
            else {
                return $false
            }
        } -PreAppend Plus
    }
    elseif ($Configuration.Steps.PublishModule.Enabled) {

        Write-TextWithTime -Text "Publishing Module to PowerShellGallery" {
            if ($Configuration.Options.PowerShellGallery.FromFile) {
                $ApiKey = Get-Content -Path $Configuration.Options.PowerShellGallery.ApiKey -ErrorAction Stop -Encoding UTF8
            }
            else {
                $ApiKey = $Configuration.Options.PowerShellGallery.ApiKey
            }
            $publishModuleSplat = @{
                Name        = $Configuration.Information.ModuleName
                Repository  = 'PSGallery'
                NuGetApiKey = $ApiKey
                Force       = $Configuration.Steps.PublishModule.RequireForce
                Verbose     = if ($Configuration.Steps.PublishModule.PSGalleryVerbose) {
                    $Configuration.Steps.PublishModule.PSGalleryVerbose 
                }
                else {
                    $false 
                }
                ErrorAction = 'Stop'
            }
            Publish-Module @publishModuleSplat
        } -PreAppend Plus
    }
}
function Start-PublishingGitHub {
    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $ChosenNuget,
        [System.Collections.IDictionary] $Configuration,
        [string] $ProjectName
    )
    if ($ChosenNuget) {
        [Array] $ListZips = if ($ChosenNuget.Id) {
            foreach ($Zip in $Configuration.CurrentSettings['Artefact']) {
                if ($Zip.Id -eq $ChosenNuget.Id) {
                    $ZipPath = $Zip.ZipPath
                    if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) {
                        $ZipPath
                    }
                }
            }
        }
        else {
            if ($Configuration.CurrentSettings['ArtefactDefault']) {

                $ZipPath = $Configuration.CurrentSettings['ArtefactDefault'].ZipPath
                if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) {
                    $ZipPath
                }
            }
            else {
            }
        }
        if ($ChosenNuget.ID) {
            $TextToUse = "Publishing to GitHub [ID: $($ChosenNuget.Id)] ($ZipPath)"
        }
        else {
            $TextToUse = "Publishing to GitHub ($ZipPath)"
        }
        if ($ListZips.Count -gt 0) {
            Write-TextWithTime -Text $TextToUse -PreAppend Information -ColorBefore Yellow {
                if ($ZipPath -and (Test-Path -LiteralPath $ZipPath)) {
                    if ($ChosenNuget.OverwriteTagName) {
                        $ModuleName = $Configuration.Information.Manifest.ModuleName
                        $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion

                        if ($Configuration.CurrentSettings.PreRelease) {
                            $ModuleVersionWithPreRelease = "$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
                            $TagModuleVersionWithPreRelease = "v$($ModuleVersionWithPreRelease)"
                        }
                        else {
                            $ModuleVersionWithPreRelease = $ModuleVersion
                            $TagModuleVersionWithPreRelease = "v$($ModuleVersion)"
                        }
                        $TagNameDefault = "v$($ModuleVersion)"

                        $TagName = $ChosenNuget.OverwriteTagName
                        $TagName = $TagName.Replace('{ModuleName}', $ModuleName)
                        $TagName = $TagName.Replace('<ModuleName>', $ModuleName)
                        $TagName = $TagName.Replace('{ModuleVersion}', $ModuleVersion)
                        $TagName = $TagName.Replace('<ModuleVersion>', $ModuleVersion)
                        $TagName = $TagName.Replace('{ModuleVersionWithPreRelease}', $ModuleVersionWithPreRelease)
                        $TagName = $TagName.Replace('<ModuleVersionWithPreRelease>', $ModuleVersionWithPreRelease)
                        $TagName = $TagName.Replace('{TagModuleVersionWithPreRelease}', $TagModuleVersionWithPreRelease)
                        $TagName = $TagName.Replace('<TagModuleVersionWithPreRelease>', $TagModuleVersionWithPreRelease)
                        $TagName = $TagName.Replace('{TagName}', $TagNameDefault)
                        $TagName = $TagName.Replace('<TagName>', $TagNameDefault)
                    }
                    else {
                        $ModuleVersion = $Configuration.Information.Manifest.ModuleVersion
                        if ($Configuration.CurrentSettings.PreRelease) {
                            $TagName = "v$($ModuleVersion)-$($Configuration.CurrentSettings.PreRelease)"
                        }
                        else {
                            $TagName = "v$($ModuleVersion)"
                        }
                    }

                    if ($Configuration.CurrentSettings.Prerelease) {
                        if ($ChosenNuget.DoNotMarkAsPreRelease) {
                            $IsPreRelease = $false
                        }
                        else {
                            $IsPreRelease = $true
                        }
                    }
                    else {
                        $IsPreRelease = $false
                    }

                    $sendGitHubReleaseSplat = [ordered] @{
                        GitHubUsername       = $ChosenNuget.UserName
                        GitHubRepositoryName = if ($ChosenNuget.RepositoryName) {
                            $ChosenNuget.RepositoryName 
                        }
                        else {
                            $ProjectName 
                        }
                        GitHubAccessToken    = $ChosenNuget.ApiKey
                        TagName              = $TagName
                        AssetFilePaths       = $ListZips
                        IsPreRelease         = $IsPreRelease

                        Verbose              = if ($ChosenNuget.Verbose) {
                            $ChosenNuget.Verbose 
                        }
                        else {
                            $false 
                        }
                    }

                    $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat

                    if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) {
                        $GithubColor = 'Green'
                        $GitHubText = '+'
                    }
                    else {
                        $GithubColor = 'Red'
                        $GitHubText = '-'
                    }

                    Write-Text "[$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor
                    Write-Text "[$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor
                    Write-Text "[$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor
                    Write-Text "[$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor
                    if ($statusGithub.ErrorMessage) {
                        Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor
                        return $false
                    }
                }
                else {
                    Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red
                    Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red
                    return $false
                }
            }
        }
        else {
            Write-Text -Text "[-] Publishing to GitHub failed. No ZIPs to process." -Color Red
            return $false
        }
    }
    else {

        if (-not $Configuration.CurrentSettings.ArtefactZipPath -or -not (Test-Path -LiteralPath $Configuration.CurrentSettings.ArtefactZipPath)) {
            Write-Text -Text "[-] Publishing to GitHub failed. File $($Configuration.CurrentSettings.ArtefactZipPath) doesn't exists" -Color Red
            return $false
        }
        $TagName = "v$($Configuration.Information.Manifest.ModuleVersion)"

        $ZipPath = $Configuration.CurrentSettings.ArtefactZipPath

        if ($Configuration.Options.GitHub.FromFile) {
            $GitHubAccessToken = Get-Content -LiteralPath $Configuration.Options.GitHub.ApiKey -Encoding UTF8
        }
        else {
            $GitHubAccessToken = $Configuration.Options.GitHub.ApiKey
        }
        if ($GitHubAccessToken) {
            if ($Configuration.Options.GitHub.RepositoryName) {
                $GitHubRepositoryName = $Configuration.Options.GitHub.RepositoryName
            }
            else {
                $GitHubRepositoryName = $ProjectName
            }
            Write-TextWithTime -Text "Publishing to GitHub ($ZipPath)" -PreAppend Information -ColorBefore Yellow {
                if (Test-Path -LiteralPath $ZipPath) {
                    if ($Configuration.Steps.PublishModule.Prerelease) {
                        $IsPreRelease = $true
                    }
                    else {
                        $IsPreRelease = $false
                    }

                    $sendGitHubReleaseSplat = [ordered] @{
                        GitHubUsername       = $Configuration.Options.GitHub.UserName
                        GitHubRepositoryName = $GitHubRepositoryName
                        GitHubAccessToken    = $GitHubAccessToken
                        TagName              = $TagName
                        AssetFilePaths       = $ZipPath
                        IsPreRelease         = $IsPreRelease

                        Verbose              = if ($Configuration.Steps.PublishModule.GitHubVerbose) {
                            $Configuration.Steps.PublishModule.GitHubVerbose 
                        }
                        else {
                            $false 
                        }
                    }

                    $StatusGithub = Send-GitHubRelease @sendGitHubReleaseSplat

                    if ($StatusGithub.ReleaseCreationSucceeded -and $statusGithub.Succeeded) {
                        $GithubColor = 'Green'
                        $GitHubText = '>'
                    }
                    else {
                        $GithubColor = 'Red'
                        $GitHubText = '-'
                    }

                    Write-Text " [$GitHubText] GitHub Release Creation Status: $($StatusGithub.ReleaseCreationSucceeded)" -Color $GithubColor
                    Write-Text " [$GitHubText] GitHub Release Succeeded: $($statusGithub.Succeeded)" -Color $GithubColor
                    Write-Text " [$GitHubText] GitHub Release Asset Upload Succeeded: $($statusGithub.AllAssetUploadsSucceeded)" -Color $GithubColor
                    Write-Text " [$GitHubText] GitHub Release URL: $($statusGitHub.ReleaseUrl)" -Color $GithubColor
                    if ($statusGithub.ErrorMessage) {
                        Write-Text "[$GitHubText] GitHub Release ErrorMessage: $($statusGithub.ErrorMessage)" -Color $GithubColor
                        return $false
                    }
                }
                else {
                    Write-Text " [e] GitHub Release Creation Status: Failed" -Color Red
                    Write-Text " [e] GitHub Release Creation Reason: $ZipPath doesn't exists. Most likely Releases option is disabled." -Color Red
                    return $false
                }
            }
        }
    }
}
function Step-Version {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Module
    Parameter description
 
    .PARAMETER ExpectedVersion
    Parameter description
 
    .PARAMETER Advanced
    Parameter description
 
    .EXAMPLE
    Step-Version -Module Testimo12 -ExpectedVersion '0.1.X'
    Step-Version -ExpectedVersion '0.1.X'
    Step-Version -ExpectedVersion '0.1.5.X'
    Step-Version -ExpectedVersion '1.2.X'
    Step-Version -Module PSWriteHTML -ExpectedVersion '0.0.X'
    Step-Version -Module PSWriteHTML1 -ExpectedVersion '0.1.X'
    Step-Version -Module PSPublishModule -ExpectedVersion '0.9.X' -Advanced -LocalPSD1 "C:\Support\GitHub\PSPublishModule\PSPublishModule.psd1"
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [string] $Module,
        [Parameter(Mandatory)][string] $ExpectedVersion,
        [switch] $Advanced,
        [string] $LocalPSD1
    )
    $Version = $null
    $VersionCheck = [version]::TryParse($ExpectedVersion, [ref] $Version)
    if ($VersionCheck) {

        @{
            Version        = $ExpectedVersion
            CurrentVersion = 'Not aquired, no auto versioning.'
        }
    }
    else {
        if ($Module) {
            if (-not $LocalPSD1) {
                try {
                    $ModuleGallery = Find-Module -Name $Module -ErrorAction Stop -Verbose:$false -WarningAction SilentlyContinue
                    $CurrentVersion = [version] $ModuleGallery.Version
                }
                catch {

                    $CurrentVersion = [version] $null
                }
            }
            else {
                if (Test-Path -LiteralPath $LocalPSD1) {
                    $PSD1Data = Import-PowerShellDataFile -Path $LocalPSD1
                    if ($PSD1Data.ModuleVersion) {
                        try {
                            $CurrentVersion = [version] $PSD1Data.ModuleVersion
                        }
                        catch {
                            Write-Warning -Message "Couldn't parse version $($PSD1Data.ModuleVersion) from PSD1 file $LocalPSD1"
                            $CurrentVersion = $null
                        }
                    }
                }
                else {
                    Write-Warning -Message "Couldn't find local PSD1 file $LocalPSD1"
                    $CurrentVersion = $null
                }
            }
        }
        else {
            $CurrentVersion = $null
        }
        $Splitted = $ExpectedVersion.Split('.')
        $PreparedVersion = [ordered] @{
            Major    = $Splitted[0]
            Minor    = $Splitted[1]
            Build    = $Splitted[2]
            Revision = $Splitted[3]
        }
        [string] $StepType = foreach ($Key in $PreparedVersion.Keys) {
            if ($PreparedVersion[$Key] -eq 'X') {
                $Key
                break
            }
        }

        if ($null -eq $CurrentVersion) {
            $PreparedVersion.$StepType = 1
        }
        else {
            $PreparedVersion.$StepType = $CurrentVersion.$StepType
        }

        if ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.') -gt $CurrentVersion) {
            $PreparedVersion.$StepType = 0
        }

        while ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.') -le $CurrentVersion) {
            $PreparedVersion.$StepType = $PreparedVersion.$StepType + 1
        }

        $ProposedVersion = ([version] (($PreparedVersion.Values | Where-Object { $null -ne $_ }) -join '.')).ToString()

        $FinalVersion = $null
        $VersionCheck = [version]::TryParse($ProposedVersion, [ref] $FinalVersion)
        if ($VersionCheck) {
            if ($Advanced) {
                [ordered] @{
                    Version        = $ProposedVersion
                    CurrentVersion = $CurrentVersion
                }
            }
            else {
                $ProposedVersion
            }
        }
        else {
            throw "Couldn't properly verify version is version. Terminating."
        }
    }
}
function Test-AllFilePathsAndThrowErrorIfOneIsNotValid {
    [CmdletBinding()]
    param(
        [string[]] $filePaths
    )
    foreach ($filePath in $filePaths) {
        [bool] $fileWasNotFoundAtPath = [string]::IsNullOrEmpty($filePath) -or !(Test-Path -Path $filePath -PathType Leaf)
        if ($fileWasNotFoundAtPath) {
            throw "There is no file at the specified path, '$filePath'."
        }
    }
}
function Test-ReparsePoint {
    [CmdletBinding()]
    param (
        [string]$path
    )
    $file = Get-Item $path -Force -ea SilentlyContinue
    return [bool]($file.Attributes -band [IO.FileAttributes]::ReparsePoint)
}
function Update-VersionInBuildScript {
    <#
    .SYNOPSIS
    Updates the version in the Build-Module.ps1 script.
 
    .DESCRIPTION
    Modifies the ModuleVersion entry in the Build-Module.ps1 script with the new version.
 
    .PARAMETER ScriptFile
    Path to the Build-Module.ps1 file.
 
    .PARAMETER Version
    The new version string to set.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ScriptFile,

        [Parameter(Mandatory = $true)]
        [string]$Version,

        [System.Collections.IDictionary] $CurrentVersionHash
    )

    if (!(Test-Path -Path $ScriptFile)) {
        Write-Warning "Build script file not found: $ScriptFile"
        return $false
    }

    $CurrentFileVersion = $CurrentVersionHash[$ScriptFile]

    try {
        $content = Get-Content -Path $ScriptFile -Raw
        $newContent = $content -replace "ModuleVersion\s*=\s*['""][\d\.]+['""]", "ModuleVersion = '$Version'"

        if ($content -eq $newContent) {
            Write-Verbose "No version change needed for $ScriptFile,"
            return $true
        }
        Write-Verbose -Message "Updating version in $ScriptFile from '$CurrentFileVersion' to '$Version'"

        if ($PSCmdlet.ShouldProcess("Build script $ScriptFile", "Update version from '$CurrentFileVersion' to '$Version'")) {
            $newContent | Set-Content -Path $ScriptFile -NoNewline
            Write-Host "Updated version in $ScriptFile to $Version" -ForegroundColor Green
        }
        return $true
    }
    catch {
        Write-Error "Error updating build script $ScriptFile`: $_"
        return $false
    }
}

function Update-VersionInCsProj {
    <#
    .SYNOPSIS
    Updates the version in a .csproj file.
 
    .DESCRIPTION
    Modifies the VersionPrefix element in a .csproj file with the new version.
 
    .PARAMETER ProjectFile
    Path to the .csproj file.
 
    .PARAMETER Version
    The new version string to set.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ProjectFile,

        [Parameter(Mandatory = $true)]
        [string]$Version,

        [System.Collections.IDictionary] $CurrentVersionHash
    )

    if (!(Test-Path -Path $ProjectFile)) {
        Write-Warning "Project file not found: $ProjectFile"
        return $false
    }

    $CurrentFileVersion = $CurrentVersionHash[$ProjectFile]

    try {
        $content = Get-Content -Path $ProjectFile -Raw
        $newContent = $content -replace '<VersionPrefix>[\d\.]+<\/VersionPrefix>', "<VersionPrefix>$Version</VersionPrefix>"

        if ($content -eq $newContent) {
            Write-Verbose "No version change needed for $ProjectFile"
            return $true
        }
        Write-Verbose -Message "Updating version in $ProjectFile from '$CurrentFileVersion' to '$Version'"
        if ($PSCmdlet.ShouldProcess("Project file $ProjectFile", "Update version from '$CurrentFileVersion' to '$Version'")) {
            $newContent | Set-Content -Path $ProjectFile -NoNewline
            Write-Host "Updated version in $ProjectFile to $Version" -ForegroundColor Green
        }
        return $true
    }
    catch {
        Write-Error "Error updating project file $ProjectFile`: $_"
        return $false
    }
}
function Update-VersionInPsd1 {
    <#
    .SYNOPSIS
    Updates the version in a PowerShell module manifest (.psd1) file.
 
    .DESCRIPTION
    Modifies the ModuleVersion entry in a module manifest with the new version.
 
    .PARAMETER ManifestFile
    Path to the .psd1 file.
 
    .PARAMETER Version
    The new version string to set.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ManifestFile,

        [Parameter(Mandatory = $true)]
        [string]$Version,

        [System.Collections.IDictionary] $CurrentVersionHash
    )

    if (!(Test-Path -Path $ManifestFile)) {
        Write-Warning "Module manifest file not found: $ManifestFile"
        return $false
    }

    $CurrentFileVersion = $CurrentVersionHash[$ManifestFile]

    try {

        $content = Get-Content -Path $ManifestFile -Raw
        $newContent = $content -replace "ModuleVersion\s*=\s*['""][\d\.]+['""]", "ModuleVersion = '$Version'"

        if ($content -eq $newContent) {
            Write-Verbose "No version change needed for $ManifestFile"
            return $true
        }
        Write-Verbose -Message "Updating version in $ManifestFile from '$CurrentFileVersion' to '$Version'"

        if ($PSCmdlet.ShouldProcess("Module manifest $ManifestFile", "Update version from '$CurrentFileVersion' to '$Version'")) {
            $newContent | Set-Content -Path $ManifestFile -NoNewline
            Write-Host "Updated version in $ManifestFile to $Version" -ForegroundColor Green
        }
        return $true
    }
    catch {
        Write-Error "Error updating module manifest $ManifestFile`: $_"
        return $false
    }
}
function Update-VersionNumber {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Version,
        [Parameter(Mandatory = $true)]
        [ValidateSet('Major', 'Minor', 'Build', 'Revision')]
        [string]$Type
    )

    $versionParts = $Version -split '\.'

    while ($versionParts.Count -lt 3) {
        $versionParts += "0"
    }

    if ($Type -eq 'Revision' -and $versionParts.Count -lt 4) {
        $versionParts += "0"
    }

    switch ($Type) {
        'Major' {
            $versionParts[0] = [string]([int]$versionParts[0] + 1)
            $versionParts[1] = "0"
            $versionParts[2] = "0"
            if ($versionParts.Count -gt 3) {
                $versionParts[3] = "0"
            }
        }
        'Minor' {
            $versionParts[1] = [string]([int]$versionParts[1] + 1)
            $versionParts[2] = "0"
            if ($versionParts.Count -gt 3) {
                $versionParts[3] = "0"
            }
        }
        'Build' {
            $versionParts[2] = [string]([int]$versionParts[2] + 1)
            if ($versionParts.Count -gt 3) {
                $versionParts[3] = "0"
            }
        }
        'Revision' {
            if ($versionParts.Count -lt 4) {
                $versionParts += "1"
            }
            else {
                $versionParts[3] = [string]([int]$versionParts[3] + 1)
            }
        }
    }

    $newVersion = $versionParts -join '.'
    $versionPartCount = ($Version -split '\.' | Measure-Object).Count
    if ($versionPartCount -eq 3 -and $Type -ne 'Revision') {
        $newVersion = ($versionParts | Select-Object -First 3) -join '.'
    }

    return $newVersion
}
function Write-PowerShellHashtable {
    <#
    .SYNOPSIS
    Takes an creates a script to recreate a hashtable
 
    .DESCRIPTION
    Allows you to take a hashtable and create a hashtable you would embed into a script.
    Handles nested hashtables and indents nested hashtables automatically.
 
    .PARAMETER InputObject
    The hashtable to turn into a script
 
    .PARAMETER AsScriptBlock
    Determines if a string or a scriptblock is returned
 
    .PARAMETER Sort
    Sorts the hashtable alphabetically
 
    .EXAMPLE
    # Corrects the presentation of a PowerShell hashtable
    @{Foo='Bar';Baz='Bing';Boo=@{Bam='Blang'}} | Write-PowerShellHashtable
 
    .NOTES
    Original idea: https://github.com/StartAutomating/Pipeworks
    Modifications by: Przemyslaw Klys
 
    #>

    [cmdletbinding()]
    [OutputType([string], [ScriptBlock])]
    param(
        [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)][PSObject] $InputObject,
        # Returns the content as a script block, rather than a string
        [Alias('ScriptBlock')][switch]$AsScriptBlock,
        # If set, items in the hashtable will be sorted alphabetically
        [Switch]$Sort
    )
    process {
        $callstack = @(foreach ($_ in (Get-PSCallStack)) {
                if ($_.Command -eq "Write-PowerShellHashtable") {
                    $_
                }
            })
        $depth = $callStack.Count
        if ($inputObject -isnot [System.Collections.IDictionary]) {

            $newInputObject = @{
                PSTypeName = @($inputobject.pstypenames)[-1]
            }
            foreach ($prop in $inputObject.psobject.properties) {
                $newInputObject[$prop.Name] = $prop.Value
            }
            $inputObject = $newInputObject
        }

        if ($inputObject -is [System.Collections.IDictionary]) {

            $scriptString = ""
            $indent = $depth * 4
            $scriptString += "@{
"


            $items = $inputObject.GetEnumerator()

            if ($Sort) {
                $items = $items | Sort-Object Key
            }

            foreach ($kv in $items) {
                $scriptString += " " * $indent

                $keyString = "$($kv.Key)"
                if ($keyString.IndexOfAny(" _.#-+:;()'!?^@#$%&".ToCharArray()) -ne -1) {
                    if ($keyString.IndexOf("'") -ne -1) {
                        $scriptString += "'$($keyString.Replace("'","''"))'="
                    }
                    else {
                        $scriptString += "'$keyString'="
                    }
                }
                elseif ($keyString) {
                    $scriptString += "$keyString="
                }

                $value = $kv.Value

                if ($value -is [string]) {
                    $value = "'" + $value.Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'"
                }
                elseif ($value -is [ScriptBlock]) {
                    $value = "{$value}"
                }
                elseif ($value -is [switch]) {
                    $value = if ($value) {
                        '$true' 
                    }
                    else {
                        '$false' 
                    }
                }
                elseif ($value -is [DateTime]) {
                    $value = if ($value) {
                        "[DateTime]'$($value.ToString("o"))'" 
                    }
                }
                elseif ($value -is [bool]) {
                    $value = if ($value) {
                        '$true' 
                    }
                    else {
                        '$false' 
                    }
                }
                elseif ($value -is [System.Collections.IList] -and $value.Count -eq 0) {
                    $value = '@()'
                }
                elseif ($value -is [System.Collections.IList] -and $value.Count -gt 0) {

                    $value = foreach ($v in $value) {
                        if ($v -is [System.Collections.IDictionary]) {
                            Write-PowerShellHashtable $v -Sort:$Sort.IsPresent
                        }
                        elseif ($v -is [Object] -and $v -isnot [string]) {
                            Write-PowerShellHashtable $v -Sort:$Sort.IsPresent
                        }
                        else {
                            ("'" + "$v".Replace("'", "''").Replace("’", "’’").Replace("‘", "‘‘") + "'")
                        }
                    }
                    $oldOfs = $ofs
                    $ofs = ",$(' ' * ($indent + 4))"
                    $value = "@($value)"
                    $ofs = $oldOfs
                }
                elseif ($value -as [System.Collections.IDictionary[]]) {
                    $value = foreach ($v in $value) {
                        Write-PowerShellHashtable $v -Sort:$Sort.IsPresent
                    }
                    $value = $value -join ","
                }
                elseif ($value -is [System.Collections.IDictionary]) {
                    $value = "$(Write-PowerShellHashtable $value -Sort:$Sort.IsPresent)"
                }
                elseif ($value -as [Double]) {
                    $value = "$value"
                }
                else {
                    $valueString = "'$value'"
                    if ($valueString[0] -eq "'" -and
                        $valueString[1] -eq "@" -and
                        $valueString[2] -eq "{") {
                        $value = Write-PowerShellHashtable -InputObject $value -Sort:$Sort.IsPresent
                    }
                    else {
                        $value = $valueString
                    }
                }
                $scriptString += "$value
"

            }
            $scriptString += " " * ($depth - 1) * 4
            $scriptString += "}"
            if ($AsScriptBlock) {
                [ScriptBlock]::Create($scriptString)
            }
            else {
                $scriptString
            }
        }
    }
}
function Write-Text {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)][string] $Text,
        [System.ConsoleColor] $Color,
        [System.ConsoleColor] $ColorBefore,
        [System.ConsoleColor] $ColorTime,
        [switch] $Start,
        [switch] $End,
        [System.Diagnostics.Stopwatch] $Time,
        [ValidateSet('Plus', 'Minus', 'Information', 'Addition', 'Error')][string] $PreAppend,
        [string] $SpacesBefore
    )
    if ($PreAppend) {
        if ($PreAppend -eq "Information") {
            $TextBefore = "$SpacesBefore[i] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Yellow
            }
        }
        elseif ($PreAppend -eq 'Minus') {
            $TextBefore = "$SpacesBefore[-] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Red
            }
        }
        elseif ($PreAppend -eq 'Plus') {
            $TextBefore = "$SpacesBefore[+] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Cyan
            }
        }
        elseif ($PreAppend -eq 'Addition') {
            $TextBefore = "$SpacesBefore[>] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Yellow
            }
        }
        elseif ($PreAppend -eq 'Error') {
            $TextBefore = "$SpacesBefore[e] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Red
            }
            if (-not $Color) {
                $Color = [System.ConsoleColor]::Red
            }
        }
        Write-Host -Object "$TextBefore" -NoNewline -ForegroundColor $ColorBefore
    }
    if (-not $Color) {
        $Color = [System.ConsoleColor]::Cyan
    }
    if (-not $ColorTime) {
        $ColorTime = [System.ConsoleColor]::Green
    }
    if (-not $Start -and -not $End) {
        Write-Host "$Text" -ForegroundColor $Color
    }
    if ($Start) {
        Write-Host "$Text" -NoNewline -ForegroundColor $Color
        $Time = [System.Diagnostics.Stopwatch]::StartNew()
    }
    if ($End) {
        $TimeToExecute = $Time.Elapsed.ToString()
        Write-Host " [Time: $TimeToExecute]" -ForegroundColor $ColorTime
        $Time.Stop()
    }
    else {
        if ($Time) {
            return $Time
        }
    }
}
function Write-TextWithTime {
    [CmdletBinding()]
    param(
        [ScriptBlock] $Content,
        [ValidateSet('Plus', 'Minus', 'Information', 'Addition')][string] $PreAppend,
        [string] $Text,
        [switch] $Continue,
        [System.ConsoleColor] $Color = [System.ConsoleColor]::Cyan,
        [System.ConsoleColor] $ColorTime = [System.ConsoleColor]::Green,
        [System.ConsoleColor] $ColorError = [System.ConsoleColor]::Red,
        [System.ConsoleColor] $ColorBefore,
        [string] $SpacesBefore
    )
    if ($PreAppend) {
        if ($PreAppend -eq "Information") {
            $TextBefore = "$SpacesBefore[i] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Yellow
            }
        }
        elseif ($PreAppend -eq 'Minus') {
            $TextBefore = "$SpacesBefore[-] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Red
            }
        }
        elseif ($PreAppend -eq 'Plus') {
            $TextBefore = "$SpacesBefore[+] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Cyan
            }
        }
        elseif ($PreAppend -eq 'Addition') {
            $TextBefore = "$SpacesBefore[>] "
            if (-not $ColorBefore) {
                $ColorBefore = [System.ConsoleColor]::Yellow
            }
        }
        Write-Host -Object "$TextBefore" -NoNewline -ForegroundColor $ColorBefore
        Write-Host -Object "$Text" -ForegroundColor $Color
    }
    else {
        Write-Host -Object "$Text" -ForegroundColor $Color
    }
    $Time = [System.Diagnostics.Stopwatch]::StartNew()
    if ($null -ne $Content) {
        try {
            $InputData = & $Content
            if ($InputData -contains $false) {
                $ErrorMessage = "Failure in action above. Check output above."
            }
            else {
                $InputData
            }
        }
        catch {
            $ErrorMessage = $_.Exception.Message + " (File: $($_.InvocationInfo.ScriptName), Line: " + $_.InvocationInfo.ScriptLineNumber + ")"
        }
    }
    $TimeToExecute = $Time.Elapsed.ToString()
    if ($ErrorMessage) {
        Write-Host -Object "$SpacesBefore[e] $Text [Error: $ErrorMessage]" -ForegroundColor $ColorError
        if ($PreAppend) {
            Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorError
        }
        Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorError
        $Time.Stop()
        return $false
        break
    }
    else {
        if ($PreAppend) {
            Write-Host -Object "$($TextBefore)" -NoNewline -ForegroundColor $ColorBefore
        }
        Write-Host -Object "$Text [Time: $TimeToExecute]" -ForegroundColor $ColorTime
    }
    if (-not $Continue) {
        $Time.Stop()
    }
}
function Convert-CommandsToList {
    [cmdletbinding()]
    param(
        [parameter(Mandatory)][string] $ModuleName,
        [string[]] $CommandTypes
    )

    $Commands = Get-Command -Module $ModuleName
    $CommandsOnly = $Commands | Where-Object { $_.CommandType -eq 'Function' }

    $List = [ordered] @{}
    foreach ($Command in $CommandsOnly) {
        if ($Command.Name.StartsWith('Get')) {
            $CommandType = 'Get'
        }
        elseif ($Command.Name.StartsWith('Set')) {
            $CommandType = 'Set'
        }
        else {
            $CommandType = 'Other'
        }
        if ($CommandType -ne 'Other') {
            $Name = $Command.Name.Replace("Get-", '').Replace("Set-", '')
            if (-not $List[$Name]) {
                $List[$Name] = [PSCustomObject] @{

                    Get = if ($CommandType -eq 'Get') {
                        $Command.Name 
                    }
                    else {
                        '' 
                    }

                    Set = if ($CommandType -eq 'Set') {
                        $Command.Name 
                    }
                    else {
                        '' 
                    }
                }
            }
            else {
                $List[$Name].$CommandType = $Command.Name
            }
        }
    }

    $List.Values
}
function Convert-ProjectEncoding {
    <#
    .SYNOPSIS
    Converts encoding for all source files in a project directory with comprehensive safety features.
 
    .DESCRIPTION
    Recursively converts encoding for PowerShell, C#, and other source code files in a project directory.
    Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection,
    and detailed reporting. Designed specifically for development projects with intelligent file type detection.
 
    .PARAMETER Path
    Path to the project directory to process.
 
    .PARAMETER ProjectType
    Type of project to process. Determines which file extensions are included.
    Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom'
 
    .PARAMETER CustomExtensions
    Custom file extensions to process when ProjectType is 'Custom'.
    Example: @('*.ps1', '*.psm1', '*.cs', '*.vb')
 
    .PARAMETER SourceEncoding
    Expected source encoding of files. When specified, only files with this encoding will be converted.
    When not specified (or set to 'Any'), files with any encoding except the target encoding will be converted.
 
    .PARAMETER TargetEncoding
    Target encoding for conversion.
    Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others.
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from processing (e.g., '.git', 'bin', 'obj').
 
    .PARAMETER CreateBackups
    Create backup files before conversion for additional safety.
 
    .PARAMETER BackupDirectory
    Directory to store backup files. If not specified, backups are created alongside original files.
 
    .PARAMETER Force
    Convert files even when their detected encoding doesn't match SourceEncoding.
 
    .PARAMETER NoRollbackOnMismatch
    Skip rolling back changes when content verification fails.
 
    .PARAMETER PassThru
    Return detailed results for each processed file.
 
    .EXAMPLE
    Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -WhatIf
    Preview encoding conversion for a PowerShell project (will convert from ANY encoding to UTF8BOM by default).
 
    .EXAMPLE
    Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell -TargetEncoding UTF8BOM
    Convert ALL files in a PowerShell project to UTF8BOM regardless of their current encoding.
 
    .EXAMPLE
    Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -SourceEncoding ASCII -TargetEncoding UTF8BOM -CreateBackups
    Convert ONLY ASCII files in a mixed project to UTF8BOM with backups.
 
    .EXAMPLE
    Convert-ProjectEncoding -Path 'C:\MyProject' -ProjectType CSharp -TargetEncoding UTF8 -PassThru
    Convert ALL files in a C# project to UTF8 without BOM and return detailed results.
 
    .NOTES
    File type mappings:
    - PowerShell: *.ps1, *.psm1, *.psd1, *.ps1xml
    - CSharp: *.cs, *.csx, *.csproj, *.sln, *.config, *.json, *.xml
    - Mixed: Combination of PowerShell and CSharp
    - All: Common source code extensions including JS, Python, etc.
 
    PowerShell Encoding Recommendations:
    - UTF8BOM is recommended for PowerShell files to ensure PS 5.1 compatibility
    - UTF8 without BOM can cause PS 5.1 to misinterpret files as ASCII
    - This can lead to broken special characters and module loading issues
    - UTF8BOM ensures proper encoding detection across all PowerShell versions
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ProjectType')]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(ParameterSetName = 'ProjectType')]
        [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All')]
        [string] $ProjectType = 'Mixed',

        [Parameter(ParameterSetName = 'Custom', Mandatory)]
        [string[]] $CustomExtensions,

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM', 'Any')]
        [string] $SourceEncoding = 'Any',

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $TargetEncoding,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'),

        [switch] $CreateBackups,

        [string] $BackupDirectory,

        [switch] $Force,
        [switch] $NoRollbackOnMismatch,
        [switch] $PassThru
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Project path '$Path' not found or is not a directory"
    }

    $extensionMappings = @{
        'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml')
        'CSharp'     = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx')
        'Mixed'      = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml')
        'All'        = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml')
    }

    if ($PSCmdlet.ParameterSetName -eq 'Custom') {
        $filePatterns = $CustomExtensions
    }
    else {
        $filePatterns = $extensionMappings[$ProjectType]
    }

    Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')"

    if (-not $PSBoundParameters.ContainsKey('TargetEncoding')) {
        switch ($ProjectType) {
            'PowerShell' {
                $TargetEncoding = 'UTF8BOM' 
            }
            'Mixed' {
                $TargetEncoding = 'UTF8BOM' 
            }  
            default {
                $TargetEncoding = 'UTF8' 
            }
        }
        Write-Verbose "Using default TargetEncoding '$TargetEncoding' for project type '$ProjectType'"
    }

    if ($CreateBackups -and $BackupDirectory) {
        if (-not (Test-Path -LiteralPath $BackupDirectory)) {
            New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null
            Write-Verbose "Created backup directory: $BackupDirectory"
        }
    }

    $target = Resolve-Encoding -Name $TargetEncoding

    $source = if ($SourceEncoding -eq 'Any') {
        $null 
    }
    else {
        Resolve-Encoding -Name $SourceEncoding 
    }

    $allFiles = @()

    foreach ($pattern in $filePatterns) {
        $params = @{
            Path    = $Path
            Filter  = $pattern
            File    = $true
            Recurse = $true
        }

        $files = Get-ChildItem @params | Where-Object {
            $file = $_
            $excluded = $false

            foreach ($excludeDir in $ExcludeDirectories) {
                if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") {
                    $excluded = $true
                    break
                }
            }

            -not $excluded
        }

        $allFiles += $files
    }

    $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString

    Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green

    if ($uniqueFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria"
        return
    }

    $results = @()
    $converted = 0
    $skipped = 0
    $errors = 0

    foreach ($file in $uniqueFiles) {
        try {

            if ($SourceEncoding -eq 'Any') {

                $currentEncodingObj = Get-FileEncoding -Path $file.FullName -AsObject
                $currentEncodingName = $currentEncodingObj.EncodingName

                $targetName = if ($target -is [System.Text.UTF8Encoding] -and $target.GetPreamble().Length -eq 3) {
                    'UTF8BOM' 
                }
                elseif ($target -is [System.Text.UTF8Encoding]) {
                    'UTF8' 
                }
                elseif ($target -is [System.Text.UnicodeEncoding]) {
                    'Unicode' 
                }
                elseif ($target -is [System.Text.UTF7Encoding]) {
                    'UTF7' 
                }
                elseif ($target -is [System.Text.UTF32Encoding]) {
                    'UTF32' 
                }
                elseif ($target -is [System.Text.ASCIIEncoding]) {
                    'ASCII' 
                }
                elseif ($target -is [System.Text.BigEndianUnicodeEncoding]) {
                    'BigEndianUnicode' 
                }
                else {
                    $target.WebName 
                }

                if ($currentEncodingName -eq $targetName) {
                    Write-Verbose "Skipping $($file.FullName) because encoding is already '$targetName'."
                    $results += @{
                        FilePath         = $file.FullName
                        Status           = 'Skipped'
                        Reason           = "Already target encoding '$targetName'"
                        DetectedEncoding = $currentEncodingName
                    }
                    $skipped++
                    continue
                }

                $convertParams = @{
                    FilePath             = $file.FullName
                    SourceEncoding       = $currentEncodingObj.Encoding
                    TargetEncoding       = $target
                    Force                = $true  
                    NoRollbackOnMismatch = $NoRollbackOnMismatch
                    WhatIf               = $WhatIfPreference
                }
            }
            else {

                $convertParams = @{
                    FilePath             = $file.FullName
                    SourceEncoding       = $source
                    TargetEncoding       = $target
                    Force                = $Force
                    NoRollbackOnMismatch = $NoRollbackOnMismatch
                    WhatIf               = $WhatIfPreference
                }
            }

            if ($CreateBackups) {
                $convertParams.CreateBackup = $true
            }

            $result = Convert-FileEncodingSingle @convertParams

            if ($result) {
                $results += $result

                switch ($result.Status) {
                    'Converted' {
                        $converted++ 
                    }
                    'Skipped' {
                        $skipped++ 
                    }
                    'Error' {
                        $errors++ 
                    }
                    'Failed' {
                        $errors++ 
                    }
                }

                if ($CreateBackups -and $BackupDirectory -and $result.BackupPath -and (Test-Path $result.BackupPath)) {
                    $relativePath = Get-RelativePath -From $Path -To $file.FullName
                    $backupTargetPath = Join-Path $BackupDirectory $relativePath
                    $backupTargetDir = Split-Path $backupTargetPath -Parent

                    if (-not (Test-Path $backupTargetDir)) {
                        New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null
                    }

                    Move-Item -Path $result.BackupPath -Destination $backupTargetPath -Force
                    $result.BackupPath = $backupTargetPath
                }
            }
        }
        catch {
            Write-Warning "Unexpected error processing $($file.FullName): $_"
            $errors++
        }
    }

    $summary = @{
        TotalFiles     = $uniqueFiles.Count
        Converted      = $converted
        Skipped        = $skipped
        Errors         = $errors
        SourceEncoding = $SourceEncoding
        TargetEncoding = $TargetEncoding
        ProjectPath    = $Path
        ProjectType    = if ($PSCmdlet.ParameterSetName -eq 'Custom') {
            "Custom ($($CustomExtensions -join ', '))" 
        }
        else {
            $ProjectType 
        }
    }

    Write-Host "`nConversion Summary:" -ForegroundColor Cyan
    Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White
    Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green
    Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow
    Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red
    Write-Host " Encoding: $($summary.SourceEncoding) → $($summary.TargetEncoding)" -ForegroundColor White

    if ($PassThru) {
        [PSCustomObject]@{
            Summary = $summary
            Results = $results
        }
    }
}

function Convert-ProjectLineEnding {
    <#
    .SYNOPSIS
    Converts line endings for all source files in a project directory with comprehensive safety features.
 
    .DESCRIPTION
    Recursively converts line endings for PowerShell, C#, and other source code files in a project directory.
    Includes comprehensive safety features: WhatIf support, automatic backups, rollback protection,
    and detailed reporting. Can convert between CRLF (Windows), LF (Unix/Linux), and fix mixed line endings.
 
    .PARAMETER Path
    Path to the project directory to process.
 
    .PARAMETER ProjectType
    Type of project to process. Determines which file extensions are included.
    Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom'
 
    .PARAMETER CustomExtensions
    Custom file extensions to process when ProjectType is 'Custom'.
    Example: @('*.ps1', '*.psm1', '*.cs', '*.vb')
 
    .PARAMETER TargetLineEnding
    Target line ending style. Valid values: 'CRLF', 'LF'
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from processing (e.g., '.git', 'bin', 'obj').
 
    .PARAMETER CreateBackups
    Create backup files before conversion for additional safety.
 
    .PARAMETER BackupDirectory
    Directory to store backup files. If not specified, backups are created alongside original files.
 
    .PARAMETER Force
    Convert all files regardless of current line ending type.
 
    .PARAMETER EnsureFinalNewline
    Ensure all files end with a newline character (POSIX compliance).
 
    .PARAMETER OnlyMissingNewline
    Only process files that are missing final newlines, leave others unchanged.
 
    .PARAMETER PassThru
    Return detailed results for each processed file.
 
    .EXAMPLE
    Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell -TargetLineEnding CRLF -WhatIf
    Preview what files would be converted to Windows-style line endings.
 
    .EXAMPLE
    Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -TargetLineEnding LF -CreateBackups
    Convert a mixed project to Unix-style line endings with backups.
 
    .EXAMPLE
    Convert-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -OnlyMixed -PassThru
    Fix only files with mixed line endings and return detailed results.
 
    .NOTES
    This function modifies files in place. Always use -WhatIf first or -CreateBackups for safety.
    Line ending types:
    - CRLF: Windows style (\\r\\n)
    - LF: Unix/Linux style (\\n)
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')]
        [string] $ProjectType = 'Mixed',

        [string[]] $CustomExtensions,

        [Parameter(Mandatory)]
        [ValidateSet('CRLF', 'LF')]
        [string] $TargetLineEnding,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'),

        [switch] $CreateBackups,
        [string] $BackupDirectory,
        [switch] $Force,
        [switch] $OnlyMixed,
        [switch] $EnsureFinalNewline,
        [switch] $OnlyMissingNewline,
        [switch] $PassThru
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Project path '$Path' not found or is not a directory"
    }

    $extensionMappings = @{
        'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml')
        'CSharp'     = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx')
        'Mixed'      = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml')
        'All'        = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml')
    }

    if ($ProjectType -eq 'Custom' -and $CustomExtensions) {
        $filePatterns = $CustomExtensions
    }
    else {
        $filePatterns = $extensionMappings[$ProjectType]
    }

    Write-Verbose "Processing project type: $ProjectType with patterns: $($filePatterns -join ', ')"    

    if ($CreateBackups -and $BackupDirectory) {
        if (-not (Test-Path -LiteralPath $BackupDirectory)) {
            New-Item -Path $BackupDirectory -ItemType Directory -Force | Out-Null
            Write-Verbose "Created backup directory: $BackupDirectory"
        }
    }

    $allFiles = @()

    foreach ($pattern in $filePatterns) {
        $params = @{
            Path    = $Path
            Filter  = $pattern
            File    = $true
            Recurse = $true
        }

        $files = Get-ChildItem @params | Where-Object {
            $file = $_
            $excluded = $false

            foreach ($excludeDir in $ExcludeDirectories) {
                if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") {
                    $excluded = $true
                    break
                }
            }

            -not $excluded
        }

        $allFiles += $files
    }

    $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString

    Write-Host "Found $($uniqueFiles.Count) files to process" -ForegroundColor Green

    if ($uniqueFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria"
        return
    }

    $results = @()
    $converted = 0
    $skipped = 0
    $errors = 0

    foreach ($file in $uniqueFiles) {
        try {
            $currentInfo = Get-CurrentLineEnding -FilePath $file.FullName
            $currentLineEnding = $currentInfo.LineEnding
            $hasFinalNewline = $currentInfo.HasFinalNewline
            $relativePath = Get-RelativePath -From $Path -To $file.FullName

            $shouldProcess = $false
            $skipReason = ""

            if ($currentLineEnding -eq 'Error') {
                $skipReason = "Could not detect line endings"
            }
            elseif ($currentLineEnding -eq 'None') {
                $skipReason = "Empty file or no line endings"
            }
            elseif ($OnlyMixed -and $currentLineEnding -ne 'Mixed') {
                $skipReason = "Not mixed line endings (OnlyMixed specified)"
            }
            elseif ($OnlyMissingNewline -and $hasFinalNewline) {
                $skipReason = "Already has final newline (OnlyMissingNewline specified)"
            }
            elseif (-not $Force -and $currentLineEnding -eq $TargetLineEnding -and ($hasFinalNewline -or -not $EnsureFinalNewline)) {
                $skipReason = "Already compliant with target settings"
            }
            else {
                $shouldProcess = $true
            }

            if (-not $shouldProcess) {
                $result = @{
                    FilePath          = $relativePath
                    FullPath          = $file.FullName
                    Status            = 'Skipped'
                    Reason            = $skipReason
                    CurrentLineEnding = $currentLineEnding
                    TargetLineEnding  = $TargetLineEnding
                    HasFinalNewline   = $hasFinalNewline
                }
                $results += [PSCustomObject]$result
                $skipped++
                Write-Verbose "Skipped $relativePath`: $skipReason"
                continue
            }

            if ($PSCmdlet.ShouldProcess($relativePath, "Convert line endings from $currentLineEnding to $TargetLineEnding$(if ($EnsureFinalNewline) { ' and ensure final newline' })")) {
                $conversionResult = Convert-LineEndingSingle -FilePath $file.FullName -TargetLineEnding $TargetLineEnding -CurrentInfo $currentInfo -CreateBackup $CreateBackups -EnsureFinalNewline $EnsureFinalNewline

                $result = @{
                    FilePath          = $relativePath
                    FullPath          = $file.FullName
                    Status            = $conversionResult.Status
                    Reason            = $conversionResult.Reason
                    CurrentLineEnding = $currentLineEnding
                    TargetLineEnding  = $TargetLineEnding
                    HasFinalNewline   = $hasFinalNewline
                    BackupPath        = $conversionResult.BackupPath
                }

                if ($CreateBackups -and $BackupDirectory -and $conversionResult.BackupPath -and (Test-Path $conversionResult.BackupPath)) {
                    $backupTargetPath = Join-Path $BackupDirectory $relativePath
                    $backupTargetDir = Split-Path $backupTargetPath -Parent

                    if (-not (Test-Path $backupTargetDir)) {
                        New-Item -Path $backupTargetDir -ItemType Directory -Force | Out-Null
                    }

                    Move-Item -Path $conversionResult.BackupPath -Destination $backupTargetPath -Force
                    $result.BackupPath = $backupTargetPath
                }

                $results += [PSCustomObject]$result

                switch ($conversionResult.Status) {
                    'Converted' {
                        $converted++
                        Write-Verbose "Converted $relativePath from $currentLineEnding to $TargetLineEnding"
                    }
                    'Error' {
                        $errors++
                        Write-Warning "Failed to convert $relativePath`: $($conversionResult.Reason)"
                    }
                    default {
                        $skipped++ 
                    }
                }
            }
        }
        catch {
            Write-Warning "Unexpected error processing $($file.FullName): $_"
            $errors++
        }
    }

    $summary = @{
        TotalFiles       = $uniqueFiles.Count
        Converted        = $converted
        Skipped          = $skipped
        Errors           = $errors
        TargetLineEnding = $TargetLineEnding
        ProjectPath      = $Path
        ProjectType      = if ($ProjectType -eq 'Custom') {
            "Custom ($($CustomExtensions -join ', '))" 
        }
        else {
            $ProjectType 
        }
    }

    Write-Host "`nLine Ending Conversion Summary:" -ForegroundColor Cyan
    Write-Host " Total files processed: $($summary.TotalFiles)" -ForegroundColor White
    Write-Host " Successfully converted: $($summary.Converted)" -ForegroundColor Green
    Write-Host " Skipped: $($summary.Skipped)" -ForegroundColor Yellow
    Write-Host " Errors: $($summary.Errors)" -ForegroundColor Red
    Write-Host " Target line ending: $($summary.TargetLineEnding)" -ForegroundColor White

    if ($PassThru) {
        [PSCustomObject]@{
            Summary = $summary
            Results = $results
        }
    }
}

function Get-MissingFunctions {
    [CmdletBinding()]
    param(
        [alias('Path')][string] $FilePath,
        [alias('ScriptBlock')][scriptblock] $Code,
        [string[]] $Functions,
        [switch] $Summary,
        [switch] $SummaryWithCommands,
        [Array] $ApprovedModules,
        [Array] $IgnoreFunctions
    )
    $ListCommands = [System.Collections.Generic.List[Object]]::new()
    if ($FilePath) {
        $CommandsUsedInCode = Get-ScriptCommands -FilePath $FilePath
    }
    elseif ($Code) {
        $CommandsUsedInCode = Get-ScriptCommands -Code $Code
    }
    else {
        return
    }
    if ($IgnoreFunctions.Count -gt 0) {
        $Result = foreach ($_ in $CommandsUsedInCode) {
            if ($IgnoreFunctions -notcontains $_) {
                $_
            }
        }
    }
    else {
        $Result = $CommandsUsedInCode
    }

    [Array] $FilteredCommands = Get-FilteredScriptCommands -Commands $Result -Functions $Functions -FilePath $FilePath -ApprovedModules $ApprovedModules
    foreach ($_ in $FilteredCommands) {
        $ListCommands.Add($_)
    }

    [Array] $FilteredCommandsName = foreach ($Name in $FilteredCommands.Name) {
        $Name
    }

    [Array] $FunctionsOutput = foreach ($_ in $ListCommands) {
        if ($_.ScriptBlock) {
            if ($ApprovedModules.Count -gt 0 -and $_.Source -in $ApprovedModules) {
                "function $($_.Name) { $($_.ScriptBlock) }"
            }
            elseif ($ApprovedModules.Count -eq 0) {
            }
        }
    }

    if ($FunctionsOutput.Count -gt 0) {
        $IgnoreAlreadyKnownCommands = ($FilteredCommandsName + $IgnoreFunctions) | Sort-Object -Unique
        $ScriptBlockMissing = [scriptblock]::Create($FunctionsOutput)
        $AnotherRun = Get-MissingFunctions -SummaryWithCommands -ApprovedModules $ApprovedModules -Code $ScriptBlockMissing -IgnoreFunctions $IgnoreAlreadyKnownCommands
    }

    if ($SummaryWithCommands) {
        if ($AnotherRun) {
            $Hash = @{ }
            $Hash.Summary = foreach ($_ in $FilteredCommands + $AnotherRun.Summary) {
                $_
            }
            $Hash.SummaryFiltered = foreach ($_ in $ListCommands + $AnotherRun.SummaryFiltered) {
                $_
            }
            $Hash.Functions = foreach ($_ in $FunctionsOutput + $AnotherRun.Functions) {
                $_
            }
        }
        else {
            $Hash = @{
                Summary         = $FilteredCommands
                SummaryFiltered = $ListCommands
                Functions       = $FunctionsOutput
            }
        }
        return $Hash
    }
    elseif ($Summary) {
        if ($AnotherRun) {
            foreach ($_ in $ListCommands + $AnotherRun.SummaryFiltered) {
                $_
            }
        }
        else {
            return $ListCommands
        }
    }
    else {
        return $FunctionsOutput
    }
}
function Get-PowerShellAssemblyMetadata {
    <#
    .SYNOPSIS
    Gets the cmdlets and aliases in a dotnet assembly.
 
    .PARAMETER Path
    The assembly to inspect.
 
    .EXAMPLE
    Get-PowerShellAssemblyMetadata -Path MyModule.dll
 
    .NOTES
    This requires the System.Reflection.MetadataLoadContext assembly to be
    loaded through Add-Type. WinPS (5.1) will also need to load its deps
        System.Memory
        System.Collections.Immutable
        System.Reflection.Metadata
        System.Runtime.CompilerServices.Unsafe
 
    https://www.nuget.org/packages/System.Reflection.MetadataLoadContext
 
    Copyright: (c) 2024, Jordan Borean (@jborean93) <jborean93@gmail.com>
    MIT License (see LICENSE or https://opensource.org/licenses/MIT)
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string] $Path
    )
    Write-Text -Text " [+] Loading assembly $Path" -Color Cyan
    try {

        $smaAssembly = [System.Management.Automation.PSObject].Assembly
        $smaAssemblyPath = $smaAssembly.Location

        if (-not $smaAssemblyPath) {
            $smaAssemblyPath = $smaAssembly.CodeBase
            if ($smaAssemblyPath -like 'file://*') {
                $smaAssemblyPath = $smaAssemblyPath -replace 'file:///', ''
                $smaAssemblyPath = [System.Uri]::UnescapeDataString($smaAssemblyPath)
            }
            else {
                Write-Text -Text "[-] Could not determine the path to System.Management.Automation assembly." -Color Red
                return $false
            }
        }

        $assemblyDirectory = Split-Path -Path $Path
        $runtimeAssemblies = Get-ChildItem -Path ([System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()) -Filter "*.dll"
        $assemblyFiles = Get-ChildItem -Path $assemblyDirectory -Filter "*.dll" -Recurse

        $resolverPaths = [string[]] @(
            $runtimeAssemblies.FullName
            $assemblyFiles.FullName
            $smaAssemblyPath  
        )

        $resolver = [System.Reflection.PathAssemblyResolver]::new($resolverPaths)
    }
    catch {
        Write-Text -Text " [-] Can't create PathAssemblyResolver. Please ensure all dependencies are present. Error: $($_.Exception.Message)" -Color Red
        return $false
    }
    try {
        $context = [System.Reflection.MetadataLoadContext]::new($resolver)

        $smaAssemblyInContext = $context.LoadFromAssemblyPath($smaAssemblyPath)
        $cmdletType = $smaAssemblyInContext.GetType('System.Management.Automation.Cmdlet')
        $cmdletAttribute = $smaAssemblyInContext.GetType('System.Management.Automation.CmdletAttribute')
        $aliasAttribute = $smaAssemblyInContext.GetType('System.Management.Automation.AliasAttribute')

        $assembly = $context.LoadFromAssemblyPath($Path)

        Write-Verbose -Message "Loaded assembly $($assembly.FullName), $($assembly.Location) searching for cmdlets and aliases"

        $cmdletsToExport = [System.Collections.Generic.List[string]]::new()
        $aliasesToExport = [System.Collections.Generic.List[string]]::new()
        $Types = $assembly.GetTypes()

        $Types | Where-Object {
            $_.IsSubclassOf($cmdletType)
        } | ForEach-Object -Process {
            $cmdletInfo = $_.CustomAttributes | Where-Object { $_.AttributeType -eq $cmdletAttribute }
            if (-not $cmdletInfo) {
                return 
            }

            $name = "$($cmdletInfo.ConstructorArguments[0].Value)-$($cmdletInfo.ConstructorArguments[1].Value)"
            $cmdletsToExport.Add($name)

            $aliases = $_.CustomAttributes | Where-Object { $_.AttributeType -eq $aliasAttribute }
            if (-not $aliases -or -not $aliases.ConstructorArguments.Value) {
                return 
            }
            $aliasesToExport.AddRange([string[]]@($aliases.ConstructorArguments.Value.Value))
        }

        [PSCustomObject]@{
            CmdletsToExport = $cmdletsToExport
            AliasesToExport = $aliasesToExport
        }
    }
    catch {
        Write-Text -Text " [-] Can't load assembly $Path. Error: $($_.Exception.Message)" -Color Red
        $context.Dispose()
        return $false
    }
    finally {
        $context.Dispose()
    }
}
function Get-ProjectConsistency {
    <#
    .SYNOPSIS
    Provides comprehensive analysis of encoding and line ending consistency across a project.
 
    .DESCRIPTION
    Combines encoding and line ending analysis to provide a complete picture of file consistency
    across a project. Identifies issues and provides recommendations for standardization.
    This is the main analysis function that should be run before any bulk conversions.
 
    .PARAMETER Path
    Path to the project directory to analyze.
 
    .PARAMETER ProjectType
    Type of project to analyze. Determines which file extensions are included.
    Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom'
 
    .PARAMETER CustomExtensions
    Custom file extensions to analyze when ProjectType is 'Custom'.
    Example: @('*.ps1', '*.psm1', '*.cs', '*.vb')
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj').
 
    .PARAMETER RecommendedEncoding
    The encoding standard you want to achieve.
    Default is 'UTF8BOM' for PowerShell projects (PS 5.1 compatibility), 'UTF8' for others.
 
    .PARAMETER RecommendedLineEnding
    The line ending standard you want to achieve. Default is 'CRLF' on Windows, 'LF' on Unix.
 
    .PARAMETER ShowDetails
    Include detailed file-by-file analysis in the output.
 
    .PARAMETER ExportPath
    Export the detailed report to a CSV file at the specified path.
 
    .EXAMPLE
    Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType PowerShell
    Analyze consistency in a PowerShell project with UTF8BOM encoding (PS 5.1 compatible).
 
    .EXAMPLE
    Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType Mixed -RecommendedEncoding UTF8BOM -RecommendedLineEnding LF -ShowDetails
    Analyze a mixed project with specific recommendations and detailed output.
 
    .EXAMPLE
    Get-ProjectConsistencyReport -Path 'C:\MyProject' -ProjectType CSharp -RecommendedEncoding UTF8 -ExportPath 'C:\Reports\consistency-report.csv'
    Analyze a C# project (UTF8 without BOM is fine) with CSV export.
 
    .NOTES
    This function combines the analysis from Get-ProjectEncoding and Get-ProjectLineEnding
    to provide a unified view of project file consistency. Use this before running conversion functions.
 
    Encoding Recommendations:
    - PowerShell: UTF8BOM (required for PS 5.1 compatibility with special characters)
    - C#: UTF8 (BOM not needed, Visual Studio handles UTF8 correctly)
    - Mixed: UTF8BOM (safest for cross-platform PowerShell compatibility)
 
    PowerShell 5.1 Compatibility:
    UTF8 without BOM can cause PowerShell 5.1 to misinterpret files as ASCII, leading to:
    - Broken special characters and accented letters
    - Module import failures
    - Incorrect string processing
    UTF8BOM ensures proper encoding detection across all PowerShell versions.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')]
        [string] $ProjectType = 'Mixed',

        [string[]] $CustomExtensions,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'),

        [ValidateSet('Ascii', 'BigEndianUnicode', 'Unicode', 'UTF7', 'UTF8', 'UTF8BOM', 'UTF32', 'Default', 'OEM')]
        [string] $RecommendedEncoding = $(
            if ($ProjectType -eq 'PowerShell') {
                'UTF8BOM' 
            }
            elseif ($ProjectType -eq 'Mixed') {
                'UTF8BOM' 
            }  # Default to PowerShell-safe for mixed projects
            else {
                'UTF8' 
            }
        ),

        [ValidateSet('CRLF', 'LF')]
        [string] $RecommendedLineEnding = $(if ($IsWindows) {
                'CRLF' 
            }
            else {
                'LF' 
            }),

        [switch] $ShowDetails,
        [string] $ExportPath
    )

    Write-Host "🔍 Analyzing project consistency..." -ForegroundColor Cyan
    Write-Host "Project: $Path" -ForegroundColor White
    Write-Host "Type: $ProjectType" -ForegroundColor White
    Write-Host "Target encoding: $RecommendedEncoding" -ForegroundColor White
    Write-Host "Target line ending: $RecommendedLineEnding" -ForegroundColor White

    Write-Host "`n📝 Analyzing file encodings..." -ForegroundColor Yellow
    $encodingParams = @{
        Path               = $Path
        ProjectType        = $ProjectType
        ExcludeDirectories = $ExcludeDirectories
        ShowFiles          = $true
    }
    if ($ProjectType -eq 'Custom' -and $CustomExtensions) {
        $encodingParams.CustomExtensions = $CustomExtensions
    }

    $encodingReport = Get-ProjectEncoding @encodingParams

    Write-Host "`n📏 Analyzing line endings..." -ForegroundColor Yellow
    $lineEndingParams = @{
        Path               = $Path
        ProjectType        = $ProjectType
        ExcludeDirectories = $ExcludeDirectories
        ShowFiles          = $true
        CheckMixed         = $true
    }
    if ($ProjectType -eq 'Custom' -and $CustomExtensions) {
        $lineEndingParams.CustomExtensions = $CustomExtensions
    }

    $lineEndingReport = Get-ProjectLineEnding @lineEndingParams

    Write-Host "`n🔄 Combining analysis..." -ForegroundColor Yellow

    $allFiles = @()
    foreach ($encFile in $encodingReport.Files) {
        $leFile = $lineEndingReport.Files | Where-Object { $_.FullPath -eq $encFile.FullPath }

        if ($leFile) {
            $needsEncodingConversion = $encFile.Encoding -ne $RecommendedEncoding
            $needsLineEndingConversion = $leFile.LineEnding -ne $RecommendedLineEnding -and $leFile.LineEnding -ne 'None'
            $hasMixedLineEndings = $leFile.LineEnding -eq 'Mixed'
            $missingFinalNewline = -not $leFile.HasFinalNewline -and $encFile.Size -gt 0 -and $encFile.Extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml')

            $fileDetail = [PSCustomObject]@{
                RelativePath              = $encFile.RelativePath
                FullPath                  = $encFile.FullPath
                Extension                 = $encFile.Extension
                CurrentEncoding           = $encFile.Encoding
                CurrentLineEnding         = $leFile.LineEnding
                RecommendedEncoding       = $RecommendedEncoding
                RecommendedLineEnding     = $RecommendedLineEnding
                NeedsEncodingConversion   = $needsEncodingConversion
                NeedsLineEndingConversion = $needsLineEndingConversion
                HasMixedLineEndings       = $hasMixedLineEndings
                MissingFinalNewline       = $missingFinalNewline
                HasIssues                 = $needsEncodingConversion -or $needsLineEndingConversion -or $hasMixedLineEndings -or $missingFinalNewline
                Size                      = $encFile.Size
                LastModified              = $encFile.LastModified
                Directory                 = $encFile.Directory
            }

            $allFiles += $fileDetail
        }
    }

    $totalFiles = $allFiles.Count
    $filesNeedingEncodingConversion = ($allFiles | Where-Object { $_.NeedsEncodingConversion }).Count
    $filesNeedingLineEndingConversion = ($allFiles | Where-Object { $_.NeedsLineEndingConversion }).Count
    $filesWithMixedLineEndings = ($allFiles | Where-Object { $_.HasMixedLineEndings }).Count
    $filesMissingFinalNewline = ($allFiles | Where-Object { $_.MissingFinalNewline }).Count
    $filesWithIssues = ($allFiles | Where-Object { $_.HasIssues }).Count
    $filesCompliant = $totalFiles - $filesWithIssues

    $extensionIssues = @{}
    foreach ($file in ($allFiles | Where-Object { $_.HasIssues })) {
        if (-not $extensionIssues.ContainsKey($file.Extension)) {
            $extensionIssues[$file.Extension] = @{
                Total            = 0
                EncodingIssues   = 0
                LineEndingIssues = 0
                MixedLineEndings = 0
            }
        }
        $extensionIssues[$file.Extension].Total++
        if ($file.NeedsEncodingConversion) {
            $extensionIssues[$file.Extension].EncodingIssues++ 
        }
        if ($file.NeedsLineEndingConversion) {
            $extensionIssues[$file.Extension].LineEndingIssues++ 
        }
        if ($file.HasMixedLineEndings) {
            $extensionIssues[$file.Extension].MixedLineEndings++ 
        }
    }

    $summary = [PSCustomObject]@{
        ProjectPath                      = $Path
        ProjectType                      = $ProjectType
        AnalysisDate                     = Get-Date

        TotalFiles                       = $totalFiles
        FilesCompliant                   = $filesCompliant
        FilesWithIssues                  = $filesWithIssues
        CompliancePercentage             = [math]::Round(($filesCompliant / $totalFiles) * 100, 1)

        CurrentEncodingDistribution      = $encodingReport.Summary.EncodingDistribution
        FilesNeedingEncodingConversion   = $filesNeedingEncodingConversion
        RecommendedEncoding              = $RecommendedEncoding

        CurrentLineEndingDistribution    = $lineEndingReport.Summary.LineEndingDistribution
        FilesNeedingLineEndingConversion = $filesNeedingLineEndingConversion
        FilesWithMixedLineEndings        = $filesWithMixedLineEndings
        FilesMissingFinalNewline         = $filesMissingFinalNewline
        RecommendedLineEnding            = $RecommendedLineEnding

        ExtensionIssues                  = $extensionIssues
    }

    Write-Host "`n📊 Project Consistency Summary:" -ForegroundColor Cyan
    Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White
    Write-Host " Files compliant with standards: $filesCompliant ($($summary.CompliancePercentage)%)" -ForegroundColor $(if ($summary.CompliancePercentage -ge 90) {
            'Green' 
        }
        elseif ($summary.CompliancePercentage -ge 70) {
            'Yellow' 
        }
        else {
            'Red' 
        })
    Write-Host " Files needing attention: $filesWithIssues" -ForegroundColor $(if ($filesWithIssues -eq 0) {
            'Green' 
        }
        else {
            'Red' 
        })

    Write-Host "`n📝 Encoding Issues:" -ForegroundColor Cyan
    Write-Host " Files needing encoding conversion: $filesNeedingEncodingConversion" -ForegroundColor $(if ($filesNeedingEncodingConversion -eq 0) {
            'Green' 
        }
        else {
            'Yellow' 
        })
    Write-Host " Target encoding: $RecommendedEncoding" -ForegroundColor White

    Write-Host "`n📏 Line Ending Issues:" -ForegroundColor Cyan
    Write-Host " Files needing line ending conversion: $filesNeedingLineEndingConversion" -ForegroundColor $(if ($filesNeedingLineEndingConversion -eq 0) {
            'Green' 
        }
        else {
            'Yellow' 
        })
    Write-Host " Files with mixed line endings: $filesWithMixedLineEndings" -ForegroundColor $(if ($filesWithMixedLineEndings -eq 0) {
            'Green' 
        }
        else {
            'Red' 
        })
    Write-Host " Files missing final newline: $filesMissingFinalNewline" -ForegroundColor $(if ($filesMissingFinalNewline -eq 0) {
            'Green' 
        }
        else {
            'Yellow' 
        })
    Write-Host " Target line ending: $RecommendedLineEnding" -ForegroundColor White

    if ($extensionIssues.Count -gt 0) {
        Write-Host "`n⚠️ Extensions with Issues:" -ForegroundColor Yellow
        foreach ($ext in ($extensionIssues.GetEnumerator() | Sort-Object { $_.Value.Total } -Descending)) {
            Write-Host " ${ext.Key}: $($ext.Value.Total) files" -ForegroundColor White
            if ($ext.Value.EncodingIssues -gt 0) {
                Write-Host " - Encoding issues: $($ext.Value.EncodingIssues)" -ForegroundColor Yellow
            }
            if ($ext.Value.LineEndingIssues -gt 0) {
                Write-Host " - Line ending issues: $($ext.Value.LineEndingIssues)" -ForegroundColor Yellow
            }
            if ($ext.Value.MixedLineEndings -gt 0) {
                Write-Host " - Mixed line endings: $($ext.Value.MixedLineEndings)" -ForegroundColor Red
            }
        }
    }

    Write-Host "`n💡 Recommendations:" -ForegroundColor Green
    if ($filesWithIssues -eq 0) {
        Write-Host " ✅ Your project is fully compliant! No action needed." -ForegroundColor Green
    }
    else {
        if ($filesWithMixedLineEndings -gt 0) {
            Write-Host " 🔴 Priority 1: Fix mixed line endings first" -ForegroundColor Red
            Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -OnlyMixed -CreateBackups" -ForegroundColor Gray
        }
        if ($filesNeedingEncodingConversion -gt 0) {
            Write-Host " 🟡 Priority 2: Standardize encoding" -ForegroundColor Yellow
            Write-Host " Convert-ProjectEncoding -Path '$Path' -ProjectType $ProjectType -TargetEncoding $RecommendedEncoding -CreateBackups" -ForegroundColor Gray
        }
        if ($filesNeedingLineEndingConversion -gt 0) {
            Write-Host " 🟡 Priority 3: Standardize line endings" -ForegroundColor Yellow
            Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -CreateBackups" -ForegroundColor Gray
        }
        if ($filesMissingFinalNewline -gt 0) {
            Write-Host " 🟡 Priority 4: Add missing final newlines" -ForegroundColor Yellow
            Write-Host " Convert-ProjectLineEnding -Path '$Path' -ProjectType $ProjectType -TargetLineEnding $RecommendedLineEnding -EnsureFinalNewline -OnlyMissingNewline -CreateBackups" -ForegroundColor Gray
        }
        Write-Host " 💾 Always use -WhatIf first and -CreateBackups for safety!" -ForegroundColor Cyan
    }

    $report = [PSCustomObject]@{
        Summary          = $summary
        EncodingReport   = $encodingReport
        LineEndingReport = $lineEndingReport
        Files            = if ($ShowDetails) {
            $allFiles 
        }
        else {
            $null 
        }
        ProblematicFiles = $allFiles | Where-Object { $_.HasIssues }
    }

    if ($ExportPath) {
        try {
            $allFiles | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
            Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green
        }
        catch {
            Write-Warning "Failed to export report to $ExportPath`: $_"
        }
    }

    return $report
}

function Get-ProjectEncoding {
    <#
    .SYNOPSIS
    Analyzes encoding consistency across all files in a project directory.
 
    .DESCRIPTION
    Scans all relevant files in a project directory and provides a comprehensive report on file encodings.
    Identifies inconsistencies, potential issues, and provides recommendations for standardization.
    Useful for auditing projects before performing encoding conversions.
 
    .PARAMETER Path
    Path to the project directory to analyze.
 
    .PARAMETER ProjectType
    Type of project to analyze. Determines which file extensions are included.
    Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom'
 
    .PARAMETER CustomExtensions
    Custom file extensions to analyze when ProjectType is 'Custom'.
    Example: @('*.ps1', '*.psm1', '*.cs', '*.vb')
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj').
 
    .PARAMETER GroupByEncoding
    Group results by encoding type for easier analysis.
 
    .PARAMETER ShowFiles
    Include individual file details in the report.
 
    .PARAMETER ExportPath
    Export the detailed report to a CSV file at the specified path.
 
    .EXAMPLE
    Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType PowerShell
    Analyze encoding consistency in a PowerShell project.
 
    .EXAMPLE
    Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType Mixed -GroupByEncoding -ShowFiles
    Get detailed encoding report grouped by encoding type with individual file listings.
 
    .EXAMPLE
    Get-ProjectEncoding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\encoding-report.csv'
    Analyze all file types and export detailed report to CSV.
 
    .NOTES
    This function is read-only and does not modify any files. Use Convert-ProjectEncoding to standardize encodings.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')]
        [string] $ProjectType = 'Mixed',

        [string[]] $CustomExtensions,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'),

        [switch] $GroupByEncoding,
        [switch] $ShowFiles,
        [string] $ExportPath
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Project path '$Path' not found or is not a directory"
    }

    $extensionMappings = @{
        'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml')
        'CSharp'     = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx')
        'Mixed'      = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml')
        'All'        = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml')
    }

    if ($ProjectType -eq 'Custom' -and $CustomExtensions) {
        $filePatterns = $CustomExtensions
    }
    else {
        $filePatterns = $extensionMappings[$ProjectType]
    }

    Write-Host "Analyzing project encoding..." -ForegroundColor Cyan
    Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')"

    $allFiles = @()

    foreach ($pattern in $filePatterns) {
        $params = @{
            Path    = $Path
            Filter  = $pattern
            File    = $true
            Recurse = $true
        }

        $files = Get-ChildItem @params | Where-Object {
            $file = $_
            $excluded = $false

            foreach ($excludeDir in $ExcludeDirectories) {
                if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") {
                    $excluded = $true
                    break
                }
            }

            -not $excluded
        }

        $allFiles += $files
    }

    $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString

    if ($uniqueFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria"
        return
    }

    Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green

    $fileDetails = @()
    $encodingStats = @{}
    $extensionStats = @{}

    foreach ($file in $uniqueFiles) {
        try {
            $encodingInfo = Get-FileEncoding -Path $file.FullName -AsObject
            $extension = $file.Extension.ToLower()
            $relativePath = Get-RelativePath -From $Path -To $file.FullName

            $fileDetail = [PSCustomObject]@{
                RelativePath = $relativePath
                FullPath     = $file.FullName
                Extension    = $extension
                Encoding     = $encodingInfo.EncodingName
                Size         = $file.Length
                LastModified = $file.LastWriteTime
                Directory    = $file.DirectoryName
            }

            $fileDetails += $fileDetail

            if (-not $encodingStats.ContainsKey($encodingInfo.EncodingName)) {
                $encodingStats[$encodingInfo.EncodingName] = 0
            }
            $encodingStats[$encodingInfo.EncodingName]++

            if (-not $extensionStats.ContainsKey($extension)) {
                $extensionStats[$extension] = @{}
            }
            if (-not $extensionStats[$extension].ContainsKey($encodingInfo.EncodingName)) {
                $extensionStats[$extension][$encodingInfo.EncodingName] = 0
            }
            $extensionStats[$extension][$encodingInfo.EncodingName]++
        }
        catch {
            Write-Warning "Failed to analyze $($file.FullName): $_"
        }
    }

    $totalFiles = $fileDetails.Count
    $uniqueEncodings = $encodingStats.Keys | Sort-Object
    $mostCommonEncoding = ($encodingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key
    $inconsistentExtensions = @()

    foreach ($ext in $extensionStats.Keys) {
        if ($extensionStats[$ext].Count -gt 1) {
            $inconsistentExtensions += $ext
        }
    }

    $summary = [PSCustomObject]@{
        ProjectPath            = $Path
        ProjectType            = $ProjectType
        TotalFiles             = $totalFiles
        UniqueEncodings        = $uniqueEncodings
        EncodingCount          = $uniqueEncodings.Count
        MostCommonEncoding     = $mostCommonEncoding
        InconsistentExtensions = $inconsistentExtensions
        EncodingDistribution   = $encodingStats
        ExtensionEncodingMap   = $extensionStats
        AnalysisDate           = Get-Date
    }

    Write-Host "`nEncoding Analysis Summary:" -ForegroundColor Cyan
    Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White
    Write-Host " Unique encodings found: $($uniqueEncodings.Count)" -ForegroundColor White

    if ($totalFiles -gt 0 -and $mostCommonEncoding) {
        Write-Host " Most common encoding: $mostCommonEncoding ($($encodingStats[$mostCommonEncoding]) files)" -ForegroundColor Green
    }
    elseif ($totalFiles -eq 0) {
        Write-Host " No files found for analysis" -ForegroundColor Yellow
        return $result
    }
    else {
        Write-Host " No encoding information available" -ForegroundColor Yellow
    }

    if ($inconsistentExtensions.Count -gt 0) {
        Write-Host " ⚠️ Extensions with mixed encodings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow
    }
    else {
        Write-Host " ✅ All file extensions have consistent encodings" -ForegroundColor Green
    }

    Write-Host "`nEncoding Distribution:" -ForegroundColor Cyan
    foreach ($encoding in ($encodingStats.GetEnumerator() | Sort-Object Value -Descending)) {
        $percentage = [math]::Round(($encoding.Value / $totalFiles) * 100, 1)
        Write-Host " $($encoding.Key): $($encoding.Value) files ($percentage%)" -ForegroundColor White
    }

    if ($inconsistentExtensions.Count -gt 0) {
        Write-Host "`nExtensions with Mixed Encodings:" -ForegroundColor Yellow
        foreach ($ext in $inconsistentExtensions) {
            Write-Host " ${ext}:" -ForegroundColor Yellow
            foreach ($encoding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) {
                Write-Host " $($encoding.Key): $($encoding.Value) files" -ForegroundColor White
            }
        }
    }

    $report = [PSCustomObject]@{
        Summary           = $summary
        Files             = if ($ShowFiles) {
            $fileDetails 
        }
        else {
            $null 
        }
        GroupedByEncoding = if ($GroupByEncoding) {
            $grouped = @{}
            foreach ($encoding in $uniqueEncodings) {
                $grouped[$encoding] = $fileDetails | Where-Object { $_.Encoding -eq $encoding }
            }
            $grouped
        }
        else {
            $null 
        }
    }

    if ($ExportPath) {
        try {
            $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
            Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green
        }
        catch {
            Write-Warning "Failed to export report to $ExportPath`: $_"
        }
    }

    return $report
}

function Get-ProjectLineEnding {
    <#
    .SYNOPSIS
    Analyzes line ending consistency across all files in a project directory.
 
    .DESCRIPTION
    Scans all relevant files in a project directory and provides a comprehensive report on line endings.
    Identifies inconsistencies between CRLF (Windows), LF (Unix/Linux), and mixed line endings.
    Helps ensure consistency across development environments and prevent Git issues.
 
    .PARAMETER Path
    Path to the project directory to analyze.
 
    .PARAMETER ProjectType
    Type of project to analyze. Determines which file extensions are included.
    Valid values: 'PowerShell', 'CSharp', 'Mixed', 'All', 'Custom'
 
    .PARAMETER CustomExtensions
    Custom file extensions to analyze when ProjectType is 'Custom'.
    Example: @('*.ps1', '*.psm1', '*.cs', '*.vb')
 
    .PARAMETER ExcludeDirectories
    Directory names to exclude from analysis (e.g., '.git', 'bin', 'obj').
 
    .PARAMETER GroupByLineEnding
    Group results by line ending type for easier analysis.
 
    .PARAMETER ShowFiles
    Include individual file details in the report.
 
    .PARAMETER CheckMixed
    Additionally check for files with mixed line endings (both CRLF and LF in same file).
 
    .PARAMETER ExportPath
    Export the detailed report to a CSV file at the specified path.
 
    .EXAMPLE
    Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType PowerShell
    Analyze line ending consistency in a PowerShell project.
 
    .EXAMPLE
    Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType Mixed -CheckMixed -ShowFiles
    Get detailed line ending report including mixed line ending detection.
 
    .EXAMPLE
    Get-ProjectLineEnding -Path 'C:\MyProject' -ProjectType All -ExportPath 'C:\Reports\lineending-report.csv'
    Analyze all file types and export detailed report to CSV.
 
    .NOTES
    Line ending types:
    - CRLF: Windows style (\\r\\n)
    - LF: Unix/Linux style (\\n)
    - CR: Classic Mac style (\\r) - rarely used
    - Mixed: File contains multiple line ending types
    - None: Empty file or single line without line ending
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [ValidateSet('PowerShell', 'CSharp', 'Mixed', 'All', 'Custom')]
        [string] $ProjectType = 'Mixed',

        [string[]] $CustomExtensions,

        [string[]] $ExcludeDirectories = @('.git', '.vs', 'bin', 'obj', 'packages', 'node_modules', '.vscode'),

        [switch] $GroupByLineEnding,
        [switch] $ShowFiles,
        [switch] $CheckMixed,
        [string] $ExportPath
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
        throw "Project path '$Path' not found or is not a directory"
    }

    $extensionMappings = @{
        'PowerShell' = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml')
        'CSharp'     = @('*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.resx')
        'Mixed'      = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml')
        'All'        = @('*.ps1', '*.psm1', '*.psd1', '*.ps1xml', '*.cs', '*.csx', '*.csproj', '*.sln', '*.config', '*.json', '*.xml', '*.js', '*.ts', '*.py', '*.rb', '*.java', '*.cpp', '*.h', '*.hpp', '*.sql', '*.md', '*.txt', '*.yaml', '*.yml')
    }

    if ($ProjectType -eq 'Custom' -and $CustomExtensions) {
        $filePatterns = $CustomExtensions
    }
    else {
        $filePatterns = $extensionMappings[$ProjectType]
    }

    Write-Host "Analyzing project line endings..." -ForegroundColor Cyan
    Write-Verbose "Project type: $ProjectType with patterns: $($filePatterns -join ', ')"    

    $allFiles = @()

    foreach ($pattern in $filePatterns) {
        $params = @{
            Path    = $Path
            Filter  = $pattern
            File    = $true
            Recurse = $true
        }

        $files = Get-ChildItem @params | Where-Object {
            $file = $_
            $excluded = $false

            foreach ($excludeDir in $ExcludeDirectories) {
                if ($file.DirectoryName -like "*\$excludeDir" -or $file.DirectoryName -like "*\$excludeDir\*") {
                    $excluded = $true
                    break
                }
            }

            -not $excluded
        }

        $allFiles += $files
    }

    $uniqueFiles = $allFiles | Sort-Object FullName | Get-Unique -AsString

    if ($uniqueFiles.Count -eq 0) {
        Write-Warning "No files found matching the specified criteria"
        return
    }

    Write-Host "Analyzing $($uniqueFiles.Count) files..." -ForegroundColor Green

    $fileDetails = @()
    $lineEndingStats = @{}
    $extensionStats = @{}
    $problemFiles = @()
    $filesWithoutFinalNewline = @()

    foreach ($file in $uniqueFiles) {
        try {
            $lineEndingInfo = Get-LineEndingType -FilePath $file.FullName
            $lineEndingType = $lineEndingInfo.LineEnding
            $hasFinalNewline = $lineEndingInfo.HasFinalNewline
            $extension = $file.Extension.ToLower()
            $relativePath = Get-RelativePath -From $Path -To $file.FullName

            $fileDetail = [PSCustomObject]@{
                RelativePath    = $relativePath
                FullPath        = $file.FullName
                Extension       = $extension
                LineEnding      = $lineEndingType
                HasFinalNewline = $hasFinalNewline
                Size            = $file.Length
                LastModified    = $file.LastWriteTime
                Directory       = $file.DirectoryName
            }

            $fileDetails += $fileDetail

            if ($lineEndingType -eq 'Mixed' -or ($CheckMixed -and $lineEndingType -eq 'Mixed')) {
                $problemFiles += $fileDetail
            }

            if (-not $hasFinalNewline -and $file.Length -gt 0 -and $extension -in @('.ps1', '.psm1', '.psd1', '.cs', '.js', '.py', '.rb', '.java', '.cpp', '.h', '.hpp', '.sql', '.md', '.txt', '.yaml', '.yml')) {
                $filesWithoutFinalNewline += $fileDetail
            }

            if (-not $lineEndingStats.ContainsKey($lineEndingType)) {
                $lineEndingStats[$lineEndingType] = 0
            }
            $lineEndingStats[$lineEndingType]++

            if (-not $extensionStats.ContainsKey($extension)) {
                $extensionStats[$extension] = @{}
            }
            if (-not $extensionStats[$extension].ContainsKey($lineEndingType)) {
                $extensionStats[$extension][$lineEndingType] = 0
            }
            $extensionStats[$extension][$lineEndingType]++
        }
        catch {
            Write-Warning "Failed to analyze $($file.FullName): $_"
        }
    }

    $totalFiles = $fileDetails.Count
    $uniqueLineEndings = $lineEndingStats.Keys | Sort-Object
    $mostCommonLineEnding = ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1).Key
    $inconsistentExtensions = @()

    foreach ($ext in $extensionStats.Keys) {
        if ($extensionStats[$ext].Count -gt 1) {
            $inconsistentExtensions += $ext
        }
    }

    $summary = [PSCustomObject]@{
        ProjectPath              = $Path
        ProjectType              = $ProjectType
        TotalFiles               = $totalFiles
        UniqueLineEndings        = $uniqueLineEndings
        LineEndingCount          = $uniqueLineEndings.Count
        MostCommonLineEnding     = $mostCommonLineEnding
        InconsistentExtensions   = $inconsistentExtensions
        ProblemFiles             = $problemFiles.Count
        FilesWithoutFinalNewline = $filesWithoutFinalNewline.Count
        LineEndingDistribution   = $lineEndingStats
        ExtensionLineEndingMap   = $extensionStats
        AnalysisDate             = Get-Date
    }

    Write-Host "`nLine Ending Analysis Summary:" -ForegroundColor Cyan
    Write-Host " Total files analyzed: $totalFiles" -ForegroundColor White
    Write-Host " Unique line endings found: $($uniqueLineEndings.Count)" -ForegroundColor White
    Write-Host " Most common line ending: $mostCommonLineEnding ($($lineEndingStats[$mostCommonLineEnding]) files)" -ForegroundColor Green

    if ($problemFiles.Count -gt 0) {
        Write-Host " ⚠️ Files with mixed line endings: $($problemFiles.Count)" -ForegroundColor Red
    }

    if ($filesWithoutFinalNewline.Count -gt 0) {
        Write-Host " ⚠️ Files without final newline: $($filesWithoutFinalNewline.Count)" -ForegroundColor Yellow
    }
    else {
        Write-Host " ✅ All files end with proper newlines" -ForegroundColor Green
    }

    if ($inconsistentExtensions.Count -gt 0) {
        Write-Host " ⚠️ Extensions with mixed line endings: $($inconsistentExtensions -join ', ')" -ForegroundColor Yellow
    }
    else {
        Write-Host " ✅ All file extensions have consistent line endings" -ForegroundColor Green
    }

    Write-Host "`nLine Ending Distribution:" -ForegroundColor Cyan
    foreach ($lineEnding in ($lineEndingStats.GetEnumerator() | Sort-Object Value -Descending)) {
        $percentage = [math]::Round(($lineEnding.Value / $totalFiles) * 100, 1)
        $color = switch ($lineEnding.Key) {
            'CRLF' {
                'Green' 
            }
            'LF' {
                'Green' 
            }
            'Mixed' {
                'Red' 
            }
            'CR' {
                'Yellow' 
            }
            'None' {
                'Gray' 
            }
            'Error' {
                'Red' 
            }
            default {
                'White' 
            }
        }
        Write-Host " $($lineEnding.Key): $($lineEnding.Value) files ($percentage%)" -ForegroundColor $color
    }

    if ($problemFiles.Count -gt 0) {
        Write-Host "`nFiles with Mixed Line Endings:" -ForegroundColor Red
        foreach ($problemFile in $problemFiles | Select-Object -First 10) {
            Write-Host " $($problemFile.RelativePath)" -ForegroundColor Yellow
        }
        if ($problemFiles.Count -gt 10) {
            Write-Host " ... and $($problemFiles.Count - 10) more files" -ForegroundColor Yellow
        }
    }

    if ($filesWithoutFinalNewline.Count -gt 0) {
        Write-Host "`nFiles Missing Final Newline:" -ForegroundColor Yellow
        foreach ($missingFile in $filesWithoutFinalNewline | Select-Object -First 10) {
            Write-Host " $($missingFile.RelativePath)" -ForegroundColor Yellow
        }
        if ($filesWithoutFinalNewline.Count -gt 10) {
            Write-Host " ... and $($filesWithoutFinalNewline.Count - 10) more files" -ForegroundColor Yellow
        }
    }

    if ($inconsistentExtensions.Count -gt 0) {
        Write-Host "`nExtensions with Mixed Line Endings:" -ForegroundColor Yellow
        foreach ($ext in $inconsistentExtensions) {
            Write-Host " ${ext}:" -ForegroundColor Yellow
            foreach ($lineEnding in ($extensionStats[$ext].GetEnumerator() | Sort-Object Value -Descending)) {
                Write-Host " $($lineEnding.Key): $($lineEnding.Value) files" -ForegroundColor White
            }
        }
    }

    $report = [PSCustomObject]@{
        Summary             = $summary
        Files               = if ($ShowFiles) {
            $fileDetails 
        }
        else {
            $null 
        }
        ProblemFiles        = $problemFiles
        GroupedByLineEnding = if ($GroupByLineEnding) {
            $grouped = @{}
            foreach ($lineEnding in $uniqueLineEndings) {
                $grouped[$lineEnding] = $fileDetails | Where-Object { $_.LineEnding -eq $lineEnding }
            }
            $grouped
        }
        else {
            $null 
        }
    }

    if ($ExportPath) {
        try {
            $fileDetails | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8
            Write-Host "`nDetailed report exported to: $ExportPath" -ForegroundColor Green
        }
        catch {
            Write-Warning "Failed to export report to $ExportPath`: $_"
        }
    }

    return $report
}

function Get-ProjectVersion {
    <#
    .SYNOPSIS
    Retrieves project version information from various project files.
 
    .DESCRIPTION
    Scans the specified path for C# projects (.csproj), PowerShell modules (.psd1),
    and PowerShell build scripts that contain 'Invoke-ModuleBuild' to extract version information.
 
    .PARAMETER ModuleName
    Optional module name to filter results to specific projects/modules.
 
    .PARAMETER Path
    The root path to search for project files. Defaults to current location.
 
    .PARAMETER ExcludeFolders
    Array of folder names to exclude from the search (in addition to default 'obj' and 'bin').
 
    .OUTPUTS
    PSCustomObject[]
    Returns objects with Version, Source, and Type properties for each found project file.
 
    .EXAMPLE
    Get-ProjectVersion
    Gets version information from all project files in the current directory.
 
    .EXAMPLE
    Get-ProjectVersion -ModuleName "MyModule" -Path "C:\Projects"
    Gets version information for the specific module from the specified path.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$ModuleName,
        [Parameter()]
        [string]$Path = (Get-Location).Path,
        [Parameter()]
        [string[]]$ExcludeFolders = @()
    )

    $RepoRoot = $Path
    $DefaultExcludes = @('obj', 'bin')
    $AllExcludes = $DefaultExcludes + $ExcludeFolders | Select-Object -Unique

    $CsprojFiles = Get-ChildItem -Path $RepoRoot -Filter "*.csproj" -Recurse | Where-Object {
        $file = $_
        ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object {
                $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
            }))
    }
    $PsdFiles = Get-ChildItem -Path $RepoRoot -Filter "*.psd1" -Recurse | Where-Object {
        $file = $_
        ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object {
                $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
            }))
    }

    $BuildScriptFiles = Get-ChildItem -Path $RepoRoot -Filter "*.ps1" -Recurse | Where-Object {
        $file = $_

        $isExcluded = ($AllExcludes.Count -gt 0 -and ($AllExcludes | Where-Object {
                    $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
                }))
        if ($isExcluded) {
            return $false
        }

        try {
            $content = Get-Content -Path $file.FullName -Raw -ErrorAction SilentlyContinue
            return $content -match 'Invoke-ModuleBuild|Build-Module'
        }
        catch {
            return $false
        }
    }

    $targetCsprojFiles = $CsprojFiles
    if ($ModuleName) {
        $targetCsprojFiles = $CsprojFiles | Where-Object { $_.BaseName -eq $ModuleName }
    }

    foreach ($csProj in $targetCsprojFiles) {
        $version = Get-CurrentVersionFromCsProj -ProjectFile $csProj.FullName
        if ($version) {
            [PSCustomObject]@{
                Version = $version
                Source  = $csProj.FullName
                Type    = "C# Project"
            }
        }
    }

    $targetPsdFiles = $PsdFiles
    if ($ModuleName) {
        $targetPsdFiles = $PsdFiles | Where-Object { $_.BaseName -eq $ModuleName }
    }

    foreach ($psd1 in $targetPsdFiles) {
        $version = Get-CurrentVersionFromPsd1 -ManifestFile $psd1.FullName
        if ($version) {
            [PSCustomObject]@{
                Version = $version
                Source  = $psd1.FullName
                Type    = "PowerShell Module"
            }
        }
    }

    foreach ($buildScript in $BuildScriptFiles) {
        $version = Get-CurrentVersionFromBuildScript -ScriptFile $buildScript.FullName
        if ($version) {
            [PSCustomObject]@{
                Version = $version
                Source  = $buildScript.FullName
                Type    = "Build Script"
            }
        }
    }
}
function Initialize-PortableModule {
    [CmdletBinding()]
    param(
        [alias('ModuleName')][string] $Name,
        [string] $Path = $PSScriptRoot,
        [switch] $Download,
        [switch] $Import
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if (-not $Name) {
        Write-Warning "Initialize-ModulePortable - Module name not given. Terminating."
        return
    }
    if (-not $Download -and -not $Import) {
        Write-Warning "Initialize-ModulePortable - Please choose Download/Import switch. Terminating."
        return
    }
    if ($Download) {
        try {
            if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
                $null = New-Item -ItemType Directory -Path $Path -Force
            }
            Save-Module -Name $Name -LiteralPath $Path -WarningVariable WarningData -WarningAction SilentlyContinue -ErrorAction Stop
        }
        catch {
            $ErrorMessage = $_.Exception.Message
            if ($WarningData) {
                Write-Warning "Initialize-ModulePortable - $WarningData"
            }
            Write-Warning "Initialize-ModulePortable - Error $ErrorMessage"
            return
        }
    }
    if ($Download -or $Import) {
        [Array] $Modules = Get-RequiredModule -Path $Path -Name $Name | Where-Object { $null -ne $_ }
        if ($null -ne $Modules) {
            [array]::Reverse($Modules)
        }
        $CleanedModules = [System.Collections.Generic.List[string]]::new()
        foreach ($_ in $Modules) {
            if ($CleanedModules -notcontains $_) {
                $CleanedModules.Add($_)
            }
        }
        $CleanedModules.Add($Name)
        $Items = foreach ($_ in $CleanedModules) {
            Get-ChildItem -LiteralPath "$Path\$_" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1
        }
        [Array] $PSD1Files = $Items.FullName
    }
    if ($Download) {
        $ListFiles = foreach ($PSD1 in $PSD1Files) {
            $PSD1.Replace("$Path", '$PSScriptRoot')
        }

        $Content = @(
            '$Modules = @('
            foreach ($_ in $ListFiles) {
                " `"$_`""
            }
            ')'
            "foreach (`$_ in `$Modules) {"
            " Import-Module `$_ -Verbose:`$false -Force"
            "}"
        )
        $Content | Set-Content -Path $Path\$Name.ps1 -Force -Encoding $Encoding
    }
    if ($Import) {
        $ListFiles = foreach ($PSD1 in $PSD1Files) {
            $PSD1
        }
        foreach ($_ in $ListFiles) {
            Import-Module $_ -Verbose:$false -Force
        }
    }
}
function Initialize-PortableScript {
    [cmdletBinding()]
    param(
        [string] $FilePath,
        [string] $OutputPath,
        [Array] $ApprovedModules
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    $Output = Get-MissingFunctions -FilePath $FilePath -SummaryWithCommands -ApprovedModules $ApprovedModules
    $Script = Get-Content -LiteralPath $FilePath -Encoding UTF8
    $FinalScript = @(
        $Output.Functions
        $Script
    )
    $FinalScript | Set-Content -LiteralPath $OutputPath -Encoding $Encoding
    $Output
}
function Initialize-ProjectManager {
    <#
    .SYNOPSIS
    Builds VSCode Project manager config from filesystem
 
    .DESCRIPTION
    Builds VSCode Project manager config from filesystem
 
    .PARAMETER Path
    Path to where the projects are located
 
    .PARAMETER DisableSorting
    Disables sorting of the projects by last modified date
 
    .EXAMPLE
    Initialize-ProjectManager -Path "C:\Support\GitHub"
 
    .EXAMPLE
    Initialize-ProjectManager -Path "C:\Support\GitHub" -DisableSorting
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][string] $Path,
        [switch] $DisableSorting
    )
    $ProjectsPath = Get-ChildItem -LiteralPath $Path -Directory

    $SortedProjects = foreach ($Project in $ProjectsPath) {
        $AllFiles = Get-ChildItem -LiteralPath $Project.FullName -Exclude ".\.git" -Recurse -Depth 2 -File
        $NewestFile = $AllFiles | Sort-Object -Descending -Property LastWriteTime | Select-Object -First 1

        [PSCustomObject] @{
            name          = $Project.name
            FullName      = $Project.FullName
            LastWriteTime = $NewestFile.LastWriteTime
        }
    }
    if (-not $DisableSorting) {
        $SortedProjects = $SortedProjects | Sort-Object -Descending -Property LastWriteTime
    }

    $ProjectManager = foreach ($Project in $SortedProjects) {
        [PSCustomObject] @{
            name     = $Project.name
            rootPath = $Project.FullName
            paths    = @()
            tags     = @()
            enabled  = $true
        }
    }
    $PathProjects = @(
        [io.path]::Combine($Env:APPDATA, "Code\User\globalStorage\alefragnani.project-manager")
        [io.path]::Combine($Env:APPDATA, "Cursor\User\globalStorage\alefragnani.project-manager")
        [io.path]::Combine($Env:APPDATA, "Cursor - Insiders\User\globalStorage\alefragnani.project-manager")
        [io.path]::Combine($Env:APPDATA, "Code - Insiders\User\globalStorage\alefragnani.project-manager")
        [io.path]::Combine($Env:APPDATA, "Windsurf\User\globalStorage\alefragnani.project-manager")
    )

    foreach ($Project in $PathProjects) {
        if (Test-Path -LiteralPath $Project) {
            $JsonPath = [io.path]::Combine($Project, 'projects.json')
            if (Test-Path -LiteralPath $JsonPath) {
                Get-Content -LiteralPath $JsonPath -Encoding UTF8 | Set-Content -LiteralPath "$JsonPath.backup"
            }
            $ProjectManager | ConvertTo-Json | Set-Content -LiteralPath $JsonPath
        }
    }
}
function Invoke-DotNetReleaseBuild {
    <#
    .SYNOPSIS
    Builds a .NET project in Release configuration and prepares release artefacts.
 
    .DESCRIPTION
    Wrapper around the build, pack and signing process typically used for publishing
    .NET projects. The function cleans the Release directory, builds the project,
    signs DLLs and NuGet packages when a certificate is provided, compresses the
    build output and returns details about the generated files.
 
    .PARAMETER ProjectPath
    Path to the folder containing the project (*.csproj) file.
 
    .PARAMETER CertificateThumbprint
    Optional certificate thumbprint used to sign the built assemblies and NuGet
    packages. When omitted no signing is performed.
 
    .PARAMETER LocalStore
    Certificate store used when searching for the signing certificate. Defaults
    to 'CurrentUser'.
 
    .PARAMETER TimeStampServer
    Timestamp server URL used while signing.
 
    .OUTPUTS
    PSCustomObject with properties Version, ReleasePath and ZipPath.
 
    .EXAMPLE
    Invoke-DotNetReleaseBuild -ProjectPath 'C:\Git\MyProject' -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703'
    Builds and signs the project located in C:\Git\MyProject and returns paths to
    the release output.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectPath,
        [Parameter()]
        [string]$CertificateThumbprint,
        [string]$LocalStore = 'CurrentUser',
        [string]$TimeStampServer = 'http://timestamp.digicert.com'
    )
    $result = [ordered]@{
        Success      = $false
        Version      = $null
        ReleasePath  = $null
        ZipPath      = $null
        Packages     = @()
        ErrorMessage = $null
    }

    if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) {
        $result.ErrorMessage = 'dotnet CLI is not available.'
        return [PSCustomObject]$result
    }
    if (-not (Test-Path -LiteralPath $ProjectPath)) {
        $result.ErrorMessage = "Project path '$ProjectPath' not found."
        return [PSCustomObject]$result
    }
    $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $csproj) {
        $result.ErrorMessage = "No csproj found in $ProjectPath"
        return [PSCustomObject]$result
    }
    try {
        [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop
    }
    catch {
        $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_"
        return [PSCustomObject]$result
    }
    $version = ($xml.Project.PropertyGroup | Where-Object { $_.VersionPrefix } | Select-Object -First 1).VersionPrefix
    if (-not $version) {
        $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'"
        return [PSCustomObject]$result
    }
    $releasePath = Join-Path -Path $csproj.Directory.FullName -ChildPath 'bin/Release'
    if (Test-Path -LiteralPath $releasePath) {
        try {
            Get-ChildItem -Path $releasePath -Recurse -File | Remove-Item -Force
            Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' | Remove-Item -Force
            Get-ChildItem -Path $releasePath -Directory | Remove-Item -Force -Recurse
        }
        catch {
            $result.ErrorMessage = "Failed to clean $($releasePath): $_"
            return [PSCustomObject]$result
        }
    }
    else {
        $null = New-Item -ItemType Directory -Path $releasePath -Force
    }

    dotnet build $csproj.FullName --configuration Release
    if ($LASTEXITCODE -ne 0) {
        $result.ErrorMessage = 'dotnet build failed.'
        return [PSCustomObject]$result
    }
    if ($CertificateThumbprint) {
        Register-Certificate -Path $releasePath -LocalStore $LocalStore -Include @('*.dll') -TimeStampServer $TimeStampServer -Thumbprint $CertificateThumbprint
    }
    $zipPath = Join-Path -Path $releasePath -ChildPath ("{0}.{1}.zip" -f $csproj.BaseName, $version)
    Compress-Archive -Path (Join-Path $releasePath '*') -DestinationPath $zipPath -Force

    dotnet pack $csproj.FullName --configuration Release --no-restore --no-build
    if ($LASTEXITCODE -ne 0) {
        $result.ErrorMessage = 'dotnet pack failed.'
        return [PSCustomObject]$result
    }
    $nupkgs = Get-ChildItem -Path $releasePath -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue
    if ($CertificateThumbprint -and $nupkgs) {
        foreach ($pkg in $nupkgs) {
            dotnet nuget sign $pkg.FullName --certificate-fingerprint $CertificateThumbprint --timestamper $TimeStampServer --overwrite
            if ($LASTEXITCODE -ne 0) {
                Write-Warning "Invoke-DotNetReleaseBuild - Failed to sign $($pkg.FullName)"
            }
        }
    }
    $result.Success = $true
    $result.Version = $version
    $result.ReleasePath = $releasePath
    $result.ZipPath = $zipPath
    $result.Packages = $nupkgs.FullName
    return [PSCustomObject]$result
}

function Invoke-ModuleBuild {
    <#
    .SYNOPSIS
    Command to create new module or update existing one.
    It will create new module structure and everything around it, or update existing one.
 
    .DESCRIPTION
    Command to create new module or update existing one.
    It will create new module structure and everything around it, or update existing one.
 
    .PARAMETER Settings
    Provide settings for the module in form of scriptblock.
    It's using DSL to define settings for the module.
 
    .PARAMETER Path
    Path to the folder where new project will be created, or existing project will be updated.
    If not provided it will be created in one up folder from the location of build script.
 
    .PARAMETER ModuleName
    Provide name of the module. It's required parameter.
 
    .PARAMETER FunctionsToExportFolder
    Public functions folder name. Default is 'Public'.
    It will be used as part of PSD1 and PSM1 to export only functions from this folder.
 
    .PARAMETER AliasesToExportFolder
    Public aliases folder name. Default is 'Public'.
    It will be used as part of PSD1 and PSM1 to export only aliases from this folder.
 
    .PARAMETER Configuration
    Provides a way to configure module using hashtable.
    It's the old way of configuring module, that requires knowledge of inner workings of the module to name proper key/value pairs
    It's required for compatibility with older versions of the module.
 
    .PARAMETER ExcludeFromPackage
    Exclude files from Artefacts. Default is '.*, 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'.
 
    .PARAMETER IncludeRoot
    Include files in the Artefacts from root of the project. Default is '*.psm1', '*.psd1', 'License*' files.
    Other files will be ignored.
 
    .PARAMETER IncludePS1
    Include *.ps1 files in the Artefacts from given folders. Default are 'Private', 'Public', 'Enums', 'Classes' folders.
    If the folder doesn't exists it will be ignored.
 
    .PARAMETER IncludeAll
    Include all files in the Artefacts from given folders. Default are 'Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data' folders.
 
    .PARAMETER IncludeCustomCode
    Parameter description
 
    .PARAMETER IncludeToArray
    Parameter description
 
    .PARAMETER LibrariesCore
    Parameter description
 
    .PARAMETER LibrariesDefault
    Parameter description
 
    .PARAMETER LibrariesStandard
    Parameter description
 
    .PARAMETER ExitCode
    Exit code to be returned to the caller. If not provided, it will not exit the script, but finish gracefully.
    Exit code 0 means success, 1 means failure.
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [alias('New-PrepareModule', 'Build-Module', 'Invoke-ModuleBuilder')]
    [CmdletBinding(DefaultParameterSetName = 'Modern')]
    param (
        [Parameter(Position = 0, ParameterSetName = 'Modern')][scriptblock] $Settings,
        [parameter(ParameterSetName = 'Modern')][string] $Path,
        [parameter(Mandatory, ParameterSetName = 'Modern')][alias('ProjectName')][string] $ModuleName,
        [parameter(ParameterSetName = 'Modern')][string] $FunctionsToExportFolder = 'Public',
        [parameter(ParameterSetName = 'Modern')][string] $AliasesToExportFolder = 'Public',
        [Parameter(Mandatory, ParameterSetName = 'Configuration')][System.Collections.IDictionary] $Configuration = [ordered] @{},
        [parameter(ParameterSetName = 'Modern')][string[]] $ExcludeFromPackage = @('.*', 'Ignore', 'Examples', 'package.json', 'Publish', 'Docs'),
        [parameter(ParameterSetName = 'Modern')][string[]] $IncludeRoot = @('*.psm1', '*.psd1', 'License*'),
        [parameter(ParameterSetName = 'Modern')][string[]] $IncludePS1 = @('Private', 'Public', 'Enums', 'Classes'),
        [parameter(ParameterSetName = 'Modern')][string[]] $IncludeAll = @('Images', 'Resources', 'Templates', 'Bin', 'Lib', 'Data'),
        [parameter(ParameterSetName = 'Modern')][scriptblock] $IncludeCustomCode,
        [parameter(ParameterSetName = 'Modern')][System.Collections.IDictionary] $IncludeToArray,
        [parameter(ParameterSetName = 'Modern')][string] $LibrariesCore = [io.path]::Combine("Lib", "Core"),
        [parameter(ParameterSetName = 'Modern')][string] $LibrariesDefault = [io.path]::Combine("Lib", "Default"),
        [parameter(ParameterSetName = 'Modern')][string] $LibrariesStandard = [io.path]::Combine("Lib", "Standard"),
        [parameter(ParameterSetName = 'Configuration')]
        [parameter(ParameterSetName = 'Modern')]
        [switch] $ExitCode
    )
    if ($PsCmdlet.ParameterSetName -eq 'Configuration') {
        $ModuleName = $Configuration.Information.ModuleName
    }

    if ($Path) {

        $FullProjectPath = [io.path]::Combine($Path, $ModuleName)
    }
    else {

        $ProjectPathToUse = [io.path]::Combine($MyInvocation.PSScriptRoot, "..")
        $FullProjectPath = Get-Item -LiteralPath $ProjectPathToUse
    }

    $GlobalTime = [System.Diagnostics.Stopwatch]::StartNew()

    if ($Path -and $ModuleName) {
        $FullProjectPath = [io.path]::Combine($Path, $ModuleName)
        if (-not (Test-Path -Path $Path)) {
            Write-Text -Text "[-] Path $Path doesn't exists. Please create it, before continuing." -Color Red
            if ($ExitCode) {
                exit 1
            }
            else {
                return
            }
        }
        else {
            $CopiedBuildModule = $false
            $CopiedPSD1 = $false
            if (Test-Path -Path $FullProjectPath) {
                Write-Text -Text "[i] Module $ModuleName ($FullProjectPath) already exists. Skipping inital steps" -Color DarkGray
            }
            else {
                Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path" -Color DarkGray
                $Folders = 'Private', 'Public', 'Examples', 'Ignore', 'Build'
                Add-Directory -Directory $FullProjectPath
                foreach ($folder in $Folders) {
                    $SubFolder = [io.path]::Combine($FullProjectPath, $Folder)
                    Add-Directory -Directory $SubFolder
                }

                $PathToData = [io.path]::Combine($PSScriptRoot, "..", "Data")
                if (-not (Test-Path -LiteralPath $PathToData)) {
                    $PathToData = [io.path]::Combine($PSScriptRoot, "Data")
                }
                $FilesToCopy = [ordered] @{
                    '.gitignore'       = @{
                        Source      = [io.path]::Combine($PathToData, "Example-Gitignore.txt")
                        Destination = [io.path]::Combine($FullProjectPath, ".gitignore")
                    }
                    'CHANGELOG.MD'     = @{
                        Source      = [io.path]::Combine($PathToData, "Example-CHANGELOG.MD")
                        Destination = [io.path]::Combine($FullProjectPath, "CHANGELOG.MD")
                    }
                    'README.MD'        = @{
                        Source      = [io.path]::Combine($PathToData, "Example-README.MD")
                        Destination = [io.path]::Combine($FullProjectPath, "README.MD")
                    }
                    'License'          = @{
                        Source      = [io.path]::Combine($PathToData, "Example-LicenseMIT.txt")
                        Destination = [io.path]::Combine($FullProjectPath, "LICENSE")
                    }
                    'Build-Module.ps1' = @{
                        Source      = [io.path]::Combine($PathToData, "Example-ModuleBuilder.txt")
                        Destination = [io.path]::Combine($FullProjectPath, "Build", "Build-Module.ps1")
                    }
                    "$ModuleName.psm1" = @{
                        Source      = [io.path]::Combine($PathToData, "Example-ModulePSM1.txt")
                        Destination = [io.path]::Combine($FullProjectPath, "$ModuleName.psm1")
                    }
                    "$ModuleName.psd1" = @{
                        Source      = [io.path]::Combine($PathToData, "Example-ModulePSD1.txt")
                        Destination = [io.path]::Combine($FullProjectPath, "$ModuleName.psd1")
                    }
                }
                foreach ($File in $FilesToCopy.Keys) {
                    $ValueToProcess = $FilesToCopy[$File]
                    $SourceFilePath = $ValueToProcess.Source
                    $DestinationFilePath = $ValueToProcess.Destination
                    if (-not (Test-Path -LiteralPath $DestinationFilePath)) {
                        Write-Text -Text " [+] Copying '$($File)' file ($SourceFilePath)" -Color DarkGray
                        Copy-Item -Path $SourceFilePath -Destination $DestinationFilePath -ErrorAction Stop
                        if ($File -eq 'Build-Module.ps1') {
                            $CopiedBuildModule = $True
                        }
                        elseif ($File -eq "$ModuleName.psd1") {
                            $CopiedPSD1 = $True
                        }
                    }
                }

                $Guid = (New-Guid).Guid
                if ($CopiedBuildModule) {
                    $FilePath = [io.path]::Combine($FullProjectPath, "Build", "Build-Module.ps1")
                    $Success = Register-DataForInitialModule -FilePath $FilePath -ModuleName $ModuleName -Guid $Guid
                    if ($Success -eq $false) {
                        if ($ExitCode) {
                            exit 1 
                        }
                        else {
                            return 
                        }
                    }
                }
                if ($CopiedPSD1) {
                    $FilePath = [io.path]::Combine($FullProjectPath, "$ModuleName.psd1")
                    $Success = Register-DataForInitialModule -FilePath $FilePath -ModuleName $ModuleName -Guid $Guid
                    if ($Success -eq $false) {
                        if ($ExitCode) {
                            exit 1 
                        }
                        else {
                            return 
                        }
                    }
                }
                Write-Text -Text "[i] Preparing module structure for $ModuleName in $Path. Completed." -Color DarkGray
            }
        }
    }

    $newPrepareStructureSplat = [ordered] @{
        Configuration           = $Configuration
        Settings                = $Settings
        PathToProject           = $FullProjectPath
        ModuleName              = $ModuleName
        FunctionsToExportFolder = $FunctionsToExportFolder
        AliasesToExportFolder   = $AliasesToExportFolder
        ExcludeFromPackage      = $ExcludeFromPackage
        IncludeRoot             = $IncludeRoot
        IncludePS1              = $IncludePS1
        IncludeAll              = $IncludeAll
        IncludeCustomCode       = $IncludeCustomCode
        IncludeToArray          = $IncludeToArray
        LibrariesCore           = $LibrariesCore
        LibrariesDefault        = $LibrariesDefault
        LibrariesStandard       = $LibrariesStandard
    }

    $ModuleOutput = New-PrepareStructure @newPrepareStructureSplat

    $Execute = "$($GlobalTime.Elapsed.Days) days, $($GlobalTime.Elapsed.Hours) hours, $($GlobalTime.Elapsed.Minutes) minutes, $($GlobalTime.Elapsed.Seconds) seconds, $($GlobalTime.Elapsed.Milliseconds) milliseconds"
    if ($ModuleOutput -notcontains $false) {
        Write-Host "[i] Module Build Completed " -NoNewline -ForegroundColor Green
        Write-Host "[Time Total: $Execute]" -ForegroundColor Green
        if ($ExitCode) {
            exit 0
        }
    }
    else {
        Write-Host "[i] Module Build Failed " -NoNewline -ForegroundColor Red
        Write-Host "[Time Total: $Execute]" -ForegroundColor Red
        if ($ExitCode) {
            exit 1
        }
    }
}
function New-ConfigurationArtefact {
    <#
    .SYNOPSIS
    Tells the module to create artefact of specified type
 
    .DESCRIPTION
    Tells the module to create artefact of specified type
    There can be multiple artefacts created (even of same type)
    At least one packed artefact is required for publishing to GitHub
 
    .PARAMETER PreScriptMerge
    ScriptBlock that will be added in the beggining of the script. It's only applicable to type of Script, PackedScript.
    If useed with PreScriptMergePath, this will be ignored.
 
    .PARAMETER PostScriptMerge
    ScriptBlock that will be added in the end of the script. It's only applicable to type of Script, PackedScript.
    If useed with PostScriptMergePath, this will be ignored.
 
    .PARAMETER PreScriptMergePath
    Path to file that will be added in the beggining of the script. It's only applicable to type of Script, PackedScript.
 
    .PARAMETER PostScriptMergePath
    Path to file that will be added in the end of the script. It's only applicable to type of Script, PackedScript.
 
    .PARAMETER Type
    There are 4 types of artefacts:
    - Unpacked - unpacked module (useful for testing)
    - Packed - packed module (as zip) - usually used for publishing to GitHub or copying somewhere
    - Script - script that is module in form of PS1 without PSD1 - only applicable to very simple modules
    - PackedScript - packed module (as zip) that is script that is module in form of PS1 without PSD1 - only applicable to very simple modules
 
    .PARAMETER ID
    Optional ID of the artefact. To be used by New-ConfigurationPublish cmdlet
    If not specified, the first packed artefact will be used for publishing to GitHub
 
    .PARAMETER Enable
    Enable artefact creation. By default artefact creation is disabled.
 
    .PARAMETER IncludeTagName
    Include tag name in artefact name. By default tag name is not included.
    Alternatively you can provide ArtefactName parameter to specify your own artefact name (with or without TagName)
 
    .PARAMETER Path
    Path where artefact will be created.
    Please choose a separate directory for each artefact type, as logic may be interfering one another.
 
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER AddRequiredModules
    Add required modules to artefact by copying them over. By default required modules are not added.
 
    .PARAMETER ModulesPath
    Path where main module or required module (if not specified otherwise in RequiredModulesPath) will be copied to.
    By default it will be put in the Path folder if not specified
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER RequiredModulesPath
    Path where required modules will be copied to. By default it will be put in the Path folder if not specified.
    If ModulesPath is specified, but RequiredModulesPath is not specified it will be put into ModulesPath folder.
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER CopyDirectories
    Provide Hashtable of directories to copy to artefact. Key is source directory, value is destination directory.
 
    .PARAMETER CopyFiles
    Provide Hashtable of files to copy to artefact. Key is source file, value is destination file.
 
    .PARAMETER CopyDirectoriesRelative
    Define if destination directories should be relative to artefact root. By default they are not.
 
    .PARAMETER CopyFilesRelative
    Define if destination files should be relative to artefact root. By default they are not.
 
    .PARAMETER ArtefactName
    The name of the artefact. If not specified, the default name will be used.
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER ScriptName
    The name of the script. If not specified, the default name will be used.
    Only applicable to Script and ScriptPacked artefacts.
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER DoNotClear
    Do not clear artefact directory before creating artefact. By default artefact directory is cleared.
 
    .EXAMPLE
    New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\Artefacts\Unpacked" -RequiredModulesPath "$PSScriptRoot\Artefacts\Unpacked\Modules"
 
    .EXAMPLE
    # standard artefact, packed with tag name without any additional modules or required modules
    New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\Artefacts\Packed" -IncludeTagName
 
    .EXAMPLE
    # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file
    New-ConfigurationArtefact -Type Script -Enable -Path "$PSScriptRoot\Artefacts\Script" -IncludeTagName
 
    .EXAMPLE
    # Create artefact in form of a script. This is useful for very simple modules that should be just single PS1 file
    # But additionally pack it into zip fileĄŚż$%#
    New-ConfigurationArtefact -Type ScriptPacked -Enable -Path "$PSScriptRoot\Artefacts\ScriptPacked" -ArtefactName "Script-<ModuleName>-$((Get-Date).ToString('yyyy-MM-dd')).zip"
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)][ScriptBlock] $PostScriptMerge,
        [Parameter(Position = 1)][ScriptBlock] $PreScriptMerge,
        [Parameter(Mandatory)][ValidateSet('Unpacked', 'Packed', 'Script', 'ScriptPacked')][string] $Type,
        [switch] $Enable,
        [switch] $IncludeTagName,
        [string] $Path,
        [alias('RequiredModules')][switch] $AddRequiredModules,
        [string] $ModulesPath,
        [string] $RequiredModulesPath,
        [System.Collections.IDictionary] $CopyDirectories,
        [System.Collections.IDictionary] $CopyFiles,
        [switch] $CopyDirectoriesRelative,
        [switch] $CopyFilesRelative,
        [switch] $DoNotClear,
        [string] $ArtefactName,
        [alias('FileName')][string] $ScriptName,
        [string] $ID,
        [string] $PostScriptMergePath,
        [string] $PreScriptMergePath
    )
    $Artefact = [ordered ] @{
        Type          = $Type 
        Configuration = [ordered] @{
            Type            = $Type
            RequiredModules = [ordered] @{}
        }
    }

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($PSBoundParameters.ContainsKey('Enable')) {
        $Artefact['Configuration']['Enabled'] = $Enable
    }
    if ($PSBoundParameters.ContainsKey('IncludeTagName')) {
        $Artefact['Configuration']['IncludeTagName'] = $IncludeTagName
    }
    if ($PSBoundParameters.ContainsKey('Path')) {
        if ($null -eq $IsWindows -or $IsWindows -eq $true) {
            $Artefact['Configuration']['Path'] = $Path.Replace('/', '\')
        }
        else {
            $Artefact['Configuration']['Path'] = $Path.Replace('\', '/')
        }
    }
    if ($PSBoundParameters.ContainsKey('RequiredModulesPath')) {
        if ($null -eq $IsWindows -or $IsWindows -eq $true) {
            $Artefact['Configuration']['RequiredModules']['Path'] = $RequiredModulesPath.Replace('/', '\')
        }
        else {
            $Artefact['Configuration']['RequiredModules']['Path'] = $RequiredModulesPath.Replace('\', '/')
        }
    }
    if ($PSBoundParameters.ContainsKey('AddRequiredModules')) {
        $Artefact['Configuration']['RequiredModules']['Enabled'] = $true
    }
    if ($PSBoundParameters.ContainsKey('ModulesPath')) {
        if ($null -eq $IsWindows -or $IsWindows -eq $true) {
            $Artefact['Configuration']['RequiredModules']['ModulesPath'] = $ModulesPath.Replace('/', '\')
        }
        else {
            $Artefact['Configuration']['RequiredModules']['ModulesPath'] = $ModulesPath.Replace('\', '/')
        }
    }
    if ($PSBoundParameters.ContainsKey('CopyDirectories')) {
        foreach ($Directory in [string[]] $CopyDirectories.Keys) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $CopyDirectories[$Directory] = $CopyDirectories[$Directory].Replace('/', '\')
            }
            else {
                $CopyDirectories[$Directory] = $CopyDirectories[$Directory].Replace('\', '/')
            }
        }
        $Artefact['Configuration']['DirectoryOutput'] = $CopyDirectories
    }
    if ($PSBoundParameters.ContainsKey('CopyDirectoriesRelative')) {
        $Artefact['Configuration']['DestinationDirectoriesRelative'] = $CopyDirectoriesRelative.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('CopyFiles')) {
        foreach ($File in [string[]] $CopyFiles.Keys) {
            if ($null -eq $IsWindows -or $IsWindows -eq $true) {
                $CopyFiles[$File] = $CopyFiles[$File].Replace('/', '\')
            }
            else {
                $CopyFiles[$File] = $CopyFiles[$File].Replace('\', '/')
            }
        }
        $Artefact['Configuration']['FilesOutput'] = $CopyFiles
    }
    if ($PSBoundParameters.ContainsKey('CopyFilesRelative')) {
        $Artefact['Configuration']['DestinationFilesRelative'] = $CopyFilesRelative.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('DoNotClear')) {
        $Artefact['Configuration']['DoNotClear'] = $DoNotClear.IsPresent
    }
    if ($PSBoundParameters.ContainsKey('ArtefactName')) {
        $Artefact['Configuration']['ArtefactName'] = $ArtefactName
    }
    if ($PSBoundParameters.ContainsKey('ScriptName')) {
        $Artefact['Configuration']['ScriptName'] = $ScriptName
    }
    if ($PSBoundParameters.ContainsKey('PreScriptMerge')) {
        try {
            $Artefact['Configuration']['PreScriptMerge'] = Invoke-Formatter -ScriptDefinition $PreScriptMerge.ToString()
        }
        catch {
            Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red
            $Artefact['Configuration']['PreScriptMerge'] = $PreScriptMerge.ToString()
        }
    }
    if ($PSBoundParameters.ContainsKey('PostScriptMerge')) {
        try {
            $Artefact['Configuration']['PostScriptMerge'] = Invoke-Formatter -ScriptDefinition $PostScriptMerge.ToString()
        }
        catch {
            Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red
            $Artefact['Configuration']['PostScriptMerge'] = $PostScriptMerge.ToString()
        }
    }

    if ($PSBoundParameters.ContainsKey('PreScriptMergePath')) {
        $ScriptContent = Get-Content -Path $PreScriptMergePath -Raw -Encoding $Encoding
        if ($ScriptContent) {
            try {
                $Artefact['Configuration']['PreScriptMerge'] = Invoke-Formatter -ScriptDefinition $ScriptContent
            }
            catch {
                Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red
                $Artefact['Configuration']['PreScriptMerge'] = $ScriptContent.ToString()
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('PostScriptMergePath')) {
        $ScriptContent = Get-Content -Path $PostScriptMergePath -Raw -Encoding $Encoding
        if ($ScriptContent) {
            try {
                $Artefact['Configuration']['PostScriptMerge'] = Invoke-Formatter -ScriptDefinition $ScriptContent.ToString()
            }
            catch {
                Write-Text -Text "[i] Unable to format merge script provided by user. Error: $($_.Exception.Message). Using original script." -Color Red
                $Artefact['Configuration']['PostScriptMerge'] = $ScriptContent.ToString()
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('ID')) {
        $Artefact['Configuration']['ID'] = $ID
    }
    $Artefact
}
function New-ConfigurationBuild {
    <#
    .SYNOPSIS
    Allows to configure build process for the module
 
    .DESCRIPTION
    Allows to configure build process for the module
 
    .PARAMETER Enable
    Enable build process
 
    .PARAMETER DeleteTargetModuleBeforeBuild
    Delete target module before build
 
    .PARAMETER MergeModuleOnBuild
    Parameter description
 
    .PARAMETER MergeFunctionsFromApprovedModules
    Parameter description
 
    .PARAMETER SignModule
    Parameter description
 
    .PARAMETER DotSourceClasses
    Parameter description
 
    .PARAMETER DotSourceLibraries
    Parameter description
 
    .PARAMETER SeparateFileLibraries
    Parameter description
 
    .PARAMETER RefreshPSD1Only
    Parameter description
 
    .PARAMETER UseWildcardForFunctions
    Parameter description
 
    .PARAMETER LocalVersioning
    Parameter description
 
    .PARAMETER DoNotAttemptToFixRelativePaths
    Configures module builder to not replace $PSScriptRoot\ with $PSScriptRoot\
    This is useful if you have a module that has a lot of relative paths that are required when using Private/Public folders,
    but for merge process those are not supposed to be there as the paths change.
    By default module builder will attempt to fix it. This option disables this functionality.
    Best practice is to use $MyInvocation.MyCommand.Module.ModuleBase or similar instead of relative paths.
 
    .PARAMETER CertificateThumbprint
    Parameter description
 
    .PARAMETER CertificatePFXPath
    Parameter description
 
    .PARAMETER CertificatePFXBase64
    Parameter description
 
    .PARAMETER CertificatePFXPassword
    Parameter description
 
    .PARAMETER NETConfiguration
    Parameter description
 
    .PARAMETER NETFramework
    Parameter description
 
    .PARAMETER NETProjectPath
    Path to the project that you want to build. This is useful if it's not in Sources folder directly within module directory
 
    .PARAMETER NETProjectName
    By default it will assume same name as project name, but you can provide different name if needed.
    It's required if NETProjectPath is provided
 
    .PARAMETER NETExcludeMainLibrary
    Exclude main library from build, this is useful if you have C# project that you want to build
    that is used mostly for generating libraries that are used in PowerShell module
    It won't include main library in the build, but it will include all other libraries
 
    .PARAMETER NETExcludeLibraryFilter
    Provide list of filters for libraries that you want to exclude from build, this is useful if you have C# project that you want to build, but don't want to include all libraries for some reason
 
    .PARAMETER NETIgnoreLibraryOnLoad
    This is to exclude libraries from being loaded in PowerShell by PSM1/Librarties.ps1 files.
    This is useful if you have a library that is not supposed to be loaded in PowerShell, but you still need it
    For example library that's not NET based and is as dependency for other libraries
 
    .PARAMETER NETBinaryModule
    Provide list of binary modules that you want to import-module in the module.
    This is useful if you're building a module that has binary modules and you want to import them in the module.
    In here you provide one or more binrary module names that you want to import in the module.
    Just the DLL name with extension without path. Path is assumed to be $PSScriptRoot\Lib\Standard or $PSScriptRoot\Lib\Default or $PSScriptRoot\Lib\Core
 
    .PARAMETER NETBinaryModuleDocumentation
    Include documentation for binary modules, this is useful if you have a lot of binary modules and you want to include documentation for them (if available in XML format)
 
    .PARAMETER NETBinaryModuleCmdletScanDisabled
    This is to disable scanning for cmdlets in binary modules, this is useful if you have a lot of binary modules and you don't want to scan them for cmdlets.
    By default it will scan for cmdlets/aliases in binary modules and add them to the module PSD1/PSM1 files.
 
    .PARAMETER NETHandleAssemblyWithSameName
    Adds try/catch block to handle assembly with same name is already loaded exception and ignore it.
    It's useful in PowerShell 7, as it's more strict about this than Windows PowerShell, and usually everything should work as expected.
 
    .PARAMETER NETLineByLineAddType
    Adds Add-Type line by line, this is useful if you have a lot of libraries and you want to see which one is causing the issue.
 
    .PARAMETER NETMergeLibraryDebugging
    Add special logic to simplify debugging of merged libraries, this is useful if you have a lot of libraries and you want to see which one is causing the issue.
 
    .PARAMETER NETResolveBinaryConflicts
    Add special logic to resolve binary conflicts. It uses by defalt the project name. If you want to use different name use NETResolveBinaryConflictsName
 
    .PARAMETER NETResolveBinaryConflictsName
    Add special logic to resolve binary conflicts for specific project name.
 
    .PARAMETER NETSearchClass
    Provide a name for class when using NETResolveBinaryConflicts or NETResolveBinaryConflictsName. By default it uses `$LibraryName.Initialize` however that may not be always the case
 
    .PARAMETER NETDoNotCopyLibrariesRecursively
    Do not copy libraries recursively. Normally all libraries are copied recursively, but this option disables that functionality so it won't copy subfolders of libraries.
 
    .PARAMETER NETHandleRuntimes
    Add special logic to handle runtimes. It's useful if you have a library that is not supposed to be loaded in PowerShell, but you still need it
    For example library that's not NET based and is as dependency for other libraries
 
    .PARAMETER SkipBuiltinReplacements
    Skip builtin replacements option disables builtin replacements that are done by module builder.
    This is useful if you use any of known replacements and you don't want them to be replaced by module builder.
    This has to be used on the PSPublishModule by default, as it would break the module on publish.
 
    Current known replacements are:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .EXAMPLE
    $newConfigurationBuildSplat = @{
        Enable = $true
        SignModule = $true
        MergeModuleOnBuild = $true
        MergeFunctionsFromApprovedModules = $true
        CertificateThumbprint = '483292C9E317AA1'
        NETResolveBinaryConflicts = $true
        NETResolveBinaryConflictsName = 'Transferetto'
        NETProjectName = 'Transferetto'
        NETConfiguration = 'Release'
        NETFramework = 'netstandard2.0'
        DotSourceLibraries = $true
        DotSourceClasses = $true
        DeleteTargetModuleBeforeBuild = $true
    }
 
    New-ConfigurationBuild @newConfigurationBuildSplat
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    param(
        [switch] $Enable,
        [switch] $DeleteTargetModuleBeforeBuild,
        [switch] $MergeModuleOnBuild,
        [switch] $MergeFunctionsFromApprovedModules,
        [switch] $SignModule,
        [switch] $DotSourceClasses,
        [switch] $DotSourceLibraries,
        [switch] $SeparateFileLibraries,
        [switch] $RefreshPSD1Only,
        [switch] $UseWildcardForFunctions,
        [switch] $LocalVersioning,

        [switch] $SkipBuiltinReplacements,
        [switch] $DoNotAttemptToFixRelativePaths,

        [string] $CertificateThumbprint,
        [string] $CertificatePFXPath,
        [string] $CertificatePFXBase64,
        [string] $CertificatePFXPassword,

        [string] $NETProjectPath,
        [ValidateSet('Release', 'Debug')][string] $NETConfiguration, # may need to allow user choice
        [string[]] $NETFramework,
        [string] $NETProjectName,
        [switch] $NETExcludeMainLibrary,
        [string[]] $NETExcludeLibraryFilter,
        [string[]] $NETIgnoreLibraryOnLoad,
        [string[]] $NETBinaryModule,
        [alias('HandleAssemblyWithSameName')][switch] $NETHandleAssemblyWithSameName,
        [switch] $NETLineByLineAddType,
        [switch] $NETBinaryModuleCmdletScanDisabled,
        [alias("MergeLibraryDebugging")][switch] $NETMergeLibraryDebugging,
        [alias("ResolveBinaryConflicts")][switch] $NETResolveBinaryConflicts,
        [alias("ResolveBinaryConflictsName")][string] $NETResolveBinaryConflictsName,
        [alias("NETDocumentation", "NETBinaryModuleDocumenation")][switch] $NETBinaryModuleDocumentation,
        [switch] $NETDoNotCopyLibrariesRecursively,
        [string] $NETSearchClass,
        [switch] $NETHandleRuntimes
    )

    if ($PSBoundParameters.ContainsKey('Enable')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                Enable = $Enable.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('DeleteTargetModuleBeforeBuild')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                DeleteBefore = $DeleteTargetModuleBeforeBuild.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('MergeModuleOnBuild')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                Merge = $MergeModuleOnBuild.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('MergeFunctionsFromApprovedModules')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                MergeMissing = $MergeFunctionsFromApprovedModules.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('SignModule')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                SignMerged = $SignModule.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('DotSourceClasses')) {

        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                ClassesDotSource = $DotSourceClasses.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('DotSourceLibraries')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                LibraryDotSource = $DotSourceLibraries.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('SeparateFileLibraries')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                LibrarySeparateFile = $SeparateFileLibraries.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('RefreshPSD1Only')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                RefreshPSD1Only = $RefreshPSD1Only.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('UseWildcardForFunctions')) {

        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                UseWildcardForFunctions = $UseWildcardForFunctions.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('LocalVersioning')) {

        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                LocalVersion = $LocalVersioning.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('DoNotAttemptToFixRelativePaths')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                DoNotAttemptToFixRelativePaths = $DoNotAttemptToFixRelativePaths.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETMergeLibraryDebugging')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                DebugDLL = $NETMergeLibraryDebugging.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('NETResolveBinaryConflictsName')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                ResolveBinaryConflicts = @{
                    ProjectName = $NETResolveBinaryConflictsName
                }
            }
        }
    }
    elseif ($PSBoundParameters.ContainsKey('NETResolveBinaryConflicts')) {
        [ordered] @{
            Type        = 'Build'
            BuildModule = [ordered] @{
                ResolveBinaryConflicts = $NETResolveBinaryConflicts.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('CertificateThumbprint')) {
        [ordered] @{
            Type    = 'Options'
            Options = [ordered] @{
                Signing = [ordered] @{
                    CertificateThumbprint = $CertificateThumbprint
                }
            }
        }
    }
    elseif ($PSBoundParameters.ContainsKey('CertificatePFXPath')) {
        if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) {

            [ordered] @{
                Type    = 'Options'
                Options = [ordered] @{
                    Signing = [ordered] @{
                        CertificatePFXPath     = $CertificatePFXPath
                        CertificatePFXPassword = $CertificatePFXPassword
                    }
                }
            }
        }
        else {
            throw "CertificatePFXPassword is required when using CertificatePFXPath"
        }
    }
    elseif ($PSBoundParameters.ContainsKey('CertificatePFXBase64')) {
        if ($PSBoundParameters.ContainsKey('CertificatePFXPassword')) {

            [ordered] @{
                Type    = 'Options'
                Options = [ordered] @{
                    Signing = [ordered] @{
                        CertificatePFXBase64   = $CertificatePFXBase64
                        CertificatePFXPassword = $CertificatePFXPassword
                    }
                }
            }
        }
        else {
            throw "CertificatePFXPassword is required when using CertificatePFXBase64"
        }
    }

    if ($PSBoundParameters.ContainsKey('NETConfiguration')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                Enable        = $true
                Configuration = $NETConfiguration
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('NETFramework')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                Enable    = $true
                Framework = $NETFramework
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('NETProjectName')) {

        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                ProjectName = $NETProjectName
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETExcludeMainLibrary')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                ExcludeMainLibrary = $NETExcludeMainLibrary.IsPresent
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('NETExcludeLibraryFilter')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                ExcludeLibraryFilter = $NETExcludeLibraryFilter
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETIgnoreLibraryOnLoad')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                IgnoreLibraryOnLoad = $NETIgnoreLibraryOnLoad
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETBinaryModule')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                BinaryModule = $NETBinaryModule
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETHandleAssemblyWithSameName')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                HandleAssemblyWithSameName = $NETHandleAssemblyWithSameName.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETLineByLineAddType')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                NETLineByLineAddType = $NETLineByLineAddType.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETProjectPath')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                NETProjectPath = $NETProjectPath
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETBinaryModuleCmdletScanDisabled')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                BinaryModuleCmdletScanDisabled = $NETBinaryModuleCmdletScanDisabled.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETSearchClass')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                SearchClass = $NETSearchClass
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETBinaryModuleDocumentation')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                NETBinaryModuleDocumentation = $NETBinaryModuleDocumentation.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETHandleRuntimes')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                HandleRuntimes = $NETHandleRuntimes.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('NETDoNotCopyLibrariesRecursively')) {
        [ordered] @{
            Type           = 'BuildLibraries'
            BuildLibraries = [ordered] @{
                NETDoNotCopyLibrariesRecursively = $NETDoNotCopyLibrariesRecursively.IsPresent
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('SkipBuiltinReplacements')) {
        [ordered] @{
            Type              = 'PlaceHolderOption'
            PlaceHolderOption = [ordered]@{
                SkipBuiltinReplacements = $true
            }
        }
    }
}
function New-ConfigurationCommand {
    [CmdletBinding()]
    param(
        [string] $ModuleName,
        [string[]] $CommandName
    )

    $Configuration = [ordered] @{
        Type          = 'Command'
        Configuration = [ordered] @{
            ModuleName  = $ModuleName
            CommandName = $CommandName
        }
    }
    $Configuration
}
function New-ConfigurationDocumentation {
    <#
    .SYNOPSIS
    Enables or disables creation of documentation from the module using PlatyPS
 
    .DESCRIPTION
    Enables or disables creation of documentation from the module using PlatyPS
 
    .PARAMETER Enable
    Enables creation of documentation from the module. If not specified, the documentation will not be created.
 
    .PARAMETER StartClean
    Removes all files from the documentation folder before creating new documentation.
    Otherwise the `Update-MarkdownHelpModule` will be used to update the documentation.
 
    .PARAMETER UpdateWhenNew
    Updates the documentation right after running `New-MarkdownHelp` due to platyPS bugs.
 
    .PARAMETER Path
    Path to the folder where documentation will be created.
 
    .PARAMETER PathReadme
    Path to the readme file that will be used for the documentation.
 
    .PARAMETER Tool
    Tool to use for documentation generation. By default `HelpOut` is used.
    Available options are `PlatyPS` and `HelpOut`.
 
    .EXAMPLE
    New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs'
 
    .EXAMPLE
    New-ConfigurationDocumentation -Enable -PathReadme 'Docs\Readme.md' -Path 'Docs'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [switch] $Enable,
        [switch] $StartClean,
        [switch] $UpdateWhenNew,
        [Parameter(Mandatory)][string] $Path,
        [Parameter(Mandatory)][string] $PathReadme,
        [ValidateSet('PlatyPS', 'HelpOut')][string] $Tool = 'PlatyPS'
    )

    if ($Tool -eq 'PlatyPS') {
        $ModuleExists = Get-Module -Name 'PlatyPS' -ListAvailable
        if (-not $ModuleExists) {
            Write-Warning "Module PlatyPS is not installed. Please install it using Install-Module PlatyPS -Force -Verbose"
            return
        }
    }
    elseif ($Tool -eq 'HelpOut') {
        $ModuleExists = Get-Module -Name 'HelpOut' -ListAvailable
        if (-not $ModuleExists) {
            Write-Warning "Module HelpOut is not installed. Please install it using Install-Module HelpOut -Force -Verbose"
            return
        }
    }
    else {
        return
    }
    if ($Path -or $PathReadme) {
        $Option = [ordered] @{
            Type          = 'Documentation'
            Configuration = [ordered] @{
                Path       = $Path
                PathReadme = $PathReadme
            }
        }
        $Option
    }

    if ($Enable -or $StartClean -or $UpdateWhenNew) {
        $Option = [ordered]@{
            Type          = 'BuildDocumentation'
            Configuration = [ordered]@{
                Enable        = $Enable
                StartClean    = $StartClean
                UpdateWhenNew = $UpdateWhenNew
                Tool          = $Tool
            }
        }
        $Option
    }
}
function New-ConfigurationExecute {
    [CmdletBinding()]
    param(

    )
}
function New-ConfigurationFormat {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [validateSet(
            'OnMergePSM1', 'OnMergePSD1',
            'DefaultPSM1', 'DefaultPSD1'
            #"DefaultPublic", 'DefaultPrivate', 'DefaultOther'
        )][string[]]$ApplyTo,

        [switch] $EnableFormatting,

        [validateSet('None', 'Asc', 'Desc')][string] $Sort,

        [switch] $RemoveComments,
        [switch] $RemoveEmptyLines,
        [switch] $RemoveAllEmptyLines,
        [switch] $RemoveCommentsInParamBlock,
        [switch] $RemoveCommentsBeforeParamBlock,

        [switch] $PlaceOpenBraceEnable,
        [switch] $PlaceOpenBraceOnSameLine,
        [switch] $PlaceOpenBraceNewLineAfter,
        [switch] $PlaceOpenBraceIgnoreOneLineBlock,

        [switch] $PlaceCloseBraceEnable,
        [switch] $PlaceCloseBraceNewLineAfter,
        [switch] $PlaceCloseBraceIgnoreOneLineBlock,
        [switch] $PlaceCloseBraceNoEmptyLineBefore,

        [switch] $UseConsistentIndentationEnable,
        [ValidateSet('space', 'tab')][string] $UseConsistentIndentationKind,
        [ValidateSet('IncreaseIndentationAfterEveryPipeline', 'NoIndentation')][string] $UseConsistentIndentationPipelineIndentation,
        [int] $UseConsistentIndentationIndentationSize,

        [switch] $UseConsistentWhitespaceEnable,
        [switch] $UseConsistentWhitespaceCheckInnerBrace,
        [switch] $UseConsistentWhitespaceCheckOpenBrace,
        [switch] $UseConsistentWhitespaceCheckOpenParen,
        [switch] $UseConsistentWhitespaceCheckOperator,
        [switch] $UseConsistentWhitespaceCheckPipe,
        [switch] $UseConsistentWhitespaceCheckSeparator,

        [switch] $AlignAssignmentStatementEnable,
        [switch] $AlignAssignmentStatementCheckHashtable,

        [switch] $UseCorrectCasingEnable,

        [ValidateSet('Minimal', 'Native')][string] $PSD1Style
    )
    $SettingsCount = 0

    $Options = [ordered] @{
        Merge    = [ordered] @{
        }
        Standard = [ordered] @{
        }
    }

    foreach ($Apply in $ApplyTo) {
        $Formatting = [ordered] @{}
        if ($PSBoundParameters.ContainsKey('RemoveComments')) {
            $Formatting.RemoveComments = $RemoveComments.IsPresent
        }
        if ($PSBoundParameters.ContainsKey('RemoveEmptyLines')) {
            $Formatting.RemoveEmptyLines = $RemoveEmptyLines.IsPresent
        }
        if ($PSBoundParameters.ContainsKey('RemoveAllEmptyLines')) {
            $Formatting.RemoveAllEmptyLines = $RemoveAllEmptyLines.IsPresent
        }
        if ($PSBoundParameters.ContainsKey('RemoveCommentsInParamBlock')) {
            $Formatting.RemoveCommentsInParamBlock = $RemoveCommentsInParamBlock.IsPresent
        }
        if ($PSBoundParameters.ContainsKey('RemoveCommentsBeforeParamBlock')) {
            $Formatting.RemoveCommentsBeforeParamBlock = $RemoveCommentsBeforeParamBlock.IsPresent
        }

        $Formatting.FormatterSettings = [ordered] @{
            IncludeRules = @(
                if ($PlaceOpenBraceEnable) {
                    'PSPlaceOpenBrace' 
                }
                if ($PlaceCloseBraceEnable) {
                    'PSPlaceCloseBrace' 
                }
                if ($UseConsistentIndentationEnable) {
                    'PSUseConsistentIndentation' 
                }
                if ($UseConsistentWhitespaceEnable) {
                    'PSUseConsistentWhitespace' 
                }
                if ($AlignAssignmentStatementEnable) {
                    'PSAlignAssignmentStatement' 
                }
                if ($UseCorrectCasingEnable) {
                    'PSUseCorrectCasing' 
                }
            )
            Rules        = [ordered] @{}
        }
        if ($PlaceOpenBraceEnable) {
            $Formatting.FormatterSettings.Rules.PSPlaceOpenBrace = [ordered] @{
                Enable             = $true
                OnSameLine         = $PlaceOpenBraceOnSameLine.IsPresent
                NewLineAfter       = $PlaceOpenBraceNewLineAfter.IsPresent
                IgnoreOneLineBlock = $PlaceOpenBraceIgnoreOneLineBlock.IsPresent
            }
        }
        if ($PlaceCloseBraceEnable) {
            $Formatting.FormatterSettings.Rules.PSPlaceCloseBrace = [ordered] @{
                Enable             = $true
                NewLineAfter       = $PlaceCloseBraceNewLineAfter.IsPresent
                IgnoreOneLineBlock = $PlaceCloseBraceIgnoreOneLineBlock.IsPresent
                NoEmptyLineBefore  = $PlaceCloseBraceNoEmptyLineBefore.IsPresent
            }
        }
        if ($UseConsistentIndentationEnable) {
            $Formatting.FormatterSettings.Rules.PSUseConsistentIndentation = [ordered] @{
                Enable              = $true
                Kind                = $UseConsistentIndentationKind
                PipelineIndentation = $UseConsistentIndentationPipelineIndentation
                IndentationSize     = $UseConsistentIndentationIndentationSize
            }
        }
        if ($UseConsistentWhitespaceEnable) {
            $Formatting.FormatterSettings.Rules.PSUseConsistentWhitespace = [ordered] @{
                Enable          = $true
                CheckInnerBrace = $UseConsistentWhitespaceCheckInnerBrace.IsPresent
                CheckOpenBrace  = $UseConsistentWhitespaceCheckOpenBrace.IsPresent
                CheckOpenParen  = $UseConsistentWhitespaceCheckOpenParen.IsPresent
                CheckOperator   = $UseConsistentWhitespaceCheckOperator.IsPresent
                CheckPipe       = $UseConsistentWhitespaceCheckPipe.IsPresent
                CheckSeparator  = $UseConsistentWhitespaceCheckSeparator.IsPresent
            }
        }
        if ($AlignAssignmentStatementEnable) {
            $Formatting.FormatterSettings.Rules.PSAlignAssignmentStatement = [ordered] @{
                Enable         = $true
                CheckHashtable = $AlignAssignmentStatementCheckHashtable.IsPresent
            }
        }
        if ($UseCorrectCasingEnable) {
            $Formatting.FormatterSettings.Rules.PSUseCorrectCasing = [ordered] @{
                Enable = $true
            }
        }
        Remove-EmptyValue -Hashtable $Formatting.FormatterSettings -Recursive
        if ($Formatting.FormatterSettings.Keys.Count -eq 0) {
            $null = $Formatting.Remove('FormatterSettings')
        }

        if ($Formatting.Count -gt 0 -or $EnableFormatting) {
            $SettingsCount++
            $Formatting.Enabled = $true

            if ($Apply -eq 'OnMergePSM1') {
                $Options.Merge.FormatCodePSM1 = $Formatting
            }
            elseif ($Apply -eq 'OnMergePSD1') {
                $Options.Merge.FormatCodePSD1 = $Formatting
            }
            elseif ($Apply -eq 'DefaultPSM1') {
                $Options.Standard.FormatCodePSM1 = $Formatting
            }
            elseif ($Apply -eq 'DefaultPSD1') {
                $Options.Standard.FormatCodePSD1 = $Formatting
            }
            elseif ($Apply -eq 'DefaultPublic') {
                $Options.Standard.FormatCodePublic = $Formatting
            }
            elseif ($Apply -eq 'DefaultPrivate') {
                $Options.Standard.FormatCodePrivate = $Formatting
            }
            elseif ($Apply -eq 'DefaultOther') {
                $Options.Standard.FormatCodeOther = $Formatting
            }
            else {
                throw "Unknown ApplyTo: $Apply"
            }
        }
        if ($PSD1Style) {
            if ($Apply -eq 'OnMergePSD1') {
                $SettingsCount++
                $Options['Merge']['Style'] = [ordered] @{}
                $Options['Merge']['Style']['PSD1'] = $PSD1Style
            }
            elseif ($Apply -eq 'DefaultPSD1') {
                $SettingsCount++
                $Options['Standard']['Style'] = [ordered] @{}
                $Options['Standard']['Style']['PSD1'] = $PSD1Style
            }
        }
    }

    if ($SettingsCount -gt 0) {
        $Output = [ordered] @{
            Type    = 'Formatting'
            Options = $Options
        }
        $Output
    }
}

function New-ConfigurationImportModule {
    <#
    .SYNOPSIS
    Creates a configuration for importing PowerShell modules.
 
    .DESCRIPTION
    This function generates a configuration object for importing PowerShell modules. It allows specifying whether to import the current module itself and/or any required modules.
 
    .PARAMETER ImportSelf
    Indicates whether to import the current module itself.
 
    .PARAMETER ImportRequiredModules
    Indicates whether to import any required modules specified in the module manifest.
 
    .EXAMPLE
    New-ConfigurationImportModule -ImportSelf -ImportRequiredModules
 
    .NOTES
    This function helps in creating a standardized import configuration for PowerShell modules.
    #>

    [CmdletBinding()]
    param(
        [switch] $ImportSelf,
        [switch] $ImportRequiredModules
    )

    $Output = [ordered] @{
        Type          = 'ImportModules'
        ImportModules = [ordered] @{}
    }
    if ($PSBoundParameters.Keys.Contains('ImportSelf')) {
        $Output['ImportModules']['Self'] = $ImportSelf
    }
    if ($PSBoundParameters.Keys.Contains('ImportRequiredModules')) {
        $Output['ImportModules']['RequiredModules'] = $ImportRequiredModules
    }
    if ($VerbosePreference) {
        $Output['ImportModules']['Verbose'] = $true
    }
    $Output
}
function New-ConfigurationInformation {
    [cmdletbinding()]
    param(
        [string] $FunctionsToExportFolder,
        [string] $AliasesToExportFolder,
        [string[]] $ExcludeFromPackage,
        [string[]] $IncludeRoot,
        [string[]] $IncludePS1,
        [string[]] $IncludeAll,
        [scriptblock] $IncludeCustomCode,
        [System.Collections.IDictionary] $IncludeToArray,
        [string] $LibrariesCore,
        [string] $LibrariesDefault,
        [string] $LibrariesStandard
    )

    $Configuration = [ordered] @{
        FunctionsToExportFolder = $FunctionsToExportFolder
        AliasesToExportFolder   = $AliasesToExportFolder
        ExcludeFromPackage      = $ExcludeFromPackage
        IncludeRoot             = $IncludeRoot
        IncludePS1              = $IncludePS1
        IncludeAll              = $IncludeAll
        IncludeCustomCode       = $IncludeCustomCode
        IncludeToArray          = $IncludeToArray
        LibrariesCore           = $LibrariesCore
        LibrariesDefault        = $LibrariesDefault
        LibrariesStandard       = $LibrariesStandard
    }
    Remove-EmptyValue -Hashtable $Configuration

    $Option = @{
        Type          = 'Information'
        Configuration = $Configuration
    }
    $Option
}
function New-ConfigurationManifest {
    <#
    .SYNOPSIS
    Creates a new configuration manifest for a PowerShell module.
 
    .DESCRIPTION
    This function generates a new configuration manifest for a PowerShell module. The manifest includes metadata about the module such as version, author, company, and other relevant information. It also allows specifying the functions, cmdlets, and aliases to export.
 
    .PARAMETER ModuleVersion
    Specifies the version of the module. When multiple versions of a module exist on a system, the latest version is loaded by default when you run Import-Module.
 
    .PARAMETER CompatiblePSEditions
    Specifies the module's compatible PowerShell editions. Valid values are 'Desktop' and 'Core'.
 
    .PARAMETER GUID
    Specifies a unique identifier for the module. The GUID is used to distinguish between modules with the same name.
 
    .PARAMETER Author
    Identifies the module author.
 
    .PARAMETER CompanyName
    Identifies the company or vendor who created the module.
 
    .PARAMETER Copyright
    Specifies a copyright statement for the module.
 
    .PARAMETER Description
    Describes the module at a high level.
 
    .PARAMETER PowerShellVersion
    Specifies the minimum version of PowerShell this module requires. Default is '5.1'.
 
    .PARAMETER Tags
    Specifies tags for the module.
 
    .PARAMETER IconUri
    Specifies the URI for the module's icon.
 
    .PARAMETER ProjectUri
    Specifies the URI for the module's project page.
 
    .PARAMETER DotNetFrameworkVersion
    Specifies the minimum version of the Microsoft .NET Framework that the module requires.
 
    .PARAMETER LicenseUri
    Specifies the URI for the module's license.
 
    .PARAMETER Prerelease
    Specifies the prerelease tag for the module.
 
    .PARAMETER FunctionsToExport
    Defines functions to export in the module manifest. By default, functions are auto-detected, but this allows you to override that.
 
    .PARAMETER AliasesToExport
    Defines aliases to export in the module manifest. By default, aliases are auto-detected, but this allows you to override that.
 
    .PARAMETER CmdletsToExport
    Defines cmdlets to export in the module manifest. By default, cmdlets are auto-detected, but this allows you to override that.
 
    .EXAMPLE
    New-ConfigurationManifest -ModuleVersion '1.0.0' -GUID '12345678-1234-1234-1234-1234567890ab' -Author 'John Doe' -CompanyName 'Example Corp' -Description 'This is an example module.'
 
    .NOTES
    This function helps in creating a standardized module manifest for PowerShell modules.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ModuleVersion,
        [ValidateSet('Desktop', 'Core')][string[]] $CompatiblePSEditions = @('Desktop', 'Core'),
        [Parameter(Mandatory)][string] $GUID,
        [Parameter(Mandatory)][string] $Author,
        [string] $CompanyName,
        [string] $Copyright,
        [string] $Description,
        [string] $PowerShellVersion = '5.1',
        [string[]] $Tags,
        [string] $IconUri,
        [string] $ProjectUri,
        [string] $DotNetFrameworkVersion,
        [string] $LicenseUri,
        [alias('PrereleaseTag')][string] $Prerelease,
        [string[]] $FunctionsToExport,
        [string[]] $CmdletsToExport,
        [string[]] $AliasesToExport
    )

    $Manifest = [ordered] @{
        ModuleVersion          = $ModuleVersion
        CompatiblePSEditions   = @($CompatiblePSEditions)
        GUID                   = $GUID
        Author                 = $Author
        CompanyName            = $CompanyName
        Copyright              = $Copyright
        Description            = $Description
        PowerShellVersion      = $PowerShellVersion
        Tags                   = $Tags
        IconUri                = $IconUri
        ProjectUri             = $ProjectUri
        DotNetFrameworkVersion = $DotNetFrameworkVersion
        LicenseUri             = $LicenseUri
        Prerelease             = $Prerelease
        FunctionsToExport      = $FunctionsToExport
        CmdletsToExport        = $CmdletsToExport
        AliasesToExport        = $AliasesToExport
    }
    Remove-EmptyValue -Hashtable $Manifest

    $Option = @{
        Type          = 'Manifest'
        Configuration = $Manifest
    }
    $Option
}
function New-ConfigurationModule {
    <#
    .SYNOPSIS
    Provides a way to configure Required Modules or External Modules that will be used in the project.
 
    .DESCRIPTION
    Provides a way to configure Required Modules or External Modules that will be used in the project.
 
    .PARAMETER Type
    Choose between RequiredModule, ExternalModule and ApprovedModule, where RequiredModule is the default.
 
    .PARAMETER Name
    Name of PowerShell module that you want your module to depend on.
 
    .PARAMETER Version
    Version of PowerShell module that you want your module to depend on.
    If you don't specify a version, any version of the module is acceptable.
    You can also use word 'Latest' to specify that you want to use the latest version of the module, and the module will be pickup up latest version available on the system.
 
    .PARAMETER RequiredVersion
    RequiredVersion of PowerShell module that you want your module to depend on.
    This forces the module to require this specific version.
    When using Version, the module will be picked up if it's equal or higher than the version specified.
    When using RequiredVersion, the module will be picked up only if it's equal to the version specified.
 
    .PARAMETER Guid
    Guid of PowerShell module that you want your module to depend on. If you don't specify a Guid, any Guid of the module is acceptable, but it is recommended to specify it.
    Alternatively you can use word 'Auto' to specify that you want to use the Guid of the module, and the module GUID
 
    .EXAMPLE
    # Add standard module dependencies (directly, but can be used with loop as well)
    New-ConfigurationModule -Type RequiredModule -Name 'platyPS' -Guid 'Auto' -Version 'Latest'
    New-ConfigurationModule -Type RequiredModule -Name 'powershellget' -Guid 'Auto' -Version 'Latest'
    New-ConfigurationModule -Type RequiredModule -Name 'PSScriptAnalyzer' -Guid 'Auto' -Version 'Latest'
 
    .EXAMPLE
    # Add external module dependencies, using loop for simplicity
    foreach ($Module in @('Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Archive', 'Microsoft.PowerShell.Management', 'Microsoft.PowerShell.Security')) {
        New-ConfigurationModule -Type ExternalModule -Name $Module
    }
 
    .EXAMPLE
    # Add approved modules, that can be used as a dependency, but only when specific function from those modules is used
    # And on that time only that function and dependant functions will be copied over
    # Keep in mind it has it's limits when "copying" functions such as it should not depend on DLLs or other external files
    New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [validateset('RequiredModule', 'ExternalModule', 'ApprovedModule')] $Type = 'RequiredModule',
        [Parameter(Mandatory)][string[]] $Name,
        [string] $Version,
        [string] $RequiredVersion,
        [string] $Guid
    )
    foreach ($N in $Name) {
        if ($Type -eq 'ApprovedModule') {

            $Configuration = $N
        }
        else {
            $ModuleInformation = [ordered] @{
                ModuleName      = $N
                ModuleVersion   = $Version
                RequiredVersion = $RequiredVersion
                Guid            = $Guid
            }
            if ($Version -and $RequiredVersion) {
                throw 'You cannot use both Version and RequiredVersion at the same time for the same module. Please choose one or the other (New-ConfigurationModule) '
            }
            Remove-EmptyValue -Hashtable $ModuleInformation
            if ($ModuleInformation.Count -eq 0) {
                return
            }
            elseif ($ModuleInformation.Count -eq 1 -and $ModuleInformation.Contains('ModuleName')) {
                $Configuration = $N
            }
            else {
                $Configuration = $ModuleInformation
            }
        }
        $Option = @{
            Type          = $Type
            Configuration = $Configuration
        }
        $Option
    }
}

Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Version -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName New-ConfigurationModule -ParameterName Guid -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    'Auto', 'Latest' | Where-Object { $_ -like "*$wordToComplete*" }
}
function New-ConfigurationModuleSkip {
    <#
    .SYNOPSIS
    Provides a way to ignore certain commands or modules during build process and continue module building on errors.
 
    .DESCRIPTION
    Provides a way to ignore certain commands or modules during build process and continue module building on errors.
    During build if a build module can't find require module or command it will fail the build process to prevent incomplete module from being created.
    This option allows to skip certain modules or commands and continue building the module.
    This is useful for commands we know are not available on all systems, or we get them different way.
 
    .PARAMETER IgnoreModuleName
    Ignore module name or names. If the module is not available on the system it will be ignored and build process will continue.
 
    .PARAMETER IgnoreFunctionName
    Ignore function name or names. If the function is not available in the module it will be ignored and build process will continue.
 
    .PARAMETER Force
    This switch will force build process to continue even if the module or command is not available (aka you know what you are doing)
 
    .EXAMPLE
    New-ConfigurationModuleSkip -IgnoreFunctionName 'Invoke-Formatter', 'Find-Module' -IgnoreModuleName 'platyPS'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string[]] $IgnoreModuleName,
        [string[]] $IgnoreFunctionName,
        [switch] $Force
    )

    $Configuration = [ordered] @{
        Type          = 'ModuleSkip'
        Configuration = [ordered] @{
            IgnoreModuleName   = $IgnoreModuleName
            IgnoreFunctionName = $IgnoreFunctionName
            Force              = $Force
        }
    }
    Remove-EmptyValue -Hashtable $Configuration.Configuration
    $Configuration
}
function New-ConfigurationPlaceHolder {
    <#
    .SYNOPSIS
    Command helping define custom placeholders replacing content within a script or module during the build process.
 
    .DESCRIPTION
    Command helping define custom placeholders replacing content within a script or module during the build process.
    It modifies only the content of the script or module (PSM1) and does not modify the sources.
 
    .PARAMETER CustomReplacement
    Hashtable array with custom placeholders to replace. Each hashtable must contain two keys: Find and Replace.
 
    .PARAMETER Find
    The string to find in the script or module content.
 
    .PARAMETER Replace
    The string to replace the Find string in the script or module content.
 
    .EXAMPLE
    New-ConfigurationPlaceHolder -Find '{CustomName}' -Replace 'SpecialCase'
 
    .EXAMPLE
    New-ConfigurationPlaceHolder -CustomReplacement @(
        @{ Find = '{CustomName}'; Replace = 'SpecialCase' }
        @{ Find = '{CustomVersion}'; Replace = '1.0.0' }
    )
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'FindAndReplace')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'CustomReplacement')][System.Collections.IDictionary[]] $CustomReplacement,
        [Parameter(Mandatory, ParameterSetName = 'FindAndReplace')][string] $Find,
        [Parameter(Mandatory, ParameterSetName = 'FindAndReplace')][string] $Replace
    )

    foreach ($Replacement in $CustomReplacement) {
        [ordered] @{
            Type          = 'PlaceHolder'
            Configuration = $Replacement
        }
    }
    if ($PSBoundParameters.ContainsKey("Find") -and $PSBoundParameters.ContainsKey("Replace")) {
        [ordered] @{
            Type          = 'PlaceHolder'
            Configuration = @{
                Find    = $Find
                Replace = $Replace
            }
        }
    }
}
function New-ConfigurationPublish {
    <#
    .SYNOPSIS
    Provide a way to configure publishing to PowerShell Gallery or GitHub
 
    .DESCRIPTION
    Provide a way to configure publishing to PowerShell Gallery or GitHub
    You can configure publishing to both at the same time
    You can publish to multiple PowerShellGalleries at the same time as well
    You can have multiple GitHub configurations at the same time as well
 
    .PARAMETER Type
    Choose between PowerShellGallery and GitHub
 
    .PARAMETER FilePath
    API Key to be used for publishing to GitHub or PowerShell Gallery in clear text in file
 
    .PARAMETER UserName
    When used for GitHub this parameter is required to know to which repository to publish.
    This parameter is not used for PSGallery publishing
 
    .PARAMETER RepositoryName
    When used for PowerShellGallery publishing this parameter provides a way to overwrite default PowerShellGallery and publish to a different repository
    When not used, the default PSGallery will be used.
    When used for GitHub publishing this parameter provides a way to overwrite default repository name and publish to a different repository
    When not used, the default repository name will be used, that matches the module name
 
    .PARAMETER ApiKey
    API Key to be used for publishing to GitHub or PowerShell Gallery in clear text
 
    .PARAMETER Enabled
    Enable publishing to GitHub or PowerShell Gallery
 
    .PARAMETER PreReleaseTag
    Allow to publish to GitHub as pre-release. By default it will be published as release
 
    .PARAMETER OverwriteTagName
    Allow to overwrite tag name when publishing to GitHub. By default "v<ModuleVersion>" will be used i.e v1.0.0
 
    You can use following variables that will be replaced with actual values:
    - <ModuleName> / {ModuleName} - the name of the module i.e PSPublishModule
    - <ModuleVersion> / {ModuleVersion} - the version of the module i.e 1.0.0
    - <ModuleVersionWithPreRelease> / {ModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e 1.0.0-Preview1
    - <TagModuleVersionWithPreRelease> / {TagModuleVersionWithPreRelease} - the version of the module with pre-release tag i.e v1.0.0-Preview1
    - <TagName> / {TagName} - the name of the tag - i.e. v1.0.0
 
    .PARAMETER DoNotMarkAsPreRelease
    Allow to publish to GitHub as release even if pre-release tag is set on the module version.
    By default it will be published as pre-release if pre-release tag is set.
    This setting prevents it.
 
    .PARAMETER Force
    Allow to publish lower version of module on PowerShell Gallery. By default it will fail if module with higher version already exists.
 
    .PARAMETER ID
    Optional ID of the artefact. If not specified, the default packed artefact will be used.
    If no packed artefact is specified, the first packed artefact will be used (if enabled)
    If no packed artefact is enabled, the publishing will fail
 
    .EXAMPLE
    New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled:$true
 
    .EXAMPLE
    New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled:$true -ID 'ToGitHub'
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ParameterSetName = 'ApiKey')]
        [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')]
        [ValidateSet('PowerShellGallery', 'GitHub')][string] $Type,

        [Parameter(Mandatory, ParameterSetName = 'ApiFromFile')][string] $FilePath,

        [Parameter(Mandatory, ParameterSetName = 'ApiKey')][string] $ApiKey,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [string] $UserName,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [string] $RepositoryName,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [switch] $Enabled,

        # [Parameter(ParameterSetName = 'ApiKey')]
        # [Parameter(ParameterSetName = 'ApiFromFile')]
        # [string] $PreReleaseTag,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [string] $OverwriteTagName,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [switch] $Force,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [string] $ID,

        [Parameter(ParameterSetName = 'ApiKey')]
        [Parameter(ParameterSetName = 'ApiFromFile')]
        [switch] $DoNotMarkAsPreRelease
    )

    if ($FilePath) {
        $ApiKeyToUse = Get-Content -Path $FilePath -ErrorAction Stop -Encoding UTF8
    }
    else {
        $ApiKeyToUse = $ApiKey
    }

    if ($Type -eq 'PowerShellGallery') {
        $TypeToUse = 'GalleryNuget'
    }
    elseif ($Type -eq 'GitHub') {
        $TypeToUse = 'GitHubNuget'
        if (-not $UserName) {
            throw 'UserName is required for GitHub. Please fix New-ConfigurationPublish and provide UserName'
        }
    }
    else {
        return
    }

    $Settings = [ordered] @{
        Type          = $TypeToUse
        Configuration = [ordered] @{
            Type                  = $Type
            ApiKey                = $ApiKeyToUse
            ID                    = $ID
            Enabled               = $Enabled
            UserName              = $UserName
            RepositoryName        = $RepositoryName
            Force                 = $Force.IsPresent
            OverwriteTagName      = $OverwriteTagName
            DoNotMarkAsPreRelease = $DoNotMarkAsPreRelease.IsPresent
            Verbose               = $VerbosePreference
        }
    }
    Remove-EmptyValue -Hashtable $Settings -Recursive 2
    $Settings
}
function New-ConfigurationTest {
    [CmdletBinding()]
    param(
        #[Parameter(Mandatory)][ValidateSet('BeforeMerge', 'AfterMerge')][string[]] $When,
        [Parameter(Mandatory)][string] $TestsPath,
        [switch] $Enable,
        [switch] $Force
    )

    if ($Enable) {
        if ($null -eq $IsWindows -or $IsWindows -eq $true) {
            $TestsPath = $TestsPath.Replace('/', '\')
        }
        else {
            $TestsPath = $TestsPath.Replace('\', '/')
        }

        $When = 'AfterMerge'
        foreach ($W in $When) {
            $Configuration = [ordered] @{
                Type          = "Tests$W"
                Configuration = [ordered] @{
                    When      = $W
                    TestsPath = $TestsPath
                    Force     = $Force.ispresent
                }
            }
            Remove-EmptyValue -Hashtable $Configuration.Configuration
            $Configuration
        }
    }
}
function Publish-GitHubReleaseAsset {
    <#
    .SYNOPSIS
    Publishes a release asset to GitHub.
 
    .DESCRIPTION
    Uses `Send-GitHubRelease` to create or update a GitHub release based on the
    project version and upload the generated zip archive.
 
    .PARAMETER ProjectPath
    Path to the project folder containing the *.csproj file.
 
    .PARAMETER GitHubUsername
    GitHub account name owning the repository.
 
    .PARAMETER GitHubRepositoryName
    Name of the GitHub repository.
 
    .PARAMETER GitHubAccessToken
    Personal access token used for authentication.
 
    .PARAMETER IsPreRelease
    Publish the release as a pre-release.
 
    .EXAMPLE
    Publish-GitHubReleaseAsset -ProjectPath 'C:\Git\MyProject' -GitHubUsername 'EvotecIT' -GitHubRepositoryName 'MyRepo' -GitHubAccessToken $Token
    Uploads the current project zip to the specified GitHub repository.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ProjectPath,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$GitHubUsername,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$GitHubRepositoryName,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$GitHubAccessToken,
        [switch]$IsPreRelease
    )
    $result = [ordered]@{
        Success      = $false
        TagName      = $null
        ZipPath      = $null
        ReleaseUrl   = $null
        ErrorMessage = $null
    }

    if (-not (Test-Path -LiteralPath $ProjectPath)) {
        $result.ErrorMessage = "Project path '$ProjectPath' not found."
        return [PSCustomObject]$result
    }
    $csproj = Get-ChildItem -Path $ProjectPath -Filter '*.csproj' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $csproj) {
        $result.ErrorMessage = "No csproj found in $ProjectPath"
        return [PSCustomObject]$result
    }
    try {
        [xml]$xml = Get-Content -LiteralPath $csproj.FullName -Raw -ErrorAction Stop
    }
    catch {
        $result.ErrorMessage = "Failed to read '$($csproj.FullName)' as XML: $_"
        return [PSCustomObject]$result
    }
    $version = ($xml.Project.PropertyGroup | Where-Object { $_.VersionPrefix } | Select-Object -First 1).VersionPrefix
    if (-not $version) {
        $result.ErrorMessage = "VersionPrefix not found in '$($csproj.FullName)'"
        return [PSCustomObject]$result
    }
    $zipPath = Join-Path -Path $csproj.Directory.FullName -ChildPath ("bin/Release/{0}.{1}.zip" -f $csproj.BaseName, $version)
    if (-not (Test-Path -LiteralPath $zipPath)) {
        $result.ErrorMessage = "Zip file '$zipPath' not found."
        return [PSCustomObject]$result
    }
    $tagName = "v$version"
    $result.TagName = $tagName
    $result.ZipPath = $zipPath
    try {
        $statusGithub = Send-GitHubRelease -GitHubUsername $GitHubUsername -GitHubRepositoryName $GitHubRepositoryName -GitHubAccessToken $GitHubAccessToken -TagName $tagName -AssetFilePaths $zipPath -IsPreRelease:$IsPreRelease.IsPresent
        $result.Success = $statusGithub.Succeeded
        $result.ReleaseUrl = $statusGithub.ReleaseUrl
        if (-not $statusGithub.Succeeded) {
            $result.ErrorMessage = $statusGithub.ErrorMessage
        }
    }
    catch {
        $result.ErrorMessage = $_.Exception.Message
    }
    return [PSCustomObject]$result
}

function Publish-NugetPackage {
    <#
    .SYNOPSIS
    Pushes NuGet packages to a feed.
 
    .DESCRIPTION
    Finds all *.nupkg files in the specified path and uploads them using
    `dotnet nuget push` with the provided API key and feed URL.
 
    .PARAMETER Path
    Directory to search for NuGet packages.
 
    .PARAMETER ApiKey
    API key used to authenticate against the NuGet feed.
 
    .PARAMETER Source
    NuGet feed URL. Defaults to https://api.nuget.org/v3/index.json.
 
    .EXAMPLE
    Publish-NugetPackage -Path 'C:\Git\Project\bin\Release' -ApiKey $MyKey
    Uploads all packages in the Release folder to NuGet.org.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Path,
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ApiKey,
        [string]$Source = 'https://api.nuget.org/v3/index.json'
    )
    $result = [ordered]@{
        Success      = $true
        Pushed       = @()
        Failed       = @()
        ErrorMessage = $null
    }

    if (-not (Test-Path -LiteralPath $Path)) {
        $result.Success = $false
        $result.ErrorMessage = "Path '$Path' not found."
        return [PSCustomObject]$result
    }
    $packages = Get-ChildItem -Path $Path -Recurse -Filter '*.nupkg' -ErrorAction SilentlyContinue
    if (-not $packages) {
        $result.Success = $false
        $result.ErrorMessage = "No packages found in $Path"
        return [PSCustomObject]$result
    }
    foreach ($pkg in $packages) {
        dotnet nuget push $pkg.FullName --api-key $ApiKey --source $Source
        if ($LASTEXITCODE -eq 0) {
            $result.Pushed += $pkg.FullName
        }
        else {
            $result.Failed += $pkg.FullName
            $result.Success = $false
        }
    }
    return [PSCustomObject]$result
}

function Register-Certificate {
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, ParameterSetName = 'PFX')][string] $CertificatePFX,
        [Parameter(Mandatory, ParameterSetName = 'Store')][ValidateSet('LocalMachine', 'CurrentUser')][string] $LocalStore,
        [alias('CertificateThumbprint')][Parameter(ParameterSetName = 'Store')][string] $Thumbprint,
        [Parameter(Mandatory)][string] $Path,
        [string] $TimeStampServer = 'http://timestamp.digicert.com',
        [ValidateSet('All', 'NotRoot', 'Signer')] [string] $IncludeChain = 'All',
        [string[]] $Include = @('*.ps1', '*.psd1', '*.psm1', '*.dll', '*.cat'),
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512')][string] $HashAlgorithm = 'SHA256'
    )
    if ($PSBoundParameters.Keys -contains 'LocalStore') {
        $Cert = Get-ChildItem -Path "Cert:\$LocalStore\My" -CodeSigningCert
        if ($Thumbprint) {
            $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint }
            if (-not $Certificate) {
                Write-Warning -Message "Register-Certificate - No certificates found by that thumbprint"
                return
            }
        }
        elseif ($Cert.Count -eq 0) {
            Write-Warning -Message "Register-Certificate - No certificates found in store."
            return
        }
        elseif ($Cert.Count -eq 1) {
            $Certificate = $Cert
        }
        else {
            if ($Thumbprint) {
                $Certificate = $Cert | Where-Object { $_.Thumbprint -eq $Thumbprint }
                if (-not $Certificate) {
                    Write-Warning -Message "Register-Certificate - No certificates found by that thumbprint"
                    return
                }
            }
            else {
                $CodeError = "Get-ChildItem -Path Cert:\$LocalStore\My -CodeSigningCert"
                Write-Warning -Message "Register-Certificate - More than one certificate found in store. Provide Thumbprint for expected certificate"
                Write-Warning -Message "Register-Certificate - Use: $CodeError"
                $Cert
                return
            }
        }
    }
    elseif ($PSBoundParameters.Keys -contains 'CertificatePFX') {
        if (Test-Path -LiteralPath $CertificatePFX) {
            $Certificate = Get-PfxCertificate -FilePath $CertificatePFX
            if (-not $Certificate) {
                Write-Warning -Message "Register-Certificate - No certificates found for PFX"
                return
            }
        }
    }
    if ($Certificate -and $Path) {
        if (Test-Path -LiteralPath $Path) {
            if ($null -ne $IsWindows -and $IsWindows -eq $false) {

                $ModuleOpenAuthenticode = Get-Module -ListAvailable -Name 'OpenAuthenticode'
                if ($null -eq $ModuleOpenAuthenticode) {
                    Write-Warning -Message "Register-Certificate - OpenAuthenticode module not found. Please install it from PSGallery"
                    return
                }
                if ($IncludeChain -eq 'All') {
                    $IncludeOption = 'WholeChain'
                }
                elseif ($IncludeChain -eq 'NotRoot') {
                    $IncludeOption = 'ExcludeRoot'
                }
                elseif ($IncludeChain -eq 'Signer') {
                    $IncludeOption = 'EndCertOnly'
                }
                else {
                    $IncludeOption = 'None'
                }
                Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object {
                    ($_ | Get-OpenAuthenticodeSignature).Status -eq 'NotSigned'
                } | Set-OpenAuthenticodeSignature -Certificate $Certificate -TimeStampServer $TimeStampServer -IncludeChain $IncludeOption -HashAlgorithm $HashAlgorithm
            }
            else {

                $ModuleSigning = Get-Command -Name Set-AuthenticodeSignature
                if (-not $ModuleSigning) {
                    Write-Warning -Message "Register-Certificate - Code signing commands not found. Skipping signing."
                    return
                }
                Get-ChildItem -Path $Path -Filter * -Include $Include -Recurse -ErrorAction SilentlyContinue | Where-Object {
                    ($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned'
                } | Set-AuthenticodeSignature -Certificate $Certificate -TimestampServer $TimeStampServer -IncludeChain $IncludeChain -HashAlgorithm $HashAlgorithm
            }
        }
    }
}
function Remove-Comments {
    <#
    .SYNOPSIS
    Remove comments from PowerShell file
 
    .DESCRIPTION
    Remove comments from PowerShell file and optionally remove empty lines
    By default comments in param block are not removed
    By default comments before param block are not removed
 
    .PARAMETER SourceFilePath
    File path to the source file
 
    .PARAMETER Content
    Content of the file
 
    .PARAMETER DestinationFilePath
    File path to the destination file. If not provided, the content will be returned
 
    .PARAMETER RemoveEmptyLines
    Remove empty lines if more than one empty line is found
 
    .PARAMETER RemoveAllEmptyLines
    Remove all empty lines from the content
 
    .PARAMETER RemoveCommentsInParamBlock
    Remove comments in param block. By default comments in param block are not removed
 
    .PARAMETER RemoveCommentsBeforeParamBlock
    Remove comments before param block. By default comments before param block are not removed
 
    .EXAMPLE
    Remove-Comments -SourceFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript.ps1' -DestinationFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript1.ps1' -RemoveAllEmptyLines -RemoveCommentsInParamBlock -RemoveCommentsBeforeParamBlock
 
    .NOTES
    Most of the work done by Chris Dent, with improvements by Przemyslaw Klys
 
    #>

    [CmdletBinding(DefaultParameterSetName = 'FilePath')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'FilePath')]
        [alias('FilePath', 'Path', 'LiteralPath')][string] $SourceFilePath,

        [Parameter(Mandatory, ParameterSetName = 'Content')][string] $Content,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [alias('Destination', 'OutputFile', 'OutputFilePath')][string] $DestinationFilePath,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveAllEmptyLines,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveEmptyLines,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveCommentsInParamBlock,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $RemoveCommentsBeforeParamBlock,

        [Parameter(ParameterSetName = 'Content')]
        [Parameter(ParameterSetName = 'FilePath')]
        [switch] $DoNotRemoveSignatureBlock
    )

    if ($PSVersionTable.PSVersion.Major -gt 5) {
        $Encoding = 'UTF8BOM'
    }
    else {
        $Encoding = 'UTF8'
    }

    if ($SourceFilePath) {
        $Fullpath = Resolve-Path -LiteralPath $SourceFilePath
        $Content = [IO.File]::ReadAllText($FullPath, [System.Text.Encoding]::UTF8)
    }

    $Tokens = $Errors = @()
    $Ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$Errors)

    $groupedTokens = $Tokens | Group-Object { $_.Extent.StartLineNumber }
    $DoNotRemove = $false
    $DoNotRemoveCommentParam = $false
    $CountParams = 0
    $ParamFound = $false
    $SignatureBlock = $false
    $toRemove = foreach ($line in $groupedTokens) {
        if ($Ast.Body.ParamBlock.Extent.StartLineNumber -gt $line.Name) {
            continue
        }
        $tokens = $line.Group
        for ($i = 0; $i -lt $line.Count; $i++) {
            $token = $tokens[$i]
            if ($token.Extent.StartOffset -lt $Ast.Body.ParamBlock.Extent.StartOffset) {
                continue
            }

            if ($token.Extent.Text -eq 'function') {
                if (-not $RemoveCommentsBeforeParamBlock) {
                    $DoNotRemove = $true
                }
                continue
            }
            if ($token.Extent.Text -eq 'param') {
                $ParamFound = $true
                $DoNotRemove = $false
            }
            if ($DoNotRemove) {
                continue
            }

            if ($token.Extent.Text -eq 'param') {
                if (-not $RemoveCommentsInParamBlock) {
                    $DoNotRemoveCommentParam = $true
                }
                continue
            }
            if ($ParamFound -and ($token.Extent.Text -eq '(' -or $token.Extent.Text -eq '@(')) {
                $CountParams += 1
            }
            elseif ($ParamFound -and $token.Extent.Text -eq ')') {
                $CountParams -= 1
            }
            if ($ParamFound -and $token.Extent.Text -eq ')') {
                if ($CountParams -eq 0) {
                    $DoNotRemoveCommentParam = $false
                    $ParamFound = $false
                }
            }
            if ($DoNotRemoveCommentParam) {
                continue
            }

            if ($token.Kind -ne 'Comment') {
                continue
            }

            if ($DoNotRemoveSignatureBlock) {
                if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # Begin signature block') {
                    $SignatureBlock = $true
                    continue
                }
                if ($SignatureBlock) {
                    if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # End signature block') {
                        $SignatureBlock = $false
                    }
                    continue
                }
            }
            $token
        }
    }
    $toRemove = $toRemove | Sort-Object { $_.Extent.StartOffset } -Descending
    foreach ($token in $toRemove) {
        $StartIndex = $token.Extent.StartOffset
        $HowManyChars = $token.Extent.EndOffset - $token.Extent.StartOffset
        $content = $content.Remove($StartIndex, $HowManyChars)
    }
    if ($RemoveEmptyLines) {

        $Content = $Content -replace '(?m)^\s*$', ''
        $Content = $Content -replace "(?:`r?`n|\n|\r)", "`r`n"
    }
    if ($RemoveAllEmptyLines) {

        $Content = $Content -replace '(?m)^\s*$(\r?\n)?', ''
    }
    if ($Content) {
        $Content = $Content.Trim()
    }
    if ($DestinationFilePath) {
        $Content | Set-Content -Path $DestinationFilePath -Encoding $Encoding
    }
    else {
        $Content
    }
}
function Send-GitHubRelease {
    <#
    .SYNOPSIS
    Creates a new Release for the given GitHub repository.
 
    .DESCRIPTION
    Uses the GitHub API to create a new Release for a given repository.
    Allows you to specify all of the Release properties, such as the Tag, Name, Assets, and if it's a Draft or Prerelease or not.
 
    .PARAMETER GitHubUsername
    The username that the GitHub repository exists under.
    e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the username is 'deadlydog'.
 
    .PARAMETER GitHubRepositoryName
    The name of the repository to create the Release for.
    e.g. For the repository https://github.com/deadlydog/New-GitHubRelease, the repository name is 'New-GitHubRelease'.
 
    .PARAMETER GitHubAccessToken
    The Access Token to use as credentials for GitHub.
    Access tokens can be generated at https://github.com/settings/tokens.
    The access token will need to have the repo/public_repo permission on it for it to be allowed to create a new Release.
 
    .PARAMETER TagName
    The name of the tag to create at the Commitish.
 
    .PARAMETER ReleaseName
    The name to use for the new release.
    If blank, the TagName will be used.
 
    .PARAMETER ReleaseNotes
    The text describing the contents of the release.
 
    .PARAMETER AssetFilePaths
    The full paths of the files to include in the release.
 
    .PARAMETER Commitish
    Specifies the commitish value that determines where the Git tag is created from.
    Can be any branch or commit SHA. Unused if the Git tag already exists.
    Default: the repository's default branch (usually master).
 
    .PARAMETER IsDraft
    True to create a draft (unpublished) release, false to create a published one.
    Default: false
 
    .PARAMETER IsPreRelease
    True to identify the release as a prerelease. false to identify the release as a full release.
    Default: false
 
    .OUTPUTS
    A hash table with the following properties is returned:
 
    Succeeded = $true if the Release was created successfully and all assets were uploaded to it, $false if some part of the process failed.
    ReleaseCreationSucceeded = $true if the Release was created successfully (does not include asset uploads), $false if the Release was not created.
    AllAssetUploadsSucceeded = $true if all assets were uploaded to the Release successfully, $false if one of them failed, $null if there were no assets to upload.
    ReleaseUrl = The URL of the new Release that was created.
    ErrorMessage = A message describing what went wrong in the case that Succeeded is $false.
 
    .EXAMPLE
    # Import the module dynamically from the PowerShell Gallery. Use CurrentUser scope to avoid having to run as admin.
    Import-Module -Name New-GitHubRelease -Scope CurrentUser
 
    # Specify the parameters required to create the release. Do it as a hash table for easier readability.
    $newGitHubReleaseParameters =
    @{
        GitHubUsername = 'deadlydog'
        GitHubRepositoryName = 'New-GitHubRelease'
        GitHubAccessToken = 'SomeLongHexidecimalString'
        ReleaseName = "New-GitHubRelease v1.0.0"
        TagName = "v1.0.0"
        ReleaseNotes = "This release contains the following changes: ..."
        AssetFilePaths = @('C:\MyProject\Installer.exe','C:\MyProject\Documentation.md')
        IsPreRelease = $false
        IsDraft = $true # Set to true when testing so we don't publish a real release (visible to everyone) by accident.
    }
 
    # Try to create the Release on GitHub and save the results.
    $result = New-GitHubRelease @newGitHubReleaseParameters
 
    # Provide some feedback to the user based on the results.
    if ($result.Succeeded -eq $true)
    {
        Write-Output "Release published successfully! View it at $($result.ReleaseUrl)"
    }
    elseif ($result.ReleaseCreationSucceeded -eq $false)
    {
        Write-Error "The release was not created. Error message is: $($result.ErrorMessage)"
    }
    elseif ($result.AllAssetUploadsSucceeded -eq $false)
    {
        Write-Error "The release was created, but not all of the assets were uploaded to it. View it at $($result.ReleaseUrl). Error message is: $($result.ErrorMessage)"
    }
 
    Attempt to create a new Release on GitHub, and provide feedback to the user indicating if it succeeded or not.
 
    .LINK
    Project home: https://github.com/deadlydog/New-GitHubRelease
 
    .NOTES
    Name: New-GitHubRelease
    Author: Daniel Schroeder (originally based on the script at https://github.com/majkinetor/au/blob/master/scripts/Github-CreateRelease.ps1)
    GitHub Release API Documentation: https://developer.github.com/v3/repos/releases/#create-a-release
    Version: 1.0.2
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, HelpMessage = "The username the repository is under (e.g. deadlydog).")]
        [string] $GitHubUsername,

        [Parameter(Mandatory = $true, HelpMessage = "The repository name to create the release in (e.g. Invoke-MsBuild).")]
        [string] $GitHubRepositoryName,

        [Parameter(Mandatory = $true, HelpMessage = "The Acess Token to use as credentials for GitHub.")]
        [string] $GitHubAccessToken,

        [Parameter(Mandatory = $true, HelpMessage = "The name of the tag to create at the the Commitish.")]
        [string] $TagName,

        [Parameter(Mandatory = $false, HelpMessage = "The name of the release. If blank, the TagName will be used.")]
        [string] $ReleaseName,

        [Parameter(Mandatory = $false, HelpMessage = "Text describing the contents of the tag.")]
        [string] $ReleaseNotes,

        [Parameter(Mandatory = $false, HelpMessage = "The full paths of the files to include in the release.")]
        [string[]] $AssetFilePaths,

        [Parameter(Mandatory = $false, HelpMessage = "Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository's default branch (usually master).")]
        [string] $Commitish,

        [Parameter(Mandatory = $false, HelpMessage = "True to create a draft (unpublished) release, false to create a published one. Default: false")]
        [bool] $IsDraft = $false,

        [Parameter(Mandatory = $false, HelpMessage = "True to identify the release as a prerelease. false to identify the release as a full release. Default: false")]
        [bool] $IsPreRelease = $false

        #[switch] $GenerateReleaseNotes,
        #[switch] $MakeLatest
    )

    begin {

        [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls

        [string] $NewLine = [Environment]::NewLine

        if ([string]::IsNullOrEmpty($ReleaseName)) {
            $ReleaseName = $TagName
        }

        Test-AllFilePathsAndThrowErrorIfOneIsNotValid $AssetFilePaths
    }

    end { 
    }

    process {

        $result = @{ }
        $result.Succeeded = $false
        $result.ReleaseCreationSucceeded = $false
        $result.AllAssetUploadsSucceeded = $false
        $result.ReleaseUrl = $null
        $result.ErrorMessage = $null

        [bool] $thereAreNoAssetsToIncludeInTheRelease = ($null -eq $AssetFilePaths) -or ($AssetFilePaths.Count -le 0)
        if ($thereAreNoAssetsToIncludeInTheRelease) {
            $result.AllAssetUploadsSucceeded = $null
        }

        $authHeader = [ordered] @{
            Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($GitHubAccessToken + ":x-oauth-basic"))
        }

        $releaseData = [ordered] @{
            tag_name         = $TagName
            target_commitish = $Commitish
            name             = $ReleaseName
            body             = $ReleaseNotes
            draft            = $IsDraft
            prerelease       = $IsPreRelease
        }

        $createReleaseWebRequestParameters = [ordered] @{
            Uri         = "https://api.github.com/repos/$GitHubUsername/$GitHubRepositoryName/releases"
            Method      = 'POST'
            Headers     = $authHeader
            ContentType = 'application/vnd.github+json'
            Body        = (ConvertTo-Json $releaseData -Compress)
        }

        try {
            Write-Verbose "Sending web request to create the new Release..."
            $createReleaseWebRequestResults = Invoke-RestMethodAndThrowDescriptiveErrorOnFailure -requestParametersHashTable $createReleaseWebRequestParameters
        }
        catch {
            $result.ReleaseCreationSucceeded = $false
            $result.ErrorMessage = $_.Exception.Message
            return $result
        }

        $result.ReleaseCreationSucceeded = $true
        $result.ReleaseUrl = $createReleaseWebRequestResults.html_url

        if ($thereAreNoAssetsToIncludeInTheRelease) {
            $result.Succeeded = $true
            return $result
        }

        [string] $urlToUploadFilesTo = $createReleaseWebRequestResults.upload_url -replace '{.+}'

        try {
            Write-Verbose "Uploading asset files to the new release..."
            Send-FilesToGitHubRelease -filePathsToUpload $AssetFilePaths -urlToUploadFilesTo $urlToUploadFilesTo -authHeader $authHeader
        }
        catch {
            $result.AllAssetUploadsSucceeded = $false
            $result.ErrorMessage = $_.Exception.Message
            return $result
        }

        $result.AllAssetUploadsSucceeded = $true
        $result.Succeeded = $true
        return $result
    }
}
function Set-ProjectVersion {
    <#
    .SYNOPSIS
    Updates version numbers across multiple project files.
 
    .DESCRIPTION
    Updates version numbers in C# projects (.csproj), PowerShell modules (.psd1),
    and PowerShell build scripts that contain 'Invoke-ModuleBuild'. Can increment
    version components or set a specific version.
 
    .PARAMETER VersionType
    The type of version increment: Major, Minor, Build, or Revision.
 
    .PARAMETER NewVersion
    Specific version number to set (format: x.x.x or x.x.x.x).
 
    .PARAMETER ModuleName
    Optional module name to filter updates to specific projects/modules.
 
    .PARAMETER Path
    The root path to search for project files. Defaults to current location.
 
    .PARAMETER ExcludeFolders
    Array of folder names to exclude from the search (in addition to default 'obj' and 'bin').
 
    .PARAMETER PassThru
    Returns the update results when specified.
 
    .OUTPUTS
    PSCustomObject[]
    When PassThru is specified, returns update results for each modified file.
 
    .EXAMPLE
    Set-ProjectVersion -VersionType Minor
    Increments the minor version in all project files.
 
    .EXAMPLE
    Set-ProjectVersion -NewVersion "2.1.0" -ModuleName "MyModule"
    Sets the version to 2.1.0 for the specific module.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [ValidateSet('Major', 'Minor', 'Build', 'Revision')]
        [string]$VersionType = '',
        [Parameter()]
        [ValidatePattern('^\d+\.\d+\.\d+(\.\d+)?$')]
        [string]$NewVersion = '',
        [Parameter()]
        [string]$ModuleName = '',
        [Parameter()]
        [string]$Path = (Get-Location).Path,
        [Parameter()]
        [string[]]$ExcludeFolders = @(),
        [switch] $PassThru
    )

    $RepoRoot = $Path
    $DefaultExcludes = @('obj', 'bin')
    $AllExcludes = $DefaultExcludes + $ExcludeFolders | Select-Object -Unique

    $CsprojFiles = Get-ChildItem -Path $RepoRoot -Filter "*.csproj" -Recurse | Where-Object {
        $file = $_
        ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object {
                $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
            }))
    }
    $PsdFiles = Get-ChildItem -Path $RepoRoot -Filter "*.psd1" -Recurse | Where-Object {
        $file = $_
        ($AllExcludes.Count -eq 0 -or -not ($AllExcludes | Where-Object {
                $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
            }))
    }

    $BuildScriptFiles = Get-ChildItem -Path $RepoRoot -Filter "*.ps1" -Recurse | Where-Object {
        $file = $_

        $isExcluded = ($AllExcludes.Count -gt 0 -and ($AllExcludes | Where-Object {
                    $_ -and $_.Trim() -ne '' -and $file.FullName -and $file.FullName.ToLower().Contains($_.ToLower())
                }))
        if ($isExcluded) {
            return $false
        }

        try {
            $content = Get-Content -Path $file.FullName -Raw -ErrorAction SilentlyContinue
            return $content -match 'Invoke-ModuleBuild|Build-Module'
        }
        catch {
            return $false
        }
    }

    $targetCsprojFiles = $CsprojFiles
    if ($ModuleName) {
        $targetCsprojFiles = $CsprojFiles | Where-Object { $_.BaseName -eq $ModuleName }
    }

    $targetPsdFiles = $PsdFiles
    if ($ModuleName) {
        $targetPsdFiles = $PsdFiles | Where-Object { $_.BaseName -eq $ModuleName }
    }

    $currentVersion = $null

    foreach ($csProj in $targetCsprojFiles) {
        $version = Get-CurrentVersionFromCsProj -ProjectFile $csProj.FullName
        if ($version) {
            $currentVersion = $version
            break
        }
    }

    if (-not $currentVersion) {
        foreach ($psd1 in $targetPsdFiles) {
            $version = Get-CurrentVersionFromPsd1 -ManifestFile $psd1.FullName
            if ($version) {
                $currentVersion = $version
                break
            }
        }
    }

    if (-not $currentVersion) {
        foreach ($buildScript in $BuildScriptFiles) {
            $version = Get-CurrentVersionFromBuildScript -ScriptFile $buildScript.FullName
            if ($version) {
                $currentVersion = $version
                break
            }
        }
    }

    if (-not $currentVersion) {
        Write-Error "Could not determine current version from any project files."
        return
    }
    if (-not [string]::IsNullOrWhiteSpace($NewVersion)) {
        $newVersion = $NewVersion
    }
    else {
        $newVersion = Update-VersionNumber -Version $currentVersion -Type $VersionType
    }

    $CurrentVersions = Get-ProjectVersion -Path $RepoRoot -ExcludeFolders $AllExcludes
    $CurrentVersionHash = @{}
    foreach ($C in $CurrentVersions) {
        $CurrentVersionHash[$C.Source] = $C.Version
    }
    $Output = @(
        foreach ($csProj in $targetCsprojFiles) {
            Update-VersionInCsProj -ProjectFile $csProj.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash
        }
        foreach ($psd1 in $targetPsdFiles) {
            Update-VersionInPsd1 -ManifestFile $psd1.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash
        }
        foreach ($buildScript in $BuildScriptFiles) {
            Update-VersionInBuildScript -ScriptFile $buildScript.FullName -Version $newVersion -WhatIf:$WhatIfPreference -CurrentVersionHash $CurrentVersionHash
        }
    )
    if ($PassThru) {
        $Output
    }
}
function Test-BasicModule {
    [cmdletBinding()]
    param(
        [string] $Path,
        [string] $Type
    )
    if ($Type -contains 'Encoding') {
        Get-ChildItem -LiteralPath $Path -Recurse -Filter '*.ps1' | Get-FileEncoding
    }
}
function Test-ScriptFile {
    <#
    .Synopsis
 
    Test a PowerShell script for cmdlets
 
    .Description
 
    This command will analyze a PowerShell script file and display a list of detected commands such as PowerShell cmdlets and functions. Commands will be compared to what is installed locally. It is recommended you run this on a Windows 8.1 client with the latest version of RSAT installed. Unknown commands could also be internally defined functions. If in doubt view the contents of the script file in the PowerShell ISE or a script editor.
    You can test any .ps1, .psm1 or .txt file.
    .Parameter Path
 
    The path to the PowerShell script file. You can test any .ps1, .psm1 or .txt file.
 
    .Example
 
    PS C:\> test-scriptfile C:\scripts\Remove-MyVM2.ps1
 
    CommandType Name ModuleName
    ----------- ---- ----------
        Cmdlet Disable-VMEventing Hyper-V
        Cmdlet ForEach-Object Microsoft.PowerShell.Core
        Cmdlet Get-VHD Hyper-V
        Cmdlet Get-VMSnapshot Hyper-V
        Cmdlet Invoke-Command Microsoft.PowerShell.Core
        Cmdlet New-PSSession Microsoft.PowerShell.Core
        Cmdlet Out-Null Microsoft.PowerShell.Core
        Cmdlet Out-String Microsoft.PowerShell.Utility
        Cmdlet Remove-Item Microsoft.PowerShell.Management
        Cmdlet Remove-PSSession Microsoft.PowerShell.Core
        Cmdlet Remove-VM Hyper-V
        Cmdlet Remove-VMSnapshot Hyper-V
        Cmdlet Write-Debug Microsoft.PowerShell.Utility
        Cmdlet Write-Verbose Microsoft.PowerShell.Utility
        Cmdlet Write-Warning Microsoft.PowerShell.Utility
 
    .EXAMPLE
 
    PS C:\> Test-ScriptFile -Path 'C:\Users\przemyslaw.klys\Documents\WindowsPowerShell\Modules\PSWinReportingV2\PSWinReportingV2.psm1' | Sort-Object -Property Source, Name | ft -AutoSize
 
    .Notes
 
    Original script provided by Jeff Hicks at (https://www.petri.com/powershell-problem-solver-find-script-commands) and https://twitter.com/donnie_taylor/status/1160920407031058432
 
    #>


    [cmdletbinding()]
    param(
        [Parameter(Position = 0, Mandatory = $True, HelpMessage = "Enter the path to a PowerShell script file,",
            ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
        [ValidatePattern( "\.(ps1|psm1|txt)$")]
        [ValidateScript( { Test-Path $_ })]
        [string]$Path
    )

    begin {
        Write-Verbose "Starting $($MyInvocation.Mycommand)"
        Write-Verbose "Defining AST variables"
        New-Variable astTokens -Force
        New-Variable astErr -Force
    }

    process {
        Write-Verbose "Parsing $path"
        $null = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)

        $h = $astTokens | Group-Object tokenflags -AsHashTable -AsString

        $commandData = $h.CommandName | Where-Object { $_.text -notmatch "-TargetResource$" } |
            ForEach-Object {
                Write-Verbose "Processing $($_.text)"
                try {
                    $cmd = $_.Text
                    $resolved = $cmd | Get-Command -ErrorAction Stop
                    if ($resolved.CommandType -eq 'Alias') {
                        Write-Verbose "Resolving an alias"

                        Write-Verbose "Detected the Where-Object alias '?'"
                        if ($cmd -eq '?') {
                            Get-Command Where-Object
                        }
                        else {

                            $Resolved = $resolved.ResolvedCommandName | Get-Command

                            [PSCustomobject]@{
                                CommandType = $resolved.CommandType
                                Name        = $resolved.Name
                                ModuleName  = $resolved.ModuleName
                                Source      = $resolved.Source
                            }
                        }
                    }
                    else {

                        [PSCustomobject]@{
                            CommandType = $resolved.CommandType
                            Name        = $resolved.Name
                            ModuleName  = $resolved.ModuleName
                            Source      = $resolved.Source
                        }
                    }
                }
                catch {
                    Write-Verbose "Command is not recognized"

                    [PSCustomobject]@{
                        CommandType = "Unknown"
                        Name        = $cmd
                        ModuleName  = "Unknown"
                        Source      = "Unknown"
                    }
                }
            }

        $CommandData
    }

    end {
        Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
    }
}
function Test-ScriptModule {
    [cmdletbinding()]
    param(
        [string] $ModuleName,
        [ValidateSet('Name', 'CommandType', 'ModuleName', 'Source')] $SortName,
        [switch] $Unique
    )
    $Module = Get-Module -ListAvailable $ModuleName
    $Path = Join-Path -Path $Module.ModuleBase -ChildPath $Module.RootModule
    $Output = Test-ScriptFile -Path $Path
    if ($Unique) {
        $Output = $Output | Sort-Object -Property 'Name' -Unique:$Unique
    }
    if ($SortName) {
        $Output | Sort-Object -Property $SortName
    }
    else {
        $Output
    }
}

if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full").Release -lt 379893) {
    Write-Warning "This module requires .NET Framework 4.5.2 or later."; return 
} 

Export-ModuleMember -Function @('Convert-CommandsToList', 'Convert-ProjectEncoding', 'Convert-ProjectLineEnding', 'Get-MissingFunctions', 'Get-PowerShellAssemblyMetadata', 'Get-ProjectConsistency', 'Get-ProjectEncoding', 'Get-ProjectLineEnding', 'Get-ProjectVersion', 'Initialize-PortableModule', 'Initialize-PortableScript', 'Initialize-ProjectManager', 'Invoke-DotNetReleaseBuild', 'Invoke-ModuleBuild', 'New-ConfigurationArtefact', 'New-ConfigurationBuild', 'New-ConfigurationCommand', 'New-ConfigurationDocumentation', 'New-ConfigurationExecute', 'New-ConfigurationFormat', 'New-ConfigurationImportModule', 'New-ConfigurationInformation', 'New-ConfigurationManifest', 'New-ConfigurationModule', 'New-ConfigurationModuleSkip', 'New-ConfigurationPlaceHolder', 'New-ConfigurationPublish', 'New-ConfigurationTest', 'Publish-GitHubReleaseAsset', 'Publish-NugetPackage', 'Register-Certificate', 'Remove-Comments', 'Send-GitHubRelease', 'Set-ProjectVersion', 'Test-BasicModule', 'Test-ScriptFile', 'Test-ScriptModule') -Alias @('Build-Module', 'Invoke-ModuleBuilder', 'New-PrepareModule')
# SIG # Begin signature block
# MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCr9qe6g5tGA48M
# FS/jQ7iwNLcixGW1njidqVPdSRxcYaCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp
# BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy
# NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD
# VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD
# ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD
# Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo
# 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO
# H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R
# eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX
# j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV
# DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J
# idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C
# acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N
# Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X
# CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB
# AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB
# Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X
# LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l
# U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho
# dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl
# cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU
# aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS
# R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB
# b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC
# zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1
# UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp
# ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE
# sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8
# a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF
# YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q
# EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE
# deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J
# ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq
# F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX
# DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz
# dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ
# cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
# ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA
# 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh
# vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn
# 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr
# W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w
# iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP
# lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf
# QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28
# eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4
# fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0
# 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC
# AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE
# FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz
# ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j
# cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3
# dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw
# AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v
# Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu
# Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB
# CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E
# BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc
# 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ
# f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51
# 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev
# xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx
# h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY
# D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/
# Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq
# G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH
# v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP
# AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw
# PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2
# IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF
# AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor
# BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3
# DQEJBDEiBCDRd9pRrg4t/nK0cCN8Mk1X3qq/fnB7DuFc2cuW1f7BwDANBgkqhkiG
# 9w0BAQEFAASCAgBDd/TfK5Kqbr+Eyn1SLC4SfIJbV2zv88jtp/MufTJ7P4Bf4u+Z
# 1+4zMLxFxsnA4blJUi4BUnki2WdCfkiEXTcemWND+IlVRqV7hqdqkDdzZe6xRlwe
# 9uewe0Lwb6ZiPSOTKyXHtU4mtJLEqiUArsPAzyOhZs3bgQOs8AenTKbQ9iD9TsI/
# EwQsVCjoTZaOfOHncm5RMqFGIDgFgrvduzGat+s352VlUGz3wzYmOaNwrHNqc2O3
# fuiRhKDPzBVbaZnDAivwWmAT1XISLBY+8rF9hK7arUK9ftKDN6TZuvVOy+5B961H
# 1BzOFWaGw73N/pBERzJ4X7k2RtDwikv7pwFoCSUbER82CkqediEHsswE5LfJ0b0X
# 2OszOwgEpyrexwlRVKCInkGMm/4kR/qCsPJYLfpEGK6bPx8OemH7NDWz8+RSvXxp
# 1xZci4VnQG3VMQP2KtU9oM2Jmu0nSH6NXL2k2KSN2rzPS5yQv6is0FuavHYh8mpX
# 5V6tPphkL6HDH0uL1DOgpiEGzVMD+PygozFBOICJL7lpUXz0MODRrEsNSLdjvtQu
# 66mcRMo5WQ3eOVbTjODqjjNNtZL/fTfiQn6+x5cepusw2mNFWbwf/AISVFZn/zb7
# RkA0b/lwyFkIqsX1t7YqaJDRCx2bKsEgoBU5/ytyG9xPExWeNJebfwLY8aGCAyAw
# ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj
# MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNTA3MDcwOTA4NDVaMC8GCSqGSIb3DQEJBDEiBCDpNiP+
# goTY/wHdts/0f1nMdMgUnBFLc9bJxTPqrtrUmjANBgkqhkiG9w0BAQEFAASCAgCd
# 7LhBdc36Ct2KtmlbKN1mI8oT3uL94c5ijH3E6RjCc1RM91K/ahX/F8GhtU/zBLWG
# fz+eY/+uIH+6IsfSGV4NhiDTa8E+D3nvUDj2InGaj965LTAylbrufxRxiunVn1ts
# IMsbvO8SGhiGAMIU0kamflYYUHmu9UzmsBDJ0CyloiM5xrgNorezQq0zdERoGkjQ
# SefSJiQZxozgmGUayszHityCptzFNdPzLWZ2OI47SvURkrPzXu82rLphjVTqGVXn
# x7YCwL4ydYmeAdr9o2E8YM/TVWd0nJnFLjE56tiC9/n6f50MT87vXLMJMgJODr8/
# td7fCu1olxgChG6kq6ktEU2K/jn7tn9518TOYBNdj4ubWtm5feKDQbW+wDNnQaoA
# CqL/9a8EjV2EXHD+HfLbkhTbvd7hvq6X4V9yXvliDPF+Vht14CS2s/wFVp0oTXrX
# WDgMARP6nqaTM/RiXG8jCacBq6Wwe2/a1axuFt4m052K3yZIGpvVAyj5s56zuFF9
# jAtH7P3/jVsKaKt/aipePiOLhzL9cpV7NhdmJmFmyO1juqFb0le40okIMyFW74N8
# hIkTHgiFw4m/jam5tjCHeF0uVZ7Lv8LGOmtC6kGSaNrpo7LEZr9bKKo/vfxAPV5t
# IAK4dmGYqhNuH4B1ZwquBHzfMQfTDi9Wu2bkIzATJw==
# SIG # End signature block