Utilities.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')]
[CmdletBinding()]
param()


if ($PSVersionTable.PSVersion -lt '6.0') {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Compatibility with PowerShell 6.0 and newer.'
    )]
    $IsWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT'
}

$scriptName = 'Utilities'
Write-Verbose "[$scriptName] - Importing module"

#region - From [public]
Write-Verbose "[$scriptName] - [public] - Processing folder"

#region - From [public] - [Base64]
Write-Verbose "[$scriptName] - [public] - [Base64] - Processing folder"

#region - From [public] - [Base64] - [ConvertFrom-Base64String]
Write-Verbose "[$scriptName] - [public] - [Base64] - [ConvertFrom-Base64String] - Importing"

filter ConvertFrom-Base64String {
    <#
        .SYNOPSIS
        Convert to string from base64

        .DESCRIPTION
        Convert to string from base64

        .EXAMPLE
        ConvertFrom-Base64String -String 'SABlAGwAbABvACAAVwBvAHIAbABkAA=='

        Hello World

        Converts the string from base64 to a regular string.
    #>

    [CmdletBinding()]
    param (
        # The string to convert from base64
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $String
    )
    $ConvertedString = [System.Convert]::FromBase64String($String)
    $DecodedText = [System.Text.Encoding]::Unicode.GetString($ConvertedString)
    $DecodedText
}

Write-Verbose "[$scriptName] - [public] - [Base64] - [ConvertFrom-Base64String] - Done"
#endregion - From [public] - [Base64] - [ConvertFrom-Base64String]
#region - From [public] - [Base64] - [ConvertTo-Base64String]
Write-Verbose "[$scriptName] - [public] - [Base64] - [ConvertTo-Base64String] - Importing"

filter ConvertTo-Base64String {
    <#
        .SYNOPSIS
        Convert a string to base64

        .DESCRIPTION
        Convert a string to base64

        .EXAMPLE
        'Hello World' | ConvertTo-Base64String

        SABlAGwAbABvACAAVwBvAHIAbABkAA==

        Converts the string 'Hello World' to base64.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The string to convert to base64
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $String
    )
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($String)
    $encodedText = [System.Convert]::ToBase64String($bytes)
    #$ADOToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)"))
    $encodedText
}

Write-Verbose "[$scriptName] - [public] - [Base64] - [ConvertTo-Base64String] - Done"
#endregion - From [public] - [Base64] - [ConvertTo-Base64String]

Write-Verbose "[$scriptName] - [public] - [Base64] - Done"
#endregion - From [public] - [Base64]

#region - From [public] - [Boolean]
Write-Verbose "[$scriptName] - [public] - [Boolean] - Processing folder"

#region - From [public] - [Boolean] - [ConvertTo-Boolean]
Write-Verbose "[$scriptName] - [public] - [Boolean] - [ConvertTo-Boolean] - Importing"

filter ConvertTo-Boolean {
    <#
        .SYNOPSIS
        Convert string to boolean.

        .DESCRIPTION
        Convert string to boolean.

        .EXAMPLE
        ConvertTo-Boolean -String 'true'

        True

        Convert string to boolean.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param(
        # The string to be converted to boolean.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $String
    )

    switch -regex ($String.Trim()) {
        '^(1|true|yes|on|enabled)$' { $true }
        default { $false }
    }
}

Write-Verbose "[$scriptName] - [public] - [Boolean] - [ConvertTo-Boolean] - Done"
#endregion - From [public] - [Boolean] - [ConvertTo-Boolean]

Write-Verbose "[$scriptName] - [public] - [Boolean] - Done"
#endregion - From [public] - [Boolean]

#region - From [public] - [Files]
Write-Verbose "[$scriptName] - [public] - [Files] - Processing folder"

#region - From [public] - [Files] - [Get-FileInfo]
Write-Verbose "[$scriptName] - [public] - [Files] - [Get-FileInfo] - Importing"

function Get-FileInfo {
    <#
        .SYNOPSIS
        Get file information

        .DESCRIPTION
        Get file information

        .EXAMPLE
        Get-FileInfo -Path 'C:\temp\test.txt'

        Gets detailed information about the file.

        .NOTES
        Supported OS: Windows
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        # The path to the file.
        [Parameter(Mandatory)]
        [string] $Path
    )

    if (-not (Test-Path -Path $Path)) {
        Write-Error 'Path does not exist' -ErrorAction Stop
    }

    $Item = Get-Item -Path $Path

    #If item is directory, fail
    if ($Item.PSIsContainer) {
        Write-Error 'Path is a directory' -ErrorAction Stop
    }

    $shell = New-Object -ComObject Shell.Application
    $shellFolder = $shell.Namespace($Item.Directory.FullName)
    $shellFile = $shellFolder.ParseName($Item.name)

    $fileDetails = New-Object pscustomobject

    foreach ($i in 0..1000) {
        $propertyName = $shellfolder.GetDetailsOf($null, $i)
        $propertyValue = $shellfolder.GetDetailsOf($shellfile, $i)
        if (-not [string]::IsNullOrEmpty($propertyValue)) {
            Write-Verbose "[$propertyName] - [$propertyValue]"
            $fileDetails | Add-Member -MemberType NoteProperty -Name $propertyName -Value $propertyValue
        }
    }
    return $fileDetails
}

Write-Verbose "[$scriptName] - [public] - [Files] - [Get-FileInfo] - Done"
#endregion - From [public] - [Files] - [Get-FileInfo]
#region - From [public] - [Files] - [Remove-EmptyFolder]
Write-Verbose "[$scriptName] - [public] - [Files] - [Remove-EmptyFolder] - Importing"

function Remove-EmptyFolder {
    <#
        .SYNOPSIS
        Removes empty folders under the folder specified

        .DESCRIPTION
        Removes empty folders under the folder specified

        .EXAMPLE
        Remove-EmptyFolder -Path .

        Removes empty folders under the current path and outputs the results to the console.
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The path to the folder to be cleaned
        [Parameter(Mandatory)]
        [string] $Path
    )

    Get-ChildItem -Path $Path -Recurse -Directory | ForEach-Object {
        if ($null -eq (Get-ChildItem $_.FullName -Force -Recurse)) {
            Write-Verbose "Removing empty folder: [$($_.FullName)]"
            if ($PSCmdlet.ShouldProcess("folder [$($_.FullName)]", 'Remove')) {
                Remove-Item $_.FullName -Force
            }
        }
    }
}

Write-Verbose "[$scriptName] - [public] - [Files] - [Remove-EmptyFolder] - Done"
#endregion - From [public] - [Files] - [Remove-EmptyFolder]
#region - From [public] - [Files] - [Show-FileContent]
Write-Verbose "[$scriptName] - [public] - [Files] - [Show-FileContent] - Importing"

function Show-FileContent {
    <#
        .SYNOPSIS
        Prints the content of a file with line numbers in front of each line.

        .DESCRIPTION
        Prints the content of a file with line numbers in front of each line.

        .EXAMPLE
        $Path = 'C:\Repos\GitHub\PSModule\Framework\PSModule.FX\src\PSModule.FX\private\Utilities\Show-FileContent.ps1'
        Show-FileContent -Path $Path

        Shows the content of the file with line numbers in front of each line.
    #>

    [CmdletBinding()]
    param (
        # The path to the file to show the content of.
        [Parameter(Mandatory)]
        [string]$Path
    )

    $content = Get-Content -Path $Path
    $lineNumber = 1
    $columnSize = $content.Count.ToString().Length
    # Foreach line print the line number in front of the line with [ ] around it.
    # The linenumber should dynamically adjust to the number of digits with the length of the file.
    foreach ($line in $content) {
        $lineNumberFormatted = $lineNumber.ToString().PadLeft($columnSize)
        '[{0}] {1}' -f $lineNumberFormatted, $line
        $lineNumber++
    }
}

Write-Verbose "[$scriptName] - [public] - [Files] - [Show-FileContent] - Done"
#endregion - From [public] - [Files] - [Show-FileContent]

Write-Verbose "[$scriptName] - [public] - [Files] - Done"
#endregion - From [public] - [Files]

#region - From [public] - [Git]
Write-Verbose "[$scriptName] - [public] - [Git] - Processing folder"

#region - From [public] - [Git] - [Clear-GitRepo]
Write-Verbose "[$scriptName] - [public] - [Git] - [Clear-GitRepo] - Importing"

function Clear-GitRepo {
    <#
        .SYNOPSIS
        Clear a Git repository of all branches except main

        .DESCRIPTION
        Clear a Git repository of all branches except main

        .EXAMPLE
        Clear-GitRepo

        Clear a Git repository of all branches except main
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param()

    git fetch --all --prune
    (git branch).Trim() | Where-Object { $_ -notmatch 'main|\*' } | ForEach-Object { git branch $_ --delete --force }
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Clear-GitRepo] - Done"
#endregion - From [public] - [Git] - [Clear-GitRepo]
#region - From [public] - [Git] - [Invoke-GitSquash]
Write-Verbose "[$scriptName] - [public] - [Git] - [Invoke-GitSquash] - Importing"

function Invoke-GitSquash {
    <#
        .SYNOPSIS
        Squash all commits on a branch into a single commit

        .DESCRIPTION
        Squash all commits on a branch into a single commit

        .EXAMPLE
        Invoke-GitSquash

        Squash all commits on a branch into a single commit
    #>

    [OutputType([void])]
    [CmdletBinding()]
    [Alias('Squash-Main')]
    param(
        # The commit message to use for the squashed commit
        [Parameter()]
        [string] $CommitMessage = 'Squash',

        # The branch to squash
        [Parameter()]
        [string] $BranchName = 'main',

        # Temporary branch name
        [Parameter()]
        [string] $TempBranchName = 'init'
    )

    git fetch --all --prune
    $gitHightFrom2ndCommit = [int](git rev-list --count --first-parent $BranchName) - 1
    git reset HEAD~$gitHightFrom2ndCommit
    git checkout -b $TempBranchName
    git add .
    git commit -m "$CommitMessage"
    git push --set-upstream origin $TempBranchName
    git checkout $BranchName
    git push --force
    git checkout $TempBranchName
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Invoke-GitSquash] - Done"
#endregion - From [public] - [Git] - [Invoke-GitSquash]
#region - From [public] - [Git] - [Invoke-SquashBranch]
Write-Verbose "[$scriptName] - [public] - [Git] - [Invoke-SquashBranch] - Importing"

