NanoServerPackage.psm1

#region Script variables

Microsoft.PowerShell.Core\Set-StrictMode -Version Latest

$script:providerName ="NanoServerPackage"
$script:WindowsPackageExtension = ".cab"
$script:onlinePackageCache = @{}
$script:imageCultureCache = @{}
$script:imagePathCache = @{}
$script:wildcardOptions = [System.Management.Automation.WildcardOptions]::CultureInvariant -bor `
                          [System.Management.Automation.WildcardOptions]::IgnoreCase

$script:WindowsPackage = "$env:LOCALAPPDATA\NanoServerPackageProvider"
$script:downloadedCabLocation = "$script:WindowsPackage\DownloadedCabs"
$script:file_modules = "$script:WindowsPackage\sources.txt"
$script:windowsPackageSources = $null
$script:defaultPackageName = "NanoServerPackageSource"
$script:defaultPackageLocation = "http://go.microsoft.com/fwlink/?LinkID=723027&clcid=0x409"
$script:isNanoServerInitialized = $false
$script:isNanoServer = $false
$script:systemSKU = -1
$script:systemVersion = $null
$script:availablePackages = @()
$separator = "|#|"

#endregion Script variables

#region Stand-Alone

function Find-NanoServerPackage
{
    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory=$false,
                        Position=0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,
        
# [string[]]
# $Repository,

        [string]
        $Culture,

        [switch]
        $Force
    )
    
    $PSBoundParameters["Provider"] = $script:providerName

    $packages = PackageManagement\Find-Package @PSBoundParameters

    foreach($package in $packages) {
        Microsoft.PowerShell.Utility\Add-Member -InputObject $package -MemberType NoteProperty -Name "Description" -Value $package.Summary
        
        try {
            if ($package.Metadata["NanoServerVersion"] -ne $null)
            {
                Microsoft.PowerShell.Utility\Add-Member -InputObject $package -MemberType NoteProperty -Name "NanoServerVersion" -Value (ConvertNanoServerVersionToString $package.Metadata["NanoServerVersion"][0])
            }
        }
        catch {}

        $package.PSTypeNames.Insert(0, "Microsoft.PowerShell.Commands.NanoServerPackageItemInfo") | Out-Null
        $package
    }
}

function Save-NanoServerPackage
{
    [CmdletBinding(DefaultParameterSetName='NameAndPathParameterSet',
                   SupportsShouldProcess=$true)]
    Param
    (
        [Parameter(Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [ValidateNotNullOrEmpty()]

        [string[]]
        $Name,
        
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='InputOjectAndPathParameterSet')]
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='InputOjectAndLiteralPathParameterSet')]
        [ValidateNotNull()]
        [PSCustomObject[]]
        $InputObject,
        
        [Parameter(Mandatory=$false, 
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$false, 
                   ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [string]
        $Culture,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Version]
        $MinimumVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Version]
        $MaximumVersion,
        
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [Alias('Version')]
        [Version]
        $RequiredVersion,

<#
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndPathParameterSet')]
        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameAndLiteralPathParameterSet')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Repository,
#>


        [Parameter(Mandatory=$true, ParameterSetName='NameAndPathParameterSet')]
        [Parameter(Mandatory=$true, ParameterSetName='InputOjectAndPathParameterSet')]
        [string]
        $Path,

        [Parameter(Mandatory=$true, ParameterSetName='NameAndLiteralPathParameterSet')]
        [Parameter(Mandatory=$true, ParameterSetName='InputOjectAndLiteralPathParameterSet')]
        [string]
        $LiteralPath,

        [Parameter()]
        [switch]
        $Force
    )

    Begin
    {
    }

    Process
    {
        # verify name does not have wild card
        foreach ($packageName in $Name)
        {            
            if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($packageName))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Name cannot contain wildcards" `
                            -ExceptionObject $packageName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData

                return
            }
        }

        if($InputObject)
        {
            $Name = $InputObject.Name
            $RequiredVersion = $InputObject.Version
            #$Repository = $InputObject.Repository
            $Culture = $InputObject.Culture

            if (-not [string]::IsNullOrWhiteSpace($Culture) -and $Culture.Contains(','))
            {
                $Culture = ''
            }
        }

        if($Path)
        {
            $destinationPath = Resolve-PathHelper -Path $Path `
                                                    -CallerPSCmdlet $PSCmdlet | Microsoft.PowerShell.Utility\Select-Object -First 1

            if(-not $destinationPath -or -not (Microsoft.PowerShell.Management\Test-path $destinationPath))
            {
                $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $Path)
                ThrowError  -ExceptionName "System.ArgumentException" `
                            -ExceptionMessage $errorMessage `
                            -ErrorId "PathNotFound" `
                            -CallerPSCmdlet $PSCmdlet `
                            -ExceptionObject $Path `
                            -ErrorCategory InvalidArgument
            }
        }
        else
        {
            $destinationPath = Resolve-PathHelper -Path $LiteralPath `
                                                    -IsLiteralPath `
                                                    -CallerPSCmdlet $PSCmdlet | Microsoft.PowerShell.Utility\Select-Object -First 1

            if(-not $destinationPath -or -not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $destinationPath))
            {
                $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $LiteralPath)
                ThrowError  -ExceptionName "System.ArgumentException" `
                            -ExceptionMessage $errorMessage `
                            -ErrorId "PathNotFound" `
                            -CallerPSCmdlet $PSCmdlet `
                            -ExceptionObject $LiteralPath `
                            -ErrorCategory InvalidArgument
            }
        }

        if($Name)
        {
            # no culture given, use culture of the system
            if ([string]::IsNullOrWhiteSpace($Culture))
            {
                $Culture = (Get-Culture).Name
            }

            $listOfNames = @()
            foreach ($packageName in $Name)
            {
                $listOfNames += $packagename
            }

            $findResults = @()
            $findResults += (Find -Name $listOfNames `
                            -MinimumVersion $MinimumVersion `
                            -MaximumVersion $MaximumVersion `
                            -RequiredVersion $RequiredVersion `
                            -Culture $Culture `
                            -Force:$Force)
# -Repository $Repository `

            if ($findResults.Count -eq 0)
            {
                Write-Error "No results found for $listOfNames"
                return
            }
            
            foreach($findResult in $findResults)
            {
                $dependenciesToBeInstalled = [System.Collections.ArrayList]::new()
                
                if (-not (Get-DependenciesToInstall -availablePackages $script:availablePackages -culture $Culture -package $findResult -dependenciesToBeInstalled $dependenciesToBeInstalled)) {
                    return
                }

                foreach ($result in $dependenciesToBeInstalled) {
                    $currLang = $result.Culture

                    $skipBase = $false

                    # check whether base package is in list of available packages, if so, don't save
                    foreach ($availablePackage in $script:availablePackages) {
                        if (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $result.Name -requiredVersion $result.Version -Culture "Base")
                        {
                            # if it is, no need to download base installer
                            $skipBase = $true
                        }                        
                    }

                    if (-not $skipBase) {
                        # Base Installer
                        $fileName_base = Get-FileName -name $result.Name `
                                                        -Culture "" `
                                                        -version $result.Version.ToString()

                        $destination_base = Join-Path $destinationPath $fileName_base

                        if($PSCmdlet.ShouldProcess($fileName_base, "Save-NanoServerPackage"))
                        {
                            if(Test-Path $destination_base)
                            {
                                if($Force)
                                {
                                    Remove-Item $destination_base

                                    $token = $result.Locations.base
                                    DownloadFile -downloadURL $token -destination $destination_base
                                }
                                else
                                {
                                    # The file exists, not downloading
                                    Write-Information "$fileName_base already existsat $destinationPath. Skipping save."
                                }
                            }
                            else
                            {
                                $token = $result.Locations.base
                                DownloadFile -downloadURL $token -destination $destination_base
                            }
                        }
                    }

                    # Language Installer
                    $fileName_lang = Get-FileName -name $result.Name `
                                                    -Culture $currLang `
                                                    -version $result.Version.ToString()

                    $destination_lang = Join-Path $destinationPath $fileName_lang

                    if($PSCmdlet.ShouldProcess($fileName_lang, "Save-NanoServerPackage"))
                    {
                        if(Test-Path $destination_lang)
                        {
                            if($Force)
                            {
                                Remove-Item $destination_lang

                                $token = $result.Locations.$currLang
                                DownloadFile -downloadURL $token -destination $destination_lang
                            }
                            else
                            {
                                # The file exists, not downloading
                                Write-Information "$fileName_lang already exists at $destinationPath. Skipping save."
                            }
                        }
                        else
                        {
                            $token = $result.Locations.$currLang
                            DownloadFile -downloadURL $token -destination $destination_lang
                        }
                    }

                    $result
                }
            }
            
        }
    }

    End
    {
    }
}

function Install-NanoServerPackage
{
    [CmdletBinding(SupportsShouldProcess=$true)]
    param
    (
        [parameter(Mandatory=$true,
            Position=0,
            ValueFromPipelineByPropertyName=$true,
            ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Name,

        <#
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0,
                   ParameterSetName='InputObject')]
        [ValidateNotNull()]
        [PSCustomObject[]]
        $InputObject
        #>


        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [ValidateNotNull()]
        [Version]
        $MinimumVersion,

        [parameter(Mandatory=$false,
            ValueFromPipelineByPropertyName=$true,
            ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [System.String]$Culture,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [Alias('Version')]
        [System.Version]$RequiredVersion,

        [Parameter(ValueFromPipelineByPropertyName=$true,
                   ParameterSetName='NameParameterSet')]
        [ValidateNotNull()]
        [System.Version]$MaximumVersion,

        [ValidateNotNullOrEmpty()]
        [System.String]$ToVhd,

        [parameter()]
        [switch]$Force,

        [parameter()]
        [switch]$NoRestart

<# [Parameter(ParameterSetName='NameParameterSet')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Repository
#>

    )

    Begin
    {
    }

    Process
    {
        # verify name does not have wild card
        foreach ($packageName in $Name)
        {
            if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($packageName))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Name cannot contain wildcards" `
                            -ExceptionObject $packageName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData

                return
            }   
        }

        # pipeline case where culture passed in is en-us, de-de, etc.
        if (-not [string]::IsNullOrWhiteSpace($Culture) -and $Culture.Contains(','))
        {
            $Culture = ''
        }

        $packagesToBeInstalled = @()

        # do a find first, if there are any errors, don't install
        $packagesToBeInstalled += (Find -Name $Name -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion -RequiredVersion $RequiredVersion `
            -Culture $Culture -ErrorAction Stop) # -Repository $Repository

        if ($packagesToBeInstalled.Count -eq 0)
        {
            return
        }

        $mountDrive = $null

        # the available packages on the system
        $availablePackages = $()

        $installedPackage = $null

        if (-not [string]::IsNullOrWhiteSpace($ToVhd))
        {
            if($PSCmdlet.ShouldProcess($ToVhd, "Mount-WindowsImage"))
            {
                $ToVhd = Resolve-PathHelper $ToVhd -callerPSCmdlet $PSCmdlet
            
                if (-not ([System.IO.File]::Exists($ToVhd)))
                {
                    $exception = New-Object System.ArgumentException "$ToVhd does not exist"
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
                    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidVhdPath", $errorCategory, $ToVhd

                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }

                # mount image
                $mountDrive = New-MountDrive

                Write-Verbose "Mounting $ToVhd to $mountDrive"

                Write-Progress -Activity "Mounting $ToVhd to $mountDrive" -PercentComplete 0

                $null = Mount-WindowsImage -ImagePath $ToVhd -Index 1 -Path $mountDrive

                $mountedVHDEdition = $null

                foreach ($packageToBeInstalled in $packagesToBeInstalled)
                {
                    # if this package can't be install on standard, should do a check
                    if (-not $packageToBeInstalled.Sku.Contains("144") -or (-not [string]::IsNullOrWhiteSpace($packageToBeInstalled.NanoServerVersion)))
                    {
                        # initialize the regkey
                        if ($mountedVHDEdition -eq $null)
                        {
                            $regKey = $null

                            $vhdNanoServerVersion = $null
                            $mountedVHDEdition = "ERROR"

                            try
                            {
                                reg load HKLM\NANOSERVERPACKAGEVHDSYS "$mountDrive\Windows\System32\config\SOFTWARE" | Out-Null
                                $regKey = dir 'HKLM:\NANOSERVERPACKAGEVHDSYS\Microsoft\Windows NT'
                                $mountedVHDEdition = $regKey.GetValue("EditionID")
                                $majorVersion = $regKey.GetValue("CurrentMajorVersionNumber")
                                $minorVersion = $regKey.GetValue("CurrentMinorVersionNumber")
                                $buildVersion = $regKey.GetValue("CurrentBuildNumber")
                                $vhdNanoServerVersion = [version]::new($majorVersion, $minorVersion, $buildVersion, 0)
                            }
                            catch
                            {
                                # ERROR
                                $mountedVHDEdition = "ERROR"
                                $vhdNanoServerVersion = $null
                            }
                            finally
                            {
                                try
                                {
                                    if ($regKey -ne $null)
                                    {
                                        $regKey.Handle.Close()
                                        [gc]::Collect()
                                        reg unload HKLM\NANOSERVERPACKAGEVHDSYS | Out-Null
                                    }
                                }
                                catch { }
                            }
                        }

                        if (-not [string]::IsNullOrWhiteSpace($packageToBeInstalled.NanoServerVersion) -and -not (NanoServerVersionMatched -dependencyVersionString $packageToBeInstalled.NanoServerVersion -version $vhdNanoServerVersion))
                        {
                            # unmount the drive
                            if ($null -ne $mountDrive)
                            {
                                Write-Progress -Activity "Unmounting mount drive $mountDrive" -PercentComplete 90
                                Write-Verbose "Unmounting mount drive $mountDrive"
                                Remove-MountDrive $mountDrive -discard $true
                                Write-Progress -Completed -Activity "Completed"
                            }

                            $exception = New-Object System.ArgumentException "The package '$name' with version $($packageToBeInstalled.Version) requires $(ConvertNanoServerVersionToString $packageToBeInstalled.NanoServerVersion). The current NanoServer has version $vhdNanoServerVersion which is out of this range. Please run 'Find-NanoServerPackage $name -AllVersions | select name,version,NanoServerVersion' and use the NanoServerVersion property, which specifies the version range of the NanoServer that a package can be installed on, to find a suitable version. After that, run 'Install-NanoServerPackage $name -RequiredVersion <Correct Version>' to install that version."
                            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData
                            $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "WrongNanoServerEdition", $errorCategory, $packageToBeInstalled.Name

                            $PSCmdlet.ThrowTerminatingError($errorRecord)                    
                        }

                        if (-not $packageToBeInstalled.Sku.Contains("144") -and $mountedVHDEdition -eq "ServerStandardNano")
                        {
                            # unmount the drive
                            if ($null -ne $mountDrive)
                            {
                                Write-Progress -Activity "Unmounting mount drive $mountDrive" -PercentComplete 90
                                Write-Verbose "Unmounting mount drive $mountDrive"
                                Remove-MountDrive $mountDrive -discard $true
                                Write-Progress -Completed -Activity "Completed"
                            }

                            $exception = New-Object System.ArgumentException "$($packageToBeInstalled.Name) cannot be installed on this edition of NanoServer"
                            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData
                            $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "WrongNanoServerEdition", $errorCategory, $packageToBeInstalled.Name

                            $PSCmdlet.ThrowTerminatingError($errorRecord)                    
                        }
                    }
                }
            }
        }
        else
        {
            foreach ($packageToBeInstalled in $packagesToBeInstalled)
            {
                # this package can't be installed on standard
                if (IsNanoServer)
                {
                    # if this is a nano, then systemSKU would be populated after isnanoserver call
                    if (-not $packageToBeInstalled.Sku.Contains($script:systemSKU.ToString()))
                    {
                        $exception = New-Object System.ArgumentException "$($packageToBeInstalled.Name) cannot be installed on this edition of NanoServer"
                        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData
                        $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "WrongNanoServerEdition", $errorCategory, $packageToBeInstalled.Name

                        $PSCmdlet.ThrowTerminatingError($errorRecord)                    
                    }

                    # if this is nanoserver, then we should also have the version populated
                    if (-not (NanoServerVersionMatched -dependencyVersionString $packageToBeInstalled.NanoServerVersion -version $script:systemVersion))
                    {
                        $exception = New-Object System.ArgumentException "The package '$($packageToBeInstalled.Name)' with version $($packageToBeInstalled.Version) requires $(ConvertNanoServerVersionToString $packageToBeInstalled.NanoServerVersion). The current NanoServer has version $script:systemVersion which is out of this range. Please run 'Find-NanoServerPackage $($packageToBeInstalled.Name) -AllVersions | select name,version,NanoServerVersion' and use the NanoServerVersion property, which specifies the version range of the NanoServer that a package can be installed on, to find a suitable version (the version range notation used is based on https://docs.nuget.org/create/versioning). After that, run 'Install-NanoServerPackage $($packageToBeInstalled.Name) -RequiredVersion <Correct Version>' to install the correct version."
                        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidData
                        $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "WrongNanoServerVersion", $errorCategory, $packageToBeInstalled.Name

                        $PSCmdlet.ThrowTerminatingError($errorRecord)
                    }
                }
            }
        }

        $discard = $false

        try
        {
            # If no force, then just check whether the packages are already installed before proceeding
            if (-not $Force)
            {
                Write-Verbose "Getting available packages"

                # installing online
                if ([string]::IsNullOrWhiteSpace($ToVhd))
                {
                    $availablePackages = (Get-WindowsPackage -Online).PackageName.ToLower()
                }
                else
                {
                    if($PSCmdlet.ShouldProcess($mountDrive, "Get-WindowsPackage"))
                    {
                        Write-Progress -Activity "Getting available packages on $mountDrive" -PercentComplete 10

                        $availablePackages = (Get-WindowsPackage -Path $mountDrive).PackageName.ToLower()
                    }
                }
            }

            if($PSCmdlet.ShouldProcess($Name, "Install-NanoServerPackage"))
            {
                [bool]$success = $false

                if (-not [string]::IsNullOrWhiteSpace($ToVhd))
                {
                    Write-Progress -Activity "Mounting $ToVhd to $mountDrive" -PercentComplete 20
                }

                #Installing the package
                $installedPackage = Install-PackageHelper -Name $Name `
                                                            -Culture $Culture `
                                                            -RequiredVersion $RequiredVersion `
                                                            -MinimumVersion $MinimumVersion `
                                                            -MaximumVersion $MaximumVersion `
                                                            -imagePath $ToVhd `
                                                            -mountDrive $mountDrive `
                                                            -availablePackages $availablePackages `
                                                            -successfullyInstalled ([ref]$success) `
                                                            -Force:$Force `
                                                            -NoRestart:$NoRestart `
                                                            -PackagesToBeInstalled $packagesToBeInstalled
#-source $source `

                if (-not $success)
                {
                    $exception = New-Object System.ArgumentException "Cannot install package $packageName with culture $Culture and version $RequiredVersion"
                    $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
                    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "FailedToInstallPackage", $errorCategory, $packageName

                    Write-Error $errorRecord
                    $discard = $true
                    break
                }

                $installedPackage
            }
        }
        catch
        {
            $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
            $errorRecord = New-Object System.Management.Automation.ErrorRecord $_.Exception, "FailedToInstallPackage", $errorCategory, $Name
            Write-Error $errorRecord
            $discard = $true
        }
        finally
        {
            # unmount the drive
            if ($null -ne $mountDrive)
            {
                Write-Progress -Activity "Unmounting mount drive $mountDrive" -PercentComplete 90
                Write-Verbose "Unmounting mount drive $mountDrive"
                Remove-MountDrive $mountDrive -discard $discard
                Write-Progress -Completed -Activity "Completed"
            }
        }
    }

    End
    {
    }
}

