ProgramManager.psm1

# Create some global variables
$script:ModuleRoot = $PSScriptRoot
$script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\ProgramManager.psd1").ModuleVersion

$script:DataPath = "$env:APPDATA\Powershell\ProgramManager"

if ((Test-Path -Path $script:DataPath) -eq $false) {
    
    # Create the module data storage folders if they don't exist
    New-Item -ItemType Directory -Path "$env:APPDATA" -Name "Powershell" -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path "$env:APPDATA\Powershell" -Name "ProgramManager"
    
}

# Detect whether at some level dotsourcing was enforced
$script:doDotSource = $global:ModuleDebugDotSource
$script:doDotSource = $true # Needed to make code coverage tests work
# Detect whether at some level loading individual module files, rather than the compiled module was enforced
$importIndividualFiles = $global:ModuleDebugIndividualFiles

# Resolve-Path function which deals with non-existent paths
function Resolve-Path_i {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]
        $Path # Path to resolve
    )
    
    # Run the command silently
    $resolvedPath = Resolve-Path $Path -ErrorAction SilentlyContinue
    
    # Variable will be null if $Path doesn't exist
    # In that case set it to an empty string
    if ($null -eq $resolvedPath) {
        $resolvedPath = ""
    }
    
    $resolvedPath
}

# If script detects its running from original dev environment, import individually since module won't be compiled
if (Test-Path (Resolve-Path_i -Path "$($script:ModuleRoot)\..\.git")) { $importIndividualFiles = $true }
if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true }

# Imports a module file, either through dot-sourcing or through invoking the script
function Import-ModuleFile {
    <#
    .SYNOPSIS
        Loads files into the module on module import.
     
    .DESCRIPTION
        This helper function is used during module initialization.
        It should always be dotsourced itself, in order to proper function.
         
        This provides a central location to react to files being imported, if later desired
     
    .PARAMETER Path
        The path to the file to load
     
    .EXAMPLE
        PS C:\> . Import-ModuleFile -File $function.FullName
         
        Imports the file stored in $function according to import policy
    #>

    [CmdletBinding()]
    Param (
        [string]
        $Path # Path of module file
    )
    
    # Get the resolved path to avoid any cross-OS issues
    $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath
    if ($doDotSource) {
        
        # Load the script through dot-sourcing
        . $resolvedPath
        
    }else {
        
        # Load the script through different method (unknown atm)
        $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) 
        
    }
}

# Load individual files if not compiled
if ($importIndividualFiles) {
    
    # Execute Preimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1"
    
    # Import all internal functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) {
                        
        . Import-ModuleFile -Path $function.FullName
        
    }
    
    # Import all public functions
    foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) {    
            
        . Import-ModuleFile -Path $function.FullName
        
    }
    
    # Execute Postimport actions
    . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1"
    
    # End execution here, do not load compiled code below
    return
}

#region Load compiled code
function Export-PackageList {
    <#
    .SYNOPSIS
        Exports package list
         
    .DESCRIPTION
        Exports ProgramManager.Package List to xml database.
         
    .PARAMETER PackageList
        The System.Collections.Generic.List[psobject] of packages to serialise.
         
    .EXAMPLE
        PS C:\> Export-Data -PackageList $packages
         
        Exports the $packages list to the module root folder
         
    #>

    
    [CmdletBinding()]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0)]
        [System.Collections.Generic.List[psobject]]
        [AllowEmptyCollection()]
        $PackageList
        
    )
    
    Export-Clixml -Path "$script:DataPath\packageDatabase.xml" -InputObject $PackageList
    
    
}

function Import-PackageList {
    <#
    .SYNOPSIS
        Imports the package list
         
    .DESCRIPTION
        Imports all ProgramManager.Package objects from the xml database.
         
    .EXAMPLE
        PS C:\> $list = Import-PackageList
         
        This populates the $list with all existing packages.
         
    #>

    
    # Create list of all PMPackage objects
    $packageList = [System.Collections.Generic.List[psobject]]@()
    
    # Check if the xml database exists
    if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) {
        
        # The xml database exists
        # Load all existing PMPrograms into a list
        $xmlData = Import-Clixml -Path "$script:DataPath\packageDatabase.xml"
        
        # Iterate through all imported objects
        foreach ($obj in $xmlData) {
            # Only operate on PMPackage objects
            if ($obj.psobject.TypeNames[0] -eq "Deserialized.ProgramManager.Package") {
                # Create new PMPackage objects
                $existingPackage = New-Object -TypeName psobject 
                $existingPackage.PSObject.TypeNames.Insert(0, "ProgramManager.Package")
                
                # Copy the properties from the Deserialized object into the new one
                foreach ($property in $obj.psobject.Properties) {
                    
                    # Copy over the deserialised object properties over to new object
                    $existingPackage | Add-Member -Type NoteProperty -Name $property.Name -Value $property.Value
                    
                }
                
                $packageList.Add($existingPackage)
                
            }
        }
        
    }
    
    # Return the list object; -NoEnumerate is used to avoid powershell converting list to Object[] array
    Write-Output $packageList -NoEnumerate
    
}

function Write-Message {
    <#
    .SYNOPSIS
        Writes a message to the screen.
         
    .DESCRIPTION
        Writes a message to the screen, as text, a warning, or an error.
         
    .PARAMETER Message
        The message to print to screen.
         
    .PARAMETER DisplayText
        Writes the message as standard text.
         
    .PARAMETER DisplayWarning
        Writes the message as a warning.
         
    .PARAMETER DisplayError
        Writes the message as an error.
         
    .EXAMPLE
        PS C:\> Write-Message -Message "invalid argument" -DisplayError
         
        Prints the error message to screen by invoking Write-Error.
         
    #>

    [CmdletBinding()]
    Param(
        
        [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)]
        [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)]
        [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)]
        [string]
        $Message,
        
        [Parameter(ParameterSetName = "DisplayText", Mandatory = $true)]
        [switch]
        $DisplayText,
        
        [Parameter(ParameterSetName = "DisplayWarning", Mandatory = $true)]
        [switch]
        $DisplayWarning,
        
        [Parameter(ParameterSetName = "DisplayError", Mandatory = $true)]
        [switch]
        $DisplayError
    )
    
    if ($DisplayText -eq $true) {
        
        Write-Host -Message $Message
        
    }elseif ($DisplayWarning -eq $true) {
        
        Write-Warning -Message $Message
        
    }elseif ($DisplayError -eq $true) {
        
        Write-Error -Message $Message
        
    }
    
    
}