function Invoke-SquashBranch {
    <#
        .SYNOPSIS
        Squash a branch to a single commit

        .DESCRIPTION
        Squash a branch to a single commit

        .EXAMPLE
        Invoke-SquashBranch
    #>

    [Alias('Squash-Branch')]
    [CmdletBinding()]
    param(
        # The name of the branch to squash
        [Parameter()]
        [string] $BranchName = 'main'
    )
    git reset $(git merge-base $BranchName $(git branch --show-current))
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Invoke-SquashBranch] - Done"
#endregion - From [public] - [Git] - [Invoke-SquashBranch]
#region - From [public] - [Git] - [Reset-GitRepo]
Write-Verbose "[$scriptName] - [public] - [Git] - [Reset-GitRepo] - Importing"

function Reset-GitRepo {
    <#
        .SYNOPSIS
        Reset a Git repository to the upstream branch

        .DESCRIPTION
        Reset a Git repository to the upstream branch

        .EXAMPLE
        Reset-GitRepo

        Reset a Git repository to the upstream branch
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The upstream repository to reset to
        [Parameter()]
        [string] $Upstream = 'upstream',

        # The branch to reset
        [Parameter()]
        [string] $Branch = 'main',

        # Whether to push the reset
        [Parameter()]
        [switch] $Push
    )

    git fetch $Upstream
    git checkout $Branch
    if ($PSCmdlet.ShouldProcess("git repo", "Reset")) {
        git reset --hard $Upstream/$Branch
    }

    if ($Push) {
        if ($PSCmdlet.ShouldProcess("git changes to origin", "Push")) {

        }
        git push origin $Branch --force
    }
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Reset-GitRepo] - Done"
#endregion - From [public] - [Git] - [Reset-GitRepo]
#region - From [public] - [Git] - [Restore-GitRepo]
Write-Verbose "[$scriptName] - [public] - [Git] - [Restore-GitRepo] - Importing"

function Restore-GitRepo {
    <#
        .SYNOPSIS
        Restore a Git repository with upstream

        .DESCRIPTION
        Restore a Git repository with upstream

        .EXAMPLE
        Restore-GitRepo
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param(
        # The name of the branch to squash
        [Parameter()]
        [string] $BranchName = 'main'
    )

    git remote add upstream https://github.com/Azure/ResourceModules.git
    git fetch upstream
    git restore --source upstream/$BranchName * ':!*global.variables.*' ':!settings.json*'
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Restore-GitRepo] - Done"
#endregion - From [public] - [Git] - [Restore-GitRepo]
#region - From [public] - [Git] - [Sync-GitRepo]
Write-Verbose "[$scriptName] - [public] - [Git] - [Sync-GitRepo] - Importing"

function Sync-GitRepo {
    <#
        .SYNOPSIS
        Sync a Git repository with upstream

        .DESCRIPTION
        Sync a Git repository with upstream

        .EXAMPLE
        Sync-GitRepo
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param()
    git fetch upstream --prune
    git pull
    git push
}
Set-Alias -Name sync -Value Sync-Git

Write-Verbose "[$scriptName] - [public] - [Git] - [Sync-GitRepo] - Done"
#endregion - From [public] - [Git] - [Sync-GitRepo]
#region - From [public] - [Git] - [Sync-Repo]
Write-Verbose "[$scriptName] - [public] - [Git] - [Sync-Repo] - Importing"

function Sync-Repo {
    <#
        .SYNOPSIS
        Sync a Git repository with upstream

        .DESCRIPTION
        Sync a Git repository with upstream

        .EXAMPLE
        Sync-Repo
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param()
    git checkout main
    git pull
    git remote update origin --prune
    git branch -vv | Select-String -Pattern ': gone]' | ForEach-Object { $_.toString().Trim().Split(' ')[0] } | ForEach-Object { git branch -D $_ }
}

Write-Verbose "[$scriptName] - [public] - [Git] - [Sync-Repo] - Done"
#endregion - From [public] - [Git] - [Sync-Repo]

Write-Verbose "[$scriptName] - [public] - [Git] - Done"
#endregion - From [public] - [Git]

#region - From [public] - [GitHub]
Write-Verbose "[$scriptName] - [public] - [GitHub] - Processing folder"

#region - From [public] - [GitHub] - [Import-Variable]
Write-Verbose "[$scriptName] - [public] - [GitHub] - [Import-Variable] - Importing"

filter Import-Variable {
    <#
        .SYNOPSIS
        Import variables from a JSON file into the current session

        .DESCRIPTION
        Import variables from a JSON file into the current session

        .EXAMPLE
        Import-Variables -Path 'C:\path\to\variables.json'
    #>

    [OutputType([void])]
    [Alias('Import-Variables')]
    [CmdletBinding()]
    param (
        # Path to the JSON file containing the variables
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $Path
    )

    Write-Output "$($MyInvocation.MyCommand) - $Path - Processing"
    if (-not (Test-Path -Path $Path)) {
        throw "$($MyInvocation.MyCommand) - $Path - File not found"
    }

    $Variables = Get-Content -Path $Path -Raw -Force | ConvertFrom-Json

    $NestedVariablesFilePaths = ($Variables.PSObject.Properties | Where-Object Name -EQ 'VariablesFilePaths').Value
    foreach ($NestedVariablesFilePath in $NestedVariablesFilePaths) {
        Write-Output "$($MyInvocation.MyCommand) - $Path - Nested variable files - $NestedVariablesFilePath"
        $NestedVariablesFilePath | Import-Variables
    }

    Write-Output "$($MyInvocation.MyCommand) - $Path - Loading variables"
    foreach ($Property in $Variables.PSObject.Properties) {
        if ($Property -match 'VariablesFilePaths') {
            continue
        }
        Set-GitHubEnv -Name $Property.Name -Value $Property.Value
    }
    Write-Output "$($MyInvocation.MyCommand) - $Path - Done"
}

Write-Verbose "[$scriptName] - [public] - [GitHub] - [Import-Variable] - Done"
#endregion - From [public] - [GitHub] - [Import-Variable]
#region - From [public] - [GitHub] - [Set-GitHubEnvironmentVariable]
Write-Verbose "[$scriptName] - [public] - [GitHub] - [Set-GitHubEnvironmentVariable] - Importing"

function Set-GitHubEnvironmentVariable {
    <#
        .SYNOPSIS
        Set a GitHub environment variable

        .DESCRIPTION
        Set a GitHub environment variable

        .EXAMPLE
        Set-GitHubEnv -Name 'MyVariable' -Value 'MyValue'
    #>

    [OutputType([void])]
    [Alias('Set-GitHubEnv')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
        Justification = 'Does not change system state significantly'
    )]
    [CmdletBinding()]
    param (
        # Name of the variable
        [Parameter(Mandatory)]
        [string] $Name,

        # Value of the variable
        [Parameter(Mandatory)]
        [string] $Value
    )
    Write-Verbose (@{ $Name = $Value } | Format-Table -Wrap -AutoSize | Out-String)
    Write-Output "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}

Write-Verbose "[$scriptName] - [public] - [GitHub] - [Set-GitHubEnvironmentVariable] - Done"
#endregion - From [public] - [GitHub] - [Set-GitHubEnvironmentVariable]
#region - From [public] - [GitHub] - [Start-LogGroup]
Write-Verbose "[$scriptName] - [public] - [GitHub] - [Start-LogGroup] - Importing"

function Start-LogGroup {
    <#
        .SYNOPSIS
        Starts a new log group.

        .DESCRIPTION
        Starts a new log group.

        .EXAMPLE
        New-LogGroup -Name 'MyGroup'

        .NOTES
        [Azure DevOps - Formatting commands](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands)
        [GitHub - Grouping log lines](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines)
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost', '',
        Justification = 'Write-Host is used to group log messages.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
        Justification = 'This function does not change state. It only logs messages.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidLongLines', '', Justification = 'Contains long links.'
    )]
    param(
        # Name of the log group.
        [Parameter(Mandatory)]
        [string] $Name
    )

    if ($env:GITHUB_ACTIONS) {
        Write-Host "::group::$Name"
    } elseif ( $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ) {
        Write-Host "##[group]$Name"
    } else {
        Write-Host "-------- $Name --------"
    }
}

Write-Verbose "[$scriptName] - [public] - [GitHub] - [Start-LogGroup] - Done"
#endregion - From [public] - [GitHub] - [Start-LogGroup]
#region - From [public] - [GitHub] - [Stop-LogGroup]
Write-Verbose "[$scriptName] - [public] - [GitHub] - [Stop-LogGroup] - Importing"

function Stop-LogGroup {
    <#
        .SYNOPSIS
        Stops a log group.

        .DESCRIPTION
        Stops a log group.

        .EXAMPLE
        Stop-LogGroup

        .NOTES
        [Azure DevOps - Formatting commands](https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands)
        [GitHub - Grouping log lines](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines)
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost', '',
        Justification = 'Write-Host is used to group log messages.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function',
        Justification = 'This function does not change state. It only logs messages.'
    )]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidLongLines', '', Justification = 'Contains long links.'
    )]
    param ()

    if ($env:GITHUB_ACTIONS) {
        Write-Host '::endgroup::'
    } elseif ( $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ) {
        Write-Host '##[endgroup]'
    } else {
        Write-Host "-------- $Name --------"
    }
}

Write-Verbose "[$scriptName] - [public] - [GitHub] - [Stop-LogGroup] - Done"
#endregion - From [public] - [GitHub] - [Stop-LogGroup]

Write-Verbose "[$scriptName] - [public] - [GitHub] - Done"
#endregion - From [public] - [GitHub]

#region - From [public] - [GUID]
Write-Verbose "[$scriptName] - [public] - [GUID] - Processing folder"

#region - From [public] - [GUID] - [Search-GUID]
Write-Verbose "[$scriptName] - [public] - [GUID] - [Search-GUID] - Importing"

filter Search-GUID {
    <#
        .SYNOPSIS
        Search a string for a GUID

        .DESCRIPTION
        Search a string for a GUID

        .EXAMPLE
        '123e4567-e89b-12d3-a456-426655440000' | Search-GUID
    #>

    [Cmdletbinding()]
    [OutputType([guid])]
    param(
        # The string to search
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $String
    )

    Write-Verbose "Looking for a GUID in $String"
    $GUID = $String.ToLower() |
        Select-String -Pattern '[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}' |
        Select-Object -ExpandProperty Matches |
        Select-Object -ExpandProperty Value
    Write-Verbose "Found GUID: $GUID"
    $GUID
}