#endregion Stand-Alone

#region Helpers

function Find
{
    [CmdletBinding()]
    param
    (
        [string[]]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,

        <#[string[]]
        $Repository,
        #>


        [string]
        $Culture,

        [switch]
        $Force
    )

    if(-not (CheckVersion $MinimumVersion $MaximumVersion $RequiredVersion $AllVersions))
    {            
        return $null
    }

    $allSources = Get-Source #$Repository

    $searchResults = @()

    if ($null -eq $Name -or $Name.Count -eq 0)
    {
        $Name = @('')
    }

    foreach($currSource in $allSources)
    {
        foreach ($singleName in $Name)
        {
            if ([string]::IsNullOrWhiteSpace($singleName) -or $singleName.Trim() -eq '*')
            {
                # if no name is supplied but min or max version is supplied, error out
                if ($null -ne $MinimumVersion -or $null -ne $MaximumVersion)
                {
                    ThrowError -CallerPSCmdlet $PSCmdlet `
                                -ExceptionName System.Exception `
                                -ExceptionMessage "Name is required when either MinimumVersion or MaximumVersion parameter is used" `
                                -ExceptionObject $Name `
                                -ErrorId NameRequiredForMinOrMaxVersion `
                                -ErrorCategory InvalidData
                }
            }

            $result = Find-Azure -Name $singleName `
                                    -MinimumVersion $MinimumVersion `
                                    -MaximumVersion $MaximumVersion `
                                    -RequiredVersion $RequiredVersion `
                                    -AllVersions:$AllVersions `
                                    -Repository $currSource `
                                    -Culture $Culture `
                                    -Force:$Force
            
            if($null -eq $result)
            {
                # Error must have been thrown already
                # Just continue
                continue
            }
            
            if ($result.GetType().IsArray -and $result.Count -eq 0)
            {
                $sourceName = $currSource.Name
                Write-Error "No matching packages could be found for $singleName in $sourceName"
                continue
            }

            $searchResults += $result
        }
    }

    return $searchResults
}

function Find-Azure
{
    param
    (
        [string]
        $Name,

        [System.Version]
        $MinimumVersion,

        [System.Version]
        $MaximumVersion,
        
        [System.Version]
        $RequiredVersion,

        [switch]
        $AllVersions,

        [System.Object]
        $Repository,

        [string]
        $Culture,

        [switch]
        $Force
    )

    $searchFile = Get-SearchIndex -Force:$Force -fwdLink $Repository.SourceLocation
    $searchFileContent = Get-Content $searchFile

    if($null -eq $searchFileContent)
    {
        return $null
    }

    if(IsNanoServer)
    {
        $jsonDll = [Microsoft.PowerShell.CoreCLR.AssemblyExtensions]::LoadFrom($PSScriptRoot + "\Json.coreclr.dll")
        $jsonParser = $jsonDll.GetTypes() | Where-Object name -match jsonparser
        $searchContent = $jsonParser::FromJson($searchFileContent)
        $searchStuff = $searchContent.Get_Item("array0")
        $searchData = @()
        foreach($searchStuffEntry in $searchStuff)
        {
            $obj = New-Object PSObject 
            $obj | Add-Member NoteProperty Name $searchStuffEntry.Name
            $obj | Add-Member NoteProperty Version $searchStuffEntry.Version
            $obj | Add-Member NoteProperty Description $searchStuffEntry.Description
            $obj | Add-Member NoteProperty SKU $searchStuffEntry.Sku
            $obj | Add-Member NoteProperty NanoServerVersion $searchStuffEntry.NanoServerVersion

            $languageObj = New-Object PSObject
            $languageDictionary = $searchStuffEntry.Language
            $languageDictionary.Keys | ForEach-Object {
                $languageObj | Add-Member NoteProperty $_ $languageDictionary.Item($_)
            }

            # process dependencies
            if ($searchStuffEntry.ContainsKey("Dependencies")) {
                $dependencies = @()
                foreach ($dep in $searchStuffEntry.Dependencies) {
                    $depObject = New-Object PSObject
                    $depObject | Add-Member NoteProperty Name $dep.Name
                    $depObject | Add-Member NoteProperty Version $dep.Version
                    $dependencies += $depObject
                }

                $obj | Add-Member NoteProperty Dependencies $dependencies
            }

            $obj | Add-Member NoteProperty Language $languageObj
            $searchData += $obj
        }
    }
    else
    {
        $searchData = $searchFileContent | ConvertFrom-Json
    }

    $searchResults = @()
    $searchDictionary = @{}

    # If name is null or whitespace, interpret as *
    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = "*"
    }

    # Handle the version not given scenario
    if((-not ($MinimumVersion -or $MaximumVersion -or $RequiredVersion -or $AllVersions)))
    {
        $MinimumVersion = [System.Version]'0.0.0.0'
    }

    foreach($entry in $searchData)
    {
        $toggle = $false

        # Check if the search string has * in it
        if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
        {
            if($entry.name -like $Name)
            {
                $toggle = $true
            }
            else
            {
                continue
            }
        }
        else
        {
            if($entry.name -eq $Name)
            {
                $toggle = $true
            }
            else
            {
                continue
            }
        }

        $thisVersion = Convert-Version $entry.version

        if($MinimumVersion)
        {
            $convertedMinimumVersion = Convert-Version $MinimumVersion

            if(($thisVersion -ge $convertedMinimumVersion))
            {
                if($searchDictionary.ContainsKey($entry.name))
                {
                    $objEntry = $searchDictionary[$entry.name]
                    $objVersion = Convert-Version $objEntry.Version

                    if($thisVersion -gt $objVersion)
                    {
                        $toggle = $true
                    }
                    else
                    {
                        $toggle = $false
                    }
                }
                else
                {
                    $toggle = $true
                }   
            }
            else
            {
                $toggle = $false
            }
        }

        if($MaximumVersion)
        {
            $convertedMaximumVersion = Convert-Version $MaximumVersion

            if(($thisVersion -le $convertedMaximumVersion))
            {
                if($searchDictionary.ContainsKey($entry.name))
                {
                    $objEntry = $searchDictionary[$entry.name]
                    $objVersion = Convert-Version $objEntry.Version

                    if($thisVersion -gt $objVersion)
                    {
                        $toggle = $true
                    }
                    else
                    {
                        $toggle = $false
                    }
                }
                else
                {
                    $toggle = $true
                }
            }
            else
            {
                $toggle = $false
            }
        }

        if($RequiredVersion)
        {
            $convertedRequiredVersion = Convert-Version $RequiredVersion

            if(($thisVersion -eq $convertedRequiredVersion))
            {
                $toggle = $true                
            }
            else
            {
                $toggle = $false
            }
        }

        if($AllVersions)
        {
            if($toggle)
            {
                $searchResults += $entry
            }
        }

        if($toggle)
        {
            if($searchDictionary.ContainsKey($entry.name))
            {
                $searchDictionary.Remove($entry.Name)
            }
            
            $searchDictionary.Add($entry.name, $entry)
        }
    }

    if(-not $AllVersions)
    {
        $searchDictionary.Keys | ForEach-Object {
                $searchResults += $searchDictionary.Item($_)
            }
    }

    $searchLanguageResults = @()

    foreach($searchEntry in $searchResults)
    {
        $EntryName = $searchEntry.Name
        $EntryVersion = $searchEntry.Version
        $EntryDescription = $searchEntry.Description
        $langDict = $searchEntry.Language
        $props= Get-Member -InputObject $langDict -MemberType NoteProperty
        $theSource = $Repository.Name
        $sku = [string]::Join(";", @($searchEntry.Sku))
        $nanoServerVersion = $searchEntry.NanoServerVersion

        $dependencies = @()
        $dependenciesProperty = Get-Member -InputObject $searchEntry -MemberType NoteProperty -Name Dependencies
        if ($null -ne $dependenciesProperty) {
            $dependencies = $searchEntry.Dependencies
        }

        if (-not [string]::IsNullOrWhiteSpace($Culture))
        {
            if(($props.Name -notcontains $Culture) -or `
                ($Culture -eq "base"))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Culture: $Culture is not supported" `
                            -ExceptionObject $EntryName `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData
                return
            }

            $languageObj = New-Object PSObject
            $languageObj | Add-Member NoteProperty "base" $langDict."base"
            $languageObj | Add-Member NoteProperty $Culture $langDict.$Culture

            $ResultEntry = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
                Name = $EntryName
                Version = $EntryVersion
                Description = $EntryDescription
                Source = $theSource
                Locations = $languageObj
                Culture = $Culture
                Sku = $sku
                Dependencies = $dependencies
                NanoServerVersion = $NanoServerVersion
            })
            $ResultEntry.PSTypeNames.Insert(0, "Microsoft.PowerShell.Commands.NanoServerPackageItemInfo")
            $searchLanguageResults += $ResultEntry
        }
        else
        {
            $langList = @()
            $langListString = ""

            $props.Name | ForEach-Object {
                $langList += $_
                if($_ -ne "base"){
                    $langListString += $_
                    $langListString += ", "
                }
            }

            $langListString = $langListString.Substring(0, $langListString.Length - 2)

            $ResultEntry = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
                Name = $EntryName
                Version = $EntryVersion
                Description = $EntryDescription
                Source = $theSource
                Locations = $langDict
                Culture = $langListString
                Sku = $sku
                Dependencies = $dependencies
                NanoServerVersion = $NanoServerVersion
            })
            $ResultEntry.PSTypeNames.Insert(0, "Microsoft.PowerShell.Commands.NanoServerPackageItemInfo")
            $searchLanguageResults += $ResultEntry
        }
    }
    
    return $searchLanguageResults
}

###
### SUMMARY: Download the file given the URI to the given location
###
function DownloadFile
{
    [CmdletBinding()]
    param($downloadURL, $destination, [switch]$noProgress)
    
    $startTime = Get-Date

    try
    {
        # Download the file
        Write-Verbose "Downloading $downloadUrl to $destination"
        $saveItemPath = $PSScriptRoot + "\SaveHTTPItemUsingBITS.psm1"
        Import-Module "$saveItemPath"
        Save-HTTPItemUsingBitsTransfer -Uri $downloadURL `
                        -Destination $destination `
                        -NoProgress:$noProgress
        Write-Verbose "Finished downloading"

        $endTime = Get-Date
        $difference = New-TimeSpan -Start $startTime -End $endTime
        $downloadTime = "Downloaded in " + $difference.Hours + " hours, " + $difference.Minutes + " minutes, " + $difference.Seconds + " seconds."
        Write-Verbose $downloadTime
    }
    catch
    {
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $downloadURL `
                    -ErrorId FailedToDownload `
                    -ErrorCategory InvalidOperation        
    }
}

function Install-PackageHelper
{
    [cmdletbinding()]
    param(
        [string[]]$Name,
        [string]$Culture,
        [string]$source,
        [string]$mountDrive,
        [string]$imagePath,
        [ref]$successfullyInstalled,
        [version]$MinimumVersion,
        [version]$MaximumVersion,
        [version][Alias('Version')]$RequiredVersion,
        [string[]]$availablePackages,
        [switch]$Force,
        [switch]$NoRestart,
        [PSCustomObject[]]$PackagesToBeInstalled
    )

    $installedWindowsPackages = @()

    $successfullyInstalled.Value = $false

    if ([string]::IsNullOrWhiteSpace($Culture))
    {
        # if the culture is null for the online case, we can find out easily

        if ([string]::IsNullOrWhiteSpace($mountDrive))
        {
            $Culture = (Get-Culture).Name
        }
        else
        {
            Write-Verbose "Determining the culture of $mountDrive"

            $fileKey = Get-FileKey -filePath $imagePath

            if (-not $script:imageCultureCache.ContainsKey($fileKey))
            {
                $Culture = Get-ImageCulture -mountDrive $mountDrive

                if ($null -eq $Culture)
                {
                    Write-Verbose "Cannot determine culture of $mountDrive with /Get-Intl. Trying to find culture using a sample package"

                    $packagesOnTheMachine = $availablePackages

                    if ($null -eq $packagesOnTheMachine -or $packagesOnTheMachine.Count -eq 0)
                    {
                        $packagesOnTheMachine = (Get-WindowsPackage -Path $mountDrive).PackageName
                    }

                    foreach ($package in $packagesOnTheMachine)
                    {
                        $Culture = $package.Split('~')[3]

                        # we have found a culture from a package installed!
                        if (-not [string]::IsNullOrWhiteSpace($Culture))
                        {
                            break
                        }
                    }
                }

                # if after all that, culture still null then we have to abort
                if ($null -eq $Culture)
                {
                    Write-Warning "Cannot determine culture of the vhd. Please supply it directly."
                    return
                }

                $script:imageCultureCache[$fileKey] = $Culture
            }
            else
            {
                $Culture = $script:imageCultureCache[$fileKey]
            }
        }

        Write-Verbose "The culture to be installed is $Culture"
    }

    foreach ($packageName in $Name)
    { 
        $randomName = [System.IO.Path]::GetRandomFileName()
        $destinationFolder = Join-Path $script:downloadedCabLocation $randomName

        $baseVersion = $null
        $languageVersion = $null

        foreach ($availablePackage in $availablePackages)
        {
            # check whether base package is already installed
            if (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $packageName -requiredVersion $RequiredVersion -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture "Base")
            {
                $baseVersion = Convert-Version ($availablePackage.Split('~')[4])
            }
            # check whether language pack is installed
            elseif (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $packageName -requiredVersion $RequiredVersion -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $Culture)
            {
                $languageVersion = Convert-Version ($availablePackage.Split('~')[4])
            }
        }

        # no force and both are installed, just returned
        if (-not $Force)
        {
            if ($null -ne $baseVersion -and $null -ne $languageVersion)
            {
                Write-Verbose "Skipping installed package $packageName"
                $successfullyInstalled.Value = $true

                # returned the package to be installed

                if ($null -ne $PackagesToBeInstalled)
                {
                    $PackagesToBeInstalled | Where-Object {$_.Name -eq $packageName} | ForEach-Object {$_.Culture = $Culture; $_}
                }

                continue
            }
        }

        # This means source is offline
        if ((-not [string]::IsNullOrWhiteSpace($source)) -and (Test-Path $source))
        {
            Write-Verbose "Installing package from $source"
            $savedCabFilesToInstall = @($source)
        }
        else
        {
            if (-not (Test-Path $destinationFolder))
            {
                $null = mkdir $destinationFolder
            }

            Write-Verbose "Downloading cab files to $destinationFolder"
            try {
                $script:availablePackages = $availablePackages
                $savedPackages = Save-NanoServerPackage -Name $packageName -Culture $Culture -RequiredVersion $RequiredVersion -MinimumVersion $MinimumVersion `
                                                    -MaximumVersion $MaximumVersion -Path $destinationFolder -Force
            }
            finally {
                $script:availablePackages = @()
            }
        }

        $savedCabFilesToInstall = @()
        $savedCabFilesToInstallTuple = @()

        foreach ($savedPackage in $savedPackages)
        {
            $basePackageFile = (Join-Path $destinationFolder (Get-FileName -name $savedPackage.Name -Culture "" -version $savedPackage.Version))

            $basePackagePath = ""

            if (Test-Path $basePackageFile) {
                $savedCabFilesToInstall += $basePackageFile
                $basePackagePath = $basePackageFile
            }

            # proceed with installation,
            $languagePackageFile = (Join-Path $destinationFolder (Get-FileName -name $savedPackage.Name -Culture $Culture -version $savedPackage.Version))

            $langPackagePath = ""

            if (Test-Path $languagePackageFile) {
                $savedCabFilesToInstall += $languagePackageFile
                $installedWindowsPackages += $savedPackage
                $langPackagePath = $languagePackageFile
            }

            $savedCabFilesToInstallTuple += ([System.Tuple]::Create($basePackagePath, $langPackagePath))
        }

        $restartNeeded = $false

        try
        {
            # Installing offline scenario
            if (-not [string]::IsNullOrWhiteSpace($mountDrive))
            {
                # in this scenario, the function that calls us already mount the drive
                Write-Verbose "Installing to mountdrive $mountDrive"
                $successfullyInstalled.Value = Install-CabOfflineFromPath -mountDrive $mountDrive -packagePaths $savedCabFilesToInstall
            }
            else
            {
                Write-Verbose "Installing cab files $savedCabFilesToInstallTuple"                
                $successfullyInstalled.Value = Install-Online $savedCabFilesToInstallTuple -restartNeeded ([ref]$restartNeeded)

                if ($restartNeeded -and (-not $NoRestart))
                {
                    Write-Warning "Restart is needed to complete installation"
                }
            }
        
        }
        catch
        {
            $successfullyInstalled.Value = $false
            ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName $_.Exception.GetType().FullName `
                        -ExceptionMessage $_.Exception.Message `
                        -ExceptionObject $Name `
                        -ErrorId FailedToInstall `
                        -ErrorCategory InvalidOperation            
        }
        finally
        {
            # Remove the online source
            if (([string]::IsNullOrWhiteSpace($source)) -or (-not (Test-Path $source)))
            {
                Remove-Item $destinationFolder -Recurse -Force
            }
        }
    }

    $installedWindowsPackages
}

###
### SUMMARY: Checks if the system is nano server or not
### Look into the win32 operating system class
### Returns True if running on Nano
### False otherwise
###
function IsNanoServer
{
    if ($script:isNanoServerInitialized)
    {
        return $script:isNanoServer
    }
    else
    {
        $script:isNanoServerInitialized = $true
        $operatingSystem = Get-CimInstance -ClassName win32_operatingsystem
        $script:systemSKU = $operatingSystem.OperatingSystemSKU
        $script:systemVersion = [System.Environment]::OSVersion.Version
        $script:isNanoServer = ($systemSKU -eq 109) -or ($systemSKU -eq 144) -or ($systemSKU -eq 143)
        return $script:isNanoServer
    }
}

###
### SUMMARY: Checks if the given destination is kosher or not
###
function CheckDestination
{
    param($Destination)

    # Check if entire path is folder structure
    $dest_item = Get-Item $Destination `
                            -ErrorAction SilentlyContinue `
                            -WarningAction SilentlyContinue

    if($dest_item -is [System.IO.DirectoryInfo])
    {
        return $true
    }
    else
    {
        Write-Verbose "Creating directory structure: $Destination"
        mkdir $Destination
        return $true
    }

    return $false
}         

function CheckVersion
{
    param
    (
        [System.Version]$MinimumVersion,
        [System.Version]$MaximumVersion,
        [System.Version]$RequiredVersion,
        [switch]$AllVersions
    )

    if($AllVersions -and $RequiredVersion)
    {
        Write-Error "AllVersions and RequiredVersion cannot be used together"
        return $false
    }

    if($AllVersions -or $RequiredVersion)
    {
        if($MinimumVersion -or $MaximumVersion)
        {
            Write-Error "AllVersions and RequiredVersion switch cannot be used with MinimumVersion or MaximumVersion"
            return $false
        }
    }

    if($MinimumVersion -and $MaximumVersion)
    {
        if($MaximumVersion -lt $MinimumVersion)
        {
            Write-Error "Minimum Version cannot be more than Maximum Version"
            return $false
        }
    }

    return $true
}

function Get-FileName
{
    param(
        [string]$Culture,
        [string]$name,
        [string]$version
    )

    $fileName = $name + "_" + $Culture + "_" + $version.replace('.','-') + $script:WindowsPackageExtension
    return $fileName
}

###
### SUMMARY: Get the search index from Azure
###
function Get-SearchIndex
{
    param
    (
        [switch]
        $Force,

        [string]
        $fwdLink
    )
    
    $fullUrl = Resolve-FwdLink $fwdLink
    $fullUrl = $fullUrl.AbsoluteUri
    $destination = $script:WindowsPackage + "\searchNanoPackageIndex.txt"

    if(Test-Path $destination)
    {
        Remove-Item $destination
        DownloadFile -downloadURL $fullUrl `
                -destination $destination `
                -noProgress
    }
    else
    {
        DownloadFile -downloadURL $fullUrl `
                    -destination $destination `
                    -noProgress
    }
    
    return $destination
} 

function Get-ImageCulture
{
    param
    (
        [string]$mountDrive
    )

    $languageSearch = dism /Image:$mountDrive /Get-Intl

    foreach ($languageString in $languageSearch)
    {
        if ($languageString -match "\s*Default\s*system\s*UI\s*language\s*:\s*([a-z][a-z]-[A-Z][A-Z])\s*")
        {
            return $matches[1]
        }
    }

}

###
### SUMMARY: Resolve the fwdlink to get the actual search URL
###
function Resolve-FwdLink
{
    param
    (
        [parameter(Mandatory=$false)]
        [System.String]$Uri
    )
    
    if(-not (IsNanoServer))
    {
        Add-Type -AssemblyName System.Net.Http
    }
    $httpClient = New-Object System.Net.Http.HttpClient
    $response = $httpclient.GetAsync($Uri)
    $link = $response.Result.RequestMessage.RequestUri


    return $link
}

function Resolve-PathHelper
{
    param 
    (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $path,

        [Parameter()]
        [switch]
        $isLiteralPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $callerPSCmdlet
    )
    
    $resolvedPaths =@()

    foreach($currentPath in $path)
    {
        try
        {
            if($isLiteralPath)
            {
                $currentResolvedPaths = Microsoft.PowerShell.Management\Resolve-Path -LiteralPath $currentPath -ErrorAction Stop
            }
            else
            {
                $currentResolvedPaths = Microsoft.PowerShell.Management\Resolve-Path -Path $currentPath -ErrorAction Stop
            }
        }
        catch
        {
            $errorMessage = ("Cannot find the path '{0}' because it does not exist" -f $currentPath)
            ThrowError  -ExceptionName "System.InvalidOperationException" `
                        -ExceptionMessage $errorMessage `
                        -ErrorId "PathNotFound" `
                        -CallerPSCmdlet $callerPSCmdlet `
                        -ErrorCategory InvalidOperation
        }

        foreach($currentResolvedPath in $currentResolvedPaths)
        {
            $resolvedPaths += $currentResolvedPath.ProviderPath
        }
    }

    $resolvedPaths
}

### Function to get package dependencies that need to be install
### This will return false if there is a dependency loop
function Get-DependenciesToInstall($availablePackages, $culture, [psobject]$package, [System.Collections.ArrayList]$dependenciesToBeInstalled)
{
    # no dependencies to be installed
    if ($null -eq $package.Dependencies -or $package.Dependencies.Count -eq 0) {
        $dependenciesToBeInstalled.Add($package) | Out-NUll
        return $true
    }

    $permanentlyMarked = [System.Collections.ArrayList]::new()
    $temporarilyMarked = [System.Collections.ArrayList]::new()

    if (-not (DepthFirstVisit -package $package `
                            -temporarilyMarked $temporarilyMarked `
                            -permanentlyMarked $permanentlyMarked `
                            -dependenciesToBeInstalled $dependenciesToBeInstalled `
                            -culture $culture `
                            -availablePackages $availablePackages)) {
        return $false
    }

    return $true
}

function DepthFirstVisit(
    [psobject]$package,
    [System.Collections.ArrayList]$permanentlyMarked,
    [System.Collections.ArrayList]$temporarilyMarked,
    [System.Collections.ArrayList]$dependenciesToBeInstalled,
    $culture,
    $availablePackages) {
    
    # get the hash of the package which is name!#!version
    $hash = $package.Name.ToLower() + "!#!" + (Convert-Version $package.Version)

    if ($temporarilyMarked.IndexOf($hash) -ge 0) {
        # dependency loop!
        return $false        
    }

    # no need to visit permanently marked node
    if ($permanentlyMarked.IndexOf($hash) -ge 0) {
        return $true
    }

    $temporarilyMarked.Add($hash) | Out-Null

    foreach ($dependency in $package.Dependencies) {
        $skip = $false

        # check which dependencies are already installed
        foreach ($availablePackage in $availablePackages)
        {
            # check whether language pack is installed (don't need to check base because if language pack is installed then base must be there)
            if (Test-PackageWithSearchQuery -fullyQualifiedName $availablePackage -name $dependency.Name -requiredVersion $dependency.Version -Culture $culture)
            {
                # if it is, skipped this dependency
                $skip = $true
            }
        }

        if ($skip) {
            continue
        }

        $dependencyPackage = Find -Name $dependency.Name -RequiredVersion $dependency.Version -Culture $culture

        if (-not (DepthFirstVisit -package $dependencyPackage -permanentlyMarked $permanentlyMarked `
                -temporarilyMarked $temporarilyMarked -culture $culture `
                -availablePackages $availablePackages -dependenciesToBeInstalled $dependenciesToBeInstalled)) {
            return $false
        }
    }

    # add to list to install later
    $dependenciesToBeInstalled.Add($package) | Out-Null

    # mark the node permanently
    $permanentlyMarked.Add($hash) | Out-Null

    # remove the temporary mark
    $temporarilyMarked.Remove($hash) | Out-Null

    return $true
}

<#
Parse and return a dependency version
The version string is either a simple version or an arithmetic range
e.g.
     1.0 --> 1.0 ≤ x
     (,1.0] --> x ≤ 1.0
     (,1.0) --> x lt 1.0
     [1.0] --> x == 1.0
     (1.0,) --> 1.0 lt x
     (1.0, 2.0) --> 1.0 lt x lt 2.0
     [1.0, 2.0] --> 1.0 ≤ x ≤ 2.0
  
#>

function NanoServerVersionMatched([string]$dependencyVersionString, [version]$version)
{
    if ([string]::IsNullOrWhiteSpace($dependencyVersionString) -or $version -eq $null)
    {
        return $true
    }

    $dependencyVersionString = $dependencyVersionString.Trim()

    $first = $dependencyVersionString[0]
    $last = $dependencyVersionString[-1]
    
    if ($first -ne '(' -and $first -ne '[' -and $last -ne ']' -and $last -ne ')')
    {
        # stand alone so it is min inclusive
        $versionToBeCompared = Convert-Version $dependencyVersionString

        return ($versionToBeCompared -ge $version)        
    }

    # now dep version string must have length > 3
    if ($dependencyVersionString.Length -lt 3)
    {
        return $true
    }

    if ($first -ne '(' -and $first -ne '[')
    {
        # first character must be either ( or [
        return $true
    }

    if ($last -ne ']' -and $last -ne ')')
    {
        # last character must be either ] or )
        return $true
    }

    # inclusive if the first or last is [ or ], otherwise exclusive
    $minInclusive = ($first -eq '[')
    $maxInclusive = ($last -eq ']')

    $dependencyVersionString = $dependencyVersionString.Substring(1, $dependencyVersionString.Length - 2)

    $parts = $dependencyVersionString.Split(',')
    
    if ($parts.Length -gt 2)
    {
        return $true
    }

    $minVersion = Convert-Version $parts[0]

    if ($parts.Length -eq 1)
    {
        $maxVersion = $minVersion
    }
    else
    {
        $maxVersion = Convert-Version $parts[1]
    }

    if ($minVersion -eq $null -and $maxVersion -eq $null)
    {
        return $true
    }

    # now we can compare
    if ($minVersion -ne $null)
    {
        if ($minInclusive)
        {
            # min inclusive so version must be >= minversion
            if ($version -lt $minVersion)
            {
                return $false
            }
        }
        else
        {
            # not mininclusive so version must be > minversion
            if ($version -le $minVersion)
            {
                return $false
            }
        }
    }

    if ($maxVersion -ne $null)
    {
        if ($maxInclusive)
        {
            if ($version -gt $maxVersion)
            {
                return $false
            }
        }
        else
        {
            if ($version -ge $maxVersion)
            {
                return $false
            }
        }
    }

    return $true
}

function ConvertNanoServerVersionToString([string]$NanoServerVersion)
{
    $result = $NanoServerVersion

    if ([string]::IsNullOrWhiteSpace($NanoServerVersion))
    {
        return $result
    }

    $NanoServerVersion = $NanoServerVersion.Trim()

    $first = $NanoServerVersion[0]
    $last = $NanoServerVersion[-1]
    
    if ($first -ne '(' -and $first -ne '[' -and $last -ne ']' -and $last -ne ')')
    {
        return "minimum NanoServer version of $NanoServerVersion (inclusive)"
    }

    # now dep version string must have length > 3
    if ($NanoServerVersion.Length -lt 3)
    {
        return $NanoServerVersion
    }

    if ($first -ne '(' -and $first -ne '[')
    {
        # first character must be either ( or [
        return $NanoServerVersion
    }

    if ($last -ne ']' -and $last -ne ')')
    {
        # last character must be either ] or )
        return $NanoServerVersion
    }

    # inclusive if the first or last is [ or ], otherwise exclusive
    $minInclusive = ($first -eq '[')
    $maxInclusive = ($last -eq ']')

    $NanoServerVersion = $NanoServerVersion.Substring(1, $NanoServerVersion.Length - 2)

    $parts = $NanoServerVersion.Split(',')
    
    if ($parts.Length -gt 2)
    {
        return $NanoServerVersion
    }

    $minVersion = $parts[0]

    if ($parts.Length -eq 1)
    {
        $maxVersion = $minVersion
    }
    else
    {
        $maxVersion = $parts[1]
    }

    if ($minVersion -eq $null -and $maxVersion -eq $null)
    {
        return $NanoServerVersion
    }

    $result = ""

    # now we can compare
    if (-not [string]::IsNullOrWhiteSpace($minVersion))
    {
        $result += "minimum NanoServer version of $minVersion"

        if ($minInclusive)
        {
            $result += " (inclusive)"
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($maxVersion))
    {
        # there is already something in result, so add an and
        if (-not [string]::IsNullOrWhiteSpace($result))
        {
            $result += " and "
        }

        $result += "maximum NanoServer version of $maxVersion"
        if ($maxInclusive)
        {
            $result += " (inclusive)"
        }
    }

    return $result
}

#endregion Helpers

#region Source

###
### SUMMARY: Gets the source from where to get the images
### Initializes the variables for find, download and install
### RETURN:
### Returns the type of
###
function Get-Source
{
    param($sources)

    Set-ModuleSourcesVariable

    $listOfSources = @()

    # if sources is supplied and we cannot find it, error out
    if((-not [string]::IsNullOrWhiteSpace($sources)) -and (-not $script:windowsPackageSources.Contains($sources)))
    {
        ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.Exception `
                            -ExceptionMessage "Unable to find package source '$sources'. Use Get-PackageSource to see all available package sources." `
                            -ExceptionObject $sources `
                            -ErrorId WildCardCharsAreNotSupported `
                            -ErrorCategory InvalidData
    }

    foreach($mySource in $script:WindowsPackageSources.Values)
    {
        if((-not $sources) -or
            (($mySource.Name -eq $sources) -or
               ($mySource.Location -eq $sources)))
       {
            $tempHolder = @{}

            $location = $mySource.SourceLocation
            $tempHolder.Add("SourceLocation", $location)
            
            $packageSourceName = $mySource.Name
            $tempHolder.Add("Name", $packageSourceName)
            
            $listOfSources += $tempHolder
        }
    }

    return $listOfSources
}

function Set-ModuleSourcesVariable
{
    if(Microsoft.PowerShell.Management\Test-Path $script:file_modules)
    {
        $script:windowsPackageSources = DeSerializePSObject -Path $script:file_modules
    }
    
    if((-not (Microsoft.PowerShell.Management\Test-Path $script:file_modules)))
    {
        $script:windowsPackageSources = [ordered]@{}
        $defaultModuleName = "NanoServerPackageSource"

        $defaultModuleSource = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
            Name = $script:defaultPackageName
            SourceLocation = $script:defaultPackageLocation
            Trusted=$false
            Registered= $true
            InstallationPolicy = "Untrusted"
        })

        $script:windowsPackageSources.Add($defaultModuleName, $defaultModuleSource)
        Save-ModuleSource
    }
}

function Get-PackageProviderName
{
    return $script:providerName
}

###
### SUMMARY: Deserializes the PSObject
###
function DeSerializePSObject
{
    [CmdletBinding(PositionalBinding=$false)]    
    Param
    (
        [Parameter(Mandatory=$true)]        
        $Path
    )
    $filecontent = Microsoft.PowerShell.Management\Get-Content -Path $Path
    [System.Management.Automation.PSSerializer]::Deserialize($filecontent)    
}

function Save-ModuleSource
{
    # check if exists
    if(-not (Test-Path $script:WindowsPackage))
    {
        $null = mkdir $script:WindowsPackage
   }

    # seralize module
    Microsoft.PowerShell.Utility\Out-File -FilePath $script:file_modules `
                                            -Force `
                                            -InputObject ([System.Management.Automation.PSSerializer]::Serialize($script:windowsPackageSources))
}

function Resolve-PackageSource
{
    Set-ModuleSourcesVariable

    $SourceName = $request.PackageSources

    if(-not $SourceName)
    {
        $SourceName = "*"
    }

    foreach($moduleSourceName in $SourceName)
    {
        if($request.IsCanceled)
        {
            return
        }
        
        $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $moduleSourceName,$script:wildcardOptions
        $moduleSourceFound = $false
        
        $script:windowsPackageSources.GetEnumerator() | 
            Microsoft.PowerShell.Core\Where-Object {$wildcardPattern.IsMatch($_.Key)} | 
                Microsoft.PowerShell.Core\ForEach-Object {
                    $moduleSource = $script:windowsPackageSources[$_.Key]
                    $packageSource = New-PackageSourceFromModuleSource -ModuleSource $moduleSource
                    Write-Output -InputObject $packageSource
                    $moduleSourceFound = $true
                }

        if(-not $moduleSourceFound)
        {
            $sourceName  = Get-SourceName -Location $moduleSourceName
            if($sourceName)
            {
                $moduleSource = $script:windowsPackageSources[$sourceName]
                $packageSource = New-PackageSourceFromModuleSource -ModuleSource $moduleSource
                Write-Output -InputObject $packageSource
            }
        }
    }
}

function Add-PackageSource
{
    [CmdletBinding()]
    param
    (
        [string]
        $Name,
         
        [string]
        $Location,

        [bool]
        $Trusted
    )

    Set-ModuleSourcesVariable
    
    $options = $request.Options
    $Default = $false

    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Default'))
        {
            $Default = $options['Default']
        }
    }

    if($Default)
    {
        $Name = $script:defaultPackageName
        $Location = $script:defaultPackageLocation
    }

    # Check if this package source already exists
    foreach($psModuleSource in $script:windowsPackageSources.Values)
    {
        if(($Name -eq $psModuleSource.Name) -or
                ($Location -eq $psModuleSource.SourceLocation))
        {
            throw "Package Source $Name with $Location already exists"
        }
    }

    # Add new module source
    $moduleSource = Microsoft.PowerShell.Utility\New-Object PSCustomObject -Property ([ordered]@{
            Name = $Name
            SourceLocation = $Location            
            Trusted=$Trusted
            Registered= $true
            InstallationPolicy = if($Trusted) {'Trusted'} else {'Untrusted'}
        })

    $script:windowsPackageSources.Add($Name, $moduleSource)
    Save-ModuleSource
    Write-Output -InputObject (New-PackageSourceFromModuleSource -ModuleSource $moduleSource)
}

function Remove-PackageSource
{
    param
    (
        [string]
        $Name
    )
    
    Set-ModuleSourcesVariable -Force

    if(-not $script:windowsPackageSources.Contains($Name))
    {
        Write-Error -Message "Package source $Name not found" `
                        -ErrorId "Package source $Name not found" `
                        -Category InvalidOperation `
                        -TargetObject $Name
        continue
    }

    $script:windowsPackageSources.Remove($Name)

    Save-ModuleSource
}

function New-PackageSourceFromModuleSource
{
    param
    (
        [Parameter(Mandatory=$true)]
        $ModuleSource
    )

    $packageSourceDetails = @{}

    # create a new package source
    $src =  New-PackageSource -Name $ModuleSource.Name `
                              -Location $ModuleSource.SourceLocation `
                              -Trusted $ModuleSource.Trusted `
                              -Registered $ModuleSource.Registered `
                              -Details $packageSourceDetails

    # return the package source object.
    Write-Output -InputObject $src
}

function Get-SourceName
{
    [CmdletBinding()]
    [OutputType("string")]
    Param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Location
    )

    Set-ModuleSourcesVariable

    foreach($psModuleSource in $script:windowsPackageSources.Values)
    {
        if(($psModuleSource.Name -eq $Location) -or
           ($psModuleSource.SourceLocation -eq $Location))
        {
            return $psModuleSource.Name
        }
    }
}

#endregion Source

#region OneGet

function Find-Package
{ 
    [CmdletBinding()]
    param
    (
        [string]
        $Name,

        [string]
        $requiredVersion,

        [string]
        $minimumVersion,

        [string]
        $maximumVersion
    )

    $options = $request.Options
    $languageChosen = $null
    $wildcardPattern = $null
    $force = $false
    $allVersions = $false

    # path to the offline nano image
    $imagePath = $null
    $source = $null

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("ImagePath"))
        {
            $imagePath = $options['ImagePath']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']
        }

        if ($options.ContainsKey('Source'))
        {
            $source = $options['Source']
        }

        if ($options.ContainsKey('AllVersions'))
        {
            $allVersions = $options['AllVersions']
        }
    }

    <# Commented out because we are not handling source yet
    # no source given then search online
    if ($null -eq $source)
    {
    }
    else
    {
        # If name is null or whitespace, interpret as *
        if ([string]::IsNullOrWhiteSpace($Name))
        {
            $Name = "*"
        }
 
        if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
        {
            $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $Name,$script:wildcardOptions
        }
 
        # For now, accept offline source like directory
        if (Test-Path $source)
        {
            $count = 0
            $cabFiles = 0
            $files = Get-ChildItem $source -File
 
            # count number of .cab
            foreach ($file in $files)
            {
                if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                {
                    $cabFiles += 1
                }
            }
 
            if ($cabFiles -le 0)
            {
                return
            }
 
            $id = Write-Progress -ParentId 1 -Activity "Finding packages in $source"
 
            if (-not $id)
            {
                $id = 1
            }
 
            if (-not [string]::IsNullOrWhiteSpace($imagePath))
            {
                if (-not ([System.IO.File]::Exists($ImagePath)))
                {
                    ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName System.ArgumentException `
                        -ExceptionMessage "$ImagePath does not exist" `
                        -ExceptionObject $imagePath `
                        -ErrorId "InvalidImagePath" `
                        -ErrorCategory InvalidData
 
                    return
                }
 
                $mountDrive = $null
 
                # have to mount
                $mountDrive = New-MountDrive
         
                Write-Progress -Activity "Mounting $imagePath to $mountDrive" -PercentComplete 0 -Id $id
 
                Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive
 
                try
                {
                    foreach ($file in $files)
                    {
                        if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                        {
                            # scale the percent from 1 to 80 to account for the initial and final step of mounting and dismounting
                            $percentComplete = (($count*80/$cabFiles) + 10) -as [int]
                            $count += 1
 
                            Write-Progress -Activity `
                                "Getting package information for $($package.PackageName) in $mountDrive" `
                                -PercentComplete $percentComplete `
                                -Id $id
 
                            $package = Get-WindowsPackage -PackagePath $file.FullName -Path $mountDrive
 
                            if (Test-PackageWithSearchQuery -fullyQualifiedName $package.PackageName -requiredVersion $RequiredVersion -Name $Name `
                                -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                            {
                                Write-Output (New-SoftwareIdentityPackage $package -src $source -InstallLocation $file.FullName)
                            }
                        }
                    }
                }
                finally
                {
                    # time to unmount
                    Write-Progress -Activity "Unmounting image from $mountDrive" -PercentComplete 90 -Id $id
                    Remove-MountDrive $mountDrive
                    Write-Progress -Completed -Id $id -Activity "Completed"
                }
            }
            else
            {
                try
                {
                    #online scenario
                    foreach ($file in $files)
                    {
                        # only checks for .cab extension
                        if ([System.IO.Path]::GetExtension($file) -eq '.cab')
                        {
                            $percentComplete = ($count*100/$cabFiles) -as [int]
                            $count += 1
                                                         
                            Write-Progress -Activity `
                                "Getting package information for $($package.PackageName)" `
                                -PercentComplete $percentComplete `
                                -Id $id
 
                            $package = Get-WindowsPackage -PackagePath $file.FullName -Online
 
                            if (Test-PackageWithSearchQuery -fullyQualifiedName $package.PackageName -requiredVersion $RequiredVersion -Name $Name `
                                -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                            {
                                Write-Output (New-SoftwareIdentityPackage $package -src $source -InstallLocation $file.FullName)
                            }
                        }
                    }
                }
                finally
                {
                    Write-Progress -Completed -Id $id -Activity "Completed"
                }
            }
 
        }
        else
        {
            ThrowError -CallerPSCmdlet $PSCmdlet `
                        -ExceptionName "System.ArgumentException" `
                        -ExceptionMessage "Source does not point to a valid directory" `
                        -ErrorId "InvalidSource" `
                        -ErrorCategory InvalidArgument `
                        -ExceptionObject $options
        }
    }
    #>


    # Let find-windowspackage handle the query
    $convertedRequiredVersion = Convert-Version $requiredVersion
    $convertedMinVersion = Convert-Version $minimumVersion
    $convertedMaxVersion = Convert-Version $maximumVersion

    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = @('*')
    }

    $packages = Find -Name $Name `
        -MinimumVersion $convertedMinVersion `
        -MaximumVersion $convertedMaxVersion `
        -RequiredVersion $convertedRequiredVersion `
        -AllVersions:$AllVersions `
        -Culture $languageChosen `
        -Force:$Force
# -Repository $Repository

    if ($null -eq $packages)
    {
        return
    }

    # check for packages that match the query
    foreach ($package in $packages)
    {
        $swid = New-SoftwareIdentityFromWindowsPackageItemInfo $package
        Write-Output $swid
    }
}

function Install-Package
{
    [CmdletBinding()]
    param
    (
        [string]
        $fastPackageReference
    )

    Write-Verbose $fastPackageReference

    # path to the offline nano image
    $imagePath = $null

    $options = $request.Options

    $NoRestart = $false

    $force = $false

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("ToVhd"))
        {
            $imagePath = $options['ToVhd']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']
        }

        if ($options.ContainsKey("NoRestart"))
        {
            $NoRestart = $options['NoRestart']
        }
    }

    # if image path is supplied and it points to non existing file, returns
    if (-not [string]::IsNullOrWhiteSpace($imagePath) -and (-not ([System.IO.File]::Exists($ImagePath))))
    {
       ThrowError -CallerPSCmdlet $PSCmdlet `
            -ExceptionName System.ArgumentException `
            -ExceptionMessage "$ImagePath does not exist" `
            -ExceptionObject $imagePath `
            -ErrorId "InvalidImagePath" `
            -ErrorCategory InvalidData

        return
    }

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None)

    $name = $resultArray[0]
    $version = $resultArray[1]
    #$source = $resultArray[2]
    $Culture = $resultArray[3]
    $Sku = $resultArray[4]
    $NanoServerVersion = $resultArray[5]

    # if culture is a string, set it to null (this means user did not supply culture)
    if ($Culture.Contains(','))
    {
        $Culture = ''
    }

    $convertedVersion = Convert-Version $version

    [bool]$success = $false

    $mountDrive = $null

    $availablePackages = @()

    if (-not [string]::IsNullOrWhiteSpace($imagePath))
    {
        $mountDrive = New-MountDrive

        Write-Verbose "Mounting $imagePath to $mountDrive"

        $null = Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive
        
        if (-not $force) {
            $fileKey = Get-FileKey -filePath $imagePath
          
            $availablePackages = @(($script:imagePathCache[$fileKey]).Keys)
        }

        # if this package does not apply to standard, we have to check whether the nano is standard or not
        if (-not $Sku.Contains("144") -or (-not [string]::IsNullOrWhiteSpace($NanoServerVersion)))
        {
            $regKey = $null

            $mountedVhdEdition = "ERROR"
            $vhdNanoServerVersion = $null

            try
            {
                reg load HKLM\NANOSERVERPACKAGEVHDSYS "$mountDrive\Windows\System32\config\SOFTWARE" | Out-Null
                $regKey = dir 'HKLM:\NANOSERVERPACKAGEVHDSYS\Microsoft\Windows NT'
                $mountedVHDEdition = $regKey.GetValue("EditionID")
                $majorVersion = $regKey.GetValue("CurrentMajorVersionNumber")
                $minorVersion = $regKey.GetValue("CurrentMinorVersionNumber")
                $buildVersion = $regKey.GetValue("CurrentBuildNumber")
                $vhdNanoServerVersion = [version]::new($majorVersion, $minorVersion, $buildVersion, 0)
            }
            catch
            {
                # ERROR
                $mountedVHDEdition = "ERROR"
                $vhdNanoServerVersion = $null
            }
            finally
            {
                try
                {
                    if ($regKey -ne $null)
                    {
                        $regKey.Handle.Close()
                        [gc]::Collect()
                        reg unload HKLM\NANOSERVERPACKAGEVHDSYS | Out-Null
                    }
                }
                catch { }
            }

            # if this is not applicable to server standard nano
            if (-not $Sku.Contains("144") -and $mountedVHDEdition -eq "ServerStandardNano")
            {
                # cannot be installed
                # unmount
                if ($null -ne $mountDrive)
                {
                    Write-Verbose "Unmounting mountdrive $mountDrive"
                    Remove-MountDrive $mountDrive -discard $true
                }

                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "$name cannot be installed on this edition of NanoServer" `
                            -ExceptionObject $fastPackageReference `
                            -ErrorId FailedToInstall `
                            -ErrorCategory InvalidData
            }

            if (-not [string]::IsNullOrWhiteSpace($NanoServerVersion) -and -not (NanoServerVersionMatched -dependencyVersionString $NanoServerVersion -version $vhdNanoServerVersion))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "The package '$name' with version $version requires $(ConvertNanoServerVersionToString $NanoServerVersion). The current NanoServer has version $vhdNanoServerVersion which is out of this range. Please run 'Find-NanoServerPackage $name -AllVersions | select name,version,NanoServerVersion' and use the NanoServerVersion property, which specifies the version range of the NanoServer that a package can be installed on, to find a suitable version. After that, run 'Install-NanoServerPackage $name -RequiredVersion <Correct Version>' to install the correct version." `
                            -ExceptionObject $fastPackageReference `
                            -ErrorId FailedToInstall `
                            -ErrorCategory InvalidData
            }
        }
    }
    else {
        if (IsNanoServer)
        {
            # if this is a nano, then systemSKU would be populated after isnanoserver call
            if (-not $Sku.Contains($script:systemSKU.ToString()))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "$name cannot be installed on this edition of NanoServer" `
                            -ExceptionObject $fastPackageReference `
                            -ErrorId FailedToInstall `
                            -ErrorCategory InvalidData
            }

            # if this is nanoserver, then we should also have the version populated
            if (-not (NanoServerVersionMatched -dependencyVersionString $NanoServerVersion -version $script:systemVersion))
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "The package '$name' with version $version requires $(ConvertNanoServerVersionToString $NanoServerVersion). The current NanoServer has version $script:systemVersion which is out of this range. Please run 'Find-NanoServerPackage $name -AllVersions | select name,version,NanoServerVersion' and use the NanoServerVersion property, which specifies the version range of the NanoServer that a package can be installed on, to find a suitable version. After that, run 'Install-NanoServerPackage $name -RequiredVersion <Correct Version>' to install the correct version." `
                            -ExceptionObject $fastPackageReference `
                            -ErrorId FailedToInstall `
                            -ErrorCategory InvalidData
            }
        }

        if (-not $force) {
            $availablePackages = @($script:onlinePackageCache.Keys)
        }
    }

    try
    {
        $installedPackages = Install-PackageHelper -Name $name `
                                                    -Culture $Culture `
                                                    -Version $convertedVersion `
                                                    -mountDrive $mountDrive `
                                                    -successfullyInstalled ([ref]$success) `
                                                    -NoRestart:$NoRestart `
                                                    -availablePackages: $availablePackages

        foreach ($installedPackage in $installedPackages)
        {        
            Write-Output (New-SoftwareIdentityFromWindowsPackageItemInfo ($installedPackage))
        }
    }
    finally
    {
        # unmount
        if ($null -ne $mountDrive)
        {
            Write-Verbose "Unmounting mountdrive $mountDrive"
            Remove-MountDrive $mountDrive -discard (-not $success)
        }
    }
}

