MyProfile.psm1

# 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

. "$PSScriptRoot\Common.ps1"

if (-not ("ddrr.PowerShell.MyProfileModule.MyProfile" -as [Type]))
{
    Add-Type @"
        namespace ddrr.PowerShell.MyProfileModule
        {
            public class MyProfile
            {
                public string Name { get; set; }
                public int LoadOrder { get; set; }
                public string Path { get; set; }
                public bool Imported { get; set; }
                public bool ManualRegistered { get; set; }
                public bool AutoRegistered { get; set; }
            }
        }
"@

}

###############################################################################
# MyProfile private functions
###############################################################################

<#
.SYNOPSIS
Check whether the specified path is a valid MyProfile.
#>

function Test-MyProfile
{
    param(
        [Parameter(Position=0)]
        [string] $MyProfilePath
    )

    # Check the path exists or not
    if ([string]::IsNullOrWhiteSpace($MyProfilePath)) { return $false }
    if (-NOT (Test-Path $MyProfilePath)) { return $false }

    # Check the manifest
    $manifestFilePath = Join-Path $MyProfilePath $MyProfileRelativeManifestPath
    if (-NOT(Test-Path $manifestFilePath)) { return $false }

    return $true
}

<#
.SYNOPSIS
Get the list of paths for my profile that has been imported.
#>

function Get-ImportededMyProfilePaths
{
    return Get-EnvironmentListVariable -Name $MyProfileImportedListVarName
}

<#
.SYNOPSIS
Get the list of manual registered my profile paths.
#>

function Get-ManualRegisteredMyProfilePaths
{
    return Get-EnvironmentListVariable -Name $MyProfileManualRegisteredListVarName
}

<#
.SYNOPSIS
Get the list of auto registered my profile paths.
#>

function Get-AutoRegisteredMyProfilePaths
{
    $ret = @()

    # Default 1: SynologyDrive
    $ret += "$($env:USERPROFILE)\SynologyDrive\MyProfile"

    # Default 2: OneDrive - Personal
    $ret += "$($env:USERPROFILE)\OneDrive\MyProfile"

    # Default 3: OneDrive - <Business>
    Get-ChildItem -Path $env:USERPROFILE -Filter "OneDrive - *" | % {
        $ret += "$($env:USERPROFILE)\$($_.Name)\MyProfile"
    }

    return $ret
}

function Get-MyProfileManifest
{
    param(
        [Parameter(Position=0)]
        [string] $MyProfilePath
    )

    $manifestFilePath = Join-Path $MyProfilePath $MyProfileRelativeManifestPath
    return Import-PowerShellDataFile $manifestFilePath
}


###############################################################################
# MyProfile public functions
###############################################################################

<#
.SYNOPSIS
Import all registered MyProfiles.
#>

function Import-MyProfiles
{
    [CmdletBinding()]
    param()

    $profilePaths = @()
    $binPaths = @()
    $psModulesPaths = @()

    Get-MyProfile | ? { $_.ManualRegistered -OR $_.AutoRegistered } | % {
        # Import single MyProfile
        Write-ColorHost "Importing $($_.Name)" -Type Keynote

        # Register MyProfile path. This will support PowerShell Profile, System Profile, Cron
        $profilePaths += $_.Path

        # Register Bin path
        $binPaths += Join-Path $_.Path $MyProfileRelativeBinPath

        # Register PowerShell Modules
        $psModulesPaths += Join-Path $_.Path $MyProfileRelativePSMoudlesPath

        # Register Toolbox

        # Invoke PSProfile and PSModules for current MyProfile
        Invoke-MyProfile $_.Path -PSProfile -PSModules
    }

    # Save paths to environment variables
    Set-EnvironmentListVariable -Name $MyProfileImportedListVarName -ListValue $profilePaths -Target User
    Set-EnvironmentListVariable -Name $MyProfileBinPathVarName -ListValue $binPaths -Target User
    Set-EnvironmentListVariable -Name $MyProfilePSModulesPathVarName -ListValue $psModulesPaths -Target User

    # Invoke SysProfile for all MyProfiles together
    Invoke-MyProfile -SysProfile
}

function Invoke-MyProfile
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Name,
        [switch] $SysProfile,
        [switch] $PSProfile,
        [switch] $PSModules
    )

    if ((-NOT $SysProfile) -AND (-NOT $PSProfile) -AND (-NOT $PSModules)) {
        $SysProfile = $true
        $PSProfile = $true
        $PSModules = $true
    }

    $sysProfiles = @()
    # Note, only allow to invoke imported MyProfile.
    Get-MyProfile $Name | ? { $_.Imported } | % {
        if ($PSProfile) {
            $curPSProfilePath = Join-Path $_.Path $MyProfileRelativePSProfilePath
            # Note, use operator '.' here to invoke all MyPrfile's PowerShell profiles in the same scope of the MyProfile's PSProfile.
            # So, these PowerShell profiles can impact each other, and the MyProfile load order is important.
            . Invoke-ScriptInMyProfileModuleCallerScope $curPSProfilePath
        }

        if ($PSModules) { Import-MyModule -MyProfileName $_.Path -Force }

        # For SysProfile, it should not impact current PS scope, so they will not be invoked via operator '.'
        # But similar to PSProfiles, all SysProfiles should share the same scope. So they will be not invoked here. Instead, they will be invoked together later.
        # This is also consistent with the normal behavior when system startup: SysProfiles are invoked after all PSProfiles are invoked.
        if ($SysProfile) { $sysProfiles += Join-Path $_.Path $MyProfileRelativeSysProfilePath }
    }

    # NOT use operator '.' here to avoid all MyProfile's system profiles impact current PS scope.
    # But these SysProfiles will share the same scope, so the MyProfile load order is important.
    if ($sysProfiles.Count -gt 0){ Invoke-ScriptInMyProfileModuleCallerScope $sysProfiles }
}