Write-Verbose "[$scriptName] - [public] - [GUID] - [Search-GUID] - Done"
#endregion - From [public] - [GUID] - [Search-GUID]
#region - From [public] - [GUID] - [Test-IsGUID]
Write-Verbose "[$scriptName] - [public] - [GUID] - [Test-IsGUID] - Importing"

filter Test-IsGUID {
    <#
        .SYNOPSIS
        Test if a string is a GUID

        .DESCRIPTION
        Test if a string is a GUID

        .EXAMPLE
        '123e4567-e89b-12d3-a456-426655440000' | Test-IsGUID

        True
    #>

    [Cmdletbinding()]
    [Alias('IsGUID')]
    [OutputType([bool])]
    param (
        # The string to test
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $String
    )

    [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'

    # Check GUID against regex
    $String -match $guidRegex
}

Write-Verbose "[$scriptName] - [public] - [GUID] - [Test-IsGUID] - Done"
#endregion - From [public] - [GUID] - [Test-IsGUID]

Write-Verbose "[$scriptName] - [public] - [GUID] - Done"
#endregion - From [public] - [GUID]

#region - From [public] - [Hashtable]
Write-Verbose "[$scriptName] - [public] - [Hashtable] - Processing folder"

#region - From [public] - [Hashtable] - [Convert-HashtableToString]
Write-Verbose "[$scriptName] - [public] - [Hashtable] - [Convert-HashtableToString] - Importing"

function Convert-HashtableToString {
    <#
        .SYNOPSIS
        Converts a hashtable to its code representation.

        .DESCRIPTION
        Recursively converts a hashtable to its code representation.
        This function is useful for exporting hashtables to .psd1 files.

        .EXAMPLE
        $hashtable = @{
            Key1 = 'Value1'
            Key2 = @{
                NestedKey1 = 'NestedValue1'
                NestedKey2 = 'NestedValue2'
            }
            Key3 = @(1, 2, 3)
            Key4 = $true
        }
        Convert-HashtableToString -Hashtable $hashtable

        This will return the following string:
        @{
            Key1 = 'Value1'
            Key2 = @{
                NestedKey1 = 'NestedValue1'
                NestedKey2 = 'NestedValue2'
            }
            Key3 = @(1, 2, 3)
            Key4 = $true
        }

        .NOTES
        General notes
    #>

    [CmdletBinding()]
    param (
        # The hashtable to convert to a string.
        [Parameter(Mandatory)]
        [object]$Hashtable,

        # The indentation level.
        [Parameter()]
        [int]$IndentLevel = 0
    )

    $lines = @()
    $lines += '@{'
    $indent = ' ' * $IndentLevel

    foreach ($key in $Hashtable.Keys) {
        Write-Verbose "Processing key: $key"
        $value = $Hashtable[$key]
        Write-Verbose "Processing value: $value"
        if ($null -eq $value) {
            Write-Verbose "Value type: `$null"
            continue
        }
        Write-Verbose "Value type: $($value.GetType().Name)"
        if (($value -is [System.Collections.Hashtable]) -or ($value -is [System.Collections.Specialized.OrderedDictionary])) {
            $nestedString = Convert-HashtableToString -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$indent $key = $nestedString"
        } elseif ($value -is [System.Management.Automation.PSCustomObject]) {
            $nestedString = Convert-HashtableToString -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$indent $key = $nestedString"
        } elseif ($value -is [System.Management.Automation.PSObject]) {
            $nestedString = Convert-HashtableToString -Hashtable $value -IndentLevel ($IndentLevel + 1)
            $lines += "$indent $key = $nestedString"
        } elseif ($value -is [bool]) {
            $lines += "$indent $key = `$$($value.ToString().ToLower())"
        } elseif ($value -is [int]) {
            $lines += "$indent $key = $value"
        } elseif ($value -is [array]) {
            if ($value.Count -eq 0) {
                $lines += "$indent $key = @()"
            } else {
                $lines += "$indent $key = @("
                $value | ForEach-Object {
                    $nestedValue = $_
                    Write-Verbose "Processing array element: $_"
                    Write-Verbose "Element type: $($_.GetType().Name)"
                    if (($nestedValue -is [System.Collections.Hashtable]) -or ($nestedValue -is [System.Collections.Specialized.OrderedDictionary])) {
                        $nestedString = Convert-HashtableToString -Hashtable $nestedValue -IndentLevel ($IndentLevel + 1)
                        $lines += "$indent $nestedString"
                    } elseif ($nestedValue -is [bool]) {
                        $lines += "$indent `$$($nestedValue.ToString().ToLower())"
                    } elseif ($nestedValue -is [int]) {
                        $lines += "$indent $nestedValue"
                    } else {
                        $lines += "$indent '$nestedValue'"
                    }
                }
                $lines += "$indent )"
            }
        } else {
            $lines += "$indent $key = '$value'"
        }
    }

    $lines += "$indent}"
    return $lines -join "`n"
}

Write-Verbose "[$scriptName] - [public] - [Hashtable] - [Convert-HashtableToString] - Done"
#endregion - From [public] - [Hashtable] - [Convert-HashtableToString]
#region - From [public] - [Hashtable] - [Merge-Hashtable]
Write-Verbose "[$scriptName] - [public] - [Hashtable] - [Merge-Hashtable] - Importing"

function Merge-Hashtable {
    <#
        .SYNOPSIS
        Merge two hashtables, with the second hashtable overriding the first

        .DESCRIPTION
        Merge two hashtables, with the second hashtable overriding the first

        .EXAMPLE
        $Main = [ordered]@{
            Action = ''
            Location = 'Main'
            Name = 'Main'
            Mode = 'Main'
        }
        $Override1 = [ordered]@{
            Action = ''
            Location = ''
            Name = 'Override1'
            Mode = 'Override1'
        }
        $Override2 = [ordered]@{
            Action = ''
            Location = ''
            Name = 'Override1'
            Mode = 'Override2'
        }
        Merge-Hashtables -Main $Main -Overrides $Override1, $Override2
    #>

    [OutputType([Hashtable])]
    [Alias('Merge-Hashtables')]
    [CmdletBinding()]
    param (
        # Main hashtable
        [Parameter(Mandatory)]
        [object] $Main,

        # Hashtable with overrides.
        # Providing a list of overrides will apply them in order.
        # Last write wins.
        [Parameter(Mandatory)]
        [object[]] $Overrides
    )
    $Output = $Main.Clone()
    foreach ($Override in $Overrides) {
        foreach ($Key in $Override.Keys) {
            if (($Output.Keys) -notcontains $Key) {
                $Output.$Key = $Override.$Key
            }
            if ($Override.item($Key) | IsNotNullOrEmpty) {
                $Output.$Key = $Override.$Key
            }
        }
    }
    return $Output
}

Write-Verbose "[$scriptName] - [public] - [Hashtable] - [Merge-Hashtable] - Done"
#endregion - From [public] - [Hashtable] - [Merge-Hashtable]

Write-Verbose "[$scriptName] - [public] - [Hashtable] - Done"
#endregion - From [public] - [Hashtable]

#region - From [public] - [PowerShell]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - Processing folder"

#region - From [public] - [PowerShell] - [Module]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - Processing folder"

#region - From [public] - [PowerShell] - [Module] - [Add-ModuleManifestData]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Add-ModuleManifestData] - Importing"

function Add-ModuleManifestData {
    <#
        .SYNOPSIS
        Add data to a module manifest file property

        .DESCRIPTION
        This function adds data to a module manifest file property.
        If the property doesn't exist, it will be created.
        If it does exist, the new data will be appended to the existing data.

        .EXAMPLE
        Add-ModuleManifestData -Path 'MyModule.psd1' -RequiredModules 'pester', 'platyPS'

        Adds the modules 'pester' and 'platyPS' to the RequiredModules property of the module manifest file 'MyModule.psd1'.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
        [string]$Path,

        # Modules that must be imported into the global environment prior to importing this module.
        [Parameter()]
        [Object[]] $RequiredModules,

        # Compatible editions of PowerShell.
        [Parameter()]
        [string[]] $CompatiblePSEditions,

        # Assemblies that must be loaded prior to importing this module.
        [Parameter()]
        [string[]] $RequiredAssemblies,

        # Script files (.ps1) that are run in the caller's environment prior to importing this module.
        [Parameter()]
        [string[]] $ScriptsToProcess,

        # Type files (.ps1xml) to be loaded when importing this module.
        [Parameter()]
        [string[]] $TypesToProcess,

        # Format files (.ps1xml) to be loaded when importing this module.
        [Parameter()]
        [string[]] $FormatsToProcess,

        # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess.
        [Parameter()]
        [Object[]] $NestedModules,

        # Functions to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no functions to export.
        [Parameter()]
        [string[]] $FunctionsToExport,

        # Cmdlets to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no cmdlets to export.
        [Parameter()]
        [string[]] $CmdletsToExport,

        # Variables to export from this module.
        [Parameter()]
        [string[]] $VariablesToExport,

        # Aliases to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no aliases to export.
        [Parameter()]
        [string[]] $AliasesToExport,

        # DSC resources to export from this module.
        [Parameter()]
        [string[]] $DscResourcesToExport,

        # List of all modules packaged with this module.
        [Parameter()]
        [Object[]] $ModuleList,

        # List of all files packaged with this module.
        [Parameter()]
        [string[]] $FileList,

        # Tags applied to this module. These help with module discovery in online galleries.
        [Parameter()]
        [string[]] $Tags,

        # External dependent modules of this module.
        [Parameter()]
        [string[]] $ExternalModuleDependencies
    )

    $moduleManifest = Get-ModuleManifest -Path $Path
    $changes = @{}

    if ($RequiredModules) {
        $RequiredModules += $moduleManifest.RequiredModules
        $changes.RequiredModules = $RequiredModules
    }
    if ($RequiredAssemblies) {
        $RequiredAssemblies += $moduleManifest.RequiredAssemblies
        $changes.RequiredAssemblies = $RequiredAssemblies
    }
    if ($CompatiblePSEditions) {
        $CompatiblePSEditions += $moduleManifest.CompatiblePSEditions
        $changes.CompatiblePSEditions = $CompatiblePSEditions
    }
    if ($ScriptsToProcess) {
        $ScriptsToProcess += $moduleManifest.ScriptsToProcess
        $changes.ScriptsToProcess = $ScriptsToProcess
    }
    if ($TypesToProcess) {
        $TypesToProcess += $moduleManifest.TypesToProcess
        $changes.TypesToProcess = $TypesToProcess
    }
    if ($FormatsToProcess) {
        $FormatsToProcess += $moduleManifest.FormatsToProcess
        $changes.FormatsToProcess = $FormatsToProcess
    }
    if ($NestedModules) {
        $NestedModules += $moduleManifest.NestedModules
        $changes.NestedModules = $NestedModules
    }
    if ($FunctionsToExport) {
        $FunctionsToExport += $moduleManifest.FunctionsToExport
        $changes.FunctionsToExport = $FunctionsToExport
    }
    if ($CmdletsToExport) {
        $CmdletsToExport += $moduleManifest.CmdletsToExport
        $changes.CmdletsToExport = $CmdletsToExport
    }
    if ($VariablesToExport) {
        $VariablesToExport += $moduleManifest.VariablesToExport
        $changes.VariablesToExport = $VariablesToExport
    }
    if ($AliasesToExport) {
        $AliasesToExport += $moduleManifest.AliasesToExport
        $changes.AliasesToExport = $AliasesToExport
    }
    if ($DscResourcesToExport) {
        $DscResourcesToExport += $moduleManifest.DscResourcesToExport
        $changes.DscResourcesToExport = $DscResourcesToExport
    }
    if ($ModuleList) {
        $ModuleList += $moduleManifest.ModuleList
        $changes.ModuleList = $ModuleList
    }
    if ($FileList) {
        $FileList += $moduleManifest.FileList
        $changes.FileList = $FileList
    }
    if ($Tags) {
        $Tags += $moduleManifest.PrivateData.PSData.Tags
        $changes.Tags = $Tags
    }
    if ($ExternalModuleDependencies) {
        $ExternalModuleDependencies += $moduleManifest.PrivateData.PSData.ExternalModuleDependencies
        $changes.ExternalModuleDependencies = $ExternalModuleDependencies
    }

    foreach ($key in $changes.GetEnumerator().Name) {
        $changes[$key] = $changes[$key] | Sort-Object -Unique | Where-Object { $_ | IsNotNullOrEmpty }
    }

    Set-ModuleManifest -Path $Path @changes

}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Add-ModuleManifestData] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Add-ModuleManifestData]
#region - From [public] - [PowerShell] - [Module] - [Add-PSModulePath]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Add-PSModulePath] - Importing"

