posh-vsdev.ps1

# Simplifies access to HashSet<string>
class Set : System.Collections.Generic.HashSet[string] {
    Set() { }
    Set([string[]] $Data) {
        foreach ($local:Item in $Data) {
            $this.Add($local:Item);
        }
    }
}

# Encapsulates environment variables and their values
class Environment : System.Collections.Generic.Dictionary[string, string] {
    hidden static [Environment] $_Clean;
    hidden static [Environment] $_Default;

    # Get a clean environment
    static [Environment] GetClean() {
        if ($null -eq [Environment]::_Clean) {
            $local:Entries = script:ExecuteCommandInNewEnvironment { Get-ChildItem env:; };
            $local:Env = [Environment]::new();
            foreach ($local:Entry in $local:Entries) {
                if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                    continue;
                }
                $local:Env[$local:Entry.Name] = $local:Entry.Value;
            }
            [Environment]::_Clean = $local:Env;
        }
        return [Environment]::_Clean;
    }

    # Get the default environment without VS environment variables
    static [Environment] GetDefault() {
        if ($null -eq [Environment]::_Default) {
            [Environment] $local:CleanEnv = [Environment]::GetClean();
            [Environment] $local:Env = $null;
            if ($env:PoshVsDevDefaultEnvironment) {
                $local:EnvDiffObject = $env:PoshVsDevDefaultEnvironment | ConvertFrom-Json -ErrorAction:SilentlyContinue;
                if ($local:EnvDiffObject) {
                    $local:EnvDiff = [EnvironmentDiff]::FromObject($local:EnvDiffObject);
                    $local:Env = $local:EnvDiff.Apply($local:CleanEnv);
                }
            }
            if (-not $local:Env -and $env:PoshVsDevEnvironment) {
                if ($script:VisualStudioVersion) {
                    $local:Env = $script:VisualStudioVersion.Unapply([Environment]::GetCurrent());
                }
                else {
                    $local:Env = $local:CleanEnv;
                }
            }
            if (-not $local:Env) {
                $local:CurrentEnv = [Environment]::GetCurrent();
                $local:EnvDiff = [EnvironmentDiff]::DiffBetween($local:CleanEnv, $local:CurrentEnv);
                $local:EnvDiffObject = [EnvironmentDiff]::ToObject($local:EnvDiff);
                $local:Env = $local:CurrentEnv;
                $env:PoshVsDevDefaultEnvironment = $local:EnvDiffObject | ConvertTo-Json;
            }
            [Environment]::_Default = $local:Env;
        }
        return [Environment]::_Default;
    }

    # Gets the current environment (excluding PoshVsDev* environment variables)
    static [Environment] GetCurrent() {
        $local:Env = [Environment]::new();
        foreach ($local:Entry in Get-ChildItem "ENV:\") {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Env[$local:Entry.Name] = $local:Entry.Value;
        }
        return $local:Env;
    }

    hidden [string] get_Item([string] $Key) {
        $Value = $null;
        [void]($this.TryGetValue($Key, [ref]$Value));
        return $Value;
    }

    # Applies the this environment's variables, replacing the current environment.
    [void] Apply([string] $Name) {
        # Clear the current environment
        $local:Current = [Environment]::GetCurrent();
        foreach ($local:Entry in $local:Current.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            if (-not $this.ContainsKey($local:Entry.Key)) {
                script:SetEnvironmentVariable $local:Entry.Key $null;
            }
        }

        # Apply the new environment.
        foreach ($local:Entry in $this.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            script:SetEnvironmentVariable $local:Entry.Key $local:Entry.Value;
        }

        # Set a PoshVsDevEnvironment variable for the current environment
        # (helps improve startup time in a nested shell)
        script:SetEnvironmentVariable "PoshVsDevEnvironment" $Name;
    }

    [Environment] Clone() {
        [Environment] $local:Env = [Environment]::new();
        foreach ($local:Entry in $this.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Env[$local:Entry.Key] = $local:Entry.Value;
        }
        return $local:Env;
    }
}

# Stores a diff between two paths
class PathDiff {
    hidden [string[]] $Added;
    hidden [Set] $AddedSet;
    hidden [string[]] $Removed;
    hidden [Set] $RemovedSet;

    hidden PathDiff([string[]] $Added, [string[]] $Removed) {
        $this.Added = @() + $Added;
        $this.AddedSet = [Set]::new($Added);
        $this.Removed = @() + $Removed;
        $this.RemovedSet = [Set]::new($Removed);
    }

    static [PathDiff] FromObject([psobject] $Object) {
        if ($null -eq $Object) { return $null; }
        if ($Object -is [PathDiff]) { return $Object; }
        return [PathDiff]::new($Object.Added, $Object.Removed);
    }

    static [psobject] ToObject([PathDiff] $Object) {
        if ($null -eq $Object) { return $null; }
        return @{
            Added = @() + $Object.Added;
            Removed = @() + $Object.Removed;
        };
    }

    static [PathDiff] DiffBetween([string[]] $OldPaths, [string[]] $NewPaths) {
        [Set] $local:OldSet = [Set]::new($OldPaths);
        [Set] $local:NewSet = [Set]::new($NewPaths);
        [string[]] $local:Added = @();
        [string[]] $local:Removed = @();
        foreach ($local:Path in $NewSet.GetEnumerator()) {
            if (-not $OldSet.Contains($local:Path)) {
                $local:Added += $local:Path;
            }
        }
        foreach ($local:Path in $OldSet.GetEnumerator()) {
            if (-not $NewSet.Contains($local:Path)) {
                $local:Removed += $local:Path;
            }
        }
        return [PathDiff]::new($local:Added, $local:Removed);
    }

    [string] Apply([string] $Path) {
        return $this.ApplyToPaths($Path -split ";") -join ";";
    }

    [string[]] Apply([string[]] $Paths) {
        return $this.ApplyToPaths($Paths);
    }

    hidden [string[]] ApplyToPaths([string[]] $Paths) {
        $local:Result = @();
        foreach ($local:Path in $Paths) {
            if ($local:Path -and $local:Path.Trim() -and -not $this.RemovedSet.Contains($local:Path)) {
                $local:Result += $local:Path;
            }
        }
        foreach ($local:Path in $this.Added) {
            if ($local:Path -and $local:Path.Trim()) {
                $local:Result += $local:Path;
            }
        }
        return $local:Result;
    }

    [string] Unapply([string] $Path) {
        return $this.UnapplyToPaths($Path -split ";") -join ";";
    }

    [string[]] Unapply([string[]] $Paths) {
        return $this.UnapplyToPaths($Paths);
    }

    hidden [string[]] UnapplyToPaths([string[]] $Paths) {
        $local:Result = @();
        foreach ($local:Path in $Paths) {
            if ($local:Path -and $local:Path.Trim() -and -not $this.AddedSet.Contains($local:Path)) {
                $local:Result += $local:Path;
            }
        }
        foreach ($local:Path in $this.Removed) {
            if ($local:Path -and $local:Path.Trim()) {
                $local:Result += $local:Path;
            }
        }
        return $local:Result;
    }
}

# Stores a diff between two environments
class EnvironmentDiff : System.Collections.Generic.Dictionary[string, psobject] {
    EnvironmentDiff() {
    }

    # Create an EnvironmentDiff from a psobject (for deserialization purposes)
    static [EnvironmentDiff] FromObject([psobject] $Object) {
        if ($null -eq $Object) {
            return $null;
        }

        if ($Object -is [EnvironmentDiff]) {
            return $Object;
        }

        $Object = script:ConvertToHashTable $Object;
        [EnvironmentDiff] $local:Changes = [EnvironmentDiff]::new();
        foreach ($local:Entry in $Object.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Key -ieq "Path") {
                $local:Value = [PathDiff]::FromObject($local:Value);
            }
            $local:Changes[$local:Key] = $local:Value;
        }
        return $local:Changes;
    }

    # Creates a psobject from an EnvironmentDiff (for serialization purposes)
    static [psobject] ToObject([EnvironmentDiff] $Object) {
        if ($null -eq $Object) {
            return $null;
        }

        $local:Changes = @{};
        foreach ($local:Entry in $Object.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Key -ieq "Path") {
                $local:Value = [PathDiff]::ToObject($local:Value);
            }
            $local:Changes[$local:Key] = $local:Value;
        }
        return $local:Changes;
    }

    # Calculates the difference between two Environments
    static [EnvironmentDiff] DiffBetween([Environment] $OldEnv, [Environment] $NewEnv) {
        [EnvironmentDiff] $local:Changes = [EnvironmentDiff]::new();
        foreach ($local:Entry in $OldEnv.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            if (-not $NewEnv.ContainsKey($local:Entry.Key)) {
                $local:Changes[$local:Entry.Key] = $null;
            }
        }
        foreach ($local:Entry in $NewEnv.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            $local:OldValue = $OldEnv[$local:Key];
            if ($local:Value -ne $local:OldValue) {
                if ($local:Key -ieq "Path") {
                    $local:Value = [PathDiff]::DiffBetween($local:OldValue, $local:Value);
                }
                $local:Changes[$local:Key] = $local:Value;
            }
        }
        return $local:Changes;
    }

    hidden [psobject] get_Item([string] $Key) {
        [psobject] $Value = $null;
        [void]($this.TryGetValue($Key, [ref]$Value));
        return $Value;
    }

    hidden [void] set_Item([string] $Key, [psobject] $Value) {
        if (-not $this.ValidateKeyValue($Key, $Value)) { return; }
        ([System.Collections.Generic.Dictionary[string, psobject]]$this)[$Key] = $Value;
    }

    hidden [void] Add([string] $Key, [psobject] $Value) {
        if (-not $this.ValidateKeyValue($Key, $Value)) { return; }
        [void](([System.Collections.Generic.Dictionary[string, psobject]]$this).Add($Key, $Value));
    }

    # Applies this EnvironmentDiff to the provided Environment, producing a new Environment
    [Environment] Apply([Environment] $Env) {
        [Environment] $local:NewEnv = $Env.Clone();
        foreach ($local:Entry in $this.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Value -is [PathDiff]) {
                $local:Value = $local:Value.Apply($Env[$local:Key]);
            }
            if ($local:Value) {
                $local:NewEnv[$local:Key] = $local:Value;
            }
            else {
                $local:NewEnv.Remove($local:Key);
            }
        }
        return $local:NewEnv;
    }

    # Unapplies this EnvironmentDiff to the provided Environment, producing a new Environment
    [Environment] Unapply([Environment] $Env) {
        [Environment] $local:NewEnv = $Env.Clone();
        foreach ($local:Entry in $this.GetEnumerator()) {
            if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                continue;
            }
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Value -is [PathDiff]) {
                $local:Value = $local:Value.Unapply($Env[$local:Key]);
            }
            if ($local:Value) {
                $local:NewEnv.Remove($local:Key);
            }
        }
        return $local:NewEnv;
    }

    hidden [bool] ValidateKeyValue([string] $Key, [psobject] $Value) {
        if (script:IsIgnoredEnvironmentVariable $Key) {
            throw [System.ArgumentException]::new("Invalid argument: Key");
            return $false;
        }
        if (($Key -ieq "Path") -and -not ($null -eq $Value -or $Value -is [PathDiff])) {
            throw [System.ArgumentException]::new("Invalid argument: Value");
            return $false;
        }
        if (($Key -ine "Path") -and -not ($null -eq $Value -or $Value -is [string])) {
            throw [System.ArgumentException]::new("Invalid argument: Value");
            return $false;
        }
        return $true;
    }
}