function Download-Package
{ 
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $FastPackageReference,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Location
    )

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None)

    $name = $resultArray[0]
    $version = $resultArray[1]
    #$source = $resultArray[2]
    $Culture = $resultArray[3]
    $convertedVersion = Convert-Version $version

    # if culture is a string, set it to null (this means user did not supply culture)
    if ($Culture.Contains(','))
    {
        $Culture = ''
    }

    # no culture given, use culture of the system
    if ([string]::IsNullOrWhiteSpace($Culture))
    {
        $Culture = (Get-Culture).Name
    }

    $force = $false
    $options = $request.Options

    if ($options)
    {
        if ($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }
    }

    $savedWindowsPackageItems = Save-NanoServerPackage -Name $name `
                                                        -Culture $Culture `
                                                        -RequiredVersion $convertedVersion `
                                                        -Path $Location `
                                                        -Force:$force

    foreach ($savedWindowsPackageItem in $savedWindowsPackageItems)
    {
        Write-Output (New-SoftwareIdentityFromWindowsPackageItemInfo $savedWindowsPackageItem)
    }
}

function Get-InstalledPackage
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [string]
        $Name,

        [Parameter()]
        [Version]
        $RequiredVersion,

        [Parameter()]
        [Version]
        $MinimumVersion,

        [Parameter()]
        [Version]
        $MaximumVersion
    )

    $options = $request.Options
    $wildcardPattern = $null
    $languageChosen = $null

    # If name is null or whitespace, interpret as *
    if ([string]::IsNullOrWhiteSpace($Name))
    {
        $Name = "*"
    }

    if ([System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name))
    {
        $wildcardPattern = New-Object System.Management.Automation.WildcardPattern $Name,$script:wildcardOptions
    }

    $force = $false

    # path to the offline nano image
    $imagePath = $null

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("FromVhd"))
        {
            $imagePath = $options['FromVhd']
        }
        elseif ($options.ContainsKey("ToVhd"))
        {
            # in case of install
            $imagePath = $options['ToVhd']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']

            $cannotConvertCulture = $false

            # try to convert the culture
            try
            {
                $convertedCulture = [cultureinfo]$languageChosen

                # apparently, converting culture 'blah' will not work but 'bla' will work ?!?
                if ($null -eq $convertedCulture -or $null -eq $convertedCulture.DisplayName -or $convertedCulture.DisplayName.Trim() -match "Unknown Language")
                {
                    $cannotConvertCulture = $true
                }
            }
            catch
            {
                $cannotConvertCulture = $true
            }

            # if we cannot convert culture, throw error
            if ($cannotConvertCulture)
            {
                ThrowError -CallerPSCmdlet $PSCmdlet `
                            -ExceptionName System.ArgumentException `
                            -ExceptionMessage "$languageChosen is not a valid culture" `
                            -ExceptionObject $languageChosen `
                            -ErrorId InvalidCulture `
                            -ErrorCategory InvalidData
            }
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($imagePath))
    {
        $mountDrive = New-MountDrive   

        Write-Verbose "Mounting $imagePath to $mountDrive"

        $id = Write-Progress -ParentId 1 -Activity "Getting packages information"
        if (-not $id)
        {
            $id = 1
        }

        Write-Progress -Activity "Mounting $imagePath to $mountDrive" -PercentComplete 0 -Id $id

        Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive

        Write-Verbose "Done Mounting"

        # Now we can try to find the packages
        try
        {
            # Get all the available packages on the mountdrive
            $packages = Get-WindowsPackage -Path $mountDrive
            Write-Verbose "Finished getting packages from $mountDrive with $($packages.Count) packages"
            $count = 0

            Write-Progress -Activity "Getting packages information from $mountDrive" -PercentComplete 5 -Id $id
            
            $packagesToBeReturned = New-Object 'System.Collections.Generic.List[string]'

            $availablePackages = $packages.PackageName.ToLower()

            # check for packages that match the query
            foreach ($fullyQualifiedName in $availablePackages)
            {
                if (Test-PackageWithSearchQuery -fullyQualifiedName $fullyQualifiedName -requiredVersion $RequiredVersion -Name $Name `
                    -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildcardPattern)
                {
                    $packagesToBeReturned.Add($fullyQualifiedName)
                }
            }

            $fileKey = Get-FileKey -filePath $imagePath

            $packageDictionary = @{}

            # try to get the cache if it exists, otherwise create one
            if (-not $script:imagePathCache.ContainsKey($fileKey))
            {                    
                $script:imagePathCache.Add($fileKey, $packageDictionary)
            }

            $packageDictionary = $script:imagePathCache[$fileKey]

            foreach ($fullyQualifiedName in $availablePackages)
            {
                if (-not $packageDictionary.ContainsKey($fullyQualifiedName))
                {
                    $packageDictionary[$fullyQualifiedName] = $null
                }
            }

            # Before we get more details, we will clump together base and language pack if they have same name and version
            if ($packagesToBeReturned.Count -gt 0)
            {
                $packagesToBeReturned = Filter-Packages $packagesToBeReturned
            }

            foreach ($package in $packagesToBeReturned)
            {
                # scale the percent from 1 to 80 to account for the initial and final step of mounting and dismounting
                $percentComplete = (($count*80/$packages.Count) + 10) -as [int]
                $count += 1

                Write-Progress -Activity `
                    "Getting package information for $package in $mountDrive" `
                    -PercentComplete $percentComplete `
                    -Id $id                

                # store the information in cache if it's not there or if user uses force
                if ((-not $packageDictionary.ContainsKey($package)) -or ($null -eq $packageDictionary[$package]) -or $force)
                {
                    Write-Debug "Getting information for package $package and storing it in cache"
                    # store the information in cache
                    $packageDictionary[$package.ToLower()] = Get-WindowsPackage -PackageName $package -Path $mountDrive
                }

                Write-Output (New-SoftwareIdentityPackage $packageDictionary[$package.ToLower()] -src $imagePath)
            }

            # Get the list of packages that are in the cache but not in the latest list we have
            $packageToBeRemoved = @()
            foreach ($pkg in $packageDictionary.GetEnumerator())
            {
                if (-not $availablePackages.Contains($pkg.Name))
                {
                    $packageToBeRemoved += $pkg.Name
                }
            }

            # Remove packages in this list from the cache
            foreach ($pkg in $packageToBeRemoved)
            {
                if ($packageDictionary.ContainsKey($pkg))
                {
                    $packageDictionary.Remove($pkg)
                }
            }
        }
        finally
        {
            Write-Progress -Activity "Unmounting image from $mountDrive" -PercentComplete 90 -Id $id
            # Unmount and delete directory
            Remove-MountDrive $mountDrive
            Write-Progress -Completed -Id $id -Activity "Completed"
        }
    }
    else
    {
        $count = 0;
        $id = Write-Progress -ParentId 1 -Activity "Getting packages information"
        if (-not $id)
        {
            $id = 1
        }
        
        Write-Progress -Activity "Getting available packages on the system" -PercentComplete 0 -Id $id
        # getting the packages on the current operating system
        # getting basic information about all the packages online
        $packages = Get-WindowsPackage -Online

        try
        {
            $packagesToBeReturned = New-Object 'System.Collections.Generic.List[string]'
            $availablePackages = $packages.PackageName.ToLower()

            # Get the list of packages that match what the user input
            foreach ($fullyQualifiedName in $availablePackages)
            {
                if (Test-PackageWithSearchQuery -fullyQualifiedName $fullyQualifiedName -requiredVersion $RequiredVersion -Name $Name `
                    -minimumVersion $MinimumVersion -maximumVersion $MaximumVersion -Culture $languageChosen -wildCardPattern $wildCardPattern)
                {
                    # Store the whole name instead of just the name without language or version
                    $packagesToBeReturned.Add($fullyQualifiedName)
                }

                if (-not ($script:onlinePackageCache.ContainsKey($fullyQualifiedName)))
                {
                    $script:onlinePackageCache[$fullyQualifiedName] = $null
                }
            }

            # nothing matched!
            if ($packagesToBeReturned.Count -gt 0)
            {
                # Before we get more details, we will clump together base and language pack if they have same name and version
                $packagesToBeReturned = Filter-Packages $packagesToBeReturned
            }

            # Only update the list of packages that the user gives
            foreach ($package in $packagesToBeReturned)
            {
                $percentComplete = ($count*90/$packages.Count + 10) -as [int]
                Write-Progress -Activity "Getting package information for $($package)" -PercentComplete $percentComplete -Id $id
                $count += 1;

                # store the information in cache if it's not there or if user uses force
                if ((-not $script:onlinePackageCache.ContainsKey($package)) -or ($null -eq $script:onlinePackageCache[$package]) -or $force)
                {
                    Write-Debug "Getting information for package $package and storing it in cache"
                    # store the information in cache
                    $script:onlinePackageCache[$package.ToLower()] = Get-WindowsPackage -Online -PackageName $package
                }

                if ($script:onlinePackageCache.ContainsKey($package))
                {
                    # convert package to swid and return
                    Write-Output (New-SoftwareIdentityPackage $script:onlinePackageCache[$package] -src "Local Machine")
                }
            }
            
            # Get the list of packages that are in the cache but not in the latest list we have
            $packageToBeRemoved = @()
            foreach ($pkg in $script:onlinePackageCache.GetEnumerator())
            {
               if (-not $availablePackages.Contains($pkg.Name))
                {
                    $packageToBeRemoved += $pkg.Name
                }
            }

            # Remove packages in this list from the cache
            foreach ($pkg in $packageToBeRemoved)
            {
                if ($script:onlinePackageCache.ContainsKey($pkg))
                {
                    $script:onlinePackageCache.Remove($pkg)
                }
            }
        }
        finally 
        {
            Write-Progress -Completed -Id $id -Activity "Completed"
        }
    }
}

function Uninstall-Package
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $fastPackageReference
    )


    Write-Verbose $fastPackageReference

    # path to the offline nano image
    $imagePath = $null

    $options = $request.Options

    $NoRestart = $false

    $force = $false

    $languageChosen = $null

    # check out what options the users give us
    if($options)
    {
        foreach( $o in $options.Keys )
        {
            Write-Debug ("OPTION dictionary: {0} => {1}" -f ($o, $options[$o]) )
        }

        if($options.ContainsKey('Force'))
        {
            $force = $options['Force']
        }

        if ($options.ContainsKey("FromVhd"))
        {
            $imagePath = $options['FromVhd']
        }

        if ($options.ContainsKey("Culture"))
        {
            $languageChosen = $options['Culture']
        }

        if ($options.ContainsKey("NoRestart"))
        {
            $NoRestart = $options['NoRestart']
        }
    }

    # if image path is supplied and it points to non existing file, returns
    if (-not [string]::IsNullOrWhiteSpace($imagePath) -and (-not ([System.IO.File]::Exists($ImagePath))))
    {
       ThrowError -CallerPSCmdlet $PSCmdlet `
            -ExceptionName System.ArgumentException `
            -ExceptionMessage "$ImagePath does not exist" `
            -ExceptionObject $imagePath `
            -ErrorId "InvalidImagePath" `
            -ErrorCategory InvalidData

        return
    }

    [string[]] $splitterArray = @("$separator")
    
    [string[]] $resultArray = $fastPackageReference.Split($splitterArray, [System.StringSplitOptions]::None)

    $packageId = $resultArray[4]

    $basePackage = $null

    if ($null -eq $languageChosen) {
        Write-Verbose "No language chosen, removing base too"

        $packageFragments = $packageId.Split("~")

        $packageFragments[3] = ""

        $basePackage = [string]::Join("~", $packageFragments)

        Write-Debug "New package id is $packageId and the base package is $basePackage"
    }

    if (-not [string]::IsNullOrWhiteSpace($imagePath)) {
        # removing from vhd
        $mountDrive = New-MountDrive

        Write-Verbose "Mounting $imagePath to $mountDrive"

        $null = Mount-WindowsImage -ImagePath $imagePath -Index 1 -Path $mountDrive
        
        $success = $false

        try {
            Write-Verbose "Removing $packageId from $mountDrive"

            # time to update the cache since we remove this package
            $fileKey = Get-FileKey -filePath $imagePath

            if ($script:imagePathCache.ContainsKey($fileKey)) {
                $packageDictionary = $script:imagePathCache[$fileKey]

                if ($null -ne $packageDictionary) {
                    if ($packageDictionary.ContainsKey($packageId)) {
                        Remove-WindowsPackage -PackageName $packageId -Path $mountDrive | Out-Null
                        $packageDictionary.Remove($packageId)
                    }
                }
                else {
                    # nothing in cache
                    Remove-WindowsPackage -PackageName $packageId -Path $mountDrive | Out-Null
                }
            }            


            if (-not ([string]::IsNullOrWhiteSpace($basePackage)))
            {
                Remove-WindowsPackage -PackageName $basePackage -Path $mountDrive | Out-Null
            }

            if ($script:imagePathCache.ContainsKey($fileKey)) {
                $packageDictionary = $script:imagePathCache[$fileKey]

                if ($null -ne $packageDictionary)
                {
                    if ($packageDictionary.ContainsKey($packageId))
                    {
                        $packageDictionary.Remove($packageId)
                    }

                    if ((-not [string]::IsNullOrWhiteSpace($basePackage)) -and $packageDictionary.ContainsKey($basePackage))
                    {
                        $packageDictionary.Remove($basePackage)
                    }
                }
            }

            $success = $true
        }
        catch {
            $success = $false
        }
        finally {
            # unmount
            if ($null -ne $mountDrive)
            {
                Write-Verbose "Unmounting mountdrive $mountDrive"
                Remove-MountDrive $mountDrive -discard (-not $success)
            }
        }
    }
    else {
        Write-Verbose "Uninstalling $packageId online"

        $messages = $null

        if ($script:onlinePackageCache.ContainsKey($packageId)) {
            # removing online
            $messages = Remove-WindowsPackage -PackageName $packageId -Online -NoRestart -WarningAction Ignore
            $script:onlinePackageCache.Remove($packageId)
        }

        $restart = $messages -ne $null -and $messages.RestartNeeded

        if (-not [string]::IsNullOrWhiteSpace($basePackage))
        {
            if ($script:onlinePackageCache.ContainsKey($basePackage)) {
                $messages = Remove-WindowsPackage -PackageName $basePackage -Online -NoRestart -WarningAction Ignore       
                $script:onlinePackageCache.Remove($basePackage)
                $restart = $restart -or ($messages -ne $null -and $messages.RestartNeeded)
            }
        }

        if ($restart -and (-not $NoRestart))
        {
            Write-Warning "Restart is needed to complete installation"
        }
    }
}

#endregion OneGet

#region OneGet Helpers

# This is to display long name
function Get-Feature 
{
    Write-Output -InputObject (New-Feature -Name "DisplayLongName")
}

function Get-DynamicOptions
{
    param
    (
        [Microsoft.PackageManagement.MetaProvider.PowerShell.OptionCategory] 
        $category
    )

    switch($category)
    {
        # This is for dynamic options used by install/uninstall and get-packages
        Install 
        {
            # Switch to display culture
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "NoRestart" -ExpectedType Switch -IsRequired $false)
            # Provides path to image
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "ToVhd" -ExpectedType File -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "FromVhd" -ExpectedType File -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "DisplayCulture" -ExpectedType Switch -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "Culture" -ExpectedType String -IsRequired $false)
        }
        Package
        {
            # Switch to display culture
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "DisplayCulture" -ExpectedType Switch -IsRequired $false)
            # Provides path to image
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "ImagePath" -ExpectedType String -IsRequired $false)
            Write-Output -InputObject (New-DynamicOption -Category $category -Name "Culture" -ExpectedType String -IsRequired $false)
        }
        Source
        {
            Write-Output -InputObject (New-DynamicOption -Category $Category -Name "Default" -ExpectedType Switch -IsRequired $false)
        }
    }
}

function Initialize-Provider
{
    write-debug "In $script:providerName - Initialize-Provider"
}

function Get-PackageProviderName
{
    return $script:providerName
}

function New-SoftwareIdentityFromWindowsPackageItemInfo
{
    [Cmdletbinding()]
    param(
        [PSCustomObject]
        $package
    )

    $details = @{}
    $Culture = $package.Culture

    $fastPackageReference = $package.Name + 
                                $separator + $package.version + 
                                $separator + $package.Source + 
                                $separator + $Culture +
                                $separator + $package.Sku +
                                $separator + $package.NanoServerVersion

    $Name = [System.IO.Path]::GetFileNameWithoutExtension($package.Name)

    $deps = (new-Object -TypeName  System.Collections.ArrayList)

    foreach( $dep in $package.Dependencies ) 
    {
        # Add each dependency and say it's from this provider.
        $newDep = New-Dependency -ProviderName $script:providerName `
                                 -PackageName $dep.Name `
                                 -Version $dep.Version
        $deps.Add( $newDep )
    }

    $details["Sku"] = $package.Sku
    $details["NanoServerVersion"] = $package.NanoServerVersion

    $params = @{FastPackageReference = $fastPackageReference;
                Name = $Name;
                Version = $package.version.ToString();
                versionScheme  = "MultiPartNumeric";
                Source = $package.Source;
                Summary = $package.Description;
                Details = $details;
                Culture = $Culture;
                Dependencies = $deps;
                }

    try
    {
        New-SoftwareIdentity @params
    }
    catch
    {
        # throw error because older version of packagemanagement does not have culture key
        $params.Remove("Culture")
        New-SoftwareIdentity @params
    }
}

# this function is used by get-installedpackage
function New-SoftwareIdentityPackage
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [Microsoft.Dism.Commands.AdvancedPackageObject]
        $package,

        $src="",

        $InstallLocation=""
    )

    $details = @{}

    $details.Add("Applicable", $package.Applicable)

    if ($null -ne $package.InstallTime)
    {
        $details.Add("InstallTime", $package.InstallTime)
    }

    if ($null -ne $package.CompletelyOfflineCapable)
    {
        $details.Add("CompletelyOfflineCapable", $package.CompletelyOfflineCapable)
    }

    if ($null -ne $package.PackageState)
    {
        $details.Add("PackageState", $package.PackageState)
    }        

    if ($null -ne $package.RestartRequired)
    {
        $details.Add("RestartRequired", $package.RestartRequired)
    }

    if (-not [string]::IsNullOrWhiteSpace($package.ReleaseType))
    {
        $details.Add("ReleaseType", $package.ReleaseType)
    }        

    if ([string]::IsNullOrWhiteSpace($Package.ProductVersion)) {
        $version = "0.0"
    }
    else {
        $version = $Package.ProductVersion
    }

    # format is name~publickeytoken~architecture~language~version

    $packageNameFractions = $Package.PackageName.Split('~')

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[0]))
    {
        $name = $packageNameFractions[0]
    }
    else
    {
        $name = $package.PackageName
    }

    # DISM team has a workaround where they add Feature in the name. We should remove that.
    # THIS IS A TEMPORARY FIX
    if ($name -like "*Feature-Package*")
    {
        $name = $name -replace "Feature-Package","Package"
    }

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[1]))
    {
        $details.Add("publickey", $packageNameFractions[1])
    }

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[2]))
    {
        $details.Add("architecture", $packageNameFractions[2])
    }

    $Culture = $packageNameFractions[3]
    
    # $details.Add("language", $language)

    if (-not [string]::IsNullOrWhiteSpace($packageNameFractions[4]))
    {
        $version = $packageNameFractions[4]
    }

    $fastPackageReference = $name + $separator + $version + $separator + $InstallLocation + $separator + $Culture + $separator + $package.PackageName

    $params = @{FastPackageReference = $fastPackageReference;
                Name = $name;
                Version = $version;
                versionScheme  = "MultiPartNumeric";
                Source = $src;
                Details = $details;
                Culture = $Culture;
                TagId = $Package.PackageName;
                }

    try
    {
        New-SoftwareIdentity @params
    }
    catch
    {
        # throw error because older version of packagemanagement does not have culture key
        $params.Remove("Culture")
        $params.Remove("TagId")
        New-SoftwareIdentity @params
    }
}

function Install-CabOfflineFromPath
{
    [CmdletBinding()]
    param
    (
        [string]$mountDrive,

        [string[]]$packagePaths
    )

    $discard = $false

    $id = Write-Progress -ParentId 1 -Activity "Installing packages"

    if (-not $id)
    {
        $id = 1
    }  

    # Now we can try to install the package
    try
    {
        $count = 0

        foreach ($packagePath in $packagePaths)
        {
            $percentComplete = ($count*100/$packagePaths.Count) -as [int]

            $count += 1

            Write-Progress -Activity `
                "Installing package $packagePath" `
                -PercentComplete $percentComplete `
                -Id $id                

            Write-Verbose "Adding $packagePath to $mountDrive"
            Add-WindowsPackage -PackagePath $packagePath -Path $mountDrive -NoRestart -WarningAction Ignore | Out-Null
        }
    }
    catch
    {
        $discard = $true
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $RequiredVersion `
                    -ErrorId FailedToInstall `
                    -ErrorCategory InvalidOperation
    }
    finally
    {
        Write-Progress -Completed -Id $id -Activity "Completed"
    }

    # returns back whether we have successfully installed or not
    return (-not $discard)
}

function Install-Online
{
    [CmdletBinding()]
    param
    (
        $packagePaths,
        [ref]$restartNeeded
    )

    $rollBack = $false

    $count = 0;
    Write-Verbose "Installing $($packagePaths.Count) packages online"
    $id = Write-Progress -ParentId 1 -Activity "Installing packages online"
    if (-not $id)
    {
        $id = 1
    }

    try
    {
        # first package of each pair is base, second is language

        foreach ($packageTuple in $packagePaths)
        {
            $packagePath = $packageTuple.Item1

            $messages = $null

            $restart = $false

            $percentComplete = $count*100/$packagePaths.Count -as [int]

            for($i = 0; $i -lt 2; $i += 1)
            {
                # valid base path
                if (-not [string]::IsNullOrWhiteSpace($packagePath))
                {
                    Write-Progress -Activity "Installing package $($packagePath)" -PercentComplete $percentComplete -Id $id

                    try
                    {
                        $messages = Add-WindowsPackage -PackagePath $packagePath -Online -NoRestart -WarningAction Ignore -ErrorAction Ignore

                        if ($messages -ne $null -and $messages.RestartNeeded)
                        {
                            $restart = $true
                        }
                    }
                    catch { }
                }

                # now install the language, even if the base fails, sometimes language will succeed
                $packagePath = $packageTuple.Item2

                if (-not [string]::IsNullOrWhiteSpace($packagePath))
                {
                    Write-Progress -Activity "Installing package $($packagePath)" -PercentComplete $percentComplete -Id $id

                    $hasError = $false

                    # first try
                    if ($i -eq 0)
                    {
                        # try catch for the first time we install
                        try
                        {
                            # don't try catch here because if this fails, that is it
                            $messages = Add-WindowsPackage -PackagePath $packagePath -Online -NoRestart -WarningAction Ignore

                            # restart or not
                            if (-not $restart -and $messages -ne $null -and $messages.RestartNeeded)
                            {
                                $restart = $true
                            }

                            if ($restart)
                            {
                                $restartNeeded.Value = $true
                            }
                        }
                        catch { $hasError = $true }

                        # no error, break out
                        if (-not $hasError)
                        {
                            break
                        }
                    }
                    else
                    {
                        Write-Verbose "Trying to install $packagePath for a second time"

                        # don't try catch here because if this fails, that is it
                        $messages = Add-WindowsPackage -PackagePath $packagePath -Online -NoRestart -WarningAction Ignore

                        # restart or not
                        if (-not $restart -and $messages -ne $null -and $messages.RestartNeeded)
                        {
                            $restart = $true
                        }

                        if ($restart)
                        {
                            $restartNeeded.Value = $true
                        }
                    }
                }
            }

            $count += 1
        }
    }
    catch
    {
        $rollBack = $true
        ThrowError -CallerPSCmdlet $PSCmdlet `
                    -ExceptionName $_.Exception.GetType().FullName `
                    -ExceptionMessage $_.Exception.Message `
                    -ExceptionObject $RequiredVersion `
                    -ErrorId FailedToInstall `
                    -ErrorCategory InvalidOperation
    }
    finally
    {
        Write-Progress -Completed -Id $id -Activity "Completed"
    }

    # returns whether we installed
    return (-not $rollBack)
}

function New-MountDrive
{
    # getting packages from an offline image
    # Mount to directory
    while ($true)
    {
        $randomName = [System.IO.Path]::GetRandomFileName()
        $mountDrive = "$env:LOCALAPPDATA\NanoServerPackageProvider\MountDirectories\$randomName"
            
        if (Test-Path $mountDrive) 
        {
            # We should create a directory that hasn't existed before
            continue;
        }
        else 
        {
            $null = mkdir $mountDrive
            return $mountDrive
        }
    }
}

function Remove-MountDrive([string]$mountDrive, [bool]$discard)
{    
    Write-Verbose "Dismounting $mountDrive"

    # Discard won't save anything we did to the image
    if ($discard)
    {
        $null = Dismount-WindowsImage -Path $mountDrive -Discard
    }
    else
    {
    # save will saves packages that we add to the image
        $null = Dismount-WindowsImage -Path $mountDrive -Save
    }

    Write-Verbose "Deleting $mountDrive"
    Remove-Item -Path $mountDrive -Recurse -Force
}

# Given a fully qualified name of a package with the format name~publickeytoken~architecture~language~version
# checks whether this matches the search query
function Test-PackageWithSearchQuery
{
    [CmdletBinding()]
    param
    (
        [Parameter(ParameterSetName="FullyQualifiedName")]
        [string]$fullyQualifiedName,

        [Parameter(ParameterSetName="WindowsPackage")]
        [PSCustomObject]$WindowsPackage,

        [string]$requiredVersion,

        [string]$minimumVersion,

        [string]$maximumVersion,

        [string]$name,

        [string]$Culture,

        [System.Management.Automation.WildcardPattern]$wildCardPattern
    )

    if ($null -eq $WindowsPackage)
    {
        # Split up the whole name since the name has language version and packagename in it
        # format is name~publickeytoken~architecture~language~version
        # now we want the package name to have en-us at the end if package is not base
        $packageNameFractions = $fullyQualifiedName.Split('~')
        $packageName = $packageNameFractions[0]
        $packageLanguage = $packageNameFractions[3]
        $version = $packageNameFractions[4]

        # DISM team has a workaround where they add Feature in the name. We should remove that.
        # THIS IS A TEMPORARY FIX
        if ($packageName -like "*Feature-Package")
        {
            $packageName = $packageName -replace "Feature-Package", "Package"
        }
    }
    else
    {
        $packageName = $WindowsPackage.Name
        $packageLanguage = $WindowsPackage.Culture
        $version = $WindowsPackage.version.ToString()
    }

    # there is a chance user supplies *<PackageLanguage>
    if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
    {
        $packageNameWithLanguage = "$packageName" + "_" + "$packageLanguage"
    }   

    if ($null -ne $wildCardPattern)
    {
        # matching already ignore case
        if (-not $wildCardPattern.IsMatch($packageName))
        {
            # we proceed if wildcard match <PackageName>_<PackageLanguage>
            if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
            {
                if (-not $wildCardPattern.IsMatch($packageLanguage))
                {
                    return $false
                }
            }
            else
            {
                return $false
            }
        }
    }
    else
    {
        # no wildcard so check for name if we are given a name
        # eq operation is case insensitive
        if (-not [string]::IsNullOrWhiteSpace($name) -and $name -ne $packageName)
        {
            # there is a chance user supplies <PackageName>_<PackageLanguage>
            if (-not [string]::IsNullOrWhiteSpace($packageLanguage))
            {
                # we proceed if name match <PackageName>_<PackageLanguage>
                if ($name -ne $packageNameWithLanguage)
                {
                    return $false
                }
            }
            else
            {
                return $false
            }
        }
    }

    # now we check language if the user providers it
    if (-not [string]::IsNullOrWhiteSpace($Culture))
    {
        # if base, then packageLanguage needs to be null
        if ($Culture -eq 'Base')
        {
            $Culture = ''
        }

        if ($packageLanguage -ne $Culture)
        {
            return $false
        }
    }

    # normalize versions
    $convertedVersion = Convert-Version $version

    # fails to normalize
    if ($null -eq $convertedVersion)
    {
        return $false
    }

    # now we check whether version is matched
    if (-not [string]::IsNullOrWhiteSpace($RequiredVersion))
    {
        $convertedRequiredVersion = Convert-Version $RequiredVersion

        # fails if conversion fails or version does not match
        if (($null -eq $convertedRequiredVersion) -or ($convertedRequiredVersion -ne $convertedVersion))
        {
            return $false
        }
    }

    # packagemanagement will make sure requiredversion is not used with either min or max so we don't have to worry about that
    if (-not [string]::IsNullOrWhiteSpace($MinimumVersion))
    {
        $convertedMinimumVersion = Convert-Version $MinimumVersion

        # the converted version should be greater or equal to min version, not the other way round
        if (($null -eq $convertedMinimumVersion) -or ($convertedMinimumVersion -gt $convertedVersion))
        {
            return $false
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($MaximumVersion))
    {
        $convertedMaximumVersion = Convert-Version $MaximumVersion

        # converted version should be the same or less than max version
        if (($null -eq $convertedMaximumVersion) -or ($convertedMaximumVersion -lt $convertedVersion))
        {
            return $false
        }
    }

    # reached here means the package satisfied the search query
    return $true
}

# Given the path of a file, returns a simple key
# This function assumes that the user test that filePath exists
function Get-FileKey([string]$filePath)
{
    $info = Get-ChildItem $filePath
    return ($filePath + $separator + $info.Length + $separator + $info.CreationTime.ToShortDateString())
}

function Convert-Version([string]$version)
{
    if ([string]::IsNullOrWhiteSpace($version))
    {
        return $null;
    }

    # not supporting semver here. let's try to normalize the versions
    if ($version.StartsWith("."))
    {
        # add leading zeros
        $version = "0" + $version
    }
        
    # let's see how many parts are we given with the version
    $parts = $version.Split(".").Count

    # add .0 dependending number of parts since we need 4 parts
    while ($parts -lt 4)
    {
        $version = $version + ".0"
        $parts += 1
    }

    [version]$convertedVersion = $null

    # try to convert
    if ([version]::TryParse($version, [ref]$convertedVersion))
    {
        return $convertedVersion
    }

    return $null;
}

function Filter-Packages ([string[]]$packagesToBeReturned)
{
    $helperDictionary = @{}
    foreach ($package in $packagesToBeReturned)
    {
        # Split up the whole name since the name has language version and packagename in it
        # format is name~publickeytoken~architecture~language~version
        # now we want the package name to have en-us at the end if package is not base
        $packageNameFractions = $package.Split('~')
        $packageName = $packageNameFractions[0]
        $packageLanguage = $packageNameFractions[3]
        $version = $packageNameFractions[4]
        
        # use name and version as key
        $key = $packageName + "~" + $version

        # haven't encountered this before
        if (-not $helperDictionary.ContainsKey($key))
        {
            $helperDictionary[$key] = @()
        }

        $helperDictionary[$key] += $package
    }

    $result = @()

    foreach ($packageArray in $helperDictionary.Values)
    {
        if ($null -eq $packageArray)
        {
            continue
        }

        # only 1 member, then return that
        if ($packageArray.Count -eq 1)
        {
            $result += $packageArray[0]
            continue
        }

        # otherwise, only returns the 1 with language
        foreach ($possiblePackage in $packageArray)
        {
            $packageNameFractions = $possiblePackage.Split('~')
            $packageName = $packageNameFractions[0]
            $packageLanguage = $packageNameFractions[3]
            $version = $packageNameFractions[4]

            if ([string]::IsNullOrWhiteSpace($packageLanguage))
            {
                continue
            }

            $result += $possiblePackage
        }
    }

    # group according to name
    $groupedName = $result | Group-Object -Property {$_.Split('~')[0]}
    foreach ($groupResult in $groupedName)
    {
        $groupResult.Group | Sort-Object -Property {Convert-Version $_.Split('~')[4]} -Descending
    }
}

# Utility to throw an errorrecord
function ThrowError
{
    param
    (        
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCmdlet]
        $CallerPSCmdlet,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]        
        $ExceptionName,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ExceptionMessage,
        
        [System.Object]
        $ExceptionObject,
        
        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $ErrorId,

        [parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [System.Management.Automation.ErrorCategory]
        $ErrorCategory
    )
        
    $exception = New-Object $ExceptionName $ExceptionMessage;
    $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $ErrorId, $ErrorCategory, $ExceptionObject    
    $CallerPSCmdlet.ThrowTerminatingError($errorRecord)
}

#endregion OneGet Helpers
# SIG # Begin signature block
# MIIdlgYJKoZIhvcNAQcCoIIdhzCCHYMCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUHLJBuQWauoWVg4B7mjjfa2aF
# QEWgghhkMIIEwzCCA6ugAwIBAgITMwAAAJvgdDfLPU2NLgAAAAAAmzANBgkqhkiG
# 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw
# HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTYwMzMwMTkyMTI5
# WhcNMTcwNjMwMTkyMTI5WjCBszELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjENMAsGA1UECxMETU9QUjEnMCUGA1UECxMebkNpcGhlciBEU0UgRVNO
# OjcyOEQtQzQ1Ri1GOUVCMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjaPiz4GL18u/
# A6Jg9jtt4tQYsDcF1Y02nA5zzk1/ohCyfEN7LBhXvKynpoZ9eaG13jJm+Y78IM2r
# c3fPd51vYJxrePPFram9W0wrVapSgEFDQWaZpfAwaIa6DyFyH8N1P5J2wQDXmSyo
# WT/BYpFtCfbO0yK6LQCfZstT0cpWOlhMIbKFo5hljMeJSkVYe6tTQJ+MarIFxf4e
# 4v8Koaii28shjXyVMN4xF4oN6V/MQnDKpBUUboQPwsL9bAJMk7FMts627OK1zZoa
# EPVI5VcQd+qB3V+EQjJwRMnKvLD790g52GB1Sa2zv2h0LpQOHL7BcHJ0EA7M22tQ
# HzHqNPpsPQIDAQABo4IBCTCCAQUwHQYDVR0OBBYEFJaVsZ4TU7pYIUY04nzHOUps
# IPB3MB8GA1UdIwQYMBaAFCM0+NlSRnAK7UD7dvuzK7DDNbMPMFQGA1UdHwRNMEsw
# SaBHoEWGQ2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz
# L01pY3Jvc29mdFRpbWVTdGFtcFBDQS5jcmwwWAYIKwYBBQUHAQEETDBKMEgGCCsG
# AQUFBzAChjxodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY3Jv
# c29mdFRpbWVTdGFtcFBDQS5jcnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZI
# hvcNAQEFBQADggEBACEds1PpO0aBofoqE+NaICS6dqU7tnfIkXIE1ur+0psiL5MI
# orBu7wKluVZe/WX2jRJ96ifeP6C4LjMy15ZaP8N0OckPqba62v4QaM+I/Y8g3rKx
# 1l0okye3wgekRyVlu1LVcU0paegLUMeMlZagXqw3OQLVXvNUKHlx2xfDQ/zNaiv5
# DzlARHwsaMjSgeiZIqsgVubk7ySGm2ZWTjvi7rhk9+WfynUK7nyWn1nhrKC31mm9
# QibS9aWHUgHsKX77BbTm2Jd8E4BxNV+TJufkX3SVcXwDjbUfdfWitmE97sRsiV5k
# BH8pS2zUSOpKSkzngm61Or9XJhHIeIDVgM0Ou2QwggYHMIID76ADAgECAgphFmg0
# AAAAAAAcMA0GCSqGSIb3DQEBBQUAMF8xEzARBgoJkiaJk/IsZAEZFgNjb20xGTAX
# BgoJkiaJk/IsZAEZFgltaWNyb3NvZnQxLTArBgNVBAMTJE1pY3Jvc29mdCBSb290
# IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0wNzA0MDMxMjUzMDlaFw0yMTA0MDMx
# MzAzMDlaMHcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xITAf
# BgNVBAMTGE1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQTCCASIwDQYJKoZIhvcNAQEB
# BQADggEPADCCAQoCggEBAJ+hbLHf20iSKnxrLhnhveLjxZlRI1Ctzt0YTiQP7tGn
# 0UytdDAgEesH1VSVFUmUG0KSrphcMCbaAGvoe73siQcP9w4EmPCJzB/LMySHnfL0
# Zxws/HvniB3q506jocEjU8qN+kXPCdBer9CwQgSi+aZsk2fXKNxGU7CG0OUoRi4n
# rIZPVVIM5AMs+2qQkDBuh/NZMJ36ftaXs+ghl3740hPzCLdTbVK0RZCfSABKR2YR
# JylmqJfk0waBSqL5hKcRRxQJgp+E7VV4/gGaHVAIhQAQMEbtt94jRrvELVSfrx54
# QTF3zJvfO4OToWECtR0Nsfz3m7IBziJLVP/5BcPCIAsCAwEAAaOCAaswggGnMA8G
# A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCM0+NlSRnAK7UD7dvuzK7DDNbMPMAsG
# A1UdDwQEAwIBhjAQBgkrBgEEAYI3FQEEAwIBADCBmAYDVR0jBIGQMIGNgBQOrIJg
# QFYnl+UlE/wq4QpTlVnkpKFjpGEwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZMBcG
# CgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJvb3Qg
# Q2VydGlmaWNhdGUgQXV0aG9yaXR5ghB5rRahSqClrUxzWPQHEy5lMFAGA1UdHwRJ
# MEcwRaBDoEGGP2h0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1
# Y3RzL21pY3Jvc29mdHJvb3RjZXJ0LmNybDBUBggrBgEFBQcBAQRIMEYwRAYIKwYB
# BQUHMAKGOGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljcm9z
# b2Z0Um9vdENlcnQuY3J0MBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEB
# BQUAA4ICAQAQl4rDXANENt3ptK132855UU0BsS50cVttDBOrzr57j7gu1BKijG1i
# uFcCy04gE1CZ3XpA4le7r1iaHOEdAYasu3jyi9DsOwHu4r6PCgXIjUji8FMV3U+r
# kuTnjWrVgMHmlPIGL4UD6ZEqJCJw+/b85HiZLg33B+JwvBhOnY5rCnKVuKE5nGct
# xVEO6mJcPxaYiyA/4gcaMvnMMUp2MT0rcgvI6nA9/4UKE9/CCmGO8Ne4F+tOi3/F
# NSteo7/rvH0LQnvUU3Ih7jDKu3hlXFsBFwoUDtLaFJj1PLlmWLMtL+f5hYbMUVbo
# nXCUbKw5TNT2eb+qGHpiKe+imyk0BncaYsk9Hm0fgvALxyy7z0Oz5fnsfbXjpKh0
# NbhOxXEjEiZ2CzxSjHFaRkMUvLOzsE1nyJ9C/4B5IYCeFTBm6EISXhrIniIh0EPp
# K+m79EjMLNTYMoBMJipIJF9a6lbvpt6Znco6b72BJ3QGEe52Ib+bgsEnVLaxaj2J
# oXZhtG6hE6a/qkfwEm/9ijJssv7fUciMI8lmvZ0dhxJkAj0tr1mPuOQh5bWwymO0
# eFQF1EEuUKyUsKV4q7OglnUa2ZKHE3UiLzKoCG6gW4wlv6DvhMoh1useT8ma7kng
# 9wFlb4kLfchpyOZu6qeXzjEp/w7FW1zYTRuh2Povnj8uVRZryROj/TCCBhAwggP4
# oAMCAQICEzMAAABkR4SUhttBGTgAAAAAAGQwDQYJKoZIhvcNAQELBQAwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMTAeFw0xNTEwMjgyMDMxNDZaFw0xNzAx
# MjgyMDMxNDZaMIGDMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ
# MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u
# MQ0wCwYDVQQLEwRNT1BSMR4wHAYDVQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24w
# ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTLtrY5j6Y2RsPZF9NqFhN
# FDv3eoT8PBExOu+JwkotQaVIXd0Snu+rZig01X0qVXtMTYrywPGy01IVi7azCLiL
# UAvdf/tqCaDcZwTE8d+8dRggQL54LJlW3e71Lt0+QvlaHzCuARSKsIK1UaDibWX+
# 9xgKjTBtTTqnxfM2Le5fLKCSALEcTOLL9/8kJX/Xj8Ddl27Oshe2xxxEpyTKfoHm
# 5jG5FtldPtFo7r7NSNCGLK7cDiHBwIrD7huTWRP2xjuAchiIU/urvzA+oHe9Uoi/
# etjosJOtoRuM1H6mEFAQvuHIHGT6hy77xEdmFsCEezavX7qFRGwCDy3gsA4boj4l
# AgMBAAGjggF/MIIBezAfBgNVHSUEGDAWBggrBgEFBQcDAwYKKwYBBAGCN0wIATAd
# BgNVHQ4EFgQUWFZxBPC9uzP1g2jM54BG91ev0iIwUQYDVR0RBEowSKRGMEQxDTAL
# BgNVBAsTBE1PUFIxMzAxBgNVBAUTKjMxNjQyKzQ5ZThjM2YzLTIzNTktNDdmNi1h
# M2JlLTZjOGM0NzUxYzRiNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUC
# lTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# b3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUF
# BwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1Ud
# EwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjiDGRDHd1crow7hSS1nUDWvWas
# W1c12fToOsBFmRBN27SQ5Mt2UYEJ8LOTTfT1EuS9SCcUqm8t12uD1ManefzTJRtG
# ynYCiDKuUFT6A/mCAcWLs2MYSmPlsf4UOwzD0/KAuDwl6WCy8FW53DVKBS3rbmdj
# vDW+vCT5wN3nxO8DIlAUBbXMn7TJKAH2W7a/CDQ0p607Ivt3F7cqhEtrO1Rypehh
# bkKQj4y/ebwc56qWHJ8VNjE8HlhfJAk8pAliHzML1v3QlctPutozuZD3jKAO4WaV
# qJn5BJRHddW6l0SeCuZmBQHmNfXcz4+XZW/s88VTfGWjdSGPXC26k0LzV6mjEaEn
# S1G4t0RqMP90JnTEieJ6xFcIpILgcIvcEydLBVe0iiP9AXKYVjAPn6wBm69FKCQr
# IPWsMDsw9wQjaL8GHk4wCj0CmnixHQanTj2hKRc2G9GL9q7tAbo0kFNIFs0EYkbx
# Cn7lBOEqhBSTyaPS6CvjJZGwD0lNuapXDu72y4Hk4pgExQ3iEv/Ij5oVWwT8okie
# +fFLNcnVgeRrjkANgwoAyX58t0iqbefHqsg3RGSgMBu9MABcZ6FQKwih3Tj0DVPc
# gnJQle3c6xN3dZpuEgFcgJh/EyDXSdppZzJR4+Bbf5XA/Rcsq7g7X7xl4bJoNKLf
# cafOabJhpxfcFOowMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0B
# AQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAG
# A1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEw
# HhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBT
# aWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# q/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2Avw
# OMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eW
# WcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1
# eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le
# 2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+
# 0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2
# zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv
# 1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLn
# JN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31n
# gOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+Hgg
# WCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAG
# CSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZ
# BgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/
# BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8E
# UzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9k
# dWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEB
# BFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9j
# ZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcw
# gZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNy
# b3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwIC
# MDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBu
# AHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOS
# mUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQ
# VdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQ
# dION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive
# /DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrC
# xq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/
# E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ
# 7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANah
# Rr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3
# S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1W
# Tk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1t
# bWrJUnMTDXpQzTGCBJwwggSYAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcg
# UENBIDIwMTECEzMAAABkR4SUhttBGTgAAAAAAGQwCQYFKw4DAhoFAKCBsDAZBgkq
# hkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGC
# NwIBFTAjBgkqhkiG9w0BCQQxFgQU2tl0aJ88fBVpQMxr8JxVRJndJqEwUAYKKwYB
# BAGCNwIBDDFCMECgFoAUAFAAbwB3AGUAcgBTAGgAZQBsAGyhJoAkaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL1Bvd2VyU2hlbGwgMA0GCSqGSIb3DQEBAQUABIIBABo7
# 2jJKolcBY5JmaxH9lP3E+43Wd+J/45p534BItdhtb9Zp1Hn9EbUCkV/pjdpN9fi6
# jCsn6Ncyt9+JfKemBTyN7w079IuglXjOQTFSht9HrrwcuZDKdEcK5cwJYkcwpaHS
# jjP8O2rU3nufvLyIJXxovbxFWinDPiGLMMYael3HmO/i8NlGgIEp8GpraqO/+8L4
# mjZsriCQCO/nx1TVHQIip9c3w0vAvxnI2bWDTMXvZ1oCBeuvqTBgM0Le1L4pyTMS
# csZFVZId5HfMb2Wdm6ElKJtsdzrzwuvDCv9EsrMhixhYe2Vc7gvX1BJizrNfPgqE
# 4QHqwoQDkXAFdbIkmr2hggIoMIICJAYJKoZIhvcNAQkGMYICFTCCAhECAQEwgY4w
# dzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMY
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBAhMzAAAAm+B0N8s9TY0uAAAAAACbMAkG
# BSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ
# BTEPFw0xNjA5MjMyMTAzMDhaMCMGCSqGSIb3DQEJBDEWBBQw91XHmZh7il8I7mN2
# jJGGKdAYujANBgkqhkiG9w0BAQUFAASCAQAvhM1pnlPFeQDTpCDJdN22L4aZu9Y1
# IOkvXWwoL0cJyR6MiO45bppdONcpG7te91ZMtZPzcTWqcDveT3vtfO2b1L0AZ4H5
# E+iYfqeo9vqPCB9wEeHxGd9w4jeK0MIuwPBmk9YZ8Id6lKuabNRVXpfqdXBD1LOc
# aUWKmthjc/l9KnKJqyqHU9o8T/Vlr5QlveORMn859h4XYtDsii7N09izDPF//zmC
# oa6PN5caj/SOVm2hZQFv0yQuUccEh3EMpPEvdEL/J9i9WytUk8TNe8Mw4rfRZQ99
# A7X/BpN6DH4NCoVvm+BLplfGwZoPZRbLGtuAA/qErdPCXcHALovF3piE
# SIG # End signature block