function Add-PSModulePath {
    <#
        .SYNOPSIS
        Adds a path to the PSModulePath environment variable.

        .DESCRIPTION
        Adds a path to the PSModulePath environment variable.
        For Linux and macOS, the path delimiter is ':' and for Windows it is ';'.

        .EXAMPLE
        Add-PSModulePath -Path 'C:\Users\user\Documents\WindowsPowerShell\Modules'

        Adds the path 'C:\Users\user\Documents\WindowsPowerShell\Modules' to the PSModulePath environment variable.
    #>

    [CmdletBinding()]
    param(
        # Path to the folder where the module source code is located.
        [Parameter(Mandatory)]
        [string] $Path
    )

    if ([System.Environment]::OSVersion.Platform -eq 'Win32NT') {
        $PSModulePathSeparator = ';'
    } else {
        $PSModulePathSeparator = ':'
    }
    $env:PSModulePath += "$PSModulePathSeparator$Path"

    Write-Verbose 'PSModulePath:'
    $env:PSModulePath.Split($PSModulePathSeparator) | ForEach-Object {
        Write-Verbose " - [$_]"
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Add-PSModulePath] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Add-PSModulePath]
#region - From [public] - [PowerShell] - [Module] - [Export-PowerShellDataFile]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Export-PowerShellDataFile] - Importing"

function Export-PowerShellDataFile {
    <#
        .SYNOPSIS
        Export a hashtable to a .psd1 file.

        .DESCRIPTION
        This function exports a hashtable to a .psd1 file. It also formats the .psd1 file using the Format-ModuleManifest cmdlet.

        .EXAMPLE
        Export-PowerShellDataFile -Hashtable @{ Name = 'MyModule'; ModuleVersion = '1.0.0' } -Path 'MyModule.psd1'
    #>

    [CmdletBinding()]
    param (
        # The hashtable to export to a .psd1 file.
        [Parameter(Mandatory)]
        [object] $Hashtable,

        # The path of the .psd1 file to export.
        [Parameter(Mandatory)]
        [string] $Path,

        # Force the export, even if the file already exists.
        [Parameter()]
        [switch] $Force
    )

    $content = Convert-HashtableToString -Hashtable $Hashtable
    $content | Out-File -FilePath $Path -Force:$Force
    Format-ModuleManifest -Path $Path
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Export-PowerShellDataFile] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Export-PowerShellDataFile]
#region - From [public] - [PowerShell] - [Module] - [Format-ModuleManifest]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Format-ModuleManifest] - Importing"

function Format-ModuleManifest {
    <#
        .SYNOPSIS
        Formats a module manifest file.

        .DESCRIPTION
        This function formats a module manifest file, by removing comments and empty lines,
        and then formatting the file using the `Invoke-Formatter` function.

        .EXAMPLE
        Format-ModuleManifest -Path 'C:\MyModule\MyModule.psd1'
    #>

    [CmdletBinding()]
    param(
        # Path to the module manifest file.
        [Parameter(Mandatory)]
        [string] $Path
    )

    $Utf8BomEncoding = New-Object System.Text.UTF8Encoding $true

    $manifestContent = Get-Content -Path $Path
    $manifestContent = $manifestContent | ForEach-Object { $_ -replace '#.*' }
    $manifestContent = $manifestContent | ForEach-Object { $_.TrimEnd() }
    $manifestContent = $manifestContent | Where-Object { $_ | IsNotNullOrEmpty }
    [System.IO.File]::WriteAllLines($Path, $manifestContent, $Utf8BomEncoding)
    $manifestContent = Get-Content -Path $Path -Raw

    $content = Invoke-Formatter -ScriptDefinition $manifestContent
    [System.IO.File]::WriteAllLines($Path, $content, $Utf8BomEncoding)

}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Format-ModuleManifest] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Format-ModuleManifest]
#region - From [public] - [PowerShell] - [Module] - [Get-ModuleManifest]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Get-ModuleManifest] - Importing"

function Get-ModuleManifest {
    <#
        .SYNOPSIS
        Get the module manifest.

        .DESCRIPTION
        Get the module manifest as a path, file info, content, or hashtable.

        .EXAMPLE
        Get-PSModuleManifest -Path 'src/PSModule/PSModule.psd1' -As Hashtable
    #>

    [OutputType([string], [System.IO.FileInfo], [System.Collections.Hashtable], [System.Collections.Specialized.OrderedDictionary])]
    [CmdletBinding()]
    param(
        # Path to the module manifest file.
        [Parameter(Mandatory)]
        [string] $Path,

        # The format of the output.
        [Parameter()]
        [ValidateSet('FileInfo', 'Content', 'Hashtable')]
        [string] $As = 'Hashtable'
    )

    if (-not (Test-Path -Path $Path)) {
        Write-Warning 'No manifest file found.'
        return $null
    }
    Write-Verbose "Found manifest file [$Path]"

    switch ($As) {
        'FileInfo' {
            return Get-Item -Path $Path
        }
        'Content' {
            return Get-Content -Path $Path
        }
        'Hashtable' {
            $manifest = [System.Collections.Specialized.OrderedDictionary]@{}
            $psData = [System.Collections.Specialized.OrderedDictionary]@{}
            $privateData = [System.Collections.Specialized.OrderedDictionary]@{}
            $tempManifest = Import-PowerShellDataFile -Path $Path
            if ($tempManifest.ContainsKey('PrivateData')) {
                $tempPrivateData = $tempManifest.PrivateData
                if ($tempPrivateData.ContainsKey('PSData')) {
                    $tempPSData = $tempPrivateData.PSData
                    $tempPrivateData.Remove('PSData')
                }
            }

            $psdataOrder = @(
                'Tags'
                'LicenseUri'
                'ProjectUri'
                'IconUri'
                'ReleaseNotes'
                'Prerelease'
                'RequireLicenseAcceptance'
                'ExternalModuleDependencies'
            )
            foreach ($key in $psdataOrder) {
                if (($null -ne $tempPSData) -and ($tempPSData.ContainsKey($key))) {
                    $psData.$key = $tempPSData.$key
                }
            }
            if ($psData.Count -gt 0) {
                $privateData.PSData = $psData
            } else {
                $privateData.Remove('PSData')
            }
            foreach ($key in $tempPrivateData.Keys) {
                $privateData.$key = $tempPrivateData.$key
            }

            $manifestOrder = @(
                'RootModule'
                'ModuleVersion'
                'CompatiblePSEditions'
                'GUID'
                'Author'
                'CompanyName'
                'Copyright'
                'Description'
                'PowerShellVersion'
                'PowerShellHostName'
                'PowerShellHostVersion'
                'DotNetFrameworkVersion'
                'ClrVersion'
                'ProcessorArchitecture'
                'RequiredModules'
                'RequiredAssemblies'
                'ScriptsToProcess'
                'TypesToProcess'
                'FormatsToProcess'
                'NestedModules'
                'FunctionsToExport'
                'CmdletsToExport'
                'VariablesToExport'
                'AliasesToExport'
                'DscResourcesToExport'
                'ModuleList'
                'FileList'
                'HelpInfoURI'
                'DefaultCommandPrefix'
                'PrivateData'
            )
            foreach ($key in $manifestOrder) {
                if ($tempManifest.ContainsKey($key)) {
                    $manifest.$key = $tempManifest.$key
                }
            }
            if ($privateData.Count -gt 0) {
                $manifest.PrivateData = $privateData
            } else {
                $manifest.Remove('PrivateData')
            }

            return $manifest
        }
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Get-ModuleManifest] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Get-ModuleManifest]
#region - From [public] - [PowerShell] - [Module] - [Invoke-PruneModule]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Invoke-PruneModule] - Importing"