function New-MyProfile
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Path = ''
    )

    if ([string]::IsNullOrWhiteSpace($Path)) { $Path = '.'}

    if (Test-Path $Path)
    {
        $Path = Resolve-Path $Path
        if ((Get-ChildItem $Path).Count -gt 0)
        {
            Write-ColorHost "The path for a new MyProfile must be a new path or an empty folder. Fail to create new MyProfile at '$Path'." -Type Error
            return
        }
    }
    else 
    {
        New-Item -Path $Path -ItemType Directory | Out-Null
    }

    Copy-Item -Path "$MyProfileModuleTemplateRootPath\*" -Destination $Path
    Register-MyProfile -Path $Path
    Write-ColorHost "A new MyProfile has been created at '<Green>$Path</Green>'. You can update the <Green>$MyProfileRelativeManifestPath</Green> file to setup the details." -Type Highlight
}


function Get-MyProfile
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Name
    )

    [array]$importedPathList = Get-ImportededMyProfilePaths
    [array]$manualRegisteredPathList = Get-ManualRegisteredMyProfilePaths
    [array]$autoRegisteredPathList = Get-AutoRegisteredMyProfilePaths

    $isPathMode = $false
    if ((-NOT [string]::IsNullOrWhiteSpace($Name)) -AND (Test-Path $Name))
    {
        [array]$profilePathList = @($Name)
        $isPathMode = $true
    }
    else
    {
        [array]$profilePathList = $manualRegisteredPathList + $autoRegisteredPathList + $importedPathList
    }

    [array]$profiles = $profilePathList | ? { Test-MyProfile $_ } | % { (Resolve-Path $_).Path.TrimEnd('\') } | Sort-Object -Unique | % {
        $curManifest = Get-MyProfileManifest $_
        if ($isPathMode -OR ([string]::IsNullOrWhiteSpace($Name) -OR ($curManifest.Name -like $Name)))
        {
            $curProfile = New-Object ddrr.PowerShell.MyProfileModule.MyProfile
            $curProfile.Name = if ([string]::IsNullOrWhiteSpace($curManifest.Name)){ $_ } else { $curManifest.Name }
            $curProfile.LoadOrder = $curManifest.LoadOrder
            $curProfile.Path = $_
            $curProfile.Imported = $importedPathList -contains $_
            $curProfile.ManualRegistered = $manualRegisteredPathList -contains $_
            $curProfile.AutoRegistered = $autoRegisteredPathList -contains $_
            $curProfile
        }
    }

    return $profiles | Sort-Object LoadOrder,Path
}

function Register-MyProfile
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Path
    )

    if (Test-MyProfile $Path)
    {
        $Path = (Resolve-Path $Path).Path.TrimEnd('\')
        Add-EnvironmentListVariableValues -Name $MyProfileManualRegisteredListVarName -Target User -ValuesToAppend $Path
    }
    else 
    {
        Write-ColorHost "'Failed to register $Path' as it is not a valid MyProfile." -Type Error
    }
}

function Unregister-MyProfile
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Path
    )

    if (Test-Path $Path)
    {
        $PathWithSlash = Join-Path $Path '\' -Resolve
    }
    else 
    {
        $PathWithSlash = if ($Path[-1] -eq '\') { $Path } else { "$Path\" }
    }

    $PathWithoutSlash = $PathWithSlash.TrimEnd('\')
    Remove-EnvironmentListVariableValues -Name $MyProfileManualRegisteredListVarName -Target User -ValuesToRemove @($PathWithSlash, $PathWithoutSlash)
}

function Import-MyModule
{
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [string] $Name,
        [string] $MyProfileName,
        [switch] $Force
    )

    if ((-NOT [string]::IsNullOrWhiteSpace($Name)) -AND (Test-Path $Name))
    {
        # Note: When $Name is a path to a module, will not check whether the corresponding MyProfile has been imported or not, as the MyProfile may not
        try {
            Import-Module $Name -Global -Force:$Force -ErrorAction Stop
            if (-NOT [string]::IsNullOrWhiteSpace($MyProfileName)) { Write-ColorHost "[$MyProfileName] " -NoNewLine -Type Highlight }
            Write-ColorHost "Imported my module '$Name'" -Type Highlight
        }
        catch {
            Write-ColorHost "Failed to import my module '$Name'" -Type Error
            Write-Host $_
        }
    }
    else
    {
        Get-MyProfile | ? { $_.Imported } | ? { [string]::IsNullOrWhiteSpace($MyProfileName) -OR (($_.Name -like $MyProfileName) -OR ($_.Path -eq $MyProfileName)) } | % {
            $curMyProfile = $_
            $psModulesPath = Join-Path $_.Path $MyProfileRelativePSMoudlesPath
            if (Test-Path $psModulesPath -PathType Container)
            {
                Get-ChildItem $psModulesPath -Directory | ? { -NOT($_.Name -like "Template*") } | ? { [string]::IsNullOrWhiteSpace($Name) -OR ($_.Name -like $Name) } | % {
                    Import-MyModule -Name (Join-Path $psModulesPath $_.Name) -MyProfileName $curMyProfile.Name -Force:$Force
                }
            }
        }
    }
}

###############################################################################
# Alias
###############################################################################
Set-Alias -Name Start-MySysProfile -Value $MyProfileModuleSysProfilePath