enum TargetArchitectureIn {
    x86 = 0;
    i686 = 0;

    amd64 = 1;
    x64 = 1;
    x86_64 = 1;

    arm = 2;

    arm64 = 3;
    aarch64 = 3;
}

enum TargetArchitecture {
    x86 = 0;
    amd64 = 1;
    arm = 2;
    arm64 = 3;
}

enum HostArchitectureIn {
    x86 = 0;

    amd64 = 1;
    x64 = 1;
    x86_64 = 1;
}

enum HostArchitecture {
    x86 = 0;
    amd64 = 1;
}

enum AppPlatform {
    Desktop = 0;
    UWP = 1;
}

class WindowsSdk {
    hidden static [WindowsSdk[]] $_None;
    hidden static [WindowsSdk[]] $_All;

    [version] $Version;
    [string] $WindowsSdkDir;
    [string] $WindowsLibPath;
    [string] $WindowsSdkBinPath;
    [AppPlatform[]] $SupportedPlatforms;

    WindowsSdk(
        [version] $Version,
        [string] $WindowsSdkDir,
        [string] $WindowsLibPath,
        [string] $WindowsSdkBinPath,
        [AppPlatform[]] $SupportedPlatforms
    ) {
        $this.Version = $Version;
        $this.WindowsSdkDir = $WindowsSdkDir;
        $this.WindowsLibPath = $WindowsLibPath;
        $this.WindowsSdkBinPath = $WindowsSdkBinPath;
        $this.SupportedPlatforms = [AppPlatform[]]($SupportedPlatforms);
        if ($null -eq $this.SupportedPlatforms) {
            $this.SupportedPlatforms = [AppPlatform[]](@());
        }
    }

    [string] GetVersionString() {
        if ($null -eq $this.Version) {
            return "none";
        }
        else {
            return $this.Version;
        }
    }

    [bool] IsNone() {
        return $null -eq $this.Version;
    }

    static [WindowsSdk] None() {
        if (-not [WindowsSdk]::_None) {
            [WindowsSdk]::_None = [WindowsSdk]::new($null, $null, $null, $null, [AppPlatform[]](@()));
        }
        return [WindowsSdk]::_None;
    }

    static [WindowsSdk[]] All() {
        if (-not [WindowsSdk]::_All) {
            [string[]]$local:RegRoots = @(
                "HKCU:\SOFTWARE\Wow6432Node",
                "HKLM:\SOFTWARE\Wow6432Node",
                "HKCU:\SOFTWARE",
                "HKLM:\SOFTWARE"
            );

            [Set] $local:Seen = [Set]::new();
            [WindowsSdk[]] $local:Sdks = @();

            foreach ($local:RegRoot in $local:RegRoots) {
                [psobject[]] $local:VersionRegKeys = Get-ChildItem "$RegRoot\Microsoft\Microsoft SDKs\Windows" -ErrorAction Ignore;
                foreach ($local:RegKey in $local:VersionRegKeys) {
                    [string] $local:VersionKey = $local:RegKey.PSChildName;
                    if ($local:VersionKey -notmatch "^v\d+\.\d+$") {
                        continue;
                    }

                    [string] $local:VersionString = $local:VersionKey.Substring(1)
                    [version] $local:Version = $local:VersionString;
                    [string] $local:WindowsSdkDir = $local:RegKey | Get-ItemPropertyValue -Name InstallationFolder;

                    # Special Case for Windows SDK <= 8.1
                    if ($local:Version.Major -le 8) {
                        if ($local:Seen.Contains($local:VersionString)) {
                            continue;
                        }

                        $local:Seen.Add($local:VersionString) | Out-Null;

                        [string] $local:WindowsLibPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"References\CommonConfiguration\Neutral";
                        [string] $local:WindowsSdkBinPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"bin";
                        [version] $local:WindowsSdkVersion = $local:Version;

                        $local:Sdks += [WindowsSdk]::new(
                            $local:WindowsSdkVersion,
                            $local:WindowsSdkDir,
                            $local:WindowsLibPath,
                            $local:WindowsSdkBinPath,
                            [AppPlatform[]](@([AppPlatform]::Desktop))
                        );
                        continue;
                    }

                    # Windows SDK >= 10

                    $local:WindowsSdkIncludeDir = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"include";
                    $local:SdkVersions = Get-ChildItem $local:WindowsSdkIncludeDir | ForEach-Object {
                        if ($_.PSChildName -match "^\d+\.\d+\.\d+\.\d+$") {
                            $_.PSChildName -as [version];
                        }
                    };

                    foreach ($local:WindowsSdkVersion in $local:SdkVersions) {
                        if ($local:Seen.Contains($local:WindowsSdkVersion)) {
                            continue;
                        }

                        [AppPlatform[]] $local:SupportedPlatforms = @();
                        [string] $local:UWPTestPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"include\$local:WindowsSdkVersion\um\Windows.h";
                        if (Test-Path -LiteralPath:$local:UWPTestPath) {
                            $local:SupportedPlatforms += @([AppPlatform]::UWP);
                        }

                        [string] $local:DestopTestPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"include\$local:WindowsSdkVersion\um\winsdkver.h";
                        if (Test-Path -LiteralPath:$local:DestopTestPath) {
                            $local:SupportedPlatforms += @([AppPlatform]::Desktop);
                        }

                        if (-not $local:SupportedPlatforms) {
                            continue;
                        }

                        [string] $local:WindowsSdkBinPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"bin";
                        [string] $local:WindowsSdkVersionedBinPath = Join-Path -Path:$local:WindowsSdkBinPath -ChildPath:$local:WindowsSdkVersion;
                        if (Test-Path -LiteralPath:$local:WindowsSdkVersionedBinPath) {
                            $local:WindowsSdkBinPath = $local:WindowsSdkVersionedBinPath;
                        }

                        [string] $local:WindowsSdkUnionMetadataPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"UnionMetadata";
                        [string] $local:WindowsSdkVersionedUnionMetadataPath = Join-Path -Path:$local:WindowsSdkUnionMetadataPath -ChildPath:$local:WindowsSdkVersion;
                        if (Test-Path -LiteralPath:$local:WindowsSdkVersionedUnionMetadataPath) {
                            $local:WindowsSdkUnionMetadataPath = $local:WindowsSdkVersionedUnionMetadataPath;
                        }

                        [string] $local:WindowsSdkReferencesPath = Join-Path -Path:$local:WindowsSdkDir -ChildPath:"References";
                        [string] $local:WindowsSdkVersionedReferencesPath = Join-Path -Path:$local:WindowsSdkReferencesPath -ChildPath:$local:WindowsSdkVersion;
                        if (Test-Path -LiteralPath:$local:WindowsSdkVersionedReferencesPath) {
                            $local:WindowsSdkReferencesPath = $local:WindowsSdkVersionedReferencesPath;
                        }

                        [string] $local:WindowsSdkLibPath = "$local:WindowsSdkUnionMetadataPath;$local:WindowsSdkReferencesPath";
                        $local:Seen.Add($local:WindowsSdkVersion) | Out-Null;
                        $local:Sdks += [WindowsSdk]::new(
                            $local:WindowsSdkVersion,
                            $local:WindowsSdkDir,
                            $local:WindowsSdkLibPath,
                            $local:WindowsSdkBinPath,
                            [AppPlatform[]]($local:SupportedPlatforms)
                        );
                    }
                }
            }
            [WindowsSdk]::_All = $local:Sdks | Sort-Object -Descending -Property:{ $_.Version -as [version] };
        }

        return [WindowsSdk]::_All;
    }

    # Find a matching WindowsSdk
    static [WindowsSdk] Match([string]$SdkVersion, [AppPlatform]$AppPlatform) {
        if (-not $SdkVersion) { $SdkVersion = "latest"; }
        switch -RegEx ($SdkVersion.ToLower()) {
            '^none$' { return [WindowsSdk]::None(); }
            '^8\.1$' { return [WindowsSdk]::All() | Where-Object { $_.Version -like "8.1" } | Select-Object -First 1; }
            '^\d+\.\d+\.\d+\.\d+$' { return [WindowsSdk]::All() | Where-Object { $_.Version -eq $SdkVersion } | Select-Object -First 1; }
            '^\d+\.\d+(\.\d+)?$' { return [WindowsSdk]::All() | Where-Object { $_.Version -like "$SdkVersion.*" } | Select-Object -First 1; }
            '^latest$' { return [WindowsSdk]::All() | Select-Object -First 1; }
        }
        throw "Unsupported SDK Version format: $SdkVersion";
    }

    static [bool] Equals([WindowsSdk] $Left, [WindowsSdk] $Right) {
        if ([object]::ReferenceEquals($null, $Left)) {
            return [object]::ReferenceEquals($null, $Right);
        }
        if ([object]::ReferenceEquals($null, $Right)) {
            return $false;
        }
        if ($Left.Version -ne $Right.Version -or
            $Left.WindowsSdkDir -ne $Right.WindowsSdkDir -or
            $Left.WindowsLibPath -ne $Right.WindowsLibPath -or
            $Left.WindowsSdkBinPath -ne $Right.WindowsSdkBinPath) {
            return $false;
        }
        if ([object]::ReferenceEquals($null, $Left.SupportedPlatforms)) {
            return [object]::ReferenceEquals($null, $Right.SupportedPlatforms);
        }
        if ([object]::ReferenceEquals($null, $Right.SupportedPlatforms)) {
            return $false;
        }
        if ($Left.SupportedPlatforms.Length -ne $Right.SupportedPlatforms.Length) {
            return $false;
        }
        foreach ($local:LeftPlatform in $Left.SupportedPlatforms) {
            if ($local:LeftPlatform -notin $Right.SupportedPlatforms) {
                return $false;
            }
        }
        return $true;
    }