function Invoke-PruneModule {
    <#
        .SYNOPSIS
        Remove all but the newest version of a module

        .DESCRIPTION
        Remove all but the newest version of a module

        .EXAMPLE
        Invoke-PruneModule -Name 'Az.*' -Scope CurrentUser
    #>

    [OutputType([void])]
    [CmdletBinding()]
    [Alias('Prune-Module')]
    param (
        # Name of the module(s) to prune
        [Parameter()]
        [string[]] $Name = '*',

        # Scope of the module(s) to prune
        [Parameter()]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string[]] $Scope = 'CurrentUser'
    )

    if ($Scope -eq 'AllUsers' -and -not (IsAdmin)) {
        $message = 'Administrator rights are required to uninstall modules for all users. Please run the command again with' +
        " elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command."

        throw $message
    }

    $UpdateableModules = Get-InstalledModule | Where-Object Name -Like "$Name"
    $UpdateableModuleNames = $UpdateableModules.Name | Sort-Object -Unique
    foreach ($UpdateableModuleName in $UpdateableModuleNames) {
        $UpdateableModule = $UpdateableModules | Where-Object Name -EQ $UpdateableModuleName | Sort-Object -Property Version -Descending
        Write-Verbose "[$($UpdateableModuleName)] - Found [$($UpdateableModule.Count)]"

        $NewestModule = $UpdateableModule | Select-Object -First 1
        Write-Verbose "[$($UpdateableModuleName)] - Newest [$($NewestModule.Version -join ', ')]"

        $OutdatedModules = $UpdateableModule | Select-Object -Skip 1
        Write-Verbose "[$($UpdateableModuleName)] - Outdated [$($OutdatedModules.Version -join ', ')]"

        foreach ($OutdatedModule in $OutdatedModules) {
            Write-Verbose "[$($UpdateableModuleName)] - [$($OutdatedModule.Version)] - Removing"
            $OutdatedModule | Remove-Module -Force
            Write-Verbose "[$($UpdateableModuleName)] - [$($OutdatedModule.Version)] - Uninstalling"
            Uninstall-Module -Name $OutdatedModule.Name -RequiredVersion -Force
            try {
                $OutdatedModule.ModuleBase | Remove-Item -Force -Recurse -ErrorAction Stop
            } catch {
                Write-Warning "[$($UpdateableModuleName)] - [$($OutdatedModule.Version)] - Failed to remove [$($OutdatedModule.ModuleBase)]"
                continue
            }
        }
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Invoke-PruneModule] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Invoke-PruneModule]
#region - From [public] - [PowerShell] - [Module] - [Invoke-ReinstallModule]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Invoke-ReinstallModule] - Importing"

function Invoke-ReinstallModule {
    <#
        .SYNOPSIS
        Reinstalls module into a given scope.

        .DESCRIPTION
        Reinstalls module into a given scope. This is useful when you want to reinstall or clean up your module versions.
        With this command you always get the newest available version of the module and all the previous version wiped out.

        .PARAMETER Name
        The name of the module to be reinstalled. Wildcards are supported.

        .PARAMETER Scope
        The scope of the module to will be reinstalled to.

        .EXAMPLE
        Reinstall-Module -Name Pester -Scope CurrentUser

        Reinstall Pester module for the current user.

        .EXAMPLE
        Reinstall-Module -Scope CurrentUser

        Reinstall all reinstallable modules into the current user.
    #>

    [CmdletBinding()]
    [Alias('Reinstall-Module')]
    param (
        # Name of the module(s) to reinstall
        [Parameter()]
        [SupportsWildcards()]
        [string[]] $Name = '*',

        # Scope of the module(s) to reinstall
        [Parameter()]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string[]] $Scope = 'CurrentUser'
    )

    if ($Scope -eq 'AllUsers' -and -not (IsAdmin)) {
        $message = 'Administrator rights are required to uninstall modules for all users. Please run the command again with' +
        " elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command."

        throw $message
    }

    $modules = Get-InstalledModule | Where-Object Name -Like "$Name"
    Write-Verbose "Found [$($modules.Count)] modules"

    $modules | ForEach-Object {
        if ($_.name -eq 'Pester') {
            Uninstall-Pester -All
            continue
        }
        Uninstall-Module -Name $_ -AllVersions -Force -ErrorAction SilentlyContinue
    }

    $modules.Name | ForEach-Object {
        Install-Module -Name $_ -Scope $Scope -Force
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Invoke-ReinstallModule] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Invoke-ReinstallModule]
#region - From [public] - [PowerShell] - [Module] - [Set-ModuleManifest]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Set-ModuleManifest] - Importing"

filter Set-ModuleManifest {
    <#
        .SYNOPSIS
        Sets the values of a module manifest file.

        .DESCRIPTION
        This function sets the values of a module manifest file.
        Very much like the Update-ModuleManifest function, but allows values to be missing.

        .EXAMPLE
        Set-ModuleManifest -Path 'C:\MyModule\MyModule.psd1' -ModuleVersion '1.0.0'
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Function does not change state.'
    )]
    [CmdletBinding()]
    param(
        # Path to the module manifest file.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $Path,

        #Script module or binary module file associated with this manifest.
        [Parameter()]
        [AllowNull()]
        [string] $RootModule,

        #Version number of this module.
        [Parameter()]
        [AllowNull()]
        [Version] $ModuleVersion,

        # Supported PSEditions.
        [Parameter()]
        [AllowNull()]
        [string[]] $CompatiblePSEditions,

        # ID used to uniquely identify this module.
        [Parameter()]
        [AllowNull()]
        [guid] $GUID,

        # Author of this module.
        [Parameter()]
        [AllowNull()]
        [string] $Author,

        # Company or vendor of this module.
        [Parameter()]
        [AllowNull()]
        [string] $CompanyName,

        # Copyright statement for this module.
        [Parameter()]
        [AllowNull()]
        [string] $Copyright,

        # Description of the functionality provided by this module.
        [Parameter()]
        [AllowNull()]
        [string] $Description,

        # Minimum version of the PowerShell engine required by this module.
        [Parameter()]
        [AllowNull()]
        [Version] $PowerShellVersion,

        # Name of the PowerShell host required by this module.
        [Parameter()]
        [AllowNull()]
        [string] $PowerShellHostName,

        # Minimum version of the PowerShell host required by this module.
        [Parameter()]
        [AllowNull()]
        [version] $PowerShellHostVersion,

        # Minimum version of Microsoft .NET Framework required by this module.
        # This prerequisite is valid for the PowerShell Desktop edition only.
        [Parameter()]
        [AllowNull()]
        [Version] $DotNetFrameworkVersion,

        # Minimum version of the common language runtime (CLR) required by this module.
        # This prerequisite is valid for the PowerShell Desktop edition only.
        [Parameter()]
        [AllowNull()]
        [Version] $ClrVersion,

        # Processor architecture (None,X86, Amd64) required by this module
        [Parameter()]
        [AllowNull()]
        [System.Reflection.ProcessorArchitecture] $ProcessorArchitecture,

        # Modules that must be imported into the global environment prior to importing this module.
        [Parameter()]
        [AllowNull()]
        [Object[]] $RequiredModules,

        # Assemblies that must be loaded prior to importing this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $RequiredAssemblies,

        # Script files (.ps1) that are run in the caller's environment prior to importing this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $ScriptsToProcess,

        # Type files (.ps1xml) to be loaded when importing this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $TypesToProcess,

        # Format files (.ps1xml) to be loaded when importing this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $FormatsToProcess,

        # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess.
        [Parameter()]
        [AllowNull()]
        [Object[]] $NestedModules,

        # Functions to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no functions to export.
        [Parameter()]
        [AllowNull()]
        [string[]] $FunctionsToExport,

        # Cmdlets to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no cmdlets to export.
        [Parameter()]
        [AllowNull()]
        [string[]] $CmdletsToExport,

        # Variables to export from this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $VariablesToExport,

        # Aliases to export from this module, for best performance, do not use wildcards and do not
        # delete the entry, use an empty array if there are no aliases to export.
        [Parameter()]
        [AllowNull()]
        [string[]] $AliasesToExport,

        # DSC resources to export from this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $DscResourcesToExport,

        # List of all modules packaged with this module.
        [Parameter()]
        [AllowNull()]
        [Object[]] $ModuleList,

        # List of all files packaged with this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $FileList,

        # Tags applied to this module. These help with module discovery in online galleries.
        [Parameter()]
        [AllowNull()]
        [string[]] $Tags,

        # A URL to the license for this module.
        [Parameter()]
        [AllowNull()]
        [uri] $LicenseUri,

        # A URL to the main site for this project.
        [Parameter()]
        [AllowNull()]
        [uri] $ProjectUri,

        # A URL to an icon representing this module.
        [Parameter()]
        [AllowNull()]
        [uri] $IconUri,

        # ReleaseNotes of this module.
        [Parameter()]
        [AllowNull()]
        [string] $ReleaseNotes,

        # Prerelease string of this module.
        [Parameter()]
        [AllowNull()]
        [string] $Prerelease,

        # Flag to indicate whether the module requires explicit user acceptance for install/update/save.
        [Parameter()]
        [AllowNull()]
        [bool] $RequireLicenseAcceptance,

        # External dependent modules of this module.
        [Parameter()]
        [AllowNull()]
        [string[]] $ExternalModuleDependencies,

        # HelpInfo URI of this module.
        [Parameter()]
        [AllowNull()]
        [String] $HelpInfoURI,

        # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
        [Parameter()]
        [AllowNull()]
        [string] $DefaultCommandPrefix,

        # Private data to pass to the module specified in RootModule/ModuleToProcess.
        # This may also contain a PSData hashtable with additional module metadata used by PowerShell.
        [Parameter()]
        [AllowNull()]
        [object] $PrivateData
    )

    $outManifest = [ordered]@{}
    $outPSData = [ordered]@{}
    $outPrivateData = [ordered]@{}

    $tempManifest = Get-ModuleManifest -Path $Path
    if ($tempManifest.Keys.Contains('PrivateData')) {
        $tempPrivateData = $tempManifest.PrivateData
        if ($tempPrivateData.Keys.Contains('PSData')) {
            $tempPSData = $tempPrivateData.PSData
            $tempPrivateData.Remove('PSData')
        }
    }

    $psdataOrder = @(
        'Tags'
        'LicenseUri'
        'ProjectUri'
        'IconUri'
        'ReleaseNotes'
        'Prerelease'
        'RequireLicenseAcceptance'
        'ExternalModuleDependencies'
    )
    foreach ($key in $psdataOrder) {
        if (($null -ne $tempPSData) -and $tempPSData.Keys.Contains($key)) {
            $outPSData[$key] = $tempPSData[$key]
        }
        if ($PSBoundParameters.Keys.Contains($key)) {
            if ($null -eq $PSBoundParameters[$key]) {
                $outPSData.Remove($key)
            } else {
                $outPSData[$key] = $PSBoundParameters[$key]
            }
        }
    }

    if ($outPSData.Count -gt 0) {
        $outPrivateData.PSData = $outPSData
    } else {
        $outPrivateData.Remove('PSData')
    }
    foreach ($key in $tempPrivateData.Keys) {
        $outPrivateData[$key] = $tempPrivateData[$key]
    }
    foreach ($key in $PrivateData.Keys) {
        $outPrivateData[$key] = $PrivateData[$key]
    }

    $manifestOrder = @(
        'RootModule'
        'ModuleVersion'
        'CompatiblePSEditions'
        'GUID'
        'Author'
        'CompanyName'
        'Copyright'
        'Description'
        'PowerShellVersion'
        'PowerShellHostName'
        'PowerShellHostVersion'
        'DotNetFrameworkVersion'
        'ClrVersion'
        'ProcessorArchitecture'
        'RequiredModules'
        'RequiredAssemblies'
        'ScriptsToProcess'
        'TypesToProcess'
        'FormatsToProcess'
        'NestedModules'
        'FunctionsToExport'
        'CmdletsToExport'
        'VariablesToExport'
        'AliasesToExport'
        'DscResourcesToExport'
        'ModuleList'
        'FileList'
        'HelpInfoURI'
        'DefaultCommandPrefix'
        'PrivateData'
    )
    foreach ($key in $manifestOrder) {
        if ($tempManifest.Keys.Contains($key)) {
            $outManifest[$key] = $tempManifest[$key]
        }
        if ($PSBoundParameters.Keys.Contains($key)) {
            if ($null -eq $PSBoundParameters[$key]) {
                $outManifest.Remove($key)
            } else {
                $outManifest[$key] = $PSBoundParameters[$key]
            }
        }
    }
    if ($outPrivateData.Count -gt 0) {
        $outManifest['PrivateData'] = $outPrivateData
    } else {
        $outManifest.Remove('PrivateData')
    }

    Remove-Item -Path $Path -Force
    Export-PowerShellDataFile -Hashtable $outManifest -Path $Path

}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Set-ModuleManifest] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Set-ModuleManifest]
#region - From [public] - [PowerShell] - [Module] - [Uninstall-Pester]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Uninstall-Pester] - Importing"

function Uninstall-Pester {
    <#
        .SYNOPSIS
        Uninstall Pester 3 from Program Files and Program Files (x86)

        .DESCRIPTION
        Uninstall Pester 3 from Program Files and Program Files (x86). This is useful
        when you want to install Pester 4 and you have Pester 3 installed.

        .PARAMETER All

        .EXAMPLE
        Uninstall-Pester

        Uninstall Pester 3 from Program Files and Program Files (x86).

        .EXAMPLE
        Uninstall-Pester -All

        Completely remove all built-in Pester 3 installations.
    #>

    [OutputType([String])]
    [CmdletBinding()]
    param (
        # Completely remove all built-in Pester 3 installations
        [Parameter()]
        [switch] $All
    )

    $pesterPaths = foreach ($programFiles in ($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
        $path = "$programFiles\WindowsPowerShell\Modules\Pester"
        if ($null -ne $programFiles -and (Test-Path $path)) {
            if ($All) {
                Get-Item $path
            } else {
                Get-ChildItem "$path\3.*"
            }
        }
    }

    if (-not $pesterPaths) {
        "There are no Pester$(if (-not $all) {' 3'}) installations in Program Files and Program Files (x86) doing nothing."
        return
    }

    foreach ($pesterPath in $pesterPaths) {
        takeown /F $pesterPath /A /R
        icacls $pesterPath /reset
        # grant permissions to Administrators group, but use SID to do
        # it because it is localized on non-us installations of Windows
        icacls $pesterPath /grant '*S-1-5-32-544:F' /inheritance:d /T
        Remove-Item -Path $pesterPath -Recurse -Force -Confirm:$false
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - [Uninstall-Pester] - Done"
#endregion - From [public] - [PowerShell] - [Module] - [Uninstall-Pester]

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Module] - Done"
#endregion - From [public] - [PowerShell] - [Module]

#region - From [public] - [PowerShell] - [Object]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Object] - Processing folder"

#region - From [public] - [PowerShell] - [Object] - [Copy-Object]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Object] - [Copy-Object] - Importing"

filter Copy-Object {
    <#
        .SYNOPSIS
        Copy an object

        .DESCRIPTION
        Copy an object

        .EXAMPLE
        $Object | Copy-Object

        Copy an object
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param (
        # Object to copy
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [Object] $InputObject
    )

    $InputObject | ConvertTo-Json -Depth 100 | ConvertFrom-Json

}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Object] - [Copy-Object] - Done"
#endregion - From [public] - [PowerShell] - [Object] - [Copy-Object]

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [Object] - Done"
#endregion - From [public] - [PowerShell] - [Object]

#region - From [public] - [PowerShell] - [PSCredential]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - Processing folder"

#region - From [public] - [PowerShell] - [PSCredential] - [New-PSCredential]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [New-PSCredential] - Importing"

function New-PSCredential {
    <#
        .SYNOPSIS
        Creates a PSCredential

        .DESCRIPTION
        Takes in a UserName and a plain text password and creates a PSCredential

        .EXAMPLE
        New-PSCredential -UserName "Admin" -Password "P@ssw0rd!"

        This creates a PSCredential with username "Admin" and password "P@ssw0rd!"

        .EXAMPLE
        New-PSCredential -UserName "Admin"

        Prompts user for password and creates a PSCredential with username "Admin" and password the user provided.

        .EXAMPLE
        $SecretPassword = "P@ssw0rd!" | ConvertTo-SecureString -Force
        New-PSCredential -UserName "Admin" -Password $SecretPassword

    #>

    [OutputType([System.Management.Automation.PSCredential])]
    [Cmdletbinding(SupportsShouldProcess)]
    param(
        # The username of the PSCredential
        [Parameter()]
        [string] $Username = (Read-Host -Prompt 'Enter a username'),

        # The plain text password of the PSCredential
        [Parameter()]
        [SecureString] $Password = (Read-Host -Prompt 'Enter Password' -AsSecureString)
    )

    if ($PSCmdlet.ShouldProcess('PSCredential', 'Create a new')) {
        New-Object -TypeName System.Management.Automation.PSCredential($Username, $Password)
    }
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [New-PSCredential] - Done"
#endregion - From [public] - [PowerShell] - [PSCredential] - [New-PSCredential]
#region - From [public] - [PowerShell] - [PSCredential] - [Restore-PSCredential]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [Restore-PSCredential] - Importing"


function Restore-PSCredential {
    <#
        .SYNOPSIS
        Restores a PSCredential from a file.

        .DESCRIPTION
        Takes in a UserName and restores a PSCredential from a file.

        .EXAMPLE
        Restore-PSCredential -UserName "Admin"

        This restores the PSCredential from the default location of $env:HOMEPATH\.creds\Admin.cred

        .EXAMPLE
        Restore-PSCredential -UserName "Admin" -Path "C:\Temp"

        This restores the PSCredential from the location of C:\Temp\Admin.cred
    #>

    [OutputType([System.Management.Automation.PSCredential])]
    [CmdletBinding()]
    param(
        # The username of the PSCredential
        [Parameter(Mandatory)]
        [string] $UserName,

        # The folder path to restore the PSCredential from.
        [Parameter()]
        [string] $Path = "$env:HOMEPATH\.creds"
    )

    $fileName = "$UserName.cred"
    $credFilePath = Join-Path -Path $Path -ChildPath $fileName
    $credFilePathExists = Test-Path $credFilePath

    if ($credFilePathExists) {
        $secureString = Get-Content $credFilePath | ConvertTo-SecureString
        $credential = New-Object -TypeName System.Management.Automation.PSCredential($UserName, $secureString)
    } else {
        throw "Unable to locate a credential file for $($Username)"
    }
    return $credential
}

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [Restore-PSCredential] - Done"
#endregion - From [public] - [PowerShell] - [PSCredential] - [Restore-PSCredential]
#region - From [public] - [PowerShell] - [PSCredential] - [Save-PSCredential]
Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [Save-PSCredential] - Importing"

filter Save-PSCredential {
    <#
        .SYNOPSIS
        Saves a PSCredential to a file.

        .DESCRIPTION
        Takes in a PSCredential and saves it to a file.

        .EXAMPLE
        $Credential = New-PSCredential -UserName "Admin" -Password "P@ssw0rd!"
        Save-PSCredential -Credential $Credential

        This saves the PSCredential to the default location of $env:HOMEPATH\.creds\Admin.cred

        .EXAMPLE
        $Credential = New-PSCredential -UserName "Admin" -Password "P@ssw0rd!"
        Save-PSCredential -Credential $Credential -Path "C:\Temp"

        This saves the PSCredential to the location of C:\Temp\Admin.cred
    #>

    [OutputType([void])]
    [CmdletBinding()]
    param(
        # The PSCredential to save.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [System.Management.Automation.PSCredential] $Credential,

        # The folder path to save the PSCredential to.
        [Parameter()]
        [string] $Path = "$env:HOMEPATH\.creds"
    )

    $fileName = "$($Credential.UserName).cred"
    $credFilePath = Join-Path -Path $Path -ChildPath $fileName
    $credFilePathExists = Test-Path $credFilePath
    if (-not $credFilePathExists) {
        try {
            $null = New-Item -ItemType File -Path $credFilePath -ErrorAction Stop -Force
        } catch {
            throw $_.Exception.Message
        }
    }
    $Credential.Password | ConvertFrom-SecureString | Out-File $credFilePath -Force
}


# $SecurePassword = ConvertTo-SecureString $PlainPassword -AsPlainText -Force
# $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)
# $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
# [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - [Save-PSCredential] - Done"
#endregion - From [public] - [PowerShell] - [PSCredential] - [Save-PSCredential]

Write-Verbose "[$scriptName] - [public] - [PowerShell] - [PSCredential] - Done"
#endregion - From [public] - [PowerShell] - [PSCredential]


Write-Verbose "[$scriptName] - [public] - [PowerShell] - Done"
#endregion - From [public] - [PowerShell]

#region - From [public] - [String]
Write-Verbose "[$scriptName] - [public] - [String] - Processing folder"

#region - From [public] - [String] - [Casing]
Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - Processing folder"

#region - From [public] - [String] - [Casing] - [Convert-StringCasingStyle]
Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Convert-StringCasingStyle] - Importing"

filter Convert-StringCasingStyle {
    <#
    .SYNOPSIS
    Convert a string to a different casing style

    .DESCRIPTION
    This function converts a string to a different casing style.

    .EXAMPLE
    'thisIsCamelCase' | Convert-StringCasingStyle -To 'snake_case'

    Convert the string 'thisIsCamelCase' to 'this_is_camel_case'

    .EXAMPLE
    'thisIsCamelCase' | Convert-StringCasingStyle -To 'UPPER_SNAKE_CASE'

    Convert the string 'thisIsCamelCase' to 'THIS_IS_CAMEL_CASE'

    .EXAMPLE
    'thisIsCamelCase' | Convert-StringCasingStyle -To 'kebab-case'

    .NOTES
    General notes
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        # The string to convert
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [string] $Text,

        # The casing style to convert the string to
        [Parameter(Mandatory)]
        [ValidateSet(
            'lowercase',
            'UPPERCASE',
            'Title Case',
            'Sentencecase',
            'PascalCase',
            'camelCase',
            'kebab-case',
            'UPPER-KEBAB-CASE',
            'snake_case',
            'UPPER_SNAKE_CASE'
        )]
        [string] $To
    )

    $currentStyle = Get-StringCasingStyle -Text $Text

    $words = Split-StringByCasingStyle -Text $Text -By $currentStyle

    # Convert the words into the target style
    switch ($To) {
        'lowercase' { ($words -join '').toLower() }
        'UPPERCASE' { ($words -join '').toUpper() }
        'Title Case' { ($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1).ToLower() }) -join ' ' }
        'Sentencecase' { $words -join '' | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1).ToLower() } }
        'kebab-case' { ($words -join '-').ToLower() }
        'snake_case' { ($words -join '_').ToLower() }
        'PascalCase' { ($words | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1).ToLower() }) -join '' }
        'camelCase' {
            $words[0].toLower() + (($words | Select-Object -Skip 1 | ForEach-Object { $_.Substring(0, 1).ToUpper() + $_.Substring(1) }) -join '')
        }
        'UPPER_SNAKE_CASE' { ($words -join '_').toUpper() }
        'UPPER-KEBAB-CASE' { ($words -join '-').toUpper() }
    }
}

Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Convert-StringCasingStyle] - Done"
#endregion - From [public] - [String] - [Casing] - [Convert-StringCasingStyle]
#region - From [public] - [String] - [Casing] - [Get-StringCasingStyle]
Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Get-StringCasingStyle] - Importing"

filter Get-StringCasingStyle {
    <#
        .SYNOPSIS
        Detects the casing style of a string

        .DESCRIPTION
        This function detects the casing style of a string.

        .EXAMPLE
        'testtesttest' | Get-StringCasingStyle

        lowercase

        .EXAMPLE
        'TESTTESTTEST' | Get-StringCasingStyle

        UPPERCASE

        .EXAMPLE
        'Testtesttest' | Get-StringCasingStyle

        Sentencecase

        .EXAMPLE
        'TestTestTest' | Get-StringCasingStyle

        PascalCase

        .EXAMPLE
        'testTestTest' | Get-StringCasingStyle

        camelCase

        .EXAMPLE
        'test-test-test' | Get-StringCasingStyle

        kebab-case

        .EXAMPLE
        'TEST-TEST-TEST' | Get-StringCasingStyle

        UPPER-KEBAB-CASE

        .EXAMPLE
        'test_test_test' | Get-StringCasingStyle

        snake_case

        .EXAMPLE
        'TEST_TEST_TEST' | Get-StringCasingStyle

        UPPER_SNAKE_CASE

        .EXAMPLE
        'Test_teSt-Test' | Get-StringCasingStyle

        Unknown
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param (
        # The string to check the casing style of
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [ValidateNotNullOrEmpty()]
        [string] $Text
    )

    $style = if ([regex]::Match($Text, '^[a-z][a-z0-9]*$').Success) {
        'lowercase'
    } elseif ([regex]::Match($Text, '^[A-Z][A-Z0-9]*$').Success) {
        'UPPERCASE'
    } elseif ([regex]::Match($Text, '^[A-Z][a-z0-9]*$').Success) {
        'Sentencecase'
    } elseif ([regex]::Match($Text, '^([A-Z][a-z]*)(\s+[A-Z][a-z]*)+$').Success) {
        'Title Case'
    } elseif ([regex]::Match($Text, '^[A-Z][a-z0-9]*([A-Z][a-z0-9]*)+$').Success) {
        'PascalCase'
    } elseif ([regex]::Match($Text, '^[a-z][a-z0-9]*([A-Z][a-z0-9]*)+$').Success) {
        'camelCase'
    } elseif ([regex]::Match($Text, '^[a-z][a-z0-9]*(-[a-z0-9]+)+$').Success) {
        'kebab-case'
    } elseif ([regex]::Match($Text, '^[A-Z][A-Z0-9]*(-[A-Z0-9]+)+$').Success) {
        'UPPER-KEBAB-CASE'
    } elseif ([regex]::Match($Text, '^[a-z][a-z0-9]*(_[a-z0-9]+)+$').Success) {
        'snake_case'
    } elseif ([regex]::Match($Text, '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$').Success) {
        'UPPER_SNAKE_CASE'
    } else {
        'Unknown'
    }

    Write-Verbose "Detected casing style: [$style]"
    $style

}

Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Get-StringCasingStyle] - Done"
#endregion - From [public] - [String] - [Casing] - [Get-StringCasingStyle]
#region - From [public] - [String] - [Casing] - [Split-StringByCasingStyle]
Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Split-StringByCasingStyle] - Importing"

filter Split-StringByCasingStyle {
    <#
        .SYNOPSIS
        Splits a kebab-case string into an array of words

        .DESCRIPTION
        This function splits a kebab-case string into an array of words.

        .EXAMPLE
        Split-StringByCasingStyle -Text 'this-is-a-kebab-case-string' -By kebab-case

        this
        is
        a
        kebab
        case
        string

        .EXAMPLE
        Split-StringByCasingStyle -Text 'this_is_a_kebab_case_string' -By 'snake_case'

        this
        is
        a
        kebab
        case
        string

        .EXAMPLE
        Split-StringByCasingStyle -Text 'ThisIsAPascalCaseString' -By 'PascalCase'

        This
        Is
        A
        Pascal
        Case
        String

        .EXAMPLE
        Split-StringByCasingStyle -Text 'thisIsACamelCaseString' -By 'camelCase'

        this
        Is
        A
        Camel
        Case
        String

        .EXAMPLE
        Split-StringByCasingStyle -Text 'this_is_a-CamelCaseString' -By kebab-case | Split-StringByCasingStyle -By snake_case

        this_is_a
        camelcasestring


    #>

    [OutputType([string[]])]
    [CmdletBinding()]
    param (
        # The string to split
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [string] $Text,

        # The casing style to split the string by
        [Parameter()]
        [ValidateSet(
            'lowercase',
            'UPPERCASE',
            'Sentencecase',
            'Title Case',
            'PascalCase',
            'camelCase',
            'kebab-case',
            'UPPER-KEBAB-CASE',
            'snake_case',
            'UPPER_SNAKE_CASE'
        )]
        [string] $By
    )

    $styles = $PSBoundParameters | Where-Object { $_.Value -eq $true } | Select-Object -ExpandProperty Name

    Write-Verbose "Splitting string [$Text] by casing style [$($styles -join ', ' )]"
    $splitText = switch ($By) {
        'PascalCase' { [regex]::Matches($Text, '([A-Z][a-z]*)').Value; break }
        'camelCase' { [regex]::Matches($Text, '([A-Z][a-z]*)|^[a-z]+').Value; break }
        'kebab-case' { $Text -split '-'; break }
        'UPPER-KEBAB-CASE' { $Text -split '-'; break }
        'snake_case' { $Text -split '_'; break }
        'UPPER_SNAKE_CASE' { $Text -split '_'; break }
        default {
            $Text -split ' '
        }
    }

    Write-Verbose "Result: [$($splitText -join ', ')]"
    $splitText
}

Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - [Split-StringByCasingStyle] - Done"
#endregion - From [public] - [String] - [Casing] - [Split-StringByCasingStyle]

Write-Verbose "[$scriptName] - [public] - [String] - [Casing] - Done"
#endregion - From [public] - [String] - [Casing]

#region - From [public] - [String] - [Test-IsNotNullOrEmpty]
Write-Verbose "[$scriptName] - [public] - [String] - [Test-IsNotNullOrEmpty] - Importing"

filter Test-IsNotNullOrEmpty {
    <#
        .SYNOPSIS
        Test if an object is not null or empty

        .DESCRIPTION
        Test if an object is not null or empty

        .EXAMPLE
        '' | Test-IsNotNullOrEmpty

        False
    #>

    [OutputType([bool])]
    [Cmdletbinding()]
    [Alias('IsNotNullOrEmpty')]
    param(
        # Object to test
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [AllowNull()]
        [object] $Object
    )
    return -not ($Object | IsNullOrEmpty)

}

Write-Verbose "[$scriptName] - [public] - [String] - [Test-IsNotNullOrEmpty] - Done"
#endregion - From [public] - [String] - [Test-IsNotNullOrEmpty]
#region - From [public] - [String] - [Test-IsNullOrEmpty]
Write-Verbose "[$scriptName] - [public] - [String] - [Test-IsNullOrEmpty] - Importing"

filter Test-IsNullOrEmpty {
    <#
        .SYNOPSIS
        Test if an object is null or empty

        .DESCRIPTION
        Test if an object is null or empty

        .EXAMPLE
        '' | IsNullOrEmpty

        True
    #>

    [OutputType([bool])]
    [Cmdletbinding()]
    [Alias('IsNullOrEmpty')]
    param(
        # The object to test
        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [AllowNull()]
        [object] $Object
    )

    try {
        if (-not ($PSBoundParameters.ContainsKey('Object'))) {
            Write-Debug 'Object was never passed, meaning its empty or null.'
            return $true
        }
        if ($null -eq $Object) {
            Write-Debug 'Object is null'
            return $true
        }
        Write-Debug "Object is: $($Object.GetType().Name)"
        if ($Object -eq 0) {
            Write-Debug 'Object is 0'
            return $true
        }
        if ($Object.Length -eq 0) {
            Write-Debug 'Object is empty array or string'
            return $true
        }
        if ($Object.GetType() -eq [string]) {
            if ([string]::IsNullOrWhiteSpace($Object)) {
                Write-Debug 'Object is empty string'
                return $true
            } else {
                Write-Debug 'Object is not an empty string'
                return $false
            }
        }
        if ($Object.Count -eq 0) {
            Write-Debug 'Object count is 0'
            return $true
        }
        if (-not $Object) {
            Write-Debug 'Object evaluates to false'
            return $true
        }
        if (($Object.GetType().Name -ne 'PSCustomObject')) {
            Write-Debug 'Casting object to PSCustomObject'
            $Object = [PSCustomObject]$Object
        }
        if (($Object.GetType().Name -eq 'PSCustomObject')) {
            Write-Debug 'Object is PSCustomObject'
            if ($Object -eq (New-Object -TypeName PSCustomObject)) {
                Write-Debug 'Object is similar to empty PSCustomObject'
                return $true
            }
            if (($Object.psobject.Properties).Count | Test-IsNullOrEmpty) {
                Write-Debug 'Object has no properties'
                return $true
            }
        }
    } catch {
        Write-Debug 'Object triggered exception'
        return $true
    }

    Write-Debug 'Object is not null or empty'
    return $false
}

Write-Verbose "[$scriptName] - [public] - [String] - [Test-IsNullOrEmpty] - Done"
#endregion - From [public] - [String] - [Test-IsNullOrEmpty]

Write-Verbose "[$scriptName] - [public] - [String] - Done"
#endregion - From [public] - [String]

#region - From [public] - [TLS]
Write-Verbose "[$scriptName] - [public] - [TLS] - Processing folder"

#region - From [public] - [TLS] - [Get-TLSConfig]
Write-Verbose "[$scriptName] - [public] - [TLS] - [Get-TLSConfig] - Importing"

function Get-TLSConfig {
    <#
        .SYNOPSIS
        Get the TLS configuration of the current session

        .DESCRIPTION
        Get the TLS configuration of the current session

        .EXAMPLE
        Get-TLSConfig

        Gets the TLS configuration of the current session

        .EXAMPLE
        Get-TLSConfig -ListAvailable

        Gets the available TLS configurations
    #>

    [OutputType(ParameterSetName = 'Default', [System.Net.SecurityProtocolType])]
    [OutputType(ParameterSetName = 'ListAvailable', [Array])]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        # List available TLS configurations
        [Parameter(ParameterSetName = 'ListAvailable')]
        [switch] $ListAvailable
    )
    if ($ListAvailable) {
        return [enum]::GetValues([System.Net.SecurityProtocolType])
    }
    return [System.Net.ServicePointManager]::SecurityProtocol
}

Write-Verbose "[$scriptName] - [public] - [TLS] - [Get-TLSConfig] - Done"
#endregion - From [public] - [TLS] - [Get-TLSConfig]
#region - From [public] - [TLS] - [Set-TLSConfig]
Write-Verbose "[$scriptName] - [public] - [TLS] - [Set-TLSConfig] - Importing"

function Set-TLSConfig {
    <#
        .SYNOPSIS
        Set the TLS configuration for the current PowerShell session

        .DESCRIPTION
        Set the TLS configuration for the current PowerShell session

        .EXAMPLE
        Set-TLSConfig -Protocol Tls12

        Set the TLS configuration for the current PowerShell session to TLS 1.2
    #>

    [OutputType([void])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        # The TLS protocol to enable
        [Parameter()]
        [System.Net.SecurityProtocolType[]] $Protocol = [System.Net.SecurityProtocolType]::Tls12
    )

    foreach ($protocolItem in $Protocol) {
        Write-Verbose "Enabling $protocolItem"
        if ($PSCmdlet.ShouldProcess("Security Protocol to [$Protocol]", 'Set')) {
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor $protocolItem
        }
    }
}

Write-Verbose "[$scriptName] - [public] - [TLS] - [Set-TLSConfig] - Done"
#endregion - From [public] - [TLS] - [Set-TLSConfig]

Write-Verbose "[$scriptName] - [public] - [TLS] - Done"
#endregion - From [public] - [TLS]

#region - From [public] - [URI]
Write-Verbose "[$scriptName] - [public] - [URI] - Processing folder"

#region - From [public] - [URI] - [ConvertTo-QueryString]
Write-Verbose "[$scriptName] - [public] - [URI] - [ConvertTo-QueryString] - Importing"

filter ConvertTo-QueryString {
    <#
        .SYNOPSIS
        Convert an object to a query string

        .DESCRIPTION
        Convert an object to a query string

        .EXAMPLE
        ConvertTo-QueryString -InputObject @{a=1;b=2}

        ?a=1&b=2

        .EXAMPLE
        ConvertTo-QueryString -InputObject @{a='this is value of a';b='valueOfB'}

        ?a=this%20is%20value%20of%20a&b=valueOfB

        .EXAMPLE
        ConvertTo-QueryString -InputObject @{a='this is value of a';b='valueOfB'} -AsURLEncoded

        ?a=this+is+value+of+a&b=valueOfB
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory,
            ValueFromPipeline
        )]
        [object] $InputObject,

        [Parameter()]
        [switch] $AsURLEncoded
    )

    if ($InputObject -isnot [hashtable]) {
        $InputObject = $InputObject | ConvertTo-HashTable
    }

    $parameters = if ($AsURLEncoded) {
        ($InputObject.GetEnumerator() | ForEach-Object {
            "$([System.Web.HttpUtility]::UrlEncode($_.Key))=$([System.Web.HttpUtility]::UrlEncode($_.Value))"
        }) -join '&'
    } else {
        ($InputObject.GetEnumerator() | ForEach-Object {
            "$([System.Uri]::EscapeDataString($_.Key))=$([System.Uri]::EscapeDataString($_.Value))"
        }) -join '&'
    }

    if ($parameters) {
        '?' + $parameters
    }
}

Write-Verbose "[$scriptName] - [public] - [URI] - [ConvertTo-QueryString] - Done"
#endregion - From [public] - [URI] - [ConvertTo-QueryString]
#region - From [public] - [URI] - [Join-Uri]
Write-Verbose "[$scriptName] - [public] - [URI] - [Join-Uri] - Importing"

function Join-Uri {
    <#
        .SYNOPSIS
        Join a base URI with a child paths.

        .DESCRIPTION
        Join a base URI with a child paths to create a new URI.
        The child paths are normalized before joining with the base URI.

        .EXAMPLE
        Join-Uri -Path 'https://example.com' -ChildPath 'foo' -AdditionalChildPath 'bar'
        https://example.com/foo/bar

        Joins the base URI <https://example.com> with the child paths 'foo' and 'bar' to create the URI <https://example.com/foo/bar>.

        .EXAMPLE
        Join-Uri 'https://example.com' '/foo/' '/bar/' '//baz/something/' '/test/'

        <https://example.com/foo/bar/baz/something/test>

        Combines the base URI <https://example.com> with the child paths '/foo/', '/bar/', '//baz/something/', and '/test/'.

    #>

    [OutputType([uri])]
    [CmdletBinding()]
    param (
        # The base URI to join with the child path.
        [Parameter(Mandatory)]
        [uri]$Path,

        # The child path to join with the base URI.
        [Parameter(Mandatory)]
        [string] $ChildPath,

        # Additional child paths to join with the base URI.
        [Parameter(ValueFromRemainingArguments)]
        [string[]] $AdditionalChildPath
    )

    $segments = $ChildPath, $AdditionalChildPath
    $normalizedSegments = $segments | ForEach-Object { $_.Trim('/') }
    $uri = $Path.ToString().TrimEnd('/') + '/' + ($normalizedSegments -join '/')
    $uri
}

Write-Verbose "[$scriptName] - [public] - [URI] - [Join-Uri] - Done"
#endregion - From [public] - [URI] - [Join-Uri]

Write-Verbose "[$scriptName] - [public] - [URI] - Done"
#endregion - From [public] - [URI]

#region - From [public] - [Windows]
Write-Verbose "[$scriptName] - [public] - [Windows] - Processing folder"

#region - From [public] - [Windows] - [Set-WindowsSetting]
Write-Verbose "[$scriptName] - [public] - [Windows] - [Set-WindowsSetting] - Importing"

filter Set-WindowsSetting {
    <#
        .SYNOPSIS
        Set a Windows setting

        .DESCRIPTION
        Set a or multiple Windows setting(s).

        .NOTES
        Supported OS: Windows
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Show file extensions in Windows Explorer
        [Parameter()]
        [switch] $ShowFileExtension,

        # Show hidden files in Windows Explorer
        [Parameter()]
        [switch] $ShowHiddenFiles
    )

    $path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
    if ($PSCmdlet.ShouldProcess("'ShowFileExtension' to [$ShowFileExtension]", 'Set')) {
        $hideFileExt = if ($ShowFileExtension) { 0 } else { 1 }
        Set-ItemProperty -Path $path -Name HideFileExt -Value $hideFileExt
    }

    if ($PSCmdlet.ShouldProcess("'ShowHiddenFiles' to [$ShowFileExtension]", 'Set')) {
        $hiddenFiles = if ($ShowHiddenFiles) { 1 } else { 2 }
        Set-ItemProperty -Path $path -Name Hidden -Value $hiddenFiles
    }

    # Refresh File Explorer
    $Shell = New-Object -ComObject Shell.Application
    $Shell.Windows() | ForEach-Object { $_.Refresh() }
}

Write-Verbose "[$scriptName] - [public] - [Windows] - [Set-WindowsSetting] - Done"
#endregion - From [public] - [Windows] - [Set-WindowsSetting]

Write-Verbose "[$scriptName] - [public] - [Windows] - Done"
#endregion - From [public] - [Windows]


Write-Verbose "[$scriptName] - [public] - Done"
#endregion - From [public]


$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'ConvertFrom-Base64String'
        'ConvertTo-Base64String'
        'ConvertTo-Boolean'
        'Get-FileInfo'
        'Remove-EmptyFolder'
        'Show-FileContent'
        'Clear-GitRepo'
        'Invoke-GitSquash'
        'Invoke-SquashBranch'
        'Reset-GitRepo'
        'Restore-GitRepo'
        'Sync-GitRepo'
        'Sync-Repo'
        'Import-Variable'
        'Set-GitHubEnvironmentVariable'
        'Start-LogGroup'
        'Stop-LogGroup'
        'Search-GUID'
        'Test-IsGUID'
        'Convert-HashtableToString'
        'Merge-Hashtable'
        'Add-ModuleManifestData'
        'Add-PSModulePath'
        'Export-PowerShellDataFile'
        'Format-ModuleManifest'
        'Get-ModuleManifest'
        'Invoke-PruneModule'
        'Invoke-ReinstallModule'
        'Set-ModuleManifest'
        'Uninstall-Pester'
        'Copy-Object'
        'New-PSCredential'
        'Restore-PSCredential'
        'Save-PSCredential'
        'Convert-StringCasingStyle'
        'Get-StringCasingStyle'
        'Split-StringByCasingStyle'
        'Test-IsNotNullOrEmpty'
        'Test-IsNullOrEmpty'
        'Get-TLSConfig'
        'Set-TLSConfig'
        'ConvertTo-QueryString'
        'Join-Uri'
        'Set-WindowsSetting'
    )
    Variable = ''
}
Export-ModuleMember @exports