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
$MyProfileInstalledListVarName = "MyProfileInstalledList"
$MyProfileBinPathVarName = "MyProfileBinPath"
$MyProfilePSModulesPathVarName = "MyProfilePSModulePath"
$MyProfileModuleDevPathVarName = "MyProfileModuleDevPath"
$MyProfileModuleLastUpdateFileTimeUtcVarName = "MyProfileModuleLastUpdateFileTimeUtc"

# MyProfile module paths
$MyProfileModuleMyProfileShortcutPath = Join-Path $PSScriptRoot "MyProfile.lnk"
$MyProfileModuleTemplateRootPath = Join-Path $PSScriptRoot "Template"

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

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

enum ColorHostWriteLevel
{
    Verbose = 0
    Debug = 1
    Information = 2
    Highlight = 3
    Warning = 4
    Keynote = 5
    Error = 6
}

function ConvertTo-ColorHostWriteLevel
{
    param(
        [Parameter(Position=0)]
        [string] $Level
    )

    [ColorHostWriteLevel]$result = [ColorHostWriteLevel]::Information
    if ([ColorHostWriteLevel]::TryParse($Level, $true, [ref] $result)) { return $result } else { return [ColorHostWriteLevel]::Information }
}

<#
.SYNOPSIS
Get the minimum write level for Write-ColorHost.
#>

function Get-ColorHostMinWriteLevel
{
    return "$(ConvertTo-ColorHostWriteLevel $Global:ColorHostMinWriteLevel)"
}

<#
.SYNOPSIS
Set the minimum write level for Write-ColorHost.
 
.DESCRIPTION
Set the minimum level to write for Write-ColorHost. All levels below this will not be writen.
 
.PARAMETER Level
The level of message to write the object as. It is used to mapping with message purpose, write level and color.
    - Verbose Color=<Default> Level=0 For verbose information which is similar to the concept in Write-Verbose.
    - Debug Color=<Default> Level=1 For Debug information which is similar to the concept in Write-Debug.
    - Information Color=<Default> Level=2 The default level which is similar to the concept in Write-Host with default color.
    - Highlight Color=Cyan Level=3 For the information to highlight. The importancy is similar to Warning, but it is more for good/important things.
    - Warning Color=Yellow Level=4 For Warning information which is similar to the concept in Write-Warning.
    - Keynote Color=Magenta Level=5 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=6 For Warning information which is similar to the concept in Write-Error.
#>

function Set-ColorHostMinWriteLevel
{
    param(
        [Parameter(Position=0)]
        [ValidateSet('Verbose', 'Debug', 'Information', 'Highlight', 'Warning', 'Keynote', 'Error')]
        [string] $Level = 'Information'
    )

    $oldLevel = Get-ColorHostMinWriteLevel
    if ($oldLevel -eq $Level) { Write-Host "The minimum write level for Write-ColorHost is the same, no need to change." }
    else {
        $Global:ColorHostMinWriteLevel = $Level
        Write-Host "The minimum write level for Write-ColorHost is updated from $oldLevel to $Global:ColorHostMinWriteLevel."
    }
    
}

<#
.SYNOPSIS
Write host with embedded color tag support and multi level message 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 Level to support multi level message printing, and to map to different color. By default, the minimum write level is "Information".
 
.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 Level
The level of message to write the object as. It is used to mapping with message purpose, write level and color.
    - Verbose Color=<Default> Level=0 For verbose information which is similar to the concept in Write-Verbose.
    - Debug Color=<Default> Level=1 For Debug information which is similar to the concept in Write-Debug.
    - Information Color=<Default> Level=2 The default level which is similar to the concept in Write-Host with default color.
    - Highlight Color=Cyan Level=3 For the information to highlight. The importancy is similar to Warning, but it is more for good/important things.
    - Warning Color=Yellow Level=4 For Warning information which is similar to the concept in Write-Warning.
    - Keynote Color=Magenta Level=5 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=6 For Warning information which is similar to the concept in Write-Error.
 