    static [bool] op_Equality([WindowsSdk] $Left, [WindowsSdk] $Right) {
        return [WindowsSdk]::Equals($Left, $Right);
    }

    static [bool] op_Inequality([WindowsSdk] $Left, [WindowsSdk] $Right) {
        return -not [WindowsSdk]::Equals($Left, $Right);
    }
}

class EnvironmentOptions {
    [TargetArchitecture] $Arch;
    [HostArchitecture] $HostArch;
    [AppPlatform] $AppPlatform;
    [string] $WindowsSdk;
    [bool] $NoExtensions;

    EnvironmentOptions() {
        $this._Init(
            [TargetArchitectureIn]::x86,
            [HostArchitectureIn]::x86,
            [AppPlatform]::Desktop,
            "latest",
            $false);
    }

    EnvironmentOptions(
        [TargetArchitectureIn] $Arch = [TargetArchitectureIn]::x86,
        [HostArchitectureIn] $HostArch = (if ($Arch -eq [TargetArchitectureIn]::amd64) { [HostArchitecture]::amd64 } else { [HostArchitecture]::x86 }),
        [AppPlatform] $AppPlatform = [AppPlatform]::Desktop,
        [string] $WindowsSdk = "latest",
        [bool] $NoExtensions = $false
    ) {
        $this._Init($Arch, $HostArch, $AppPlatform, $WindowsSdk, $NoExtensions);
    }

    hidden _Init(
        [TargetArchitectureIn] $Arch,
        [HostArchitectureIn] $HostArch,
        [AppPlatform] $AppPlatform,
        [string] $WindowsSdk,
        [bool] $NoExtensions
    ) {
        $this.Arch = [TargetArchitecture]$Arch;
        $this.HostArch = [HostArchitecture]$HostArch;
        $this.AppPlatform = $AppPlatform;
        $this.NoExtensions = $NoExtensions;

        $local:MatchingWindowsSdk = [WindowsSdk]::Match($WindowsSdk, $AppPlatform);
        if ($local:MatchingWindowsSdk) {
            $this.WindowsSdk = $local:MatchingWindowsSdk.GetVersionString();
        } elseif ($WindowsSdk -eq "latest") {
            $this.WindowsSdk = $null;
        } else {
            throw "Windows SDK $WindowsSdk could not be found";
        }
    }

    [bool] Equals([object]$Other) {
        return [EnvironmentOptions]::Equals($this, $Other);
    }

    [int] GetHashCode() {
        return $this.Arch.GetHashCode()
            -bxor $this.HostArch.GetHashCode()
            -bxor $this.AppPlatform.GetHashCode()
            -bxor [System.Collections.Generic.EqualityComparer[object]]::Default.GetHashCode($this.WindowsSdk)
            -bxor $this.NoExtensions.GetHashCode();
    }

    [string] ToString() {
        [string] $local:CommandArgs = "";
        if ([TargetArchitecture]::x86 -ne $this.Arch) { $local:CommandArgs += " -arch=$($this.Arch)"; }
        if ($this.Arch -ne $this.HostArch) { $local:CommandArgs += " -host_arch=$($this.HostArch)"; }
        if ([AppPlatform]::Desktop -ne $this.AppPlatform) { $local:CommandArgs += " -app_platform=$($this.AppPlatform)"; }
        if ($this.WindowsSdk) { $local:CommandArgs += " -winsdk=$($this.WindowsSdk)"; }
        if ($this.NoExtensions) { $local:CommandArgs += " -no_ext"; }
        return $local:CommandArgs;
    }

    static [bool] Equals([EnvironmentOptions] $Left, [EnvironmentOptions] $Right) {
        if ([object]::ReferenceEquals($null, $Left)) {
            return [object]::ReferenceEquals($null, $Right);
        }
        if ([object]::ReferenceEquals($null, $Right)) {
            return $false;
        }
        return $Left.Arch -eq $Right.Arch -and
            $Left.HostArch -eq $Right.HostArch -and
            $Left.AppPlatform -eq $Right.AppPlatform -and
            $Left.WindowsSdk -eq $Right.WindowsSdk -and
            $Left.NoExtensions -eq $Right.NoExtensions;
    }

    static [bool] op_Equality([EnvironmentOptions] $Left, [EnvironmentOptions] $Right) {
        return [EnvironmentOptions]::Equals($Left, $Right);
    }

    static [bool] op_Inequality([EnvironmentOptions] $Left, [EnvironmentOptions] $Right) {
        return -not [EnvironmentOptions]::Equals($Left, $Right);
    }

    # Create an EnvironmentOptions from a psobject (for deserialization purposes)
    static [EnvironmentOptions] FromObject([psobject] $Object) {
        if ($null -eq $Object) {
            return $null;
        }
        if ($Object -is [EnvironmentOptions]) {
            return $Object;
        }
        return [EnvironmentOptions]::new(
            $Object.Arch,
            $Object.HostArch,
            $Object.AppPlatform,
            $Object.WindowsSdk,
            $Object.NoExtensions
        );
    }

    # Create a psobject from an EnvironmentOptions (for serialization purposes)
    static [psobject] ToObject([EnvironmentOptions] $Object) {
        if ($null -eq $Object) {
            return $null;
        }
        return @{
            Arch = [string]($Object.Arch);
            HostArch = [string]($Object.HostArch);
            AppPlatform = [string]($Object.AppPlatform);
            WindowsSdk = $Object.WindowsSdk;
            NoExtensions = $Object.NoExtensions;
        };
    }
}

class EnvironmentDiffMap : System.Collections.DictionaryBase, System.Collections.IEnumerable {
    [bool] TryGetValue([EnvironmentOptions] $Key, [ref] $Value) {
        if ($this.InnerHashtable.Contains($Key)) {
            $Value.Value = $this.InnerHashtable[$Key];
            return $true;
        }
        $Value.Value = $null;
        return $false;
    }

    [void] Add([EnvironmentOptions] $Key, [EnvironmentDiff] $Value) {
        $this.InnerHashtable.Add($Key, $Value);
    }

    [bool] TryAdd([EnvironmentOptions] $Key, [EnvironmentDiff] $Value) {
        return $this.InnerHashtable.TryAdd($Key, $Value);
    }

    [bool] ContainsKey([EnvironmentOptions] $Key) {
        return $this.InnerHashtable.ContainsKey($Key);
    }

    [bool] ContainsValue([EnvironmentDiff] $Value) {
        return $this.InnerHashtable.ContainsValue($Value);
    }

    [bool] Remove([EnvironmentOptions] $Key) {
        return $this.InnerHashtable.Remove($Key);
    }

    hidden [System.Collections.Generic.ICollection[EnvironmentOptions]] get_Keys() {
        return [EnvironmentOptions[]](@($this.InnerHashtable.Keys));
    }

    hidden [System.Collections.Generic.ICollection[EnvironmentDiff]] get_Values() {
        return [EnvironmentOptions[]](@($this.InnerHashtable.Values));
    }

    hidden [EnvironmentDiff] get_Item([EnvironmentOptions] $Key) {
        [EnvironmentDiff] $Value = $null;
        [void]($this.TryGetValue($Key, [ref]$Value));
        return $Value;
    }

    hidden [void] set_Item([EnvironmentOptions] $Key, [EnvironmentDiff] $Value) {
        $this.InnerHashtable[$Key] = $Value;
    }

    hidden [object] OnGet([object] $Key, [object] $CurrentValue) {
        if (-not $Key -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid key");
        }
        return $CurrentValue;
    }

    hidden [void] OnSet([object] $Key, [object] $OldValue, [object] $NewValue) {
        if (-not $Key -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid key");
        }
        if (-not $NewValue -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid value");
        }
    }

    hidden [void] OnInsert([object] $Key, [object] $Value) {
        if (-not $Key -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid key");
        }
        if (-not $Value -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid value");
        }
    }

    hidden [void] OnRemove([object] $Key, [object] $Value) {
        if (-not $Key -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid key");
        }
    }

    hidden [void] OnValidate([object] $Key, [object] $Value) {
        if (-not $Key -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid key");
        }
        if (-not $Value -is [EnvironmentOptions]) {
            throw [System.ArgumentException]::new("Invalid value");
        }
    }

    [System.Collections.Generic.IEnumerator[System.Collections.Generic.KeyValuePair[EnvironmentOptions, EnvironmentDiff]]] GetEnumerator() {
        $OfTypeMethodOpen = [System.Linq.Enumerable].GetMethod("OfType");
        $OfTypeMethodClosed = $OfTypeMethodOpen.MakeGenericMethod([System.Collections.DictionaryEntry]);
        $DictionaryEntries = [System.Collections.Generic.IEnumerable[System.Collections.DictionaryEntry]]$OfTypeMethodClosed.Invoke($null, @($this.InnerHashtable));
        $Projection = [System.Func[System.Collections.DictionaryEntry, System.Collections.Generic.KeyValuePair[EnvironmentOptions, EnvironmentDiff]]]{
            param($pair);
            return [System.Collections.Generic.KeyValuePair[EnvironmentOptions, EnvironmentDiff]]::new($pair.Key, $pair.Value);
        };
        $Selected = [System.Linq.Enumerable]::Select($DictionaryEntries, $Projection);
        return $Selected.GetEnumerator();
    }

    # Create an EnvironmentDiffMap from a psobject (for deserialization purposes)
    static [EnvironmentDiffMap] FromObject([psobject] $Object) {
        if ($null -eq $Object) {
            return $null;
        }
        if ($Object -is [EnvironmentDiffMap]) {
            return $Object
        }

        $local:Map = [EnvironmentDiffMap]::new();
        foreach ($local:Entry in $Object) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            $local:Map.Add([EnvironmentOptions]::FromObject($local:Key), [EnvironmentDiff]::FromObject($local:Value));
        }

        return $local:Map;
    }

    # Create a psobject from an EnvironmentDiffMap (for serialization purposes)
    static [psobject] ToObject([EnvironmentDiffMap] $Object) {
        if ($null -eq $Object) {
            return $null;
        }

        [psobject[]] $Result = @();
        foreach ($local:Entry in $Object.GetEnumerator()) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            $local:Result += @(@{
                Key = [EnvironmentOptions]::ToObject($local:Key);
                Value = [EnvironmentDiff]::ToObject($local:Value);
            });
        }
        return [System.Collections.Generic.List[object]]::new($Result);
    }
}

# Represents an instance of Visual Studio
class VisualStudioInstance {
    [string] $Name;
    [string] $Channel;
    [version] $Version;
    [string] $Path;
    hidden [EnvironmentDiffMap] $Envs;

    VisualStudioInstance([string] $Name, [string] $Channel, [version] $Version, [string] $Path, [EnvironmentDiffMap] $Envs) {
        $this.Name = $Name;
        $this.Channel = $Channel;
        $this.Version = $Version;
        $this.Path = $Path;
        $this.Envs = $Envs;
        if (-not $this.Envs) {
            $this.Envs = [EnvironmentDiffMap]::new();
        }
    }

    static [VisualStudioInstance] FromObject([psobject] $Object) {
        if ($null -eq $Object) {
            return $null;
        }
        if ($Object -is [VisualStudioInstance]) {
            return $Object;
        }
        return [VisualStudioInstance]::new(
            $Object.Name,
            $Object.Channel,
            $Object.Version -as [version],
            $Object.Path,
            [EnvironmentDiffMap]::FromObject($Object.Envs)
        );
    }

    static [psobject] ToObject([VisualStudioInstance] $Object) {
        if ($null -eq $Object) {
            return $null;
        }
        return @{
            Name = $Object.Name;
            Channel = $Object.Channel;
            Version = $Object.Version -as [string];
            Path = $Object.Path;
            Envs = [EnvironmentDiffMap]::ToObject($Object.Envs);
        };
    }

    hidden [string] GetPoshVsDevEnvironmentId([EnvironmentOptions] $Options) {
        return @{
            Name = $this.Name;
            Channel = $this.Channel;
            Version = $this.Version -as [string];
            Options = [EnvironmentOptions]::ToObject($Options);
        } | ConvertTo-Json;
    }

    [EnvironmentDiff] GetEnvironmentDiff(
        [TargetArchitectureIn]$Arch = [TargetArchitectureIn]::x86,
        [HostArchitecture]$HostArch = [HostArchitecture]::x86,
        [AppPlatform]$AppPlatform = [AppPlatform]::Desktop,
        [string]$WindowsSdk = "latest",
        [bool]$NoExtensions = $false
    ) {
        return $this.GetEnvironmentDiff([EnvironmentOptions]::new(
            $Arch,
            $HostArch,
            $AppPlatform,
            $WindowsSdk,
            $NoExtensions
        ));
    }

    [EnvironmentDiff] GetEnvironmentDiff([EnvironmentOptions] $Options) {
        [EnvironmentDiff] $local:EnvDiff = $null;
        if (-not $this.Envs) {
            $this.Envs = [EnvironmentDiffMap]::new();
        }
        if (-not $this.Envs.TryGetValue($Options, [ref]$local:EnvDiff)) {
            $local:CommandPath = Join-Path -Path:$this.Path -ChildPath:$script:VSDEVCMD_PATH;
            $local:Command = [scriptblock]::Create("
                & `"$local:CommandPath`" $($Options.ToString()) -no_logo | Out-Null;
                Get-ChildItem env:
            "
);

            $local:Entries = script:ExecuteCommandInNewEnvironment $local:Command;
            $local:NewEnv = [Environment]::new();
            foreach ($local:Entry in $local:Entries) {
                if (script:IsIgnoredEnvironmentVariable $local:Entry.Key) {
                    continue;
                }
                $local:NewEnv[$local:Entry.Name] = $local:Entry.Value;
            }

            $local:CleanEnv = [Environment]::GetClean();
            $local:EnvDiff = [EnvironmentDiff]::DiffBetween($local:CleanEnv, $local:NewEnv);
            $this.Envs.Add($Options, $local:EnvDiff);
            $script:HasChanges = $true;
        }
        return $local:EnvDiff;
    }

    hidden [void] Apply(
        [TargetArchitectureIn]$Arch = [TargetArchitectureIn]::x86,
        [HostArchitecture]$HostArch = [HostArchitecture]::x86,
        [AppPlatform]$AppPlatform = [AppPlatform]::Desktop,
        [string]$WindowsSdk = "latest",
        [bool]$NoExtensions = $false
    ) {
        $this.Apply([EnvironmentOptions]::new(
            $Arch,
            $HostArch,
            $AppPlatform,
            $WindowsSdk,
            $NoExtensions
        ));
    }

    hidden [void] Apply([EnvironmentOptions] $Options) {
        $this.GetEnvironmentDiff($Options).
            Apply([Environment]::GetCurrent()).
            Apply($this.GetPoshVsDevEnvironmentId($Options));
    }

    hidden [void] Unapply([EnvironmentOptions] $Options) {
        $this.GetEnvironmentDiff($Options).
            Unapply([Environment]::GetCurrent()).
            Apply($null);
    }

    [void] Save() {
        $script:HasChanges = $true;
        script:SaveChanges;
    }
}

$script:OperatorPattern = "(?<Operator>[<>=^~]|<=|>=)?";
$script:RevisionPattern = "(?:\.(?<Revision>[*x]|\d+))?";
$script:BuildPattern = "(?:\.(?:(?<Build>[*x])|(?<Build>\d+)$script:RevisionPattern))?";
$script:MinorPattern = "(?:\.(?:(?<Minor>[*x])|(?<Minor>\d+)$script:BuildPattern))?";
$script:MajorPattern = "(?:(?<Major>[*x])|${script:OperatorPattern}v?(?<Major>\d+)$script:MinorPattern)";
$script:VersionSpecPattern = "^\s*(?:${script:MajorPattern})?\s*$";
$script:TransientEnvironmentVariables = @(
    "PoshVsDevVsName",
    "PoshVsDevVsChannel",
    "PoshVsDevVsVersion",
    "PoshVsDevArch",
    "PoshVsDevHostArch",
    "PoshVsDevPlatform",
    "PoshVsDevWindowsSdk",
    "PoshVsDevNoExtensions",
    "PoshVsDevClean"
);
$script:IgnoredEnvironmentVariables = $script:TransientEnvironmentVariables + @(
    "PoshVsDevEnvironment",
    "PoshVsDevDefaultEnvironment"
);

class VersionSpec {
    hidden static $VERSION_FRAGMENT_UNSPECIFIED = -1;
    hidden static $VERSION_FRAGMENT_STAR = -2;
    hidden static $VERSION_OPERATORS = @{
        ""   = [VersionComparisonOperator]::None;
        "="  = [VersionComparisonOperator]::Equal;
        "<"  = [VersionComparisonOperator]::LessThan;
        "<=" = [VersionComparisonOperator]::LessThanOrEqual;
        ">"  = [VersionComparisonOperator]::GreaterThan;
        ">=" = [VersionComparisonOperator]::GreaterThanOrEqual;
        "~"  = [VersionComparisonOperator]::Tilde;
        "^"  = [VersionComparisonOperator]::Caret;
    };

    static [bool] TryParse([string] $Text, [ref] $Value) {

        function TryParseLogicalOr([string] $Text, [ref] $Value) {
            $Text = $Text.Trim();
            [int] $local:End = $Text.IndexOf('||');
            if ($local:End -eq 0) {
                $Value.Value = $null;
                return $false;
            }

            if ($local:End -eq -1) {
                return TryParseRange $Text $Value;
            }

            $local:Left = $null;
            $local:Right = $null;
            if (-not (TryParseRange $Text.Substring(0, $End) ([ref]$local:Left)) -or
                -not (TryParseLogicalOr $Text.Substring($End + 2) ([ref]$local:Right))) {
                $Value.Value = $null;
                return $false;
            }


            $Value.Value = [VersionSpec]::Or($local:Left, $local:Right);
            return $true;
        }

        function TryParseRange([string] $Text, [ref] $Value) {
            $Text = $Text.Trim();
            [int] $local:End = $Text.IndexOf(' ');
            if ($local:End -eq -1) {
                return TryParsePrimitive $Text $Value;
            }

            $local:Left = $null;
            $local:Right = $null;
            if (-not (TryParsePrimitive $Text.Substring(0, $End) ([ref]$local:Left)) -or
                -not (TryParseRange $Text.Substring($End + 1) ([ref]$local:Right))) {
                $Value.Value = $null;
                return $false;
            }


            $Value.Value = [VersionSpec]::Range($local:Left, $local:Right);
            return $true;
        }

        function TryParsePrimitive([string] $Text, [ref] $Value) {
            [int] $local:Major = [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED;
            [int] $local:Minor = [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED;
            [int] $local:Build = [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED;
            [int] $local:Revision = [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED;
            if (-not ($Text -match $script:VersionSpecPattern) -or
                -not (TryParseXOrNumber $Matches.Major ([ref]$local:Major)) -or
                -not (TryParseXOrNumber $Matches.Minor ([ref]$local:Minor)) -or
                -not (TryParseXOrNumber $Matches.Build ([ref]$local:Build)) -or
                -not (TryParseXOrNumber $Matches.Revision ([ref]$local:Revision)) -or (
                    $Matches.Operator -and (
                        $local:Major -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED -or
                        $local:Major -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                        $local:Minor -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                        $local:Build -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                        $local:Revision -eq [VersionSpec]::VERSION_FRAGMENT_STAR))) {
                $Value.Value = $null;
                return $false;
            }
            [VersionComparisonOperator] $local:Operator = [VersionSpec]::VERSION_OPERATORS[$Matches.Operator -as [string]];
            $Value.Value = [VersionPrimitive]::new($local:Operator, $local:Major, $local:Minor, $local:Build, $local:Revision);
            return $true;
        }

        function TryParseXOrNumber([string] $Text, [ref] $Value) {
            if ($Text.Length -eq 0) {
                $Value.Value = [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED;
                return $true;
            }
            if ($Text.Length -eq 1 -and ($Text -eq '*' -or $Text -eq 'x' -or $Text -eq 'X')) {
                $Value.Value = [VersionSpec]::VERSION_FRAGMENT_STAR;
                return $true;
            }
            if ([int]::TryParse($Text, $Value) -and $Value.Value -ge 0) {
                return $true;
            }
            $Value.Value = 0;
            return $false;
        }

        if (TryParseLogicalOr $Text $Value) {
            $Value.Value = $Value.Value.Normalize();
            return $true;
        }
        return $false;
    }

    static [VersionRange] Range([VersionSpec] $Left, [VersionSpec] $Right) {
        return [VersionRange]::new([VersionRangeOperator]::Range, $Left, $Right);
    }

    static [VersionRange] Or([VersionSpec] $Left, [VersionSpec] $Right) {
        return [VersionRange]::new([VersionRangeOperator]::Or, $Left, $Right);
    }

    static [VersionPrimitive] Primitive([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::None, $Major, $Minor, $Build, $Revision);
    }

    static [VersionPrimitive] EQ([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::Equal, $Major, $Minor, $Build, $Revision);
    }

    static [VersionPrimitive] LT([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::LessThan, $Major, $Minor, $Build, $Revision);
    }

    static [VersionPrimitive] LE([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::LessThanOrEqual, $Major, $Minor, $Build, $Revision);
    }

    static [VersionPrimitive] GT([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::GreaterThan, $Major, $Minor, $Build, $Revision);
    }

    static [VersionPrimitive] GE([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        return [VersionPrimitive]::new([VersionComparisonOperator]::GreaterThanOrEqual, $Major, $Minor, $Build, $Revision);
    }

    [VersionSpec] Normalize() {
        return $this;
    }

    [bool] IsMatch([Version] $Version) {
        return $false;
    }
}

enum VersionRangeOperator {
    Range = 0;
    Or = 1;
}

class VersionRange : VersionSpec {
    [VersionRangeOperator] $Operator;
    [VersionSpec] $Left;
    [VersionSpec] $Right;

    VersionRange([VersionRangeOperator] $Operator, [VersionSpec] $Left, [VersionSpec] $Right) {
        $this.Operator = $Operator;
        $this.Left = $Left;
        $this.Right = $Right;
    }

    [VersionSpec] Normalize() {
        $local:Left = $this.Left.Normalize();
        $local:Right = $this.Right.Normalize();
        if ($this.Left -ne $local:Left -or $this.Right -ne $local:Right) {
            return [VersionRange]::new($this.Operator, $local:Left, $local:Right);
        }
        return $this;
    }

    [bool] IsMatch([Version] $Version) {
        if ($this.Operator -eq [VersionRangeOperator]::Range) {
            return $this.Left.IsMatch($Version) -and $this.Right.IsMatch($Version);
        }
        else {
            return $this.Left.IsMatch($Version) -or $this.Right.IsMatch($Version);
        }
    }

    [string] ToString() {
        if ($this.Operator -eq [VersionRangeOperator]::Range) {
            return "$($this.Left) $($this.Right)";
        }
        else {
            return "$($this.Left) || $($this.Right)";
        }
    }
}

enum VersionComparisonOperator {
    None = 0;
    Equal = 1;
    LessThan = 2;
    GreaterThan = 3;
    LessThanOrEqual = 4;
    GreaterThanOrEqual = 5;
    Tilde = 6;
    Caret = 7;
}

class VersionPrimitive : VersionSpec {
    [VersionComparisonOperator] $Operator = [VersionComparisonOperator]::GreaterThanOrEqual;
    [int] $Major = 0;
    [int] $Minor = 0;
    [int] $Build = 0;
    [int] $Revision = 0;

    VersionPrimitive([VersionComparisonOperator] $Operator, [int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        $this.Operator = $Operator;
        $this.Major = $Major;
        $this.Minor = $Minor;
        $this.Build = $Build;
        $this.Revision = $Revision;
    }

    [VersionSpec] Normalize() {
        if ($this.Operator -eq [VersionComparisonOperator]::None) {
            if ($this.Major -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                $this.Major -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionPrimitive]::GE(0, 0, 0, 0);
            }
            if ($this.Minor -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                $this.Minor -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, 0, 0, 0),
                    [VersionSpec]::LT($this.Major + 1, 0, 0, 0)
                );
            }
            if ($this.Build -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                $this.Build -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, $this.Minor, 0, 0),
                    [VersionSpec]::LT($this.Major, $this.Minor + 1, 0, 0)
                );
            }
            if ($this.Revision -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
                $this.Revision -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, $this.Minor, $this.Build, 0),
                    [VersionSpec]::LT($this.Major, $this.Minor, $this.Build + 1, 0)
                );
            }
            return [VersionSpec]::EQ($this.Major, $this.Minor, $this.Build, $this.Revision);
        }
        elseif ($this.Operator -eq [VersionComparisonOperator]::Tilde) {
            if ($this.Revision -ne [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, $this.Minor, $this.Build, $this.Revision),
                    [VersionSpec]::LT($this.Major, $this.Minor, $this.Build + 1, 0)
                );
            }
            if ($this.Build -ne [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, $this.Minor, $this.Build, 0),
                    [VersionSpec]::LT($this.Major, $this.Minor + 1, 0, 0)
                );
            }
            if ($this.Minor -ne [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, $this.Minor, 0, 0),
                    [VersionSpec]::LT($this.Major + 1, 0, 0, 0)
                );
            }
        }
        elseif ($this.Operator -eq [VersionComparisonOperator]::Caret) {
            if ($this.Major -gt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE($this.Major, [Math]::Max(0, $this.Minor), [Math]::Max($this.Build, 0), [Math]::Max($this.Revision, 0)),
                    [VersionSpec]::LT($this.Major + 1, 0, 0, 0)
                );
            }
            if ($this.Minor -gt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, $this.Minor, [Math]::Max($this.Build, 0), [Math]::Max($this.Revision, 0)),
                    [VersionSpec]::LT(0, $this.Minor + 1, 0, 0)
                );
            }
            if ($this.Minor -lt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, 0, 0, 0),
                    [VersionSpec]::LT(1, 0, 0, 0)
                );
            }
            if ($this.Build -gt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, 0, $this.Build, [Math]::Max($this.Revision, 0)),
                    [VersionSpec]::LT(0, 0, $this.Build + 1, 0)
                );
            }
            if ($this.Build -lt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, 0, 0, 0),
                    [VersionSpec]::LT(0, 1, 0, 0)
                );
            }
            if ($this.Revision -gt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, 0, 0, $this.Revision),
                    [VersionSpec]::LT(0, 0, 0, $this.Revision + 1)
                );
            }
            if ($this.Revision -lt 0) {
                return [VersionSpec]::Range(
                    [VersionSpec]::GE(0, 0, 0, 0),
                    [VersionSpec]::LT(0, 0, 1, 0)
                );
            }
        }
        return $this.Update(
            [Math]::Max(0, $this.Major),
            [Math]::Max(0, $this.Minor),
            [Math]::Max(0, $this.Build),
            [Math]::Max(0, $this.Revision)
        );
    }

    [VersionPrimitive] Update([int] $Major, [int] $Minor, [int] $Build, [int] $Revision) {
        if ($this.Major -ne $Major -or
            $this.Minor -ne $Minor -or
            $this.Build -ne $Build -or
            $this.Revision -ne $Revision) {
            return [VersionPrimitive]::new($this.Operator, $this.Major, $this.Minor, $this.Build, $this.Revision);
        }
        return $this;
    }

    [bool] IsMatch([Version] $Version) {
        [version] $local:NormalThis = [version]::new($this.Major, $this.Minor, $this.Build, $this.Revision);
        [version] $local:NormalVersion = [version]::new($Version.Major, $Version.Minor, [Math]::Max(0, $Version.Build), [Math]::Max(0, $Version.Revision));
        [int] $local:Result = $local:NormalVersion.CompareTo($local:NormalThis);
        switch ($this.Operator) {
            Equal {
                return $local:Result -eq 0;
            }
            LessThan {
                return $local:Result -lt 0;
            }
            LessThanOrEqual {
                return $local:Result -le 0;
            }
            GreaterThan {
                return $local:Result -gt 0;
            }
            GreaterThanOrEqual {
                return $local:Result -ge 0;
            }
        }
        throw "How did we get here! $($this.Operator)";
    }

    [string] ToString() {
        [string] $local:Text = switch ($this.Operator) {
            Equal { "=" }
            LessThan { "<" }
            LessThanOrEqual { "<=" }
            GreaterThan { ">" }
            GreaterThanOrEqual { ">=" }
            Tilde { "~" }
            Caret { "^"}
            default { "" }
        };
        if ($this.Major -eq [VersionSpec]::VERSION_FRAGMENT_STAR -or
            $this.Major -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
            return $local:Text + "*";
        }

        $local:Text += $this.Major -as [string];

        if ($this.Minor -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
            return $local:Text;
        }

        $local:Text += ".";

        if ($this.Minor -eq [VersionSpec]::VERSION_FRAGMENT_STAR) {
            return $local:Text + "*";
        }

        $local:Text += $this.Minor -as [string];

        if ($this.Build -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
            return $local:Text;
        }

        $local:Text += ".";

        if ($this.Build -eq [VersionSpec]::VERSION_FRAGMENT_STAR) {
            return $local:Text + "*";
        }

        $local:Text += $this.Build -as [string];

        if ($this.Revision -eq [VersionSpec]::VERSION_FRAGMENT_UNSPECIFIED) {
            return $local:Text;
        }

        $local:Text += ".";

        if ($this.Revision -eq [VersionSpec]::VERSION_FRAGMENT_STAR) {
            return $local:Text + "*";
        }

        $local:Text += $this.Revision -as [string];

        return $local:Text;
    }
}

function script:ExecuteCommandInNewEnvironment([scriptblock] $Command) {
    $local:TempOutputFile = New-TemporaryFile;
    $local:TempErrorFile = New-TemporaryFile;
    $local:EncodedCommand = script:EncodeCommand $Command;
    $local:ProcessArgs = @(
        "-NoLogo",
        "-NoProfile",
        "-OutputFormat", "XML",
        "-EncodedCommand", $local:EncodedCommand
    );
    Start-Process `
        -UseNewEnvironment `
        -NoNewWindow `
        -Wait `
        -FilePath "powershell" `
        -ArgumentList:$local:ProcessArgs `
        -RedirectStandardOutput:$local:TempOutputFile `
        -RedirectStandardError:$local:TempErrorFile `
        | Out-Null;
    $local:Content = Import-Clixml -LiteralPath:$local:TempOutputFile.FullName;
    Remove-Item $local:TempOutputFile -Force | Out-Null;
    Remove-Item $local:TempErrorFile -Force | Out-Null;
    $local:Content | ForEach-Object { $_ };
}

function script:EncodeCommand([scriptblock] $Command) {
    return [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Command.ToString()))
}

function script:IsIgnoredEnvironmentVariable([string] $Key) {
    return $Key -iin $script:IgnoredEnvironmentVariables;
}

function script:ClearTransientEnvironmentVariables() {
    foreach ($local:Key in $script:TransientEnvironmentVariables) {
        script:SetEnvironmentVariable $local:Key $null;
    }
}

# Converts a JSON object (from ConvertFrom-Json) into a Hashtable
function script:ConvertToHashTable([psobject] $Object) {
    if ($null -eq $Object) {
        return $null;
    }
    if ($Object -is [hashtable]) {
        return $Object;
    }
    $local:Table = @{};
    foreach ($local:Key in $Object | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) {
        $local:Value = $Object | Select-Object -ExpandProperty $local:Key;
        $local:Table[$local:Key] = $local:Value;
    }
    return $local:Table;
}

# Sets or removes an environment variable
function script:SetEnvironmentVariable([string] $Key, [string] $Value) {
    if ($null -ne $Value) {
        [void](Set-Item -Force "ENV:\$Key" -Value $Value);
    }
    else {
        [void](Remove-Item -Force "ENV:\$Key");
    }
}

# Populates $script:VisualStudioVersions from cache if it is empty
function script:PopulateVisualStudioVersionsFromCache() {
    if ($null -eq $script:VisualStudioVersions) {
        if (Test-Path $script:CACHE_PATH) {
            $script:VisualStudioVersions = (Get-Content $script:CACHE_PATH | ConvertFrom-Json) `
                | ForEach-Object {
                    [VisualStudioInstance]::FromObject($_);
                };
        }
    }
}

# Gets the installed legacy visual studio instances from the registry
function script:GetLegacyVisualStudioInstancesFromRegistry() {
    Get-ChildItem -Path:"HKCU:\Software\Microsoft\VisualStudio\*.0" -PipelineVariable:ProductKey `
        | ForEach-Object -PipelineVariable:ConfigKey {
            Join-Path -Path:$local:ProductKey.PSParentPath -ChildPath:($local:ProductKey.PSChildName + "_Config") `
                | Get-Item -ErrorAction:SilentlyContinue;
        } `
        | ForEach-Object -PipelineVariable:ProfileKey {
            Join-Path -Path:$local:ProductKey.PSPath -ChildPath:"Profile" `
                | Get-Item -ErrorAction:SilentlyContinue
        } `
        | ForEach-Object {
            $local:Version = $local:ProfileKey | Get-ItemPropertyValue -Name BuildNum;
            $local:Path = $local:ConfigKey | Get-ItemPropertyValue -Name ShellFolder;
            $local:Name = "VisualStudio/$local:Version";
            if (Join-Path -Path:$local:Path -ChildPath:$script:VSDEVCMD_PATH | Test-Path) {
                [VisualStudioInstance]::new(
                    $local:Name,
                    "Release",
                    $local:Version -as [version],
                    $local:Path,
                    $null
                );
            }
        };
}

# Gets the installed visual studio instances from the VS instances directory
function script:GetVisualStudioInstancesFromVSInstancesDir() {
    Get-ChildItem $script:VS_INSTANCES_DIR `
        | ForEach-Object {
            $local:StatePath = Join-Path -Path:$_.FullName -ChildPath:"state.json";
            $local:State = Get-Content -Path:$local:StatePath | ConvertFrom-Json;
            $local:VsDevCmdPath = Join-Path -Path:$local:State.installationPath -ChildPath:$script:VSDEVCMD_PATH;
            if (Test-Path -LiteralPath:$local:VsDevCmdPath) {
                # other interesting data:
                # $local:State.installDate
                # $local:State.catalogInfo.buildBranch
                # $local:State.catalogInfo.productDisplayVersion
                # $local:State.catalogInfo.productSemanticVersion
                # $local:State.catalogInfo.productLineVersion
                # $local:State.catalogInfo.productMilestone
                # $local:State.catalogInfo.productMilestoneIsPreRelease
                # $local:State.catalogInfo.productName
                # $local:State.catalogInfo.productPatchVersion
                # $local:State.catalogInfo.productRelease
                # $local:State.catalogInfo.channelUri
                # $local:State.launchParams.fileName
                [VisualStudioInstance]::new(
                    $local:State.installationName,
                    $local:State.channelId,
                    $local:State.installationVersion -as [version],
                    $local:State.installationPath,
                    $null
                );
            }
        };
}

# Gets the installed visual studio instances
function script:GetVisualStudioInstances() {
    script:GetLegacyVisualStudioInstancesFromRegistry;
    script:GetVisualStudioInstancesFromVSInstancesDir;
}

# Populates $script:VisualStudioVersions from disk if it is empty
function script:PopulateVisualStudioVersions() {
    if ($null -eq $script:VisualStudioVersions) {
        $script:VisualStudioVersions = script:GetVisualStudioInstances `
            | Sort-Object -Property Version -Descending;

        if ($script:VisualStudioVersions) {
            $script:HasChanges = $true;
        }
    }
}

# Saves any changes to the $script:VisualStudioVersions cache to disk
function script:SaveChanges() {
    if ($script:HasChanges -and $script:VisualStudioVersions) {
        $local:Content = $script:VisualStudioVersions `
            | ForEach-Object {
                [VisualStudioInstance]::ToObject($_);
            } `
            | ConvertTo-Json -Depth 10;

        if ($script:VisualStudioVersions.Length -eq 1) {
            $local:Content = "[" + $local:Content + "]";
        }

        $local:CacheDir = Split-Path $script:CACHE_PATH -Parent;

        if (-not (Test-Path $local:CacheDir)) {
            [void](mkdir $local:CacheDir -ErrorAction:SilentlyContinue);
        }

        $local:Content | Out-File $script:CACHE_PATH;
        $script:HasChanges = $false;
    }
}

# Indicates whether the specified profile path exists
function script:HasProfile([string] $ProfilePath) {
    if (-not $ProfilePath) {
        return $false;
    }
    if (-not (Test-Path -LiteralPath:$ProfilePath)) {
        return $false;
    }
    return $true;
}

# Indicates whether "posh-vsdev" is referenced in the specified profile
function script:IsInProfile([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) {
        return $false;
    }
    $local:Content = Get-Content $ProfilePath -ErrorAction:SilentlyContinue;
    if ($local:Content -match "posh-vsdev") {
        return $true;
    }
    return $false;
}

# Indicates whether the Use-VisualStudioEnvironment cmdlet is referenced in the specified profile
function script:IsUsingEnvironment([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) {
        return $false;
    }
    $local:Content = Get-Content $ProfilePath -ErrorAction:SilentlyContinue;
    if ($local:Content -match "Use-VisualStudioEnvironment") {
        return $true;
    }
    return $false;
}

# Indicates whether the specified profile is signed
function script:IsProfileSigned([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) {
        return $false;
    }
    $local:Sig = Get-AuthenticodeSignature $ProfilePath;
    if (-not $local:Sig) {
        return $false;
    }
    if (-not $local:Sig.SignerCertificate) {
        return $false;
    }
    return $true;
}

# Indicates whether this module is installed within a PowerShell common module path
function script:IsInModulePaths() {
    foreach ($local:Path in $env:PSModulePath -split ";") {
        if (-not $local:Path.EndsWith("\")) {
            $local:Path += "\";
        }
        if ($PSScriptRoot.StartsWith($local:Path, [System.StringComparison]::InvariantCultureIgnoreCase)) {
            return $true;
        }
    }
    return $false;
}

function script:VisualStudioArgumentCompleter {
    param (
        $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters
    )

    $local:InstanceArgs = @{};
    $local:Name = $null;
    $local:LiteralName = $null;
    if ($fakeBoundParameters.ContainsKey("Name")) {
        $local:Name = $fakeBoundParameters["Name"];
        $local:InstanceArgs["Name"] = $local:Name;
    }
    elseif ($fakeBoundParameters.ContainsKey("LiteralName")) {
        $local:LiteralName = $fakeBoundParameters["LiteralName"];
        $local:InstanceArgs["LiteralName"] = $local:LiteralName;
    }

    $local:Channel = $null;
    $local:LiteralChannel = $null;
    if ($fakeBoundParameters.ContainsKey("Channel")) {
        $local:Channel = $fakeBoundParameters["Channel"];
        $local:InstanceArgs["Channel"] = $local:Channel;
    }
    elseif ($fakeBoundParameters.ContainsKey("LiteralChannel")) {
        $local:LiteralChannel = $fakeBoundParameters["LiteralChannel"];
        $local:InstanceArgs["LiteralChannel"] = $local:LiteralChannel;
    }

    $local:Version = $null;
    if ($fakeBoundParameters.ContainsKey("Version")) {
        $local:Version = $fakeBoundParameters["Version"];
        $local:InstanceArgs["Version"] = $local:Version;
    }

    $local:InstanceArgs.Remove($parameterName) | Out-Null;

    [string[]]$local:possibleValues = @();

    switch ($parameterName) {
        'Name' {
            $local:possibleValues = Get-VisualStudioInstance `
                @local:InstanceArgs `
                | ForEach-Object { $_.Name; };
        }
        'LiteralName' {
            $local:possibleValues = Get-VisualStudioInstance `
                @local:InstanceArgs `
                | ForEach-Object { $_.Name; };
        }
        'Channel' {
            $local:possibleValues = Get-VisualStudioInstance `
                @local:InstanceArgs `
                | ForEach-Object { $_.Channel; };
        }
        'LiteralChannel' {
            $local:possibleValues = Get-VisualStudioInstance `
                @local:InstanceArgs `
                | ForEach-Object { $_.Channel; };
        }
        'Version' {
            $local:possibleValues = Get-VisualStudioInstance `
                @local:InstanceArgs `
                | ForEach-Object { $_.Version; };
        }
    }

    $local:possibleValues | Where-Object {
        $_ -ilike "$wordToComplete*"
    };
}

function script:WindowsSdkArgumentCompleter {
    param (
        $commandName,
        $parameterName,
        $wordToComplete,
        $commandAst,
        $fakeBoundParameters
    )

    [bool]$local:HasAppPlatform = $false;
    [AppPlatform]$local:AppPlatform = [AppPlatform]::Desktop;
    if ($fakeBoundParameters.ContainsKey("AppPlatform")) {
        try {
            $local:AppPlatform = [AppPlatform]($fakeBoundParameters["AppPlatform"]);
            $local:HasAppPlatform = $true;
        }
        catch {
        }
    }

    [string[]]$local:possibleValues = @(
        "latest"
    );

    $local:possibleValues += [WindowsSdk]::All() `
        | Where-Object {
            (-not $local:HasAppPlatform) -or ($local:AppPlatform -in $_.SupportedPlatforms);
        } `
        | ForEach-Object {
            $_.Version -as [string];
        };

    $local:possibleValues | Where-Object { $_ -ilike "$wordToComplete*" };
}

<#
.SYNOPSIS
    Get installed Visual Studio instances.
.DESCRIPTION
    The Get-VisualStudioInstance cmdlet gets information about the installed Visual Studio instances on this machine.
.PARAMETER Name
    Specifies a name pattern that can be used to filter the results.
.PARAMETER LiteralName
    Specifies a literal name that can be used to filter the results.
.PARAMETER Channel
    Specifies a release channel pattern that can be used to filter the results.
.PARAMETER LiteralChannel
    Specifies a literal release channel that can be used to filter the results.
.PARAMETER Version
    Specifies a version number specification that can be used to filter the results.
.INPUTS
    None. You cannot pipe objects to Get-VisualStudioInstance.
.OUTPUTS
    VisualStudioInstance. Get-VisualStudioInstance returns a VisualStudioInstance object for each matching instance.
.EXAMPLE
    PS> Get-VisualStudioInstance
    Name Channel Version Path
    ---- ------- ------- ----
    Microsoft Visual Studio 14.0 Release 14.0 C:\Program Files (x86)\Microsoft Visual Studio 14.0
.EXAMPLE
    PS> Get-VisualStudioInstance -Channel Release
    Name Channel Version Path
    ---- ------- ------- ----
    Microsoft Visual Studio 14.0 Release 14.0 C:\Program Files (x86)\Microsoft Visual Studio 14.0
#>

function Get-VisualStudioInstance {
    [CmdletBinding(DefaultParameterSetName="NameChannel")]
    param (
        [Parameter(Position=0, ParameterSetName="NameChannel")]
        [Parameter(Position=0, ParameterSetName="NameLiteralChannel")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Name,

        [Parameter(Position=0, ParameterSetName="LiteralNameChannel")]
        [Parameter(Position=0, ParameterSetName="LiteralNameLiteralChannel")]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $LiteralName,

        [Parameter(Position=1, ParameterSetName="NameChannel")]
        [Parameter(Position=1, ParameterSetName="LiteralNameChannel")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Channel,

        [Parameter(Position=1, ParameterSetName="NameLiteralChannel")]
        [Parameter(Position=1, ParameterSetName="LiteralNameLiteralChannel")]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $LiteralChannel,

        [Parameter()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Version
    );

    script:PopulateVisualStudioVersionsFromCache;
    script:PopulateVisualStudioVersions;
    [VisualStudioInstance[]] $local:Versions = $script:VisualStudioVersions;
    if ($Name) {
        $local:Versions = $local:Versions | Where-Object -Property Name -ILike $Name;
    }
    if ($LiteralName) {
        $local:Versions = $local:Versions | Where-Object -Property Name -IEQ $LiteralName;
    }
    if ($Channel) {
        $local:Versions = $local:Versions | Where-Object -Property Channel -ILike $Channel;
    }
    if ($LiteralChannel) {
        $local:Versions = $local:Versions | Where-Object -Property Channel -IEQ $LiteralChannel;
    }
    if ($Version) {
        $local:VersionSpec = $null;
        if ([VersionSpec]::TryParse($Version, [ref]$local:VersionSpec)) {
            $local:Versions = $local:Versions | Where-Object { return $local:VersionSpec.IsMatch($_.Version); };
        }
        else {
            throw [System.FormatException]::new();
        }
    }
    $local:Versions;
    script:SaveChanges;
}

<#
.SYNOPSIS
    Uses the developer environment variables for an instance of Visual Studio.
.DESCRIPTION
    The Use-VisualStudioEnvironment cmdlet overwrites the current environment variables with ones from the
    Developer Command Prompt for a specific instance of Visual Studio.
    If a developer environment is already in use, the environment is first reset to the state at the time
    the "posh-vsdev" module was loaded.
.PARAMETER Name
    Specifies a name that can be used to filter the results.
.PARAMETER Channel
    Specifies a release channel that can be used to filter the results.
.PARAMETER Version
    Specifies a version number that can be used to filter the results.
.PARAMETER InputObject
    A VisualStudioInstance whose environment should be used.
.PARAMETER None
    Indicates no environment should be used (similar to calling Reset-VisualStudioEnvironment).
.PARAMETER Arch
    Indicates the target compilation architecture (default: `x86`).
.PARAMETER HostArch
    Indicates the host tools architecture (default: the same value as -Arch).
.PARAMETER AppPlatform
    Indicates the intended application platform (default: `Desktop`).
.PARAMETER WindowsSdk
    Indicates the Windows SDK to use for build tools (default: `latest`).
.INPUTS
    VisualStudioInstance.
        You can pipe a VisualStudioInstance to Use-VisualStudioEnvironment.
.OUTPUTS
    None.
.EXAMPLE
    PS> Use-VisualStudioVersion
    Using Development Environment from 'Microsoft Visual Studio 14.0'.
#>

function Use-VisualStudioEnvironment {
    [CmdletBinding(DefaultParameterSetName = "NameChannel")]
    param (
        [Parameter(Position=0, ParameterSetName = "NameChannel")]
        [Parameter(Position=0, ParameterSetName = "NameChannelOptions")]
        [Parameter(Position=0, ParameterSetName = "NameLiteralChannel")]
        [Parameter(Position=0, ParameterSetName = "NameLiteralChannelOptions")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Name = $env:PoshVsDevVsName,

        [Parameter(Position=0, ParameterSetName = "LiteralNameChannel")]
        [Parameter(Position=0, ParameterSetName = "LiteralNameChannelOptions")]
        [Parameter(Position=0, ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(Position=0, ParameterSetName = "LiteralNameLiteralChannelOptions")]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $LiteralName = $env:PoshVsDevVsName,

        [Parameter(Position=1, ParameterSetName = "NameChannel")]
        [Parameter(Position=1, ParameterSetName = "NameChannelOptions")]
        [Parameter(Position=1, ParameterSetName = "LiteralNameChannel")]
        [Parameter(Position=1, ParameterSetName = "LiteralNameChannelOptions")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Channel = $env:PoshVsDevVsChannel,

        [Parameter(Position=1, ParameterSetName = "NameLiteralChannel")]
        [Parameter(Position=1, ParameterSetName = "NameLiteralChannelOptions")]
        [Parameter(Position=1, ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(Position=1, ParameterSetName = "LiteralNameLiteralChannelOptions")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $LiteralChannel = $env:PoshVsDevVsChannel,

        [Parameter(Position=2, ParameterSetName = "NameChannel")]
        [Parameter(Position=2, ParameterSetName = "NameChannelOptions")]
        [Parameter(Position=2, ParameterSetName = "NameLiteralChannel")]
        [Parameter(Position=2, ParameterSetName = "NameLiteralChannelOptions")]
        [Parameter(Position=2, ParameterSetName = "LiteralNameChannel")]
        [Parameter(Position=2, ParameterSetName = "LiteralNameChannelOptions")]
        [Parameter(Position=2, ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(Position=2, ParameterSetName = "LiteralNameLiteralChannelOptions")]
        [ArgumentCompleter({ script:VisualStudioArgumentCompleter @args })]
        [string] $Version = $env:PoshVsDevVsVersion,

        [Parameter(ParameterSetName = "Pipeline", Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [Parameter(ParameterSetName = "PipelineOptions", Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [psobject] $InputObject,

        [Parameter(ParameterSetName = "None")]
        [switch] $None = $($env:PoshVsDevClean -iin ("true","t","1")),

        [Parameter(ParameterSetName = "NameChannel")]
        [Parameter(ParameterSetName = "NameLiteralChannel")]
        [Parameter(ParameterSetName = "LiteralNameChannel")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(ParameterSetName = "Pipeline")]
        [ArgumentCompletions('x86', 'i686', 'amd64', 'x64', 'x86_64', 'arm', 'arm64', 'aarch64')]
        [TargetArchitectureIn] $Arch = $(
            if ($env:PoshVsDevArch) {
                $env:PoshVsDevArch
            } else {
                [TargetArchitectureIn]::x86
            }
        ),

        [Parameter(ParameterSetName = "NameChannel")]
        [Parameter(ParameterSetName = "NameLiteralChannel")]
        [Parameter(ParameterSetName = "LiteralNameChannel")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(ParameterSetName = "Pipeline")]
        [ArgumentCompletions('x86', 'amd64', 'x64', 'x86_64')]
        [HostArchitectureIn] $HostArch = $(
            if ($env:PoshVsDevHostArch) {
                $env:PoshVsDevHostArch
            } elseif ($Arch -eq [TargetArchitectureIn]::amd64) {
                [HostArchitectureIn]::amd64
            } else {
                [HostArchitectureIn]::x86
            }
        ),

        [Parameter(ParameterSetName = "NameChannel")]
        [Parameter(ParameterSetName = "NameLiteralChannel")]
        [Parameter(ParameterSetName = "LiteralNameChannel")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(ParameterSetName = "Pipeline")]
        [ArgumentCompletions('Desktop', 'UWP')]
        [AppPlatform] $AppPlatform = $(
            if ($env:PoshVsDevAppPlatform) {
                $env:PoshVsDevAppPlatform
            } else {
                [AppPlatform]::Desktop
            }
        ),

        [Parameter(ParameterSetName = "NameChannel")]
        [Parameter(ParameterSetName = "NameLiteralChannel")]
        [Parameter(ParameterSetName = "LiteralNameChannel")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(ParameterSetName = "Pipeline")]
        [ArgumentCompleter({ script:WindowsSdkArgumentCompleter @args })]
        [string] $WindowsSdk = $(
            if ($env:PoshVsDevWindowsSdk) {
                $env:PoshVsDevWindowsSdk
            } else {
                "latest"
            }
        ),

        [Parameter(ParameterSetName = "NameChannel")]
        [Parameter(ParameterSetName = "NameLiteralChannel")]
        [Parameter(ParameterSetName = "LiteralNameChannel")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannel")]
        [Parameter(ParameterSetName = "Pipeline")]
        [bool] $NoExtensions = $env:PoshVsDevNoExtensions -iin ("true","t","1"),

        [Parameter(ParameterSetName = "NameChannelOptions")]
        [Parameter(ParameterSetName = "NameLiteralChannelOptions")]
        [Parameter(ParameterSetName = "LiteralNameChannelOptions")]
        [Parameter(ParameterSetName = "LiteralNameLiteralChannelOptions")]
        [Parameter(ParameterSetName = "PipelineOptions")]
        [EnvironmentOptions] $Options,

        [switch] $Force
    );

    script:ClearTransientEnvironmentVariables;

    if ($None) {
        Reset-VisualStudioEnvironment;
        return;
    }

    if (-not $PSBoundParameters.ContainsKey("Options")) {
        $Options = [EnvironmentOptions]::new(
            $Arch,
            $HostArch,
            $AppPlatform,
            $WindowsSdk,
            $NoExtensions
        )
    }

    if (-not $Options) {
        $Options = [EnvironmentOptions]::new();
    }

    [VisualStudioInstance] $local:Instance = $null;
    if ($InputObject) {
        $local:Instance = [VisualStudioInstance]::FromObject($InputObject);
    } else {
        $local:InstanceArgs = @{};
        if ($Name) { $local:InstanceArgs["Name"] = $Name; }
        elseif ($LiteralName) { $local:InstanceArgs["LiteralName"] = $LiteralName; }

        if ($Channel) { $local:InstanceArgs["Channel"] = $Channel; }
        elseif ($LiteralChannel) { $local:InstanceArgs["LiteralChannel"] = $LiteralChannel; }

        $local:Instance = Get-VisualStudioInstance `
            @local:InstanceArgs `
            -Version:$Version `
            | Select-Object -First:1;
    }

    if ($local:Instance) {
        $local:PoshVsDevEnvironment = $local:Instance.GetPoshVsDevEnvironmentId($Options);
        if ($Force -or 
            ($env:PoshVsDevEnvironment -ine $local:PoshVsDevEnvironment) -or 
            ($script:VisualStudioVersion -ne $local:Instance) -or
            ($script:CurrentEnvironmentOptions -ne $Options)) {
            if ($script:VisualStudioVersion -and $script:CurrentEnvironmentOptions) {
                $script:VisualStudioVersion.Unapply($script:CurrentEnvironmentOptions);
            }
            $local:Instance.Apply($Options);
            script:SaveChanges;
            Write-Host "Using Development Environment from '$($local:Instance.Name)' [$($Options.ToString().Trim())]." -ForegroundColor:DarkGray;
            $script:VisualStudioVersion = $local:Instance;
            $script:CurrentEnvironmentOptions = $Options;
        }
    }
    else {
        [string] $local:Message = "Could not find Visual Studio";
        [string[]] $local:MessageParts = @();
        if ($Name) {
            $local:MessageParts += "Name='$Name'";
        }
        elseif ($LiteralName) {
            $local:MessageParts += "LiteralName='$LiteralName'";
        }
        if ($Channel) {
            $local:MessageParts += "Channel='$Channel'";
        }
        elseif ($LiteralChannel) {
            $local:MessageParts += "LiteralChannel='$LiteralChannel'";
        }
        if ($Version) {
            $local:MessageParts += "Version='$Version'";
        }
        if ($local:MessageParts.Length -gt 0) {
            $local:Message += "for " + $local:MessageParts[0];
            if ($local:MessageParts.Length -eq 2) {
                $local:Message += " and " + $local:MessageParts[1];
            }
            elseif ($local:MessageParts.Length -gt 2) {
                for ($local:I = 1; $local:I -lt $local:MessageParts.Length - 1; $local:I++) {
                    $local:Message += ", " + $local:MessageParts[$local:I];
                }
                if ($local:MessageParts.Length -gt 2) {
                    $local:Message += ", and " + $local:MessageParts[$local:MessageParts.Length - 1];
                }
            }
        }
        $local:Message += ".";
        Write-Warning $local:Message;
    }
}

<#
.SYNOPSIS
    Restores the original enironment.
.DESCRIPTION
    The Reset-VisualStudioEnvironment cmdlet restores all environment variables to their values
    at the point the "posh-vsdev" module was first imported.
.PARAMETER Force
    Indicates that all environment variables should be restored even if no development environment
    was used.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioEnvironment.
.OUTPUTS
    None.
#>

function Reset-VisualStudioEnvironment {
    [CmdletBinding()]
    param (
        [switch] $Force
    );

    script:ClearTransientEnvironmentVariables;

    if ($Force -or $script:VisualStudioVersion -or $env:PoshVsDevEnvironment) {
        if ($script:VisualStudioVersion -and $script:CurrentEnvironmentOptions) {
            $script:VisualStudioVersion.Unapply($script:CurrentEnvironmentOptions);
        }
        else {
            [Environment]::GetDefault().Apply($null);
        }
        $script:VisualStudioVersion = $null;
        $script:CurrentEnvironmentOptions = $null;
        Write-Host "Restored default environment" -ForegroundColor DarkGray;
    }
}

<#
.SYNOPSIS
    Resets the cache of installed Visual Studio instances.
.DESCRIPTION
    Resets the cache of installed Visual Studio instances and their respective environment
    settings.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioInstanceCache.
.OUTPUTS
    None.
#>

function Reset-VisualStudioInstanceCache {
    [CmdletBinding()]
    param (
    );

    $script:VisualStudioVersions = $null;
    if (Test-Path $script:CACHE_PATH) {
        Remove-Item $script:CACHE_PATH -Force | Out-Null;
    }
}

<#
.SYNOPSIS
    Adds "posh-vsdev" to your profile.
.DESCRIPTION
    Adds an import to "posh-vsdev" to your PowerShell profile.
.PARAMETER AllHosts
    Specifies that "posh-vsdev" should be installed to your PowerShell profile for all PowerShell hosts.
    If not provided, only the current profile is used.
.PARAMETER UseEnvironment
    Specifies that an invocation of the Use-VisualStudioEnvironment cmdlet should be added to your
    PowerShell profile.
.PARAMETER Force
    Indicates that "posh-vsdev" should be added to your profile, even if it may already be present.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioInstanceCache.
.OUTPUTS
    None.
#>

function Add-VisualStudioEnvironmentToProfile {
    [CmdletBinding()]
    param (
        [switch] $AllHosts,
        [switch] $UseEnvironment,
        [switch] $Force
    );

    [string] $local:ProfilePath = (
        if ($AllHosts) {
            $profile.CurrentUserAllHosts
        } else {
            $profile.CurrentUserCurrentHost
        }
    );
    [bool] $local:IsInProfile = script:IsInProfile $local:ProfilePath;
    [bool] $local:IsUsingEnvironment = script:IsUsingEnvironment $local:ProfilePath;
    if (-not $Force -and $local:IsInProfile -and -not $UseEnvironment) {
        Write-Warning "'posh-vsdev' is already installed.";
        return;
    }
    if (-not $Force -and $local:IsUsingEnvironment -and $UseEnvironment) {
        Write-Warning "'posh-vsdev' is already using a VisualStudio environment.";
        return;
    }
    if (script:IsProfileSigned $local:ProfilePath) {
        Write-Warning "Cannot modify signed profile.";
        return;
    }
    [string] $local:Content = $null;
    if ($Force -or -not $local:IsInProfile) {
        if (-not (script:HasProfile $local:ProfilePath)) {
            $local:ProfileDir = Split-Path $local:ProfilePath -Parent;
            if (-not (Test-Path -LiteralPath:$local:ProfileDir)) {
                [void](mkdir $local:ProfileDir -ErrorAction:SilentlyContinue);
            }
        }
        if (script:IsInModulePaths) {
            $local:Content += "`nImport-Module posh-vsdev;";
        }
        else {
            $local:Content += "`nImport-Module `"$PSScriptRoot\posh-vsdev.psd1`";";
        }
    }
    if ($Force -or (-not $local:IsUsingEnvironment -and $UseEnvironment)) {
        $local:Content += "`nUse-VisualStudioEnvironment;";
    }
    if ($local:Content) {
        Add-Content -LiteralPath:$local:ProfilePath -Value $local:Content -Encoding UTF8;
    }
}

<#
.SYNOPSIS
    Get matching Windows SDKs installed on the machine.
.DESCRIPTION
    Gets matching Windows SDKs installed on the machine.
.PARAMETER SdkVersion
    The Windows SDK version to match.
.INPUTS
    None. You cannot pipe objects to Get-WindowsSdk
.OUTPUTS
    WindowsSdk. Get-WindowsSdk returns one or more WindowsSdk objects matching the provided inputs.
#>

function Get-WindowsSdk {
    [CmdletBinding(DefaultParameterSetName="SdkVersion")]
    param (
        [Parameter(Position=0, ParameterSetName="SdkVersion")]
        [SupportsWildcards()]
        [ArgumentCompleter({ script:WindowsSdkArgumentCompleter @args })]
        [string] $SdkVersion,

        [Parameter(Position=0, ParameterSetName="LiteralSdkVersion")]
        [ArgumentCompleter({ script:WindowsSdkArgumentCompleter @args })]
        [string] $LiteralSdkVersion,

        [ArgumentCompletions("Desktop", "UWP")]
        [AppPlatform] $AppPlatform
    )

    if ($LiteralSdkVersion -ieq "latest" -or $SdkVersion -ieq "latest") {
        return [WindowsSdk]::All() | Select-Object -First 1;
    }

    [WindowsSdk]::All() |
        Where-Object {
            ((-not $SdkVersion) -or ($_.Version -ilike $SdkVersion)) -and `
            ((-not $LiteralSdkVersion) -or ($_.Version -ieq $LiteralSdkVersion)) -and `
            ((-not $AppPlatform) -or ($AppPlatform -in $_.SupportedPlatforms));
        } |
        ForEach-Object {
            [WindowsSdk]::new(
                $_.Version,
                $_.WindowsSdkDir,
                $_.WindowsLibPath,
                $_.WindowsSdkBinPath,
                $_.SupportedPlatforms
            );
        };
}

# constants
[string] $script:VSDEVCMD_PATH = "Common7\Tools\VsDevCmd.bat";
[string] $script:VS_INSTANCES_DIR = "$env:ProgramData\Microsoft\VisualStudio\Packages\_Instances";
[string] $script:CONFIG_DIR = "$env:USERPROFILE\.posh-vsdev";
[string] $script:CACHE_PATH = "$script:CONFIG_DIR\instances.json";

# state
[bool] $script:HasChanges = $false;                             # Indicates whether the in-memory cache has changes
[VisualStudioInstance[]] $script:VisualStudioVersions = $null;  # In-memory cache of instances
[VisualStudioInstance] $script:VisualStudioVersion = $null;     # Current VS instance
[EnvironmentOptions] $script:CurrentEnvironmentOptions = $null; # Current Environment Options

if ($env:PoshVsDevEnvironment) {
    $local:PoshVsDevEnvironmentObject = $env:PoshVsDevEnvironment | ConvertFrom-Json -ErrorAction:SilentlyContinue;
    if ($local:PoshVsDevEnvironmentObject) {
        $script:VisualStudioVersion = Get-VisualStudioInstance `
            -LiteralName:$local:PoshVsDevEnvironmentObject.Name `
            -LiteralChannel:$local:PoshVsDevEnvironmentObject.Channel `
            -Version:$local:PoshVsDevEnvironmentObject.Version;
        $script:CurrentEnvironmentOptions = [EnvironmentOptions]::FromObject($local:PoshVsDevEnvironmentObject.Options);
    }
}

[Environment]::GetDefault() | Out-Null;