function Get-PMPackage {
    <#
    .SYNOPSIS
        Get information about a specified ProgramManager package.
         
    .DESCRIPTION
        Returns the specified ProgramManager.Package object, for display to terminal, or for passing down the pipeline.
         
    .PARAMETER PackageName
        The name of the package to retrieve.
         
    .PARAMETER ShowFullDetail
        Toggles whether it shows a overview of the package with the "usually" important properties,
        or whether it shows every single property of the package, some of which will not have much use for the user.
         
    .EXAMPLE
        PS C:\> Get-PMPackage -PackageName "notepad"
         
        Returns information about the "notepad" package.
         
    .INPUTS
        None
         
    .OUTPUTS
        None
         
    #>

    
    [CmdletBinding()]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [string]
        $PackageName,
        
        [Parameter(Position = 1)]
        [switch]
        $ShowFullDetail
        
    )
    
    # Import all PMPackage objects from the database file
    Write-Verbose "Loading existing packages from database"
    $packageList = Import-PackageList
    
    # Check that the name is not empty
    if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) {
        
        Write-Message -Message "The name cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check if package exists
    $package = $packageList | Where-Object { $_.Name -eq $PackageName }
    if ($null -eq $package) {
        
        Write-Message -Message "There is no package called: $PackageName" -DisplayWarning
        return
        
    }
    
    # Append the View object type to control the visual output of the object depending on the user's preference
    if ($ShowFullDetail -eq $true) {
        
        Write-Verbose "Detected -ShowFullDetail flag."
        $package.PSObject.TypeNames.Insert(1, "ProgramManager.Package-View.Full")
        
    }else {
        
        Write-Verbose "Not detected -ShowFullDetail flag."
        $package.PSObject.TypeNames.Insert(1, "ProgramManager.Package-View.Overview")
        
    }
    
    # Output the package object
    Write-Output $package
    
}

function Invoke-PMInstall {
    <#
    .SYNOPSIS
        Installs a ProgramManager package.
         
    .DESCRIPTION
        Invokes an installation process on a ProgramManager.Package which has been earlier added to the database.
         
    .PARAMETER PackageName
        The name of the ProgramManager package to install.
         
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
    .EXAMPLE
        PS C:\> Invoke-PMInstall -Name "notepad"
         
        This command will install the package named "notepad", executing any scriptblocks along with it.
         
    .EXAMPLE
        PS C:\> Get-PMPackage "notepad" | Invoke-PMInstall
         
        This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example.
        This command will install the package named "notepad", executing any scriptblocks along with it.
         
    .INPUTS
        System.String[]
         
    .OUTPUTS
        None
         
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [Alias("Name")]
        [string[]]
        $PackageName
        
    )
    
    # Check if the xml database exists
    if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) {
        
        # Load all existing PMPackages into a list
        Write-Verbose "Loading existing packages from database"
        $packageList = Import-PackageList
        
    }else {
        
        # The xml database doesn't exist; warn user
        Write-Message -Message "The database file doesn't exist. Run New-PMPackage to initialise it." -DisplayWarning
        return
        
    }
    
    # Iterate through all passed package names
    foreach ($name in $PackageName) {
        
        Write-Verbose "Installing package:{$name}"
        
        # Check that the name is not empty
        if ([System.String]::IsNullOrWhiteSpace($name) -eq $true) {
            
            Write-Message -Message "The name cannot be empty" -DisplayWarning
            return
            
        }
        
        # Get the package by name
        Write-Verbose "Retrieving the ProgramManager.Package Object"
        $package = $packageList | Where-Object { $_.Name -eq $name }
        
        # Warn the user if the name is invalid
        if ($null -eq $package) {
            
            Write-Message -Message "There is no package called: $Name" -DisplayWarning
            return
            
        }
        
        # Check if the package has a pre-install scriptblock to run
        Write-Verbose "Checking for pre-install scriptblock"
        if ([System.String]::IsNullOrWhiteSpace($package.PreInstallScriptblock) -eq $false) {
            
            if ($PSCmdlet.ShouldProcess("pre-install scriptblock from package:{$name}", "Execute scriptblock")) {
                
                # Convert the string into a scriptblock and execute
                Write-Verbose "Coverting scriptblock and executing it"
                $scriptblock = [scriptblock]::Create($package.PreInstallScriptblock)
                Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package
                
            }
            
        }
        
        # If the package is a url-package, download it and define extra properties to allow the installation code to run correctly
        if ($package.Type -eq "UrlPackage") {
            
            # Get the absolute url, after any redirection, which points to the actual file
            Write-Verbose "Getting absolute url from link given"
            $url = [System.Net.HttpWebRequest]::Create($package.Url).GetResponse().ResponseUri.AbsoluteUri
            
            # Get the file extension in order to save it correctly
            $regex = [regex]::Match($url, ".*\.(.*)")
            $extension = $regex.Groups[1].Value
            
            if ($PSCmdlet.ShouldProcess("installer at url:$url", "Download")){
                
                # Download the installer from the url
                Write-Verbose "Downloading installer to \packages\$($package.Name)\"
                New-Item -ItemType Directory -Path "$script:DataPath\packages\$($package.Name)\" | Out-Null
                Invoke-WebRequest -Uri $url -OutFile "$script:DataPath\packages\$($package.Name)\installer.$extension"
                
            }
            
            # Set executable properties in the package object to allow later code to run correctly
            Write-Verbose "Adding properties to allow for installation"
            $package | Add-Member -Type NoteProperty -Name "ExecutableName" -Value "installer.$extension"
            $package | Add-Member -Type NoteProperty -Name "ExecutableType" -Value ".$extension"
            
        }
        
        # Main installation logic
        if ($package.Type -eq "LocalPackage" -or $package.Type -eq "UrlPackage") {
            
            # Differentiate between exe and msi installers
            if ($package.ExecutableType -eq ".exe") {
                
                if ($PSCmdlet.ShouldProcess(".exe installer:$($package.ExecutableName)", "Start process")){
                    
                    # Start the exe installer and wait for finish
                    Write-Verbose "Starting the .exe installer"
                    Start-Process -FilePath "$script:DataPath\packages\$($package.Name)\$($package.ExecutableName)" -Wait
                    
                }
                
            }elseif ($package.ExecutableType -eq ".msi") {
                <# SET PROPERTIES OF MSI INSTALLER, removed for now since very few msi installers around anyway
                # Set the display argument for msiexec
                if ($ShowUI -eq $true) {
                    $dislayArgument = "/qr "
                }else {
                    $dislayArgument = "/qn "
                }
                 
                # Set the logging argument for msiexec
                if ($NoLog -eq $true) {
                    $logArgument = ""
                }else {
                    $logArgument = "/l*v `"$script:DataPath\install-$($apckage.Name)-$(Get-Date -Format "yyyy/MM/dd HH:mm").txt`""
                }
                 
                # If the package has a defined install directory, set the msiexec argument(s) to that
                if ([System.String]::IsNullOrWhiteSpace($package.InstallDirectory) -eq $false) {
                    $paramArgument += "INSTALL_PREFIX_1=`"$($package.InstallDirectory)`" "
                    $paramArgument += "TARGETDIR=`"$($package.InstallDirectory)`" "
                    $paramArgument += "INSTALLDIR=`"$($package.InstallDirectory)`" "
                    $paramArgument += "INSTALLDIRECTORY=`"$($package.InstallDirectory)`" "
                    $paramArgument += "TARGETDIRECTORY=`"$($package.InstallDirectory)`" "
                    $paramArgument += "TARGETPATH=`"$($package.InstallDirectory)`" "
                    $paramArgument += "INSTALLPATH=`"$($package.InstallDirectory)`" "
                }
                 
                # Set the msiexec arguments
                $processStartupInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
                    FileName = "msiexec.exe"
                    Arguments = "$script:DataPath\packages\$($package.Name)\$($package.ExecutableName) " + $dislayArgument + $logArgument + $paramArgument
                    UseShellExecute = $false
                }#>

                
                if ($PSCmdlet.ShouldProcess(".msi installer:$($package.ExecutableName)", "Start process")) {
                    
                    # Start the msiexec process and wait for finish
                    Write-Verbose "Starting the .msi installer"
                    Start-Process -FilePath "msiexec.exe" -ArgumentList "/i $script:DataPath\packages\$($package.Name)\$($package.ExecutableName) /qf /l*v `"$script:DataPath\log-$($package.Name)-$(Get-Date -Format "yyyy-MM-dd HH:mm").txt`"" -Wait
                    
                }
                
            }
            
        }elseif ($package.Type -eq "PortablePackage") {
            
            # Check that the install directory doesn't contain any characters which could cause potential issues
            if ($package.InstallDirectory -like "*.``**" -or $package.InstallDirectory -like "*``**" -or $package.InstallDirectory -like "*.*") {
                
                Write-Message -Message "The package install directory contains invalid characters" -DisplayWarning
                return
                
            }
            
            # Check that the install directory exists, otherwise abort
            if ((Test-Path -Path $package.InstallDirectory) -eq $false) {
                
                Write-Message -Message "The install directory doesn't exist: $($package.InstallDirectory)" -DisplayWarning
                return
                
            }
            
            if ($PSCmdlet.ShouldProcess("Package:{$name} files", "Copy to the installation directory")) {
                                
                # Copy package folder to install directory
                Write-Verbose "Copying over the package files to $($package.InstallDirectory)"
                Copy-Item -Path "$script:DataPath\packages\$($package.Name)" -Destination $package.InstallDirectory -Container -Recurse
                
            }
            
        }elseif ($package.Type -eq "ChocolateyPackage") {
            
            # TODO: Invoke chocolatey install
            
        }
        
        # Clean up temporary url-package properties
        if ($package.Type -eq "UrlPackage") {
            
            # Remove the executable properties
            Write-Verbose "Cleaning up temporary properties"
            $package.psobject.Properties.Remove("ExecutableName")
            $package.psobject.Properties.Remove("ExecutableType")
            
            # Check in-case the installer wasn't actually downloaded
            if ((Test-Path -Path "$script:DataPath\packages\$($package.Name)") -eq $true) {
                
                if ($PSCmdlet.ShouldProcess("package:{$name} installer", "Delete")) {
                    
                    # Remove the temporarily downloaded installer
                    Write-Verbose "Deleting the downloaded installer"
                    Remove-Item -Path "$script:DataPath\packages\$($package.Name)" -Recurse -Force
                    
                }
                
            }
            
        }
        
        # Check if the package has a post-install scriptblock to run
        Write-Verbose "Checking for post-install scriptblock"
        if ([System.String]::IsNullOrWhiteSpace($package.PostInstallScriptblock) -eq $false) {
            
            if ($PSCmdlet.ShouldProcess("post-install scriptblock from package:{$name}", "Execute scriptblock")) {
                
                # Convert string to scriptblock and execute
                Write-Verbose "Coverting scriptblock and executing it"
                $scriptblock = [scriptblock]::Create($package.PostInstallScriptblock)
                Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package
                
            }
            
        }
        
        # Set the installed flag for the package
        Write-Verbose "Setting installed flag to true"
        $package.IsInstalled = $true
    
    }
    
    if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Update the package:{$PackageName} installation status")) {
        
        # Override xml database with updated package property
        Write-Verbose "Writing-out data back to database"
        Export-PackageList -PackageList $packageList
        
    }
    
}

function Invoke-PMUninstall {
    <#
    .SYNOPSIS
        Uninstalls a ProgramManager package.
         
    .DESCRIPTION
        Invokes an uninstallation process on a ProgramManager.Package that is already installed.
         
    .PARAMETER PackageName
        The name of the ProgramManager package to uninstall.
         
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
    .EXAMPLE
        PS C:\> Invoke-PMUninstall -Name "notepad"
         
        This command will uninstall the package named "notepad".
         
    .EXAMPLE
        PS C:\> Get-PMPackage "notepad" | Invoke-PMUninstall
         
        This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example.
        This command will uninstall the package named "notepad".
         
    .INPUTS
        System.String[]
         
    .OUTPUTS
        None
         
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [Alias("Name")]
        [string[]]
        $PackageName
        
    )
    
    # Check if the xml database exists
    if ((Test-Path -Path "$script:DataPath\packageDatabase.xml") -eq $true) {
        
        # Load all existing PMPackages into a list
        Write-Verbose "Loading existing packages from database"
        $packageList = Import-PackageList
        
    }else {
        
        # The xml database doesn't exist; warn user
        Write-Message -Message "The database file doesn't exist. Run New-PMPackage to initialise it." -DisplayWarning
        return
        
    }
    
    # Iterate through all passed package names
    foreach ($name in $PackageName) {
        
        Write-Verbose "Uninstalling package:{$name}"
        
        # Check that the name is not empty
        if ([System.String]::IsNullOrWhiteSpace($name) -eq $true) {
            
            Write-Message -Message "The name cannot be empty" -DisplayWarning
            return
            
        }
        
        # Get the package by name
        Write-Verbose "Retrieving the ProgramManager.Package Object"
        $package = $packageList | Where-Object { $_.Name -eq $name }
        # Warn the user if the name is invalid
        if ($null -eq $package) {
            
            Write-Message -Message "There is no package called: $name" -DisplayWarning
            return
            
        }
        
        # Check that the package is actually installed
        if ($package.IsInstalled -eq $false) {
            
            Write-Message -Message "The package:{$name} is not installed." -DisplayWarning
            return
            
        }
        
        # Check if the package has a on-uninstall scriptblock to run
        Write-Verbose "Checking for uninstall scriptblock"
        if ([System.String]::IsNullOrWhiteSpace($package.UninstallScriptblock) -eq $false) {
            
            if ($PSCmdlet.ShouldProcess("uninstall scriptblock from package:{$name}", "Execute scriptblock")) {
                
                # Convert the string into a scriptblock and execute
                Write-Verbose "Coverting scriptblock and executing it"
                $scriptblock = [scriptblock]::Create($package.UninstallScriptblock)
                Invoke-Command -ScriptBlock $scriptblock -ArgumentList $package
                
            }
            
        }
        
        # Main uninstallation logic
        if ($package.Type -eq "LocalPackage" -or $package.Type -eq "UrlPackage") {
            
            # Launch control panel to allow user to uninstall program, since there is no real
            # way of uninstalling programs installed through a standard exe installer
            Write-Verbose "Launching Control Panel"
            Start-Process appwiz.cpl
            
        }elseif ($package.Type -eq "PortablePackage") {
            
            # Check whether the folder the package was installed to actually exists
            if ((Test-Path -Path "$($package.InstallDirectory)\$($package.Name)") -eq $false) {
                
                Write-Message -Message "Can't find the package at the expected directory. Was the package folder renamed?" -DisplayWarning
                return
                
            }
            
            if ($PSCmdlet.ShouldProcess("Package:{$name} files", "Delete from the installation directory")) {
                
                # Delete the package files
                Write-Verbose "Deleting package files"
                Remove-Item -Path "$($package.InstallDirectory)\$($package.Name)" -Recurse -Force
                
            }
            
        }elseif ($package.Type -eq "ChocolateyPackage") {
            
            # TODO: invoke chocolatey uninstall
            
        }
        
        # Set the installed flag for the package
        Write-Verbose "Setting installed flag to false"
        $package.IsInstalled = $false        
        
    }
    
    if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Update the package:{$PackageName} installation status")) {
        
        # Override xml database with updated package property
        Write-Verbose "Writing-out data back to database"
        Export-PackageList -PackageList $packageList
        
    }
    
}

function New-PMPackage {
    <#
    .SYNOPSIS
        Adds a program to the ProgramManager database.
         
    .DESCRIPTION
        Adds a new ProgramManager.Package to the database for future installation.
        Accepts the following:
        - msi/exe installer (local file or url download)
        - zip binary
        - chocolatey package
         
    .PARAMETER Name
        The name of the program to add to the database.
         
    .PARAMETER LocalPackage
        Specifies the use of a local installer file located at an available path.
         
    .PARAMETER UrlPackage
        Specifies the use of an installer file located at a url.
         
    .PARAMETER PortablePackage
        Specifies the use of a portable binary file located at a path or url.
         
    .PARAMETER ChocolateyPackage
        Specifies the use of a chocolatey package.
         
    .PARAMETER PackageLocation
        The location of the package.
        - For Local-Package: file path pointing to the executable
        - For Url-Package: url pointing to download link
        - For Portable-Package: file path pointing to the folder/archive/executable
         
    .PARAMETER InstallDirectory
        The directory to which install the pacakge to.
         
    .PARAMETER PackageName
        The name of a chocolatey package. To be used with the -ChocolateyPackage switch.
         
    .PARAMETER Note
        A short note/description to explain what the package entry is.
         
    .PARAMETER PreInstallScriptblock
        A script block which will be executed before the main package installation process.
         
    .PARAMETER PostInstallScriptblock
        A script block which will be executed after the main package installation process.
         
    .PARAMETER UninstallScriptblock
        A script block which will be executed before the uninstallation process.
         
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
    .EXAMPLE
        PS C:\> New-PMPackage -Name "notepad" -LocalPackage -PackageLocation "~\Downloads\notepad.msi" -Note "Notepad msi installer"
         
        Adds the locally stored program to the database with the specified name and short note.
         
    .EXAMPLE
        PS C:\> New-PMPackage -Name "notepad" -UrlPackage -PackageLocation "https://download/" -PreInstallScriptblock {Write-Host "installing notepad"}
         
        Adds the url-downloaded program to the database with the specified pre-install scriptblock.
         
    .EXAMPLE
        PS C:\> New-PMPackage -Name "notepad" -PortablePackage -PackageLocation "~\Downloads\program.zip" -InstallDirectory "D:\Programs\"
         
        Adds the portable program to the database with the specified install directory.
         
    .INPUTS
        None
         
    .OUTPUTS
        None
         
    .NOTES
        For detailed information regarding the scriptblocks, see: about_ProgramManager_scriptblocks
         
    #>
    
    
    [CmdletBinding(DefaultParameterSetName = "LocalInstaller", SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    Param (
        
        [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 0)]
        [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 0)]
        [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 0)]
        [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [string]
        $Name,
        
        
        [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 1)]
        [switch]
        $LocalPackage,
        
        [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 1)]
        [switch]
        $UrlPackage,
        
        [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 1)]
        [switch]
        $PortablePackage,
        
        [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 1)]
        [switch]
        $ChocolateyPackage,
        
        
        [Parameter(ParameterSetName = "LocalPackage", Mandatory = $true, Position = 2)]
        [Parameter(ParameterSetName = "UrlPackage", Mandatory = $true, Position = 2)]
        [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 2)]
        [AllowEmptyString()]
        [string]
        $PackageLocation,
        
        [Parameter(ParameterSetName = "LocalPackage", Position = 3)]
        [Parameter(ParameterSetName = "UrlPackage", Position = 3)]        
        [Parameter(ParameterSetName = "PortablePackage", Mandatory = $true, Position = 3)]
        [AllowEmptyString()]
        [string]
        $InstallDirectory,
                
        [Parameter(ParameterSetName = "ChocolateyPackage", Mandatory = $true, Position = 2)]
        [string]
        $PackageName,
        
        
        [Parameter(ParameterSetName = "LocalPackage")]
        [Parameter(ParameterSetName = "UrlPackage")]
        [Parameter(ParameterSetName = "PortablePackage")]
        [Parameter(ParameterSetName = "ChocolateyPackage")]
        [AllowEmptyString()]
        [string]
        $Note,
        
        [Parameter(ParameterSetName = "LocalPackage")]
        [Parameter(ParameterSetName = "UrlPackage")]
        [Parameter(ParameterSetName = "PortablePackage")]
        [Parameter(ParameterSetName = "ChocolateyPackage")]
        [AllowEmptyString()]
        [scriptblock]
        $PreInstallScriptblock,
        
        [Parameter(ParameterSetName = "LocalPackage")]
        [Parameter(ParameterSetName = "UrlPackage")]
        [Parameter(ParameterSetName = "PortablePackage")]
        [Parameter(ParameterSetName = "ChocolateyPackage")]
        [AllowEmptyString()]
        [scriptblock]
        $PostInstallScriptblock,
        
        [Parameter(ParameterSetName = "LocalPackage")]
        [Parameter(ParameterSetName = "UrlPackage")]
        [Parameter(ParameterSetName = "PortablePackage")]
        [Parameter(ParameterSetName = "ChocolateyPackage")]
        [AllowEmptyString()]
        [scriptblock]
        $UninstallScriptblock
        
    )
    
    # Import all PMPackage objects from the database file
    Write-Verbose "Loading existing packages from database"
    $packageList = Import-PackageList
    
    # Check that the name is not empty
    if ([System.String]::IsNullOrWhiteSpace($Name) -eq $true) {
        
        Write-Message -Message "The name cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check that the name doesn't contain any characters which could cause potential issues further down the line
    if ($Name -like "*.*" -or $Name -like "*``**" -or $Name -like "*.``**") {
        
        Write-Message -Message "The name contains invalid characters" -DisplayWarning
        return
        
    }
    
    # Check if name is already taken to avoid conflicts
    $package = $packageList | Where-Object { $_.Name -eq $Name }
    if ($null -ne $package) {
        
        Write-Message -Message "There already exists a package called: $Name" -DisplayWarning
        return
        
    }
    
    # Check that the scriptblocks actually contain code
    if ($null -ne $PreInstallScriptblock) {
        
        if ([System.String]::IsNullOrWhiteSpace($PreInstallScriptblock.ToString()) -eq $true) {
            
            Write-Message -Message "The Pre-Install Scriptblock cannot be empty" -DisplayWarning
            return
            
        }
        
        if ($PreInstallScriptblock.ToString() -like "``*") {
            
            Write-Message -Message "The Pre-Install Scriptblock cannot just be '*'" -DisplayWarning
            return
            
        }
        
    }
    
    if ($null -ne $PostInstallScriptblock) {
        
        if ([System.String]::IsNullOrWhiteSpace($PostInstallScriptblock.ToString()) -eq $true) {
            
            Write-Message -Message "The Post-Install Scriptblock cannot be empty" -DisplayWarning
            return
            
        }
        
        if ($PostInstallScriptblock.ToString() -like "``*") {
            
            Write-Message -Message "The Post-Install Scriptblock cannot just be '*'" -DisplayWarning
            return
            
        }
        
    }
    
    if ($null -ne $UninstallScriptblock) {
        
        if ([System.String]::IsNullOrWhiteSpace($UninstallScriptblock.ToString()) -eq $true) {
            
            Write-Message -Message "The Uninstall Scriptblock cannot be empty" -DisplayWarning
            return
            
        }
        
        if ($UninstallScriptblock.ToString() -like "``*") {
            
            Write-Message -Message "The Uninstall Scriptblock cannot just be '*'" -DisplayWarning
            return
            
        }
        
    }
    
    # Create new PMpackage object and set its object type
    Write-Verbose "Creating new ProgramManager.Package Object"
    $package = New-Object -TypeName psobject 
    $package.PSObject.TypeNames.Insert(0, "ProgramManager.Package")
    
    # Add compulsory properties which all packages will have irrespective of type
    Write-Verbose "Adding universal properties to object: name,type,isinstalled"
    $package | Add-Member -Type NoteProperty -Name "Name" -Value $Name
    $package | Add-Member -Type NoteProperty -Name "Type" -Value $PSCmdlet.ParameterSetName
    $package | Add-Member -Type NoteProperty -Name "IsInstalled" -Value $false
    
    if ((Test-Path -Path "$script:DataPath\packages\") -eq $false) {
        
        if ($PSCmdlet.ShouldProcess("$script:DataPath\packages", "Create the package store")) {
            
            # The packages subfolder doesn't exist. Create it to avoid errors with Move-Item
            New-Item -ItemType Directory -Path "$script:DataPath\packages\" -Confirm:$false | Out-Null
            
        }
        
    }
    
    # Check that the path is not empty
    if ([System.String]::IsNullOrWhiteSpace($Path) -eq $true) {
        
        Write-Message -Message "The path cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check that the path doesn't contain any characters which could cause potential issues or undesirable effects
    if ($PackageLocation -like "." -or $PackageLocation -like "~" -or $PackageLocation -like ".." `
        -or $PackageLocation -like "..." -or $PackageLocation -like "*``**" -or $PackageLocation -like "*.") {
        
        Write-Message -Message "The path provided is not accepted for safety reasons" -DisplayWarning
        return
        
    }
    
    if ($LocalPackage -eq $true) {
        Write-Verbose "Detected Local-Pacakge"
        
        # Check that the installer path is valid
        if ((Test-Path -Path $PackageLocation) -eq $false) {
            
            Write-Message -Message "There is no valid path pointing to: $PackageLocation" -DisplayWarning
            return
            
        }
        
        # Get the details of the executable and check whether it is actually a file
        $executable = Get-Item -Path $PackageLocation
        if ($executable.PSIsContainer -eq $true -or $executable.GetType().Name -eq "Object[]") {
            
            Write-Message -Message "There is no (single) executable located at the path: $PackageLocation" -DisplayWarning
            return
            
        }
        
        if (($executable.Extension -match ".exe|.msi") -eq $false) {
            
            Write-Message -Message "There is no installer file located at the path: $PackageLocation" -DisplayWarning
            return
            
        }
        
        if ($PSCmdlet.ShouldProcess("File: $PackageLocation", "Move the installer to the package store")) {
            
            # Move the executable to the package store
            Write-Verbose "Copying over installer to \packages\$Name\"
            New-Item -ItemType Directory -Path "$script:DataPath\packages\$Name\" -Confirm:$false | Out-Null
            Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name\$($executable.Name)"    -Confirm:$false
            
        }
        
        # Add executable properties
        Write-Verbose "Adding properties: executable name & type"
        $package | Add-Member -Type NoteProperty -Name "ExecutableName" -Value $executable.Name
        $package | Add-Member -Type NoteProperty -Name "ExecutableType" -Value $executable.Extension
        
        # Add install directory if passed in
        if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $false) {
            
            Write-Verbose "Adding property: install directory"
            $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory
        
        }
        
    }elseif ($UrlPackage -eq $true) {
        Write-Verbose "Detected Url-Pacakge"
        
        # Add url property
        Write-Verbose "Adding property: download link"
        $package | Add-Member -Type NoteProperty -Name "Url" -Value $PackageLocation
        
        # Add install directory if passed in
        if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $false) {
            
            Write-Verbose "Adding property: install directory"
            $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory
        
        }
        
    }elseif ($PortablePackage -eq $true) {
        Write-Verbose "Detected Portable-Pacakge"
        
        # Check that a install directory parameter is given in
        if ([System.String]::IsNullOrWhiteSpace($InstallDirectory) -eq $true) {
            
            Write-Message -Message "The install directory path must not be empty" -DisplayWarning
            return
            
        }
        
        # Check that the file path is valid
        if ((Test-Path -Path $PackageLocation) -eq $false) {
            
            Write-Message -Message "There is no folder/file located at the path: $PackageLocation" -DisplayWarning
            return
            
        }
        
        Write-Verbose "Retrieving item located at: $PackageLocation"
        $item = Get-Item -Path $PackageLocation
        
        # There are multiple items collected under this file path so reject it
        if ($item.GetType().Name -eq "Object[]") {
            
            Write-Message -Message "You cannot specify multiple items in the filepath" -DisplayWarning
            return
            
        }
        
        if ((Get-Item -Path $PackageLocation).PSIsContainer -eq $true) {
            
            if ($PSCmdlet.ShouldProcess("Folder: $PackageLocation", "Move the package container to the package store")) {
                
                # This is a folder so can be moved straight to the package store
                Write-Verbose "Detected container. Moving folder to \packages\$Name\"
                Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name" -Confirm:$false
                
            }
            
        }else {
            
            # This is a file so check if its an archive to extract
            $file = Get-Item -Path $PackageLocation
            
            # Check if the file has an 'archive' attribute
            if ($file.Extension -eq ".zip" -or $file.Extension -eq ".tar") {
                
                if ($PSCmdlet.ShouldProcess("Archive: $PackageLocation", "Extract the archive to temporary path")){
                    
                    # Extract archive to parent location and delete the original
                    # Must set do this trickery to stop confirmation prompts, since passing -Confirm:$false to Expand-Archive
                    # doen't propogate that to the individual New-Item -Directory commands, and each one would generate a prompt
                    Write-Verbose "Detected archive. Extracting archive to \temp\"
                    $originalConfirmPrefrence = $ConfirmPreference
                    $ConfirmPreference = "None"
                    Expand-Archive -Path $PackageLocation -DestinationPath "$script:DataPath\temp"
                    $ConfirmPreference = $originalConfirmPrefrence
                    Remove-Item -Path $PackageLocation -Force -Confirm:$false
                    
                }
                
                # Set the current directory to the extracted-archive location, initialising loop
                $currentDir = "$script:DataPath\temp"
                
                # Recursively look into the folder heirarchy until there is no more folders containing a single folder
                # i.e. stops having structure: folder1 -> folder2 -> folder3 -> contents(exe,libs,etc)
                do {
                    
                    # Get all children within the current folder
                    $children = Get-ChildItem -Path $currentDir
                    
                    # If there is only a single child and its a folder, move down the tree
                    if ($children.Count -eq 1 -and $children[0].PSIsContainer -eq $true) {
                        
                        $currentDir = $children.FullName
                        
                    }else {
                        
                        # Otherwise move the item to the package store
                        if ($PSCmdlet.ShouldProcess("Folder: $currentDir", "Move the package container to the package store")) {
                            
                            Write-Verbose "Moving folder to \packages\$Name\"
                            Move-Item -Path $currentDir -Destination "$script:DataPath\packages\$Name" -Confirm:$false
                        }
                        
                        
                    }
                    
                } while ($children.Count -eq 1 -and $children[0].PSIsContainer -eq $true)
                
                # Clean up the left-over folders
                Write-Verbose "Cleaning up \temp\"
                Remove-Item -Path "$script:DataPath\temp\" -Recurse -Force -Confirm:$false
                
            }elseif ($file.Extension -eq ".exe") {
                
                if ($PSCmdlet.ShouldProcess("File: $PackageLocation", "Move the executable to the package store")) {
                    
                    # This is a portable package with only a single exe file, so move it straight to package store
                    Write-Verbose "Detected executable. Moving program to \packages\$Name\"
                    New-Item -ItemType Directory -Path "$script:DataPath\packages\$Name\" -Confirm:$false | Out-Null
                    Move-Item -Path $PackageLocation -Destination "$script:DataPath\packages\$Name\$($file.Name)" -Confirm:$false
                    
                }
                
            }else {
                
                # This is a file of an invalid type for this package type
                Write-Message -Message "The file specified is neither a executable nor an archive" -DisplayWarning
                return
                
            }
            
        }
        
        # Add necessary properties
        Write-Verbose "Adding property: install directory"
        $package | Add-Member -Type NoteProperty -Name "InstallDirectory" -Value $InstallDirectory
        
    }elseif ($ChocolateyPackage -eq $true) {
        Write-Verbose "Detected Chocolatey-Pacakge"
        
        # Add necessary info for chocolatey to work
        Write-Verbose "Adding property: chocolatey package name"
        $package | Add-Member -Type NoteProperty -Name "PackageName" -Value $PackageName
        
    }
    
    # Add optional note property if passed in
    if ([System.String]::IsNullOrWhiteSpace($Note) -eq $false) {
        
        Write-Verbose "Adding property: note"
        $package | Add-Member -Type NoteProperty -Name "Note" -Value $Note    
            
    }
    
    # Add optional scriptblock proprties if passed in
    if ($null -ne $PreInstallScriptblock) {
        
        Write-Verbose "Adding property: pre-install scriptblock"
        $package | Add-Member -Type NoteProperty -Name "PreInstallScriptblock" -Value $PreInstallScriptblock.ToString()
    
    }
    
    if ($null -ne $PostInstallScriptblock) {
        
        Write-Verbose "Adding property: post-install scriptblock"
        $package | Add-Member -Type NoteProperty -Name "PostInstallScriptblock" -Value $PostInstallScriptblock.ToString()
    
    }
    
    if ($null -ne $UninstallScriptblock) {
        
        Write-Verbose "Adding property: post-install scriptblock"
        $package | Add-Member -Type NoteProperty -Name "UninstallScriptblock" -Value $UninstallScriptblock.ToString()
    
    }
    
    
    if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Add the package `'$Name`'")) {
        
        # Add new PMPackage to list
        $packageList.Add($package)
        
        # Export-out package list to xml file
        Write-Verbose "Writing-out data back to database"
        Export-PackageList -PackageList $packageList
        
    }
    
    
}

function Remove-PMPackage {
    <#
    .SYNOPSIS
        Removes a ProgramManager package from the database.
         
    .DESCRIPTION
        Erases the data of a ProgramManager.Package object from the database.
         
    .PARAMETER PackageName
        The name of the package to remove.
         
    .PARAMETER RetainFiles
        If removing a local or portable package, which has files/installers saved within the package store,
        this switch will move those files to a specific location rather than deleting them.
         
    .PARAMETER Path
        To be used in conjunction with -RetainFiles.
        This is the path where the package files will be moved into.
         
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
    .EXAMPLE
        PS C:\> Remove-PMPackage -PackageName "notepad"
         
        This command will remove the package "notepad" from the database and delete any physical files, such as the installer executable.
         
    .EXAMPLE
        PS C:\ Remove-PMPackage -PackageName "notepad" -RetainFiles -Path "~\Downloads\"
         
        This command will remove the package "notepad" from the database, but will not delete any physical files, such as the installer executable.
        It will move the files to the ~\Download\ foder
         
    .EXAMPLE
        PS C:\ Get-PMPackage "notepad" | Remove-PMPackage
         
        This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example.
        This command will remove the package "notepad" from the database and delete any physical files, such as the installer executable.
                 
    .INPUTS
        System.String
         
    .OUTPUTS
        None
         
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [Alias("Name")]
        [string]
        $PackageName,
        
        [Parameter(Position = 1)]
        [switch]
        $RetainFiles,
        
        [Parameter(Position = 2)]
        [AllowEmptyString()]
        [string]
        $Path
        
    )
    
    # Import all PMPackage objects from the database file
    Write-Verbose "Loading existing packages from database"
    $packageList = Import-PackageList
    
    # Check that the name is not empty
    if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) {
        
        Write-Message -Message "The package name cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check if package name exists
    Write-Verbose "Retrieving the ProgramManager.Package Object"
    $package = $packageList | Where-Object { $_.Name -eq $PackageName }
    if ($null -eq $package) {
        
        Write-Message -Message "There is no package called: $PackageName" -DisplayWarning
        return
        
    }
    
    # Check that the path is not empty
    if ($RetainFiles -eq $true -and [System.String]::IsNullOrWhiteSpace($Path) -eq $true) {
        
        Write-Message -Message "The path cannot be empty" -DisplayWarning
        return
        
    }
    
    # Url or chocolatey packages dont store any files
    if ($package.Type -eq "LocalPackage" -or $package.Type -eq "PortablePackage") {
        Write-Verbose "Detected Local-Package or Portable-Package"
        
        # Check in case there is no folder for some reason?
        if ((Test-Path -Path "$script:DataPath\packages\$PackageName\") -eq $false) {
            
            Write-Message -Message "There are no files for this package in the package store. This should not happen." -DisplayError
            return
        }
        
        
        if ($RetainFiles -eq $true) {
            Write-Verbose "Detected -RetainFiles flag"
            
            # Check that the path to move the files to is valid
            if ((Test-Path -Path $Path) -eq $false) {
                
                Write-Message -Message "The file path does not exist" -DisplayWarning
                return
                
            }
            
            # Check that the path doesn't contain any characters which could cause potential issues
            if ($Path -like "*.*" -or $Path -like "*``**") {
                
                Write-Message -Message "The path contains invalid characters" -DisplayWarning
                return
                
            }
            
            if ($PSCmdlet.ShouldProcess("Package `'$PackageName`'", "Move the package files to $Path")){
                
                # Move the package files to the specified path
                Write-Verbose "Moving package files to $Path"
                Move-Item -Path "$script:DataPath\packages\$PackageName\" -Destination $Path
                
            }
            
        }else {
            
            if ($PSCmdlet.ShouldProcess("Package `'$PackageName`'", "Delete the package files")) {
                
                # Remove the package from the package store
                Write-Verbose "Deleting package files at \packages\$PackageName"
                Remove-Item -Path "$script:DataPath\packages\$PackageName\" -Recurse -Force
                
            }
        
        }
        
    }elseif ($package.Type -eq "UrlPackage" -and $RetainFiles -eq $true) {
        
        # Notify user that -RetainFiles flag has no effect on a url package
        Write-Message -Message "The flag -RetainFiles has no effect on a url package" -DisplayWarning
        
    }
    
    
    if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Remove the package `'$PackageName`'")){
        
        # Remove the PMPackage from the list
        Write-Verbose "Removing package object from list"
        $packageList.Remove($package) | Out-Null
        
        # Export-out package list to xml file
        Write-Verbose "Writing-out data back to database"
        Export-PackageList -PackageList $packageList
        
    }
    
}

function Set-PMPackage {
    <#
    .SYNOPSIS
        Sets a property of a ProgramManager package.
         
    .DESCRIPTION
        Modifies an existing property for a ProgramManager.Package object.
         
    .PARAMETER PackageName
        The name of the pacakge to modify.
         
    .PARAMETER PropertyName
        The name of the property to modify.
         
    .PARAMETER PropertyValue
        The new value of the property to set.
         
    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.
         
    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.
         
    .EXAMPLE
        PS C:\> Set-PMPackage -PackageName "notepad" -PropertyName "Note" -PropertyValue "A new description"
         
        This will set the 'Note' property to the newly passed in value for the 'notepad' package.
        If this property already exists, it will be modified.
         
    .EXAMPLE
        PS C:\ Get-PMPackage "notepad" | Set-PMPackage -PropertyName "Note" -PropertyValue "A new description"
         
        This command supports passing in a ProgramManager.Package object, by retrieving it using Get-PMPacakge for example.
        This will set the 'Note' property to the newly passed in value for the 'notepad' package.
         
    .INPUTS
        System.String
         
    .OUTPUTS
        None
         
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
    Param (
        
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [AllowEmptyString()]
        [Alias("Name")]
        [string]
        $PackageName,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [AllowEmptyString()]
        [string]
        $PropertyName,
        
        [Parameter(Mandatory = $true, Position = 2)]
        [AllowEmptyString()]
        [string]
        $PropertyValue
        
    )
    
    # Import all PMPackage objects from the database file
    Write-Verbose "Loading existing packages from database"
    $packageList = Import-PackageList
    
    # Check that the name is not empty
    if ([System.String]::IsNullOrWhiteSpace($PackageName) -eq $true) {
        
        Write-Message -Message "The package name cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check if package name exists
    Write-Verbose "Retrieving the ProgramManager.Package Object"
    $package = $packageList | Where-Object { $_.Name -eq $PackageName }
    if ($null -eq $package) {
        
        Write-Message -Message "There is no package called: $PackageName" -DisplayWarning
        return
        
    }
    
    # Check that the property name is not empty
    if ([System.String]::IsNullOrWhiteSpace($PropertyName) -eq $true) {
        
        Write-Message -Message "The property name cannot be empty" -DisplayWarning
        return
        
    }
    
    # Check that the property name is valid
    Write-Verbose "Retrieving property from ProgramManager.Package Object"
    $property = $package.psobject.properties | Where-Object { $_.Name -eq $PropertyName }
    if ($null -eq $property) {
        
        Write-Message -Message "There is no property called: $PropertyName in package $PackageName" -DisplayWarning
        return
        
    }
    
    # Set the value to the newly specified value
    Write-Verbose "Setting property:{$PropertyName} value to $PropertyValue"
    $property.Value = $PropertyValue
    
    if ($PSCmdlet.ShouldProcess("$script:DataPath\packageDatabase.xml", "Edit the package `'$PackageName`'")){
        
        # Export-out package list to xml file
        Write-Verbose "Writing-out data back to database"
        Export-PackageList -PackageList $packageList
        
    }
    
    
}

<#
Stored scriptblocks are available in [PsfValidateScript()] attributes.
This makes it easier to centrally provide the same scriptblock multiple times,
without having to maintain it in separate locations.
 
It also prevents lengthy validation scriptblocks from making your parameter block
hard to read.
 
Set-PSFScriptblock -Name 'ProgramManager.ScriptBlockName' -Scriptblock {
     
}
#>


$argCompleter_PackageNames = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Import all PMPackage objects from the database file
    $packageList = Import-PackageList    
    
    if ($packageList.Count -eq 0) {
        Write-Output ""
    }
    
    $packageList.Name | Where-Object { $_ -like "$wordToComplete*" }
    
}

$argCompleter_PackagePropertyName = {
    param ($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    
    # Get the already typed in package name
    $packageName = $fakeBoundParameters.PackageName
    
    if ($null -ne $packageName) {
        
        # Import all PMPackage objects from the database file
        $packageList = Import-PackageList
        
        # Get the package object
        $package = $packageList | Where-Object { $_.Name -eq $packageName }
        
        if ($null -ne $package){
            
            # Get all properties and return ones which match the filter
            $properties = $package.psobject.properties.Name            
            $properties | Where-Object { $_ -like "$wordToComplete*" }
            
        }
        
    }
    
    
}

Register-ArgumentCompleter -CommandName Invoke-PMInstall -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames
Register-ArgumentCompleter -CommandName Invoke-PMUninstall -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames
Register-ArgumentCompleter -CommandName Get-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames
Register-ArgumentCompleter -CommandName Set-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames
Register-ArgumentCompleter -CommandName Remove-PMPackage -ParameterName PackageName -ScriptBlock $argCompleter_PackageNames

Register-ArgumentCompleter -CommandName Set-PMPackage -ParameterName PropertyName -ScriptBlock $argCompleter_PackagePropertyName

#endregion Load compiled code