.EXAMPLE
Write-ColorHost "This is an <RED>single</RED> tag example!"
Write-ColorHost "This is an <Red>multi</Red> <Green>tags</Green> example!" -Level Highlight
Write-ColorHost "This is <Blue>an <Red>nested</Red> tags example</Blue>!" -Level Error
#>

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

    # Verify the write level
    $curWriteLevel = ConvertTo-ColorHostWriteLevel $Level
    $minWriteLevel = ConvertTo-ColorHostWriteLevel $ColorHostMinWriteLevel
    if ($curWriteLevel -lt $minWriteLevel) { return }

    # Start to really write content to host.
    $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 ($Level)
    {
        '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' -AND ([string]::IsNullOrEmpty($Value) -OR $Value.Contains('%')))
        {
            # Use another way to save with "REG_EXPAND_SZ" type as [Environment]::GetEnvironmentVariable only saved it as "REG_SZ"
            $regPath = switch ($Target) {
                'User' { "Registry::HKEY_CURRENT_USER\Environment" }
                "Machine" { "Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" }
            }

            Set-ItemProperty -Path $regPath -Name $Name -Value $Value -Type ExpandString
        }
    }
    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',
        [switch] $NotExpand
    )

    $targetType = Convert-EnvironmentVariableTarget $Target

    if ($NotExpand -AND $Target -ne 'Process')
    {
        $registrySubKey = switch ($Target) {
            "User" { [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Environment", [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadSubTree) }
            "Machine" { [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Control\Session Manager\Environment", [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadSubTree) }
        }

        return $registrySubKey.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
    }

    return [Environment]::GetEnvironmentVariable($Name, $targetType)
}

function Set-EnvironmentListVariable
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0, Mandatory = $true)]
        [string] $Name,

        [Parameter(Position=1, Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [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',
        [switch] $NotExpand
    )

    return @((Get-EnvironmentVariable -Name $Name -Target $Target -NotExpand:$NotExpand) -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 -NotExpand
    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
    }
}

function InjectCodeBlock
{
    param(
        [string] $Target,
        [string] $CodeId,
        [string] $CodeBlock
    )

    if (($Target -is [string]) -AND ($Target.Contains("<$CodeId>")))
    {
        $Target = ReplaceCodeBlock -Target $Target -CodeId $CodeId -CodeBlock $CodeBlock
    }
    else
    {
        $Target = $Target.TrimEnd()
        $Target += "`r`n`r`n# <$CodeId>`r`n"
        $Target += "$CodeBlock"
        $Target += "`r`n# </$CodeId>`r`n"
    }
    
    return $Target.Trim()
}

function ReplaceCodeBlock
{
    param(
        [string] $Target,
        [string] $CodeId,
        [string] $CodeBlock
    )

    $powerShellProfile -replace "(?ms)^(`r`n)*# <$CodeId>.*# </$CodeId>(`r`n)*","`r`n`# <$CodeId>`r`n$CodeBlock`r`n# </$CodeId>`r`n`r`n"
}

function RemoveCodeBlock
{
    param(
        [string] $Target,
        [string] $CodeId
    )

    $powerShellProfile -replace "(?ms)^(`r`n)*# <$CodeId>.*# </$CodeId>(`r`n)*","`r`n"
}

function InstallPsProfile
{
    param(
        [string] $CodeId,

        [Parameter(Mandatory=$true, ParameterSetName="ScriptString")]
        [string] $ScriptString,

        [Parameter(Mandatory=$true, ParameterSetName="ScriptBlock")]
        [ScriptBlock] $ScriptBlock
    )

    $powerShellProfileDir = Join-Path $([Environment]::GetFolderPath("MyDocuments")) "WindowsPowerShell"
    $powerShellProfilePath = Join-Path $PowerShellProfileDir "Microsoft.PowerShell_profile.ps1"

    # Create a empty profile if it does not exists
    if (-not (Test-Path $powerShellProfilePath))
    {
        New-Item -Path $powerShellProfilePath -ItemType File -Force
    }

    if ($PsCmdlet.ParameterSetName -eq "ScriptBlock") { $ScriptString = $ScriptBlock.ToString() }
    $ScriptString = $ScriptString.Trim()

    $powerShellProfile = Get-Content $powerShellProfilePath -Raw
    InjectCodeBlock -Target $powerShellProfile -CodeId $CodeId -CodeBlock $ScriptString | Set-Content $powerShellProfilePath
}