Common.ps1

# References:
# 1. Below are the list of predefined vars that can be used:
# - $PSScriptRoot [System defined] The folder path for current scipt file, NOT the caller script to call this function

using namespace System.Management.Automation

# MyProfile module env vars
$MyProfileCustomizedListVarName = "MyProfileCustomizedList"
$MyProfileRegisteredListVarName = "MyProfileRegisteredList"
$MyProfileBinPathVarName = "MyProfileBinPath"
$MyProfilePSModulesPathVarName = "MyProfilePSModulePath"

# MyProfile module paths
$MyProfileModulePsProfilePath = Join-Path $PSScriptRoot "PSProfile.ps1"
$MyProfileModuleSysProfilePath = Join-Path $PSScriptRoot "SysProfile.ps1"
$MyProfileModuleMyProfileShortcutPath = Join-Path $PSScriptRoot "MyProfile.lnk"
$MyProfileModuleTemplateRootPath = Join-Path $PSScriptRoot "Template"

# MyProfile template relative vars
$MyProfileRelativeManifestPath = "MyProfileManifest.psd1"
$MyProfileRelativeBinPath = "bin"
$MyProfileRelativePSMoudlesPath = "PowerShell\Modules"
$MyProfileRelativePSProfilePath = "PowerShell\PSProfile.ps1"
$MyProfileRelativeSysProfilePath = "System\SysProfile.ps1"

###############################################################################
# Helper functions
###############################################################################

<#
.SYNOPSIS
Write host with embedded color tag support and multi level message type support.
 
.DESCRIPTION
A new way to write host with color by adding color tag inside output string.
    1. All colors in Write-Host can also be used as tag here.
       E.g.: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
    2. Color tag is case insensitive.
    3. Use Type to support multi level message printing, and to map to different color. By default, [Default, Highlight, Warning] and [Keynote, Error] are enabled.
 
.PARAMETER Object
The Object to write. For string, embedded color tags are supported.
 
.PARAMETER NoNewLine
Specifies that the content displayed in the console does not end with a newline character.
 
.PARAMETER Type
The type of message to write the object as. It is used to mapping with message purpose, level and color.
    - Verbose Color=<Default> Level=[Verbose] For verbose information which is similar to the concept in Write-Verbose.
    - Debug Color=<Default> Level=[Debug] For Debug information which is similar to the concept in Write-Debug.
    - Information Color=<Default> Level=Information For Information information which is similar to the concept in Write-Information.
    - Default Color=<Default> Level=[Default,Highlight,Warning] For Default information which is similar to the concept in Write-Host with default color.
    - Highlight Color=Cyan Level=[Default,Highlight,Warning] For the information to highlight. The importancy is similar to Warning, but it is more for good/important things.
    - Warning Color=Yellow Level=[Keynote,Error] For Warning information which is similar to the concept in Write-Warning.
    - Keynote Color=Magenta Level=[Keynote,Error] For information which is considered as keynote. The importancy is similar to Error, but it is more for good/important things.
    - Error Color=Red Level=[Keynote,Error] For Warning information which is similar to the concept in Write-Error.
 
Unlike the behavior of "-Verbose, -Debug, -InformationAction, -WarningAction, -ErrorAction" in standard powershell which control the message output individually,
This function combines the behavior of them to support multi level message printing. The message level is "[Verbose] < [Debug] < [Information] < [Default, Highlight, Warning] < [Keynote, Error]".
A type of object/message can be write only when its level or any lower level is enabled (via -Verbose, -WarningAction=Continue, etc.).
 
By default, the lowest enabled level is "[Default,Highlight,Warning]". Below are the details about how to enable/disable a specified level.
    - [Verbose] Disabled by default, Use "-Verbose" to enable it
    - [Debug] Disabled by default, Use "-Debug" to enable it
    - [Information] Disabled by default, Use "-InformationAction Continue" to enable it
    - [Default,Highlight,Warning] Enabled by default, Use "-WarningAction SilentlyContinue" to disable it
    - [Keynote,Error] Enabled by default, Use "-ErrorAction SilentlyContinue" to disable it
 
Ref: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.1
 
.EXAMPLE
Write-ColorHost "This is an <RED>single</RED> tag example!"
Write-ColorHost "This is an <Red>multi</Red> <Green>tags</Green> example!" -Type Highlight
Write-ColorHost "This is <Blue>an <Red>nested</Red> tags example</Blue>!" -Type Error
#>

function Write-ColorHost
{
    param
    (
        [Parameter(Position=0)]
        [object] $Object,
        [switch] $NoNewLine,
        [ValidateSet('Verbose', 'Debug', 'Information', 'Default', 'Highlight', 'Warning', 'Keynote', 'Error')]
        [string] $Type = 'Default'
    )

    if ($VerbosePreference -eq [ActionPreference]::SilentlyContinue) {
        if ($Type -eq 'Verbose') { return }
        if ($DebugPreference -eq [ActionPreference]::SilentlyContinue) {
            if ($Type -eq 'Debug') { return }
            if ($InformationPreference -eq [ActionPreference]::SilentlyContinue) {
                if ($Type -eq 'Information') { return }
                if ($WarningPreference -eq [ActionPreference]::SilentlyContinue) {
                    if ($Type -eq 'Warning' -OR $Type -eq 'Highlight' -OR $Type -eq 'Default') { return }
                    if ($ErrorActionPreference -eq [ActionPreference]::SilentlyContinue) { return }
                }
            }
        }
    }

    $colorTagRegex = '(<\/?(?:Black|DarkBlue|DarkGreen|DarkCyan|DarkRed|DarkMagenta|DarkYellow|Gray|DarkGray|Blue|Green|Cyan|Red|Magenta|Yellow|White)>)'
    $colorStack = New-Object 'system.collections.generic.stack[string]'
    $curColor = switch ($Type)
    {
        'Highlight' { 'Cyan' }      # Reason: Blue looks not good, Green is often used to show something success. So Cyan is the best one.
        'Warning'   { 'Yellow' }    # Reason: Match the color of Write-Warning
        'Keynote'   { 'Magenta' }   # Reason: This color is most similar to the color for Error.
        'Error'     { 'Red' }       # Reason: Match the color of Write-Error
        default     { '' }
    }

    if ($Object -is [string])
    {
        $message = [string]$Object
        [regex]::Split($Message, $colorTagRegex, 1) | % {
            if ([regex]::IsMatch($_, $colorTagRegex, 1))
            {
                $color = $_ -replace '[<>\/]'
                if ($_ -like '</*>')
                {
                    $curColor = if ($colorStack.Count -gt 0) { $colorStack.Pop() } else { '' }
                }
                else
                {
                    $colorStack.Push($curColor)
                    $curColor = $color
                }
            }
            else
            {
                if ([string]::IsNullOrWhiteSpace($curColor)) { Write-Host $_ -NoNewline } else { Write-Host $_ -NoNewline -ForegroundColor $curColor }
            }
        }

        if (-not $NoNewLine) { Write-Host '' }
    }
    else 
    {
        if ([string]::IsNullOrWhiteSpace($curColor)) { Write-Host $Object -NoNewline:$NoNewLine } else { Write-Host $Object -NoNewline:$NoNewLine -ForegroundColor $curColor }
    }
}

function Convert-EnvironmentVariableTarget
{
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target
    )

    $targetMapping = @{
        Machine = [EnvironmentVariableTarget]::Machine
        User    = [EnvironmentVariableTarget]::User
        Process = [EnvironmentVariableTarget]::Process
    }

    return $targetMapping[$Target]
}

function Set-EnvironmentVariable
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1, Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyString()]
        [string] $Value,
        [Parameter(Position=2, Mandatory = $true)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target
    )

    $targetType = Convert-EnvironmentVariableTarget $Target

    # Always expand the value when targeting to "Process"
    if ($Target -eq 'Process') { $Value = [Environment]::ExpandEnvironmentVariables($Value) }

    $oldValue = [Environment]::GetEnvironmentVariable($Name, $targetType)
    if ($oldValue -ne $value){ [Environment]::SetEnvironmentVariable($Name, $Value, $targetType) }
    if ($Target -ne 'Process')
    {
        [Environment]::SetEnvironmentVariable($Name, [Environment]::ExpandEnvironmentVariables($Value), [EnvironmentVariableTarget]::Process)
    }
}

function Get-EnvironmentVariable
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target = 'Process'
    )

    $targetType = Convert-EnvironmentVariableTarget $Target
    return [Environment]::GetEnvironmentVariable($Name, $targetType)
}

function Set-EnvironmentListVariable
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1, Mandatory = $true)]
        [AllowNull()]
        [string[]] $ListValue,
        [Parameter(Position=2, Mandatory = $true)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target,
        [switch] $AllowDuplicate
    )

    if (-NOT $AllowDuplicate) { $ListValue = $ListValue | Select-Object -Unique }
    $value = ($ListValue | ? { $_ -ne $null }) -join ';'
    Set-EnvironmentVariable -Name $Name -Value $value -Target $Target
}

function Get-EnvironmentListVariable
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target = 'Process'
    )

    return @((Get-EnvironmentVariable -Name $Name -Target $Target) -split ';' | % { $_.Trim() } | ? { -NOT [string]::IsNullOrWhiteSpace($_) })
}

function Add-EnvironmentListVariableValues
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1, Mandatory = $true)]
        [string[]] $ValuesToAppend,
        [Parameter(Position=2, Mandatory = $true)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target,
        [switch] $AppendFront,
        [switch] $AllowDuplicate
    )

    $curValueList = Get-EnvironmentListVariable -Name $Name -Target $Target
    if (-NOT $AllowDuplicate)
    {
        $curValueList = $curValueList | Select-Object -Unique
    }

    $newValueList = if ($AppendFront) { @($ValuesToAppend) + $curValueList } else { @($curValueList) + $ValuesToAppend }
    if (-NOT $AllowDuplicate)
    {
        $newValueList = $newValueList | Select-Object -Unique
    }
    
    if ($newValueList.Count -gt $curValueList.Count)
    {
        Set-EnvironmentListVariable -Name $Name -ListValue $newValueList -Target $Target
    }
}

function Remove-EnvironmentListVariableValues
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,
        [Parameter(Position=1, Mandatory = $true)]
        [string[]] $ValuesToRemove,
        [Parameter(Position=2, Mandatory = $true)]
        [ValidateSet('Machine', 'User', 'Process')]
        [string] $Target
    )

    $curValueList = Get-EnvironmentListVariable -Name $Name -Target $Target
    $newValueList = $curValueList | ? { -NOT($ValuesToRemove -contains $_) }

    if ($curValueList.Count -gt $newValueList.Count)
    {
        Set-EnvironmentListVariable -Name $Name -ListValue $newValueList -Target $Target
    }
}