AppManiProgramManager.psm1

# Version: 1.19.0 Date: 150423 Last update by: rod@appmani.com
# + Added functions Get-M365SupportedVersion and Get-M365UpdateChannel
# Version: 1.18.7 Date: 020423 Last update by: rod@appmani.com
# ! Fixed formatting errors on install/uninstall commands
# Version: 1.18.6 Date: 020423 Last update by: rod@appmani.com
# ! Fixed Adobe Acrobat/Digital Editions script stuck bug when Get-LatestVersion is called the second time.
# Version: 1.18.5 Date: 310323 Last update by: rod@appmani.com
# / Added pre-install steps to Adobe Digital Edition to add some registry keys to skip Norton installation prompt.
# Version: 1.18.4 Date: 270323 Last update by: rod@appmani.com
# ! Fixed bug where the script terminates after running Disable-IEFirstRunCustomization
# + Added function Get-IgnoreExitCodes to cater installations where we have to make exceptions on certain exit codes e.g. Adobe Digital Edition
# + Added function Get-IgnoreExitCodes to adjust for installations that gives an unusual exit code
# + Added Adobe Digital Editions to supported applications
# Version: 1.18.3 Date: 240323 Last update by: rod@appmani.com
# ! Added a delay after uninstalling programs before continuing updates.
# Version: 1.18.2 Date: 220323 Last update by: rod@appmani.com
# ! Provided an override for Digisign's uninstall command
# Version: 1.18.1 Date: 210323 Last update by: rod@appmani.com
# ! Fixed install command for Zoom
# ! Fixed code for retrieving latest version number for Adobe Acrobat
# Version: 1.18.0 Date: 170323 Last update by: rod@appmani.com
# / Set the option to use strings.exe via a switch parameter
# + Added function Approve-Installation and Invoke-ResolveArchitectureConflicts to remediate duplicate installations on different/same architectures before installing. The logic for determining if a program is up for an update is moved here too.
# + Added function Get-ProcessesToTerminate
# Version: 1.17.3 Date: 250223 Last update by: rod@appmani.com
# ! Fixed bug where Get-UninstallCommand still tries to use strings.exe
# Version: 1.17.2 Date: 240223 Last update by: rod@appmani.com
# + Added Microsoft 365 functions
# Version: 1.17.1 Date: 190223 Last update by: rod@appmani.com
# + Added Synology Active Backup for Business Agent functions
# Version: 1.17.0 Date: 190223 Last update by: rod@appmani.com
# / Changed program name for Python to 'Python 3'
# + Added functions Get-InstallCommand and Get-PreInstallScriptBlock
# * Add-RegistryKey can now recursively create missing registry keys recursively
# / Add-RegistryValue can now only add single values from multiple values. Looping will be handled in the scripts.
# Version: 1.16.0 Date: 050223 Last update by: rod@appmani.com
# + Added registry management functions
# / Changed $Error references to $Global:Error
# Version: 1.15.0 Date: 290123 Last update by: rod@appmani.com
# / Replaced method of determining installer tool from strings.exe to PowerShell native commands
# Version: 1.14.0 Date: 280123 Last update by: rod@appmani.com
# * Improved program architecture detection function
# Version: 1.13.1 Date: 210123 Last update by: rod@appmani.com
# ! Fixed bug where SYSTEM is unable to run strings.exe
# Version: 1.13.0 Date: 210123 Last update by: rod@appmani.com
# + Added Synology Drive Client latest version and download link retrieval
# + Added step on Get-UninstallCommand where the function can try to determine silent uninstall switch based on what tool the uninstall executable was made from.
# Version: 1.12.3 Date: 110123 Last Updated by: rod@appmani.com
# ! Fixed Adobe Acrobat latest version retrieval bug
# Version: 1.12.2 Date: 080123 Last Update by: rod@appmani.com
# + Added download link and latest version retrieval code for LegalAid Templates
# ! Fixed bug where comparing versions errors when it's too short.
# Version: 1.12.0 Date: 080123 Last Update by: rod@appmani.com
# + Added Get-MultipleInstalledProgram function
# / Get-UninstallCommand now uses wildcards to find uninstall command overrides
# + Added Get-ProgramRegistryDisplayRegex
# + Added Approve-SelectedPrograms
# Version: 1.11.3 Date: 231222 Last Update by: rod@appmani.com
# ! Fixed Adobe Acrobat bug
# Version: 1.11.2 Date: 221222 Last Update by: rod@appmani.com
# ! Overriden the version on Adobe Acrobat while fixing bug that occurs when the code detects a version that has an 'x' on it.
# Version: 1.11.0 Date: 221221 Last Update by: rod@appmani.com
# + Added functions Get-UninstallCommand and UninstallProgram
# + Added new registry paths for Get-InstalledProgram
# / Get-InstalledProgram only returns the topmost result if more than 1 results are returned.
# ! Fixed IrfanView download link retrieval function
# ! Fixed message when download link cannot be retrieved.
# Version: 1.10.0 Date: 221202 Last Update by: rod@appmani.coms
# + Added function Set-Alert
# Version: 1.9.2 Date: 221122 Last Update by: rod@appmani.com
# * Improved error handling on Get-LatestVersion/DownloadLink/Installer
# Version: 1.9.1 Date: 221117 Last Update by: rod@appmani.com
# + Added security measures to Remove-InstallerFolder to prevent deleting C:\ uninstentionally
# * Improved error handling in Confirm-Update and Get-LatestVersion
# Version: 1.9.0 Date: 221113 Last Updated by: rod@appmani.com
# + Add function Get-LatestVersion, Confirm-LogFolder, Write-Log, Disable-IEFirstRunCustomization
# / Replaced usage of Write-Host functions to Write-Log
# * Downloading installers now able to supply download file name independently
# / Get-InstalledProgram now uses a regex so you can match specific programs
# / Confirm-Program/ServiceInstallation changed retry times to 3 from 30
# / Changed Compare-Versions to Confirm-Update where this function utilizes the Get-LatestVersion function to determine if program is due for an update
# Version: 1.8.0 Date: 221011 Last Updated by: rod@appmani.com
# + Added function Set-AgentRefresh
# Version: 1.7.0 Date: 221011 Last Updated by: rod@appmani.com
# + Added function Confirm-InstallerValidity
# Version: 1.6.0 Date: 221009 Last Updated by: rod@appmani.com
# + Added function Get-DownloadLink
# Version: 1.5.0 Date: 220916 Last Updated by: rod@appmani.com
# + Added function Invoke-ModuleForUpdate
# Version: 1.4.1 Date: 220817 Last Updated by: rod@appmani.com
# / Fixed Get-ProgramArchitecture's output from x32 to x86
# Version: 1.4.0 Date: 220817 Last Updated by: rod@appmani.com
# / Changed Get-InstalledProgram/Service parameter 'Program' to 'ProgramName'
# / Get-InstalledProgram's use of wildcards will only be used depending on function call's parameters
# + Added new functions Set-RegistryItem, Get-ProgramArchitecture
# * Improved error handling responses
# 1.3.2 ! Fixed a syntax error
# 1.3.1 - Removed some lines for debugging
# 1.3.0 * Set ProgressPreference to SilentlyContinue to improve download time
# + Now displays current and latest available version of program
# 1.2.2 + Added functions Add-InstallerFolder and Remove-InstallerFolder in functions to export in module manifest
# 1.2.1 - Removed uneeded files in package
# 1.2.0 + Added Add-InstallerFolder function so all installer related files will go to a single folder
# / Changed Remove-Installer function to Remove-InstallerFolder
# 1.1.2 / Changed version just to test updates
# 1.1.1 / Changed author to Appmani
# 1.1.0 + Added Confirm-ServiceInstallation function
# 1.0.0 + First upload

# This function is to mitigate the Invoke-WebRequest error where it won't run because IE First Run Customization hasn't been done yet. Using the switch parameter UseBasicParsing would work for regular web requests, but not for Downloads
Function Test-WebRequest {
    Param (
        $URI
    )

    # Loop until system is able to successfully invoke a web request
    while ($null -eq $webRequest) {
        try {
            $webRequest = Invoke-WebRequest -Uri $URI
        }
        # Catches the exception where IE first run customization has not been done yet
        catch [System.NotSupportedException] {
            Write-Host "Disabling IE First RunCustomization..." -NoNewline
            try {
                Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main" -Name "DisableFirstRunCustomize" -Value 2
            }
            catch {
                Write-Log -LogType ERROR -Message "Failed to disable IE First RunCustomization: $($Global:Error[0])"
                return $null
            }
        }

        # catches other exceptions
        catch {
            Write-Log -LogType ERROR -Message "Failed to execute test webrequest: $($Global:Error[0])"
            return $null
        }
    }
    return $webrequest
}
   
# Downloads installer
Function Get-Installer {
    Param (
        $DownloadLink,
        $SavePath
    )
    $ProgressPreference = 'SilentlyContinue'

    # Tests if save path is existing
    if (Test-Path $savePath) {
        
        $attempts = 1
        $maxAttempts = 3
        $null = $fileName

        # Downloads the file
        while ($attempts -le $maxAttempts) {
            Write-Log -LogType INFO -Message "Attempts: $attempts" 
            # Downloads the file
            try {
                $download = Invoke-WebRequest -Uri $downloadLink
            }
            catch [System.NotSupportedException] {
                Disable-IEFirstRunCustomization
                continue
            }
            catch {
                Write-Log -LogType ERROR -Message "Unable to download installer: $($Global:Error[0])"
                return $null
            } 
            
            # Tries to get filename from Content-Disposition header
            if ($download.Headers["Content-Disposition"]) {
                try {
                    $content = [System.Net.Mime.ContentDisposition]::new($download.Headers["Content-Disposition"])
                    $fileName = $content.FileName
                }
                catch {
                }

            }
            
            # If not, get capture filename from download link
            if (!$fileName) {
                $matches = @()
                # uses GetFileName, decodes any HTTP encoding, removes the character '?' and preceeding characters, and matches it with a filename regex
                Add-Type -AssemblyName System.Web
                ([System.Web.HTTPUtility]::UrlDecode([System.IO.Path]::GetFileName($downloadLink)) -replace '\?.*$') -match '.+\..+$' | Out-Null
                if ($matches[0]) {
                    $fileName = $matches[0]
                }
                # If that still doesn't work use fallback filename
                else {
                    $fileName = "$global:programNameNoSpace.exe"
                }
            
            }
        
            # Actually saves the file to disk
            $installerPath = $SavePath + '\' + $fileName
            #Write-Host $SavePath
            $f = [IO.File]::OpenWrite($installerPath); 
            try { 
                $download.RawContentStream.WriteTo($f); 
            }
            finally { 
                $f.Dispose(); 
            }

            if (Test-Path $installerPath) {
                return $installerPath
            }

            $attempts++
        }

    }
    else {
        Write-Log -LogType ERROR -Message "Download path $SavePath not existing. Please specify a valid path."
     
    }
    
    return $null
    
}

# Creates folder for storing installation files e.g. msi, exe, config files, etc
Function Add-InstallerFolder {
    Param (
        $Path
    )
    If (Test-Path -Path $Path) {
        try {
            Remove-Item -Path $Path -Force -Recurse #-ErrorAction Stop
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to delete installer folder and its contents: $($Global:Error[0])"
            return $null
        }
    }

    try {
        $installerFolder = New-Item -Path $Path -ItemType Directory
        return $installerFolder
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to create installer folder: $($Global:Error[0])"
        return $null
    }
}


#Deletes installer
Function Remove-InstallerFolder {
    Param (
        $Path,
        $CleanupDelay
    )

    $minimumPathLength = 4
    if ($Path.Length -lt $minimumPathLength) {
        Write-Log -LogType ERROR "Invalid path. As a security measure, this function needs a path with a minimum length of $minimumPathLength to proceed."
        return $null
    }

    # Cleans up installer folders
    Write-Log -LogType INFO -Message "Clean up will be performed in $cleanupDelay seconds."
    Start-Sleep -Seconds $CleanupDelay
    
    Write-Log -LogType INFO -Message "Cleaning up..." -NoNewline
    if ((Get-Location).Path -eq $Path) {
        Set-Location ..
    }
    # Removes a file
    try {
        Remove-Item -Path $Path -Recurse -Force
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to delete installer folder: $($Global:Error[0])"
        return $null
    }

    Write-Log -LogType INFO -Message "Removed folder and contents of $Path." 
}

# Checks the registry for entries of the installed program and returns information about it
Function Get-InstalledProgram {
    Param (
        $RegistryDisplayName,
        [Switch]$All
    )
    
    $HKUPSDrive = Get-PSDrive HKU -ErrorAction SilentlyContinue
    if (!($HKUPSDrive)) {
        New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -Scope Script | Out-Null
    }

    Write-Log -LogType INFO -Message "Searching registry for installations."

    $Apps = @()
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" # 32 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"             # 64 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties"
    $Apps += Get-ItemProperty "HKU:\*\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    
    $installedPrograms = $Apps | Where-Object DisplayName -match $RegistryDisplayName

    Write-Log -LogType INFO -Message "Search returned $(($installedPrograms | Measure-Object).Count) results."

    $installedPrograms = Get-InstallationsArchitecture -InstalledPrograms $installedPrograms

    if ($installedPrograms) {

        if ($All) {
            
            return $installedPrograms
        }
        else {
            Write-Log -LogType INFO -Message "Returning topmost object."
            return $installedPrograms[0]
        }

    }
}

Function Get-MultipleInstalledProgram {
    Param (
        $RegistryDisplayName,
        $RegistryDisplayVersion
    )
    
    $HKUPSDrive = Get-PSDrive HKU -ErrorAction SilentlyContinue
    if (!($HKUPSDrive)) {
        New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -Scope Script | Out-Null
    }

    $Apps = @()
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" # 32 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"             # 64 Bit
    $Apps += Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties"
    $Apps += Get-ItemProperty "HKU:\*\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    
    $installedPrograms = $Apps | Where-Object DisplayName -match $RegistryDisplayName

    if ($installedPrograms) {
        $installedPrograms = $Apps | Where-Object { ($_.DisplayName -match $RegistryDisplayName) -and ($_.DisplayVersion -match $RegistryDisplayVersion) }
        return $installedPrograms
    }
}

# Checks the registry for entries of the isntalled service and returns information about it
Function Get-InstalledService {
    Param (
        $ServiceDisplayName
    )
    $RegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceDisplayName"

    $installedService = Get-ItemProperty -Path $RegistryPath -ErrorAction SilentlyContinue

    return $installedService

}

# Gets exit codes to ignore when installing
Function Get-IgnoreExitCodes {
    Param (
        $ProgramName
    )

    $IgnoreExitCodes = @{
        'Adobe Digital Editions' = '1223'
    }

    $IgnoreExitCode = $IgnoreExitCodes.$ProgramName
    if (!$IgnoreExitCode) { return $null }
    else { return $IgnoreExitCode }
}

#Installs program using a one-liner msiexec or calls the installer executable with additional arguments
Function Install-Program {
    Param (
        $location,
        $installCommand
    )

    if (Test-Path $location) {
        Set-Location $location
    }
    else {
        Write-Log -LogType ERROR -Message "Unable to change location to $location."
    }

    Write-Log -LogType INFO -Message "Executing command $installCommand"

    try {
        cmd /c $installCommand
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to install: $($Global:Error[0])"
        return 1
    }

    Write-Log -LogType INFO -Message "Execution completed with exit code $LASTEXITCODE"

    return $LASTEXITCODE
}

Function Confirm-ProgramInstallation {
    Param (
        $RegistryDisplayName
    )
    # Loops X number of times to check registry keys for the program
    $tries = 0
    while ($tries -le 3) {
        $tries++
        Write-Log -LogType INFO -Message "Verifying installation. Tries: $tries"

        $installedProgram = Get-InstalledProgram -RegistryDisplayName $RegistryDisplayName
        
        if ($null -ne $installedProgram) {
            return $installedProgram
        }

        Start-Sleep -s 15
    }
    Write-Log -LogType ERROR -Message "Script has reached the maximum number of retries on installation verification. Please investigate for issues."
    return $null
}

Function Confirm-ServiceInstallation {
    Param (
        $ServiceDisplayName
    )
    # Loops X number of times to check registry keys for the service
    $tries = 0
    while ($tries -le 3) {
        $tries++
        Write-Log -LogType INFO -Message "Verifying installation. Tries: $tries"

        $installedService = Get-InstalledService -ServiceDisplayName $ServiceDisplayName
        
        if ($null -ne $installedService) {
            return $installedService
        }

        Start-Sleep -s 15
    }
    Write-Log -LogType ERROR -Message "Script has reached the maximum number of retries on installation verification. Please investigate for issues."
    return $null
}

# Compares current version of a program from the registry and what's on the download link. There are programs that won't have registry entries and programs that won't have their versions on the download link, so please check first before using
Function Confirm-Update {
    Param (
        $ProgramName,
        $InstalledVersion,
        $VersionMatchRegex
    )

    # Sets a default regex if it's blank or null
    if (!$VersionMatchRegex) { $VersionMatchRegex = '.*' }

    # Removes whitespaces for programs who has a space on its Display Version for some reason like CutePDF: ' 4.1'
    $InstalledVersion = $($InstalledVersion -replace '\s', '')
    
    # Gets latest version available
    $latestVersion = $(Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber
    if (!$latestVersion) { return $null }

    Write-Log -LogType INFO -Message "Latest version available: $latestVersion"
    Write-Log -LogType INFO -Message "Installed version: $InstalledVersion"

    # Matches versions to a regex. This is for installed vs scraped latest versions that do not follow the same format e.g. 1.2.3456(registry display version) vs 1.2(scraped from website)
    if (!($InstalledVersion -match $VersionMatchRegex)) { 
        Write-Log -LogType ERROR -Message "Installed version doesn't match version match regex."
        return $false 
    }
    else { $InstalledVersion = $matches[0] }

    if (!($latestVersion -match $VersionMatchRegex)) { 
        Write-Log -LogType ERROR -Message "Latest version doesn't match version match regex."
        return $false 
    }
    else { $latestVersion = $matches[0] }

    try {
        $isProgramForUpdate = [version]$InstalledVersion -lt [version]$latestVersion
        if (!$isProgramForUpdate) {
            Write-Log -LogType INFO "Currently installed version is equal to or higher than latest version retrieved."
        }
        else {
            Write-Log -LogType INFO "$ProgramName will be updated."
        }
    
    }
    catch [System.Management.Automation.RuntimeException] {
        $isProgramForUpdate = $InstalledVersion -lt $latestVersion
    }

    return $isProgramForUpdate
}

Function Set-RegistryItem {
    Param (
        $RegistryPath,
        $Name,
        $Value,
        $PropertyType
    )
    # Create the key if it does not exist
    If (-NOT (Test-Path $RegistryPath)) {
        try {
            New-Item -Path $RegistryPath -Force -ErrorAction Stop #| Out-Null
            Write-Log -LogType INFO -Message "Mew registry path $RegistryPath created."
        }
        catch {
            return $null
        }
    }
  
    # Now set the value
    try {
        New-ItemProperty -Path $RegistryPath -Name $Name -Value $Value -PropertyType DWORD -Force -ErrorAction Stop
        Write-Log -LogType INFO -Message "Registry item $RegistryPath\$Name set to $Value."
    }
    catch {
        return $null
    }
}

Function Get-ProgramArchitecture {
    Param (
        [Parameter(ValueFromPipeline = $true)]
        $Program
    )

    $ProgramName = $Program.DisplayName
    switch -regex ($ProgramName) {
        "(64 bit|64\-bit|x64|64bit)" {
            # Write-Log -LogType INFO -Message "Architecture determined using DisplayName"
            return "x64"
        }
        "(32 bit|32\-bit|x86|32bit)" {
            # Write-Log -LogType INFO -Message "Architecture determined using DisplayName"
            return "x86"
        }
    }

    $InstallLocation = $Program.InstallLocation
    switch -regex ($InstallLocation) {
        "C:\\Program Files\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using InstallLocation"
            return "x64"
        }
        "C:\\Program Files \(x86\)\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using Program InstallLocation"
            return "x86"
        }
    }

    $UninstallString = $Program.UninstallString
    switch -regex ($UninstallString) {
        "C:\\Program Files\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using UninstallString"
            return "x64"
        }
        "C:\\Program Files \(x86\)\\" {
            # Write-Log -LogType INFO -Message "Architecture determined using UninstallString"
            return "x86"
        }
    }

    if ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall') {
        # Write-Log -LogType INFO -Message "Architecture determined using PSParentPath"
        return "x86"
    }
    elseif ($Program.PSParentPath -eq 'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') {
        # Write-Log -LogType INFO -Message "Architecture determined using PSParentPath"
        return "x64"
    }
    else {
        Write-Log -LogType WARNING -Message "Unable to determine architecture."
        return $null
    }
}

Function Send-Keys {
    Param (
        $ApplicationWindowTitle,
        $Keys
    )

    $wshell = New-Object -ComObject wscript.shell;
    $wshell.AppActivate($ApplicationWindowTitle)
    $wshell.SendKeys($Keys)
}

Function Invoke-ModuleForUpdate {
    Param (
        $ModuleName
    )
    
    if (!(Get-PackageProvider -ListAvailable | Where-Object Name -eq 'Nuget')) {
        Write-Log -LogType INFO -Message "Installing Nuget..."
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        Install-PackageProvider -Name Nuget -Force | Out-Null
    }
    Write-Log -LogType INFO -Message "Retrieving installed module..."
    $installedModule = Get-InstalledModule $ModuleName -ErrorAction SilentlyContinue

    # If not install module from PSGallery
    if ($null -eq $installedModule) {
        Write-Log -LogType INFO -Message "$ModuleName module not installed. Please install $ModuleName first."
    }
    # If module is installed check for updates and import
    else {            
        # Gets latest module version available in PSGallery
        $latestModuleVersion = Find-Module $ModuleName -ErrorAction Ignore
        if ($latestModuleVersion) {
                
            # Checks if installed module version needs an update
            if ($latestModuleVersion.Version -ne $installedModule.Version) {
                Write-Log -LogType INFO -Message "Installing new version of $ModuleName..." -NoNewline
                try {
                    Update-Module $ModuleName -Force -ErrorAction Stop
                    Write-Log -LogType INFO -Message "Done!"
                }
                catch {
                    Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                    return $null
                }
            }
            Else {
                Write-Log -LogType INFO -Message "Module $ModuleName is already up to date."
            }
        }

    }
}

Function Get-DownloadLink {
    Param (
        $ProgramName,
        $Architecture
    )

    $null = $Global:ArchitectureUsed

    $attempts = 1
    $maxAttempts = 3

    while ($attempts -le $maxAttempts) {
        Write-Log -LogType INFO -Message "Attempts: $attempts"
        try {
            switch ($ProgramName) {
                '7-zip' {
                        
                    $HomePage = "https://www.7-zip.org/"
            
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $architectureFilter = '-x64'
                    }
                    else {
                        $architectureFilter = ''
                    }
            
                    $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html'
                    $Pattern = '<A href=\"(?<link>a/7z\d+{0}\.exe)\">Download</A>' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $link = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                
                    if ($null -ne $link) {
                        $downloadLink = $HomePage + "$($link)"
                        #$Global:ArchitectureUsed =
                    }
                    else {
                        Write-Log -LogType ERROR -Message "Version requested is not available."
                        return $null
                    }
                }
                'Adobe Acrobat' {
                    # $versionOverride = '22.003.20282'
            
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $downloadLinkFormat = 'https://ardownload2.adobe.com/pub/adobe/acrobat/win/AcrobatDC/{0}/'
                        $fileNameFormat = 'AcroRdrDCx64{0}_en_US.exe'
                    }
                    elseif ($architecture -eq 'x86') {
                        $downloadLinkFormat = 'https://ardownload2.adobe.com/pub/adobe/reader/win/AcrobatDC/{0}/'
                        $fileNameFormat = 'AcroRdrDC{0}_en_US.exe'
                        
                    }
            
                    $latestVersion = (Get-LatestVersionNumber -ProgramName $ProgramName).VersionNumber

                    if ($versionOverride) {
                        Write-Log -LogType INFO -Message "Version override detected: $versionOverride"
                        $latestVersion = $versionOverride
                    }

                    # If no latest version was retrieved and no override, return null
                    if (!$latestVersion) {
                        Write-Log -LogType INFO -Message "Unable to retrieve download link."
                        return $null
                    }
                
                    $latestVersion = $latestVersion -replace '[.]', ''
                    
                    $filenameFormat = $filenameFormat -f $latestVersion
                    $downloadLink = $("$downloadLinkFormat" + "$filenameFormat") -f $latestVersion
                    #return $downloadLink
                }
                'Adobe Digital Editions' {
                    $HTML = Invoke-RestMethod 'https://www.adobe.com/nz/solutions/ebook/digital-editions/download.html'
                    $Pattern = '<a href=\"(?<link>.+?Installer\.exe)\">'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                }
                'Audacity' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '64 bit'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = '32 bit'
                    }
                    
                    $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/'
                    $Pattern = '<a href=\"(?<link>.*)\">Audacity .+? {0} installer</a>' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    
                    #return $downloadLink
                }
                'Bitwarden' {
                    $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Citrix Workspace' {
                    $downloadLink = ((Invoke-WebRequest -URI 'https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html').Links | Where-Object { ($_.outerText -like 'Download *') -and ($_.rel -like '*CitrixWorkspaceApp.exe*') }).rel[0]
                    $downloadLink = "https:" + $downloadLink
                    #return $downloadLink
                }
                'CutePDF Writer' {
                    $downloadLink = 'https://www.cutepdf.com/download/CuteWriter.exe'
                    #return $downloadLink
                }
                'Digisign' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class=\"button\" href=\"(?<link>.*)\">Digisign.+?</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    #return $downloadLink
                }
                'Dropbox' {
                    $originalLink = 'https://www.dropbox.com/download?full=1&plat=win'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Microsoft Edge' {
                    $originalLink = 'https://go.microsoft.com/fwlink/?linkid=2109047&Channel=Stable&language=en&consent=1'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Filezilla' {
            
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture

                    if ($architecture -eq 'x64') {
                        $architectureFilter = 'win64'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = 'win32'
                    }
                        
                    $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                    $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
                    $HTML = Invoke-RestMethod -UseBasicParsing -Uri "https://filezilla-project.org/download.php?show_all=1" -WebSession $session                    
                    $Pattern = '<a href=\"(?<link>.*)\" rel="nofollow">FileZilla_.+?_{0}-setup.exe</a>' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    
                    #return $downloadLink
                }
                'Foxit PDF Reader' {
                    $originalLink = 'https://www.foxit.com/downloads/latest.html?product=Foxit-Reader&platform=Windows&version=&package_type=&language=English&distID='
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Google Drive' {
                    $downloadLink = 'https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe'
                    #return $downloadLink
                }
                'GPL Ghostscript' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') {
                        $architectureFilter = 'w64'
                    }
                    else {
                        $architectureFilter = 'w32'
                    }
                        
                    $link = 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/latest'
                    $latestLink = (Invoke-WebRequest -Uri $link -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    $latestLink -match 'gs(.+?)$' | Out-Null
                    $downloadLink = $latestLink.replace('tag', 'download') + "/gs$($matches[1])$architectureFilter.exe"
                    #return $downloadLink
                }
                'Google Chrome' {
                    $downloadLink = 'http://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi'
                    #return $downloadLink
                }
                'HP Support Assistant' {
                    $downloadLink = 'https://ftp.ext.hp.com/pub/softpaq/sp141501-142000/sp141886.exe'
                    #return $downloadLink
                }
                'IrfanView' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture

                    if ($Architecture -eq 'x86') {
                        $ArchitectureFilter = ''
                    }
                    
                    $test = (Invoke-WebRequest -Uri "https://www.fosshub.com/IrfanView.html" -UseBasicParsing).content 
                    #$data = ($test | Select-String -Pattern '(?<=\s=).*').matches.value | ConvertFrom-Json
                    $test -match 'var settings =(.+?)\n' | Out-Null
                    $data = $matches[1] | ConvertFrom-Json


                
                    try { 
                        $Url = 'https://api.fosshub.com/download' 
                        $Params = @{ 
                            Uri             = $Url 
                            Body            = @{ 
                                projectId  = "$($data.projectId)" 
                                releaseId  = "$($data.pool.f.r | Select -Unique)" 
                                projectUri = 'IrfanView.html' 
                                fileName   = $((($data).pool.f | Where-Object { $_.n -match ('iview(\d+)_?{0}_setup\.exe' -f $ArchitectureFilter) }))[0].n
                                source     = "$($data.pool.c)" 
                            }
                            Headers         = @{
                                'User-Agent' = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                            }
                            Method          = 'POST'
                            UseBasicParsing = $true
                        }
                        $info = (Invoke-WebRequest @Params).Content | ConvertFrom-Json
                        $Global:ErrorType = $Response.error
                        if ($Global:ErrorType -ne $Null) {
                            throw "ERROR RETURNED $Global:ErrorType"
                            return $Null
                        }
                        $downloadLink = ($info.data)[0].url
                    }
                    catch {
                        Write-Error $_
                    }
                }
                'Jabra Direct' {
                    $downloadLink = 'https://jabraxpressonlineprdstor.blob.core.windows.net/jdo/JabraDirectSetup.exe'
                    #return $downloadLink
                }
                'Java 8' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '\(64-bit\)'
                    }
                    else {
                        $architectureFilter = 'Offline'
                    }
            
                    $URL = "https://www.java.com/en/download/manual.jsp"
                    $global:ie = New-Object -com "InternetExplorer.Application"
                    $global:ie.visible = $false
                    $global:ie.Navigate($URL)
            
                    DO { Start-Sleep -s 1 }UNTIL(!($global:ie.Busy))
                    Start-Sleep 5
                    if ($global:ie.Document.body.innerHTML) {
                        $HTML = $global:ie.Document.body.innerHTML.ToString()
                    }
                    else { return $null }
                    
                    $Pattern = '<a title=\"Download Java software for Windows {0}\" href=\"(?<link>.*)\"><img' -f $architectureFilter
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
    
                    #return $downloadLink
                }
                'LegalAid Templates' {
                    $domainName = 'https://www.justice.govt.nz'
                    $HTML = Invoke-RestMethod "$domainName/about/lawyers-and-service-providers/legal-aid-lawyers/forms/download-word-template-package/"
                    $Pattern = '<a title=\"LegalAid Templates Installer Version \d+\" href=\"(?<link>.*)\">Word'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    $downloadLink = $domainName + $downloadLink
                }
                'LOLComponents' {
                    $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads'
                    $Pattern = '<a class="button" href=\"(?<link>.*)\">Landonline Client Components \(ZIP .+?\)</a>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    #return $downloadLink
                }
                'Microsoft 365' {
                    $HTML = Invoke-RestMethod 'https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117'
                    $Pattern = '<td class=\"file-link\"><a href=\"(?<link>.*)\"><span'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $downloadLink = ($AllMatches[0].Groups.Where{ $_.Name -like 'link' }).Value
                    #return $downloadLink
                }
                'Mozilla Firefox' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($architecture -eq 'x64') {
                        $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                    }
                    elseif ($architecture -eq 'x86') {
                        $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win&lang=en-US'
                    }
                    
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Net Monitor for Employees Agent' {
                    $downloadLink = 'https://networklookout.com/dwn/nmemplpro_agent.msi'                    
                    #return $downloadLink
                }
                'Notepad++' {
                        
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    $NPlusPlusWebsite = "https://notepad-plus-plus.org"
                    $DownloadPage = $NPlusPlusWebsite + "/downloads"
                    
                    # $filter = '*Installer.exe'
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '*Installer.x64.exe'
                    }
                    else {
                        $architectureFilter = '*Installer.exe'
                    }
                    
                    try {
                        $currentVersion = $($(Invoke-WebRequest -Uri $DownloadPage).Links | Where-Object innerText -like 'Current Version*').href
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                    
                    if ($null -ne $currentVersion) {
                        $NPlusPlusCurrentVersionDownloadPage = $NPlusPlusWebsite + $currentVersion
                        
                        try {
                            $downloadLink = $(Invoke-WebRequest $NPlusPlusCurrentVersionDownloadPage).Links.href -like $architectureFilter | Select-Object -First 1
                            #return $downloadLink
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    }
                    else {
                        Write-Log -LogType ERROR -Message "Unable to find current version."
                        return $null
                    }
                }
                'PDFCreator' {
                    $originalLink = 'https://download.pdfforge.org/download/pdfcreator/PDFCreator-stable?download'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'Putty' {
                    $PuttyDownloadPage = "https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html"
            
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') { $ArchitectureFilter = 'w64' }
                    elseif ($Architecture -eq 'x86') { $ArchitectureFilter = 'w32' }
            
                    try {
                        $links = (Invoke-WebRequest $PuttyDownloadPage).Links.href 
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
            
                    if ($links) {
                        $downloadLink = $links | Where-Object { ($_ -match "$ArchitectureFilter/(.+?)-installer\.msi$") }
                    }
                    else {
                        Write-Log -LogType ERROR -Message "No links found."
                        return $null
                    }
            
                    # if ($downloadLink) {
                    # return $downloadLink
                    # }
                    # else {
                    # Write-Log -LogType ERROR -Message "Unable to capture download link."
                    # return $null
                    # }
                }
                'Python 3' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    $downloadLink = $(Invoke-WebRequest -Uri 'https://www.python.org/downloads/').Links.href | Where-Object { $_ -like '*.exe' }
                    if ($architecture -eq 'x86') {
                        $downloadLink = $downloadLink -replace "-amd64", ""
                    }
                }
                'Synology Drive Client' {
                    $downloadLinkFormat = 'https://global.download.synology.com/download/Utility/SynologyDriveClient/{0}/Windows/Installer/Synology%20Drive%20Client-{0}.msi?model=DS220%2B&bays=2&dsm_version=7.1.1'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'
                    $downloadLink = $downloadLinkFormat -f $latestVersion
                    #return $downloadLink
                }
                'Synology Active Backup for Business Agent' {
                    
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') { $architectureFilter = 'x86_64' }
                    elseif ($Architecture -eq 'x86') { $architectureFilter = 'i686' }
                    
                    $downloadLinkFormat = 'https://global.synologydownload.com/download/Utility/ActiveBackupBusinessAgent/{0}/Windows/{1}/Synology%20Active%20Backup%20for%20Business%20Agent-{0}-{2}.msi?model=DS220%2B&bays=2&dsm_version=7.1.1'
                    $latestVersion = (Get-LatestVersionNumber $ProgramName).VersionNumber -replace '(\d+\.\d+\.\d+)\.(\d+)', '$1-$2'
                    $downloadLink = $downloadLinkFormat -f $latestVersion, $architectureFilter, $architecture
                    #return $downloadLink
                }
                'Sysmon64' {
                    $downloadLink = 'https://download.sysinternals.com/files/Sysmon.zip'
                    #return $downloadLink
                }
                'Microsoft Teams' {
                    $originalLink = 'https://teams.microsoft.com/downloads/desktopcontextualinstaller?env=prod&intent=work&plat=windows&download=true'
                    $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                    #return $downloadLink
                }
                'TreeSize Free' {
                    $downloadLink = 'https://downloads.jam-software.de/treesize_free/TreeSizeFreeSetup.exe'
                    #return $downloadLink
                }
                'UniPrint' {
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    $regex = "UniPrintClientMSI_\d+_$architecture.zip$"
                    $downloadLink = (Invoke-WebRequest -Uri 'https://www.uniprint.net/en/uniprint-client/' -UseBasicParsing).Links.href | Where-Object { $_ -match $regex }
                    #return $downloadLink
                }
                'VLC' {
                    
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    $VLCDownloadPage = "https://www.videolan.org/vlc/download-windows.html"
                    if ($architecture -eq 'x64') {
                        $architectureFilter = '*win64.exe'
                    }
                    elseif ($architecture -eq 'x86') {
                        $architectureFilter = '*win32.exe'
                    }
                        
                    try {
                        $versionDownloadPage = $(Invoke-WebRequest -Uri $VLCDownloadPage).Links | Where-Object href -like $architectureFilter
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                        
                    $versionDownloadPage = "https:" + $versionDownloadPage.href
                    try {
                        $downloadLink = $((Invoke-WebRequest -Uri $versionDownloadPage).Links | Where-Object href -like $architectureFilter | Select-Object -First 1).href
                        #return $downloadLink
                    }
                    catch {
                        Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                        return $null
                    }
                }
                'Wireshark' {

                    $Release = 'Stable Release'
                    $wiresharkDownloadPage = "https://www.wireshark.org/download.html"
                        
                    $versionRegex = '\((.+?)\)'
            
                    $Architecture = if ($Architecture) { $Architecture }
                    else { 'x64' }

                    $Global:ArchitectureUsed = $Architecture
                    
                    if ($Architecture -eq 'x64') {
                        $architectureFilter = 'win64'
                    }
                    elseif ($Architecture -eq 'x86') {
                        Write-Log -LogType INFO "32-bit is no longer available on latest version. The 64-bit version will be downloaded."
                        #$architectureFilter = 'win32'
                        $architectureFilter = 'win64'
                    }
            
                    $webRequest = Invoke-WebRequest -Uri $WiresharkDownloadPage
                
                    # Checks webrequest links for the version number e.g. "Stable 3.6.6" then finds the download link for it
                        ($webRequest).Links.innerHTML | Foreach-Object {
                        if ($_ -match "^$release") {
                            $_ -match $versionRegex | Out-Null
                            $version = $matches[1]
                        }
                    }
            
                    $downloadLink = ($webRequest.Links.href | Where-Object { $_ -match "$architectureFilter-$version" })[0]
                    #return $downloadLink
                }
                'Zoom' {
                    $downloadLink = 'http://zoom.us/client/latest/ZoomInstallerFull.msi'
                    #return $downloadLink
                }
                Default {
                    Write-Log -LogType INFO -Message "No matching function to retrieve download link for $programName"
                    return $null
                }
            }
        }
        catch [System.NotSupportedException] {
            Disable-IEFirstRunCustomization
            continue
        }
        catch {
            Write-Log -LogType ERROR "Unable to retrieve download link for $ProgramName. $($Global:Error[0])"
        }

        if ($downloadLink) {
            return $downloadLink
        }

        $attempts++
        #Start-Sleep 5
    }

    Write-Log -LogType ERROR -Message "The maximum number of attempts to retrieve the download link has been reached."
    return $null
}

Function Get-LatestVersionNumber {
    Param (
        $ProgramName
    )

    try {
        switch ($ProgramName) {
            '7-zip' {            
                $HTML = Invoke-RestMethod 'https://www.7-zip.org/download.html' -ErrorAction Stop
                $Pattern = '<B>Download 7-Zip (?<version>.*) \((.+?)\)</B>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Adobe Acrobat' {
                $versionRegex = '\d{2}\.\d{3}\.\w{5}'
                $acrobatReleaseNotesURL = 'https://helpx.adobe.com/acrobat/release-note/release-notes-acrobat-reader.html'
                $versionsAvailable = $(invoke-webrequest -uri $acrobatReleaseNotesURL -UseBasicParsing).Links | Where-Object { $_.outerHTML -match $versionRegex }
                $latestVersionLink = $versionsAvailable[0].href
                        
                $latestVersionHTML = Invoke-RestMethod $latestVersionLink -UseBasicParsing -Headers @{"accept"="*/*"} -ErrorAction Stop
                $Pattern = '\">AcrobatDCUpd(\d{2})(\d{3})(\d{5})\.msp</a>'
                $latestVersionHTML -match $Pattern | Out-Null
                $latestVersion = "$($matches[1]).$($matches[2]).$($matches[3])"
                break
            }
            'Adobe Digital Editions' {
                $HTML = Invoke-RestMethod 'https://www.adobe.com/nz/solutions/ebook/digital-editions/download.html' -UseBasicParsing -Headers @{"accept"="*/*"} -ErrorAction Stop
                $Pattern = '<h2><b>Adobe Digital Editions (?<version>.*) Installers</b></h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Audacity' {
                $HTML = Invoke-RestMethod 'https://www.audacityteam.org/download/windows/'  -ErrorAction Stop
                $Pattern = '<h2>Current Version: (?<version>.*)</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Bitwarden' {
                $originalLink = 'https://vault.bitwarden.com/download/?app=desktop&platform=windows'
                $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                $Pattern = '/Bitwarden-Installer-(?<version>.*)\.exe'
                $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'CutePDF Writer' {
                $HTML = Invoke-RestMethod 'https://www.cutepdf.com/products/CutePDF/writer.asp' -ErrorAction Stop
                $Pattern = 'Ver\. (?<version>.*); .+? MB\)'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Citrix Workspace' {
                $HTML = Invoke-RestMethod 'https://www.citrix.com/downloads/workspace-app/windows/workspace-app-for-windows-latest.html' -ErrorAction Stop
                $Pattern = '<p><b>Version:</b>&nbsp;(?<version>[\d\.]+).*</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Digisign' {
                $HTML = Invoke-RestMethod 'https://www.linz.govt.nz/guidance/landonline-support/legacy-landonline-support/software-downloads-and-installation/software-downloads' -ErrorAction Stop
                $Pattern = '<p><a class=\"button\" href=\".+?\">Digisign.+? \(EXE .+?MB v(?<version>[\d\.]+) .+?\)</a></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Dropbox' {
                $HTML = Invoke-RestMethod 'https://www.dropboxforum.com/t5/forums/filteredbylabelpage/board-id/101003016/label-name/stable%20build' -ErrorAction Stop
                $Pattern = '<h3><a href=".*">Stable Build (?<version>.*)</a></h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft Edge' {
                $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/deployedge/microsoft-edge-relnote-stable-channel' -ErrorAction Stop
                $Pattern = '<h2 id=".*">Version (?<version>.*):.*</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'FileZilla' {
                $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
                $session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
                $HTML = Invoke-RestMethod -UseBasicParsing -Uri "https://filezilla-project.org/download.php?show_all=1" -WebSession $session -ErrorAction Stop
                $Pattern = '<p>The latest stable version of FileZilla Client is (?<version>.*)</p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Foxit PDF Reader' {
                $HTML = Invoke-RestMethod 'https://www.foxit.com/pdf-reader/version-history.html' -ErrorAction Stop
                $Pattern = '<p><h3>Version (?<version>.*)</h3></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'GPL Ghostscript' {
                $HTML = Invoke-RestMethod 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/latest' -ErrorAction Stop
                $Pattern = '<h1 data-view-component="true" class="d-inline mr-3">Ghostscript/GhostPDL (?<version>.*)</h1>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Google Drive' {
                $HTML = Invoke-RestMethod 'https://support.google.com/a/answer/7577057?hl=en' -ErrorAction Stop
                $Pattern = '<p><em><strong>Windows( and macOS)?:</strong>&nbsp;Version (?<version>.*)</em></p>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Google Chrome' {
                $HTML = Invoke-RestMethod 'https://chromereleases.googleblog.com/search/label/Desktop%20Update' -ErrorAction Stop
                $Pattern = 'The Stable channel has been updated to (?<version>[\d\.]+) for'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'HP Support Assistant' {
                $HTML = Invoke-RestMethod 'https://support.hp.com/us-en/help/hp-support-assistant' -ErrorAction Stop
                $Pattern = '<span class="bannerVersion">Version <span class="ver">(?<version>.*)</span>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'IrfanView' {
                $HTML = Invoke-RestMethod 'https://www.irfanview.com/' -ErrorAction Stop
                $Pattern = '<h2>Get IrfanView \(<strong>version (?<version>.*)</strong>\)</h2>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Jabra Direct' {
                $HTML = Invoke-RestMethod 'https://www.jabra.co.nz/Support/release-notes/release-note-jabra-direct' -ErrorAction Stop
                $Pattern = '<p><strong>Release version:</strong> (?<version>.*)<br>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Java' {
                $latestVersion = $null
                $maxAttempts = 5
            
                $attempts = 0
                while (($null -eq $latestVersion) -and ($attempts -lt $maxAttempts)) {
            
                    $URL = "https://www.java.com/en/download/manual.jsp"
                    $global:ie = New-Object -com "InternetExplorer.Application"
                    $global:ie.visible = $false
                    $global:ie.Navigate($URL)
            
                    DO { Start-Sleep -s 1 }UNTIL(!($global:ie.Busy))
                    Start-Sleep 5
                    $HTML = $global:ie.Document.body.innerHTML.ToString()
                    $Pattern = '<h4 class="sub">Recommended (?<version>.*)</h4>'
                    $AllMatches = ([regex]$Pattern).Matches($HTML)
                    $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
            
                    $attempts++
                }
                break
            }
            'LegalAid Templates' {
                $domainName = 'https://www.justice.govt.nz'
                $HTML = Invoke-RestMethod "$domainName/about/lawyers-and-service-providers/legal-aid-lawyers/forms/download-word-template-package/"
                $Pattern = '<a title=\"LegalAid Templates Installer Version (?<version>[\d\.]+)\"'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Current Channel' {
                $UpdateChannel = 'Current Channel' 
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Monthly Enterprise Channel' {
                $UpdateChannel = 'Monthly Enterprise Channel'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Semi-Annual Enterprise Channel (Preview)' {
                $UpdateChannel = 'Semi-Annual Enterprise Channel \(Preview\)'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft 365 - Semi-Annual Enterprise Channel' {
                $UpdateChannel = 'Semi-Annual Enterprise Channel'
    
                $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
                $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Mozilla Firefox' {
                $Pattern = 'releases/(?<version>[\d\.]+)/'
                $originalLink = 'https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US'
                $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                $latestVersion = '16.0.' + ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                $break
            }
            'Network Lookout for Employees Pro' {
                $HTML = Invoke-RestMethod 'https://networklookout.com/' -ErrorAction Stop
                $Pattern = '<p>ver. (?<version>[\d\.]+)'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Notepad++' {
                $HTML = Invoke-RestMethod 'https://notepad-plus-plus.org/downloads/' -ErrorAction Stop
                $Pattern = '<a href=".*"><strong>Current Version (?<version>.*)</strong></a>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'PDFCreator' {
                $HTML = (Invoke-WebRequest 'https://docs.pdfforge.org/pdfcreator/en/pdfcreator/introduction/whats-new/' -UseBasicParsing -ErrorAction Stop).Content
                $Pattern = '<h2>PDFCreator (?<version>.*)<a'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Putty' {
                $HTML = Invoke-RestMethod 'https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html' -ErrorAction Stop
                $Pattern = '<TITLE>Download PuTTY: latest release \((?<version>.*)\)</TITLE>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Python 3' {
                $HTML = Invoke-RestMethod 'https://www.python.org/downloads/windows/' -ErrorAction Stop
                $Pattern = '<li><a href="/downloads/release/python-.+?/">Latest Python 3 Release - Python (?<version>.*)</a></li>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Synology Drive Client' {
                $HTML = Invoke-RestMethod 'https://www.synology.com/en-global/releaseNote/SynologyDriveClient' -ErrorAction Stop
                $Pattern = '<h3>Version: (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = (($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value) -replace '-', '.'
                break
            }
            'Synology Active Backup for Business Agent' {
                $HTML = Invoke-RestMethod 'https://www.synology.com/en-global/releaseNote/ActiveBackupBusinessAgent' -ErrorAction Stop
                $Pattern = '<h3>Version: (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = (($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value) -replace '-', '.'
                break
            }
            'Sysmon' {
                $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon' -ErrorAction Stop
                $Pattern = '<h1 id="sysmon-.*">Sysmon v(?<version>.*)</h1>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Microsoft Teams' {
                $HTML = Invoke-RestMethod 'https://docs.microsoft.com/en-us/officeupdates/teams-app-versioning' -ErrorAction Stop
                $Pattern = '<h3 id="windows-public-cloud-version-history">Windows \(Public Cloud\) version history</h3>(\n.*){14}\n<td style="text-align: left;">(?<version>.*)</td>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'TreeSize Free' {
                $HTML = Invoke-RestMethod 'https://www.jam-software.com/treesize_free/changes.shtml' -ErrorAction Stop
                $Pattern = '<h3 class="collapsed-item__ttl">Version (?<version>.*)</h3>'
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'UniPrint' {
                $HTML = Invoke-WebRequest 'https://www.uniprint.net/en/uniprint-client/' -ErrorAction Stop
                $Pattern = "<p>UniPrint Client .*; (?<version>.*) Autodetect and Install</p>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'VLC' {
                $HTML = Invoke-RestMethod 'https://www.videolan.org/vlc/download-windows.html' -ErrorAction Stop
                $Pattern = "<span id='downloadVersion'>\n\s*(?<version>[\d\.]+)</span>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Wireshark' {
                $HTML = Invoke-RestMethod 'https://www.wireshark.org/download.html' -ErrorAction Stop
                $Pattern = "<a.*>Stable Release \((?<version>.*)\)</a>"
                $AllMatches = ([regex]$Pattern).Matches($HTML)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            'Zoom' {
                $Pattern = 'prod/(?<version>.*)/ZoomInstallerFull\.msi'
                $originalLink = 'https://zoom.us/client/latest/ZoomInstallerFull.msi'
                $downloadLink = (Invoke-WebRequest -Uri $originalLink  -MaximumRedirection 0 -ErrorAction Ignore).Headers.Location
                $AllMatches = ([regex]$Pattern).Matches($downloadLink)
                $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
                break
            }
            default {
                $latestVersion = $null
            }
        }
    
    }
    catch [System.NotSupportedException] {
        Disable-IEFirstRunCustomization
    }
    catch {
        Write-Log -LogType ERROR "Unable to retrieve latest version number for $ProgramName. $($Global:Error[0])"
        return $null
    }

    $obj = [PSCustomObject]@{
        ProgramName   = $ProgramName
        VersionNumber = $latestVersion
    }

    return $obj

}

Function Confirm-InstallerValidity {
    Param(
        $FilePath
    )

    $varChain = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Chain
    try {
        $verification = $varChain.Build((Get-AuthenticodeSignature -FilePath "$FilePath").SignerCertificate)
        return $verification
    }
    catch [System.Management.Automation.MethodInvocationException] {
        $err = ( "'$FilePath' did not contain a valid digital certificate. " +
            "Something may have corrupted/modified the file during the download process. " +
            "Suggest trying again, contact support@appmani.com if it fails >2 times")
        Write-Log -LogType ERROR -Message $err
        return $null
    }
}

Function Set-AgentRefresh {
    Param (
        $NewRefreshCheckValue
    )

    $auditRefreshRegistryPath = 'HKLM:\SOFTWARE\NZCS\ServiceCAT'
    $auditRefreshRegistryItemName = 'RefreshCheck'

    $RegistryValueObj += [PSCustomObject]@{ValueName = $auditRefreshRegistryItemName; ValueData = $NewRefreshCheckValue; ValueType = 'DWORD' }

    # Gets refresh check value
    $CurrentRefreshCheckValue = Read-RegistryValueData -RegistryKey $auditRefreshRegistryPath -ValueName $auditRefreshRegistryItemName

    # Checks if new and current RefreshCheck values are different
    if ($NewRefreshCheckValue -ne $CurrentRefreshCheckValue) {
        # Sets new RefreshCheck value if they are different
        Add-RegistryValue -RegistryKey $auditRefreshRegistryPath -RegistryValueObj $RegistryValueObj | Out-Null
    
    }
}

Function Confirm-LogFolder {

    $parentFolderPath = 'C:\Windows\Temp\AppManiProgramManagerLogs\'
    $logFolderPath = $parentFolderPath + $global:scriptName

    $logFolder = Test-Path $logFolderPath

    if (!($logFolder)) {
        try {
            New-Item -Path $logFolderPath -ItemType Directory | Out-Null            
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to create log folder: $($Global:Error[0])"
            return $false
        }
    }

    Write-Log -LogType INFO -Message "Logs will be saved at $logFolderPath."
    return $true
}

Function Write-Log {
    Param (
        $LogType,
        $Message
    )

    $parentFolderPath = 'C:\Windows\Temp\AppManiProgramManagerLogs\'
    $logFolderPath = $parentFolderPath + $global:scriptName
    $logFileName = "$global:scriptName-" + (Get-Date -Format 'yyMMdd') + '.log'
    $logFilePath = "$logFolderPath\" + $logFileName

    $longDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

    $msg = '{0} {1} {2}: {3}' -f $longDate, $global:scriptName, $LogType, $Message
    Add-Content -Path $logFilePath -Value $msg -ErrorAction Ignore
    Write-Host $msg

    return
}

Function Disable-IEFirstRunCustomization {
    Write-Log -LogType INFO -Message "Unable to execute WebRequest. Disabling IE First RunCustomization..."
    try {
        Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Internet Explorer\Main" -Name "DisableFirstRunCustomize" -Value 2
    }
    catch {
        Write-Log -LogType ERROR -Message "Failed to disable IE First RunCustomization: $($Global:Error[0])"
        return $null
    }
}

Function Set-Alert {
    Param (
        $AlertsFolderPath,
        [int]$Category = 1999,
        $Subject,
        $Body,
        [int]$Priority = 3,
        $AutoComplete
    )

    Function Get-GUID($maxSize = 10) {
        $g = [guid]::NewGuid()
        $v = [string]$g
        $v = $v.Replace("-", "")
        return $v.substring(0, $maxSize)
    }

    $newAlert = [PSCustomObject]@{
        Category     = $Category
        Subject      = $Subject
        Body         = $Body
        Priority     = $Priority
        AutoComplete = $AutoComplete
    }

    if (!(Test-Path $AlertsFolderPath)) {
        try {
            New-Item -Path $AlertsFolderPath -ItemType Directory | Out-Null
            Write-Host "Alert folder created."
        }
        catch {
            Write-Log -LogType ERROR -Message "Failed to create alerts folder: $($Global:Error[0])"
            return $null
        }
    }

    $newAlertFilePath = $AlertsFolderPath + '\alert_' + $(Get-GUID) + '.json'

    try {
        $newAlert | ConvertTo-Json | Out-File $newAlertFilePath
        Write-Log -LogType INFO -Message "Alert generated at $newAlertFilePath."
    }
    catch {
        Write-Log -LogType ERROR -Mesage "Unable to write alert to alerts.json: $($Global:Error[0].Exception)"
    }
}

Function Get-UninstallCommand {
    Param (
        [Parameter(ValueFromPipeline = $true)]    
        $InstalledProgram,
        [Switch]$UseStringsExe
    )

    # Extracts the path from the uninstall command
    Function Find-Path {
        Param (
            $Command
        )
    
        $matches = @()
        $pathRegex = '[a-zA-Z]:\\(((?![<>:"\/\\|?*]).)+((?<![ .])\\)?)*'
        if (!($Command -match $pathRegex)) { 
            Write-Log -LogType ERROR -Message "Unable to extract uninstall path."
            return $null 
        }
    
        $Path = $($matches[0]).Trim()
        return $Path
    }

    
    # This function adds double-quotes to the uninstall executable path so paths with spaces won't error when invoked
    Function Add-QuotesToPath {
        Param (
            $Command
        )

        $matches = @()
        $path = Find-Path $Command
        $quoteCheckRegex = '\"{0}\"' -f [regex]::Escape($path)
        if (!($Command -match $quoteCheckRegex)) {
            $Command = $Command.replace($path, "`"$path`"")
            return $Command
        }
        return $Command
    }
    
    # Checks if UninstallString property is present
    $uninstallCommand = $InstalledProgram.UninstallString
    if (!($uninstallCommand)) {
        Write-Log -LogType ERROR "Uninstall string missing from registry."
        return $null
    }

    # Uninstall string overrides and customizations'
    switch -Wildcard ($InstalledProgram.DisplayName) {
        'CutePDF Writer*' {
            Write-Log -LogType INFO -Message "Override found."

            # The uninstall command doesn't have quotes on the uninstall exe path which has a space on it so it will fail when called from cmd (C:\Program Files (x86)\CutePDF Writer\unInstcpw64.exe /uninstall)
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' /s'
            return $uninstallCommand
        }
        'Digisign*' {
            Write-Log -LogType INFO -Message "Override found."

            # The uninstall command doesn't have quotes on the uninstall exe path which has a space on it so it will fail when called from cmd (C:\Program Files (x86)\CutePDF Writer\unInstcpw64.exe /uninstall)
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' /qn'
            return $uninstallCommand
        }
        'Citrix Workspace*' {
            Write-Log -LogType INFO -Message "Override found."

            $uninstallCommand += ' /silent'
            return $uninstallCommand
        }
        # 'Dropbox*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand

        # }
        # 'FileZilla*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        'Google Drive*' {
            Write-Log -LogType INFO -Message "Override found."

            # The uninstall command doesn't have quotes on the uninstall exe path which has a space on it so it will fail when called from cmd (C:\Program Files (x86)\CutePDF Writer\unInstcpw64.exe /uninstall)
            $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
            $uninstallCommand += ' --silent --force_stop'
            return $uninstallCommand
        }
        # 'GPL Ghostscript*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        'IrfanView*' {
            Write-Log -LogType INFO -Message "Override found."

            $uninstallCommand += ' /silent'
            return $uninstallCommand
        }
        'Jabra Direct*' {
            Write-Log -LogType INFO -Message "Override found."

            $App = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object DisplayName -eq $ProgramName
            $uninstallCommand = $App.QuietUninstallString
            return $uninstallCommand
        }
        # 'Mozilla Firefox*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }
        # 'Python 3*' {
        # Write-Log -LogType INFO -Message "Override found."
        # $installerLocationFolder = 'C:\Windows\Temp\Python3\'
        # $installerFilename = (Split-Path $installedProgram.BundleCachePath -leaf)
        # $installerLocation = $installerLocationFolder + $installerFileName

        # #if (Test-Path '')
        # }
        # 'TreeSize Free*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /VERYSILENT /NORESTART'
        # return $uninstallCommand
        # }
        # 'VLC*' {
        # Write-Log -LogType INFO -Message "Override found."

        # $uninstallCommand += ' /S'
        # return $uninstallCommand
        # }

        Default {
            Write-Log -LogType INFO -Message "No override found."
        }
    }

    # If QuietUninstallString property is present, return right away
    if ($InstalledProgram.QuietUninstallString) {
        Write-Log -LogType INFO -Message "Quiet uninstall command found from registry."
        return $InstalledProgram.QuietUninstallString
    }


    # If UninstallString uses MsiExec, make sure we are using /X and not /I and add /QN at the end
    if ($uninstallCommand -like 'MsiExec*') {
        Write-Log -LogType INFO -Message "Msiexec uninstall command found from registry."
        $msiExecRegex = '{(.+?)}'
        if (!($uninstallCommand -match $msiExecRegex)) { 
            Write-Log -LogType ERROR -Message "Unable to extract product GUID."
            return $null 
        }
        else { $GUID = $matches[1] }

        $uninstallCommand = "MsiExec.exe /X{$GUID} /qn"
        return $uninstallCommand
    }

    #Write-Log -LogType ERROR "No silent install command has been configured for $ProgramName."
    Write-Log -LogType INFO "Attempting to determine quiet uninstall command for $($InstalledProgram.DisplayName)."
    $uninstallExePath = Find-Path -Command $uninstallCommand

    if ($UseStringsExe) {
        Write-Log -LogType INFO "Retrieving via strings.exe."
        $stringsPath = 'C:\Windows\TEMP\strings.exe'
        if (!(Test-Path $stringsPath)) {
            Write-Log -LogType ERROR -Message "Unable to locate $stringsPath."
            return $null
        }
        
        $installerTool = C:\Windows\TEMP\strings.exe $uninstallExePath /accepteula | Select-String -Pattern @("InstallAware", "Inno Setup", "InstallShield", "Nullsoft", "Advanced Installer") | Select -First 1
    }
    else {

        Write-Log -LogType INFO "Retrieving via native functions."

        # Does what strings.exe does
        $str = ""
        $binary = [System.IO.File]::ReadAllBytes($uninstallExePath)
        $encoding = [System.Text.Encoding]::GetEncoding("Windows-1252")
        $stringBuilder = New-Object System.Text.StringBuilder  
        for ($i = 0; $i -lt $binary.Length; $i++) {
            # Check if the current byte is a valid ANSI character
            $chars = $encoding.GetChars($binary, $i, 1)
            if ([regex]::IsMatch($chars, "[\x20-\x7E]+")) {
                $stringBuilder.Append($chars) | Out-Null
            }
            elseif ($stringBuilder.Length -gt 0) {
                $str += $stringBuilder.ToString()
                $stringBuilder.Clear() | Out-Null
            }
        } 

        $installerTool = $str | Select-String -Pattern @("InstallAware", "Inno Setup", "InstallShield", "Nullsoft", "Advanced Installer") | Select-Object -First 1

    }



    

    if (!$installerTool) {
        Write-Log -LogType INFO -Message "Unable to determine installer tool."
    }

    $uninstallCommand = Add-QuotesToPath -Command $uninstallCommand
    switch -Wildcard ($installerTool) {
        '*InstallAware*' {
            Write-Log -LogType INFO -Message "Installer created using InstallAware."
            $uninstallCommand += ' /s'
            return $uninstallCommand
        }
        '*Inno Setup*' {
            Write-Log -LogType INFO -Message "Installer created using Inno Setup."
            $uninstallCommand += ' /VERYSILENT /NORESTART'
            return $uninstallCommand
        }
        '*InstallShield*' {
            Write-Log -LogType INFO -Message "Installer created using InstallShield."
            $uninstallCommand += ' -s'
            return $uninstallCommand
        }
        '*Nullsoft*' {
            Write-Log -LogType INFO -Message "Installer created using Nullsoft."
            $uninstallCommand += ' /S'
            return $uninstallCommand
        }
        '*Advanced Installer*' {
            Write-Log -LogType INFO -Message "Installer created using Advanced Installer."
            $uninstallCommand += ' /quiet'
            return $uninstallCommand
        }
    }

    Write-Log -LogType INFO -Message "Unable to determine quiet uninstall command. Please contact an Administrator."
    return $null

}

Function Uninstall-Program {
    Param (
        $uninstallCommand
    )

    Write-Log -LogType INFO -Message "Executing command $uninstallCommand"

    try {
        cmd /c $uninstallCommand
    }
    catch {
        Write-Log -LogType ERROR -Message "Unable to uninstall: $($Global:Error[0])"
        return 1
    }

    Write-Log -LogType INFO -Message "Execution completed with exit code $LASTEXITCODE"
    return $LASTEXITCODE
}

Function Get-ProgramRegistryDisplayRegex {
    Param (
        $ProgramName
    )

    $RegistryDisplayNameRegexes = @{
        'Citrix Workspace' = '^Citrix Workspace \d+$'
        'Dropbox'          = '^Dropbox$'
        'Microsoft Edge'   = '^Microsoft Edge$'
        'Notepad++'        = 'Notepad\+\+'
        'Python 3'         = 'Python 3\.[\d\.]+ \(.+\)'
    }

    $regex = $RegistryDisplayNameRegexes.$ProgramName
    if (!$regex) { return $ProgramName }
    else { return $regex }
}


Function Get-InstallCommand {
    Param (
        $ProgramName
    )

    $InstallCommands = @{
        '7-Zip'                                     = '"{0}" /S'
        'Adobe Acrobat'                             = '"{0}" /sAll /rs /rps /msi /norestart /quiet EULA_ACCEPT=YES'
        'Adobe Digital Editions'                    = '"{0}" /S'
        'Audacity'                                  = '"{0}" /VERYSILENT /NORESTART'
        'BitWarden'                                 = '"{0}" /allusers /S'
        'Citrix Workspace'                          = '"{0}" /silent /noreboot'
        'CutePDF Writer'                            = '"{0}" /VERYSILENT /NORESTART'
        'Digisign'                                  = '"{0}" /quiet'
        'Dropbox'                                   = '"{0}" /NOLAUNCH'
        'Filezilla'                                 = '"{0}" /S'
        'Foxit PDF Reader'                          = '"{0}" /VERYSILENT /NORESTART'
        'Google Chrome'                             = 'msiexec.exe /i "{0}" /qn /norestart'
        'Google Drive'                              = '"{0}" --silent --desktop_shortcut'
        'GPL Ghostscript'                           = '"{0}" /S'
        'HP Support Assistant'                      = '"{0}" /S /v/qn'
        'IrfanView'                                 = '"{0}" /silent /group=1 /allusers=1'
        'Jabra Direct'                              = '"{0}" /install /quiet /norestart'
        'Java 8'                                    = '"{0}" /s REBOOT=0 SPONSORS=0 AUTO_UPDATE=0'
        'LegalAid Templates'                        = 'msiexec /q /i "{0}"'
        'LOLComponents'                             = 'msiexec /q /i "{0}"'
        'Microsoft Edge'                            = '"{0}" /silent /install'
        'Microsoft Teams'                           = '"{0}" -s'
        'Microsoft 365'                             = '"{0}" /configure "{1}\{2}"'
        'Mozilla Firefox'                           = '"{0}" -ms -ma'
        'Notepad++'                                 = '"{0}" /S'
        'PDFCreator'                                = '"{0}" /VERYSILENT /NORESTART'
        'Putty'                                     = 'msiexec.exe /i "{0}" /qn'
        'Python 3'                                  = '"{0}" /quiet InstallAllUsers=1 PrependPath=1'
        'Synology Drive Client'                     = 'msiexec.exe /i "{0}" /qn /norestart'
        'Synology Active Backup for Business Agent' = 'msiexec.exe /i "{0}" ADDRESS={1} USERNAME={2} PASSWORD={3} /qn /norestart'
        'TreeSize Free'                             = '"{0}" /VERYSILENT /NORESTART'
        'UniPrint'                                  = 'msiexec /q /i "{0}"'
        'VLC'                                       = '"{0}" /L=1033 /S'
        'Wireshark'                                 = '"{0}" /S'
        'Zoom'                                      = 'msiexec.exe /i "{0}" /qn /norestart'

    }

    $installCommand = $InstallCommands.$ProgramName
    if (!$installCommand) { return $null }
    else { return $installCommand }
}

# Checks if program requested is valid to be installed/updated/uninstalled
Function Approve-SelectedProgram {
    Param (
        $ProgramName
    )

    $ApprovedPrograms = '7-Zip',
    'Adobe Acrobat',
    'Audacity',
    'Bitwarden',
    'CutePDF Writer',
    'Citrix Workspace',
    'Digisign',
    'Dropbox',
    'FileZilla',
    'Foxit PDF Reader',
    'GPL Ghostscript',
    'Google Chrome',
    'Google Drive',
    'IrfanView',
    'Jabra Direct',
    'Mozilla Firefox',
    'Notepad++',
    'PDF Creator',
    'Putty',
    'Python 3'
    'TreeSize Free',
    'UniPrint',
    'VLC',
    'Zoom'

    if ($ProgramName -notin $ApprovedPrograms) { return $null }
    return $true
}

Function Find-RootKeyFromPath {
    Param (
        $RegistryKey
    )

    # Matches rootKey from path using a regex
    $RootKeyRegex = '^(.+?)\:\\'
    if ($RegistryKey -match $RootKeyRegex) {
        $rootKey = $matches[1]
        # Write-Log -LogType INFO -Message "Root key: $rootKey"

        return $rootKey
    }

    Write-Log -LogType ERROR -Message "Unable to determine root key from provided path."
    return $null
}

Function Assert-RootKeyPSDrive {
    Param (
        $RegistryKey
    )

    $rootKey = Find-RootKeyFromPath -RegistryKey $RegistryKey
    if (!$rootKey) { return $null }

    # Checks if there is an existing PSDrive for RootKey
    $PSDrive = Get-PSDrive $rootKey -ErrorAction SilentlyContinue
    if (!($PSDrive)) {
        switch ($rootKey) {
            'HKCR' { $root = 'HKEY_CLASSES_ROOT' }
            'HKCU' { $root = 'HKEY_CURRENT_USER' }
            'HLKM' { $root = 'HKEY_LOCAL_MACHINE' }
            'HKU' { $root = 'HKEY_USERS' }
            'HKCC' { $root = 'HKEY_CURRENT_CONFIG' }
            default { 
                Write-Log -LogType ERROR -Message "Invalid root key."
                return $null
            }
        }
            
        Write-Log -LogType INFO -Message "Creating PSDrive for $rootKey."
        try {
            $newPSDrive = New-PSDrive -PSProvider registry -Root $root -Name $rootKey -Scope Global
            if ($newPSDrive) {
                Write-Log -LogType INFO -Message "PSDrive for $rootKey created."
                return $newPSDrive
            }
        }
        catch {
            Write-Warning "Error in creating PSDrive for $($rootKey):\ : $($Global:Error[0])"
            return $null
        }
    }

    #Write-Log -LogType INFO -Message "PSDrive for $rootKey is already available."
    return $PSDrive

}

Function Read-RegistryValueData {
    Param (
        $RegistryKey,
        $ValueName
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path -Path $RegistryKey)) {
        Write-Log -LogType ERROR -Message "Registry key $RegistryKey not found."
        return $null
    }

    try {
        $registryValueData = Get-ItemProperty -Path $RegistryKey -Name $ValueName -ErrorAction Stop
        return $registryValueData."$ValueName"
    }
    catch {
        Write-Log -LogType ERROR "Error in reading value data: $($Global:Error[0])"
        return $null
    }

    
}

Function Add-RegistryKey {
    Param (
        $RegistryKey
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    $components = $RegistryKey.Split("\")
    $currentPath = $components[0]

    for ($i = 1; $i -lt $components.Count; $i++) {
        $currentPath = $currentPath + "\" + $components[$i]
        if (!(Test-Path -Path $currentPath)) {
            try {
                $newRegistryKey = New-Item $RegistryKey -Force -ErrorAction Stop
                Write-Log -LogType INFO -Message "Registry key $RegistryKey created."
            }
            catch {
                Write-Log -LogType ERROR "Registry key $RegistryKey creation failed: $($Global:Error[0])"
                return $null
            }
        }
    }

    return $newRegistryKey
}

Function Add-RegistryValue {
    Param (
        $RegistryKey,
        $RegistryValueObj,
        $CreateKeyIfMissing = $true
    )

    #Write-Log -LogType INFO "Registry key: $RegistryKey"

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path $RegistryKey)) {
        if ($CreateKeyIfMissing) {
            $newRegistryKey = Add-RegistryKey -RegistryKey $RegistryKey
            if (!$newRegistryKey) { return $null }
        }
        else {
            Write-Log -LogType ERROR "Registry key $RegistryKey not found."
            return $null
        }
    }

    #foreach ($registryValueObj in $RegistryValueHash) {
    $ValueName = $registryValueObj.ValueName
    $ValueData = $registryValueObj.ValueData
    $ValueType = $registryValueObj.ValueType

    try {
        $newRegistryValue = New-ItemProperty -Path $RegistryKey -Name $ValueName -PropertyType $ValueType -ErrorAction Stop
        if ($newRegistryValue) {
            Write-Log -LogType INFO "Registry value $ValueName created as $ValueType."
        }
    }
    # Happens when registry value already exists.
    catch [System.IO.IOException] {
        # Do nothing
    }
    catch {
        Write-Log -LogType ERROR "Registry value $ValueName creation failed: $($Global:Error[0])"
    }

    try {
        $newRegistryValueData = Set-ItemProperty -Path $RegistryKey -Name $ValueName -Value $ValueData -PassThru -ErrorAction Stop
        if ($newRegistryValueData) {
            Write-Log -LogType INFO "Registry value $ValueName set to $ValueData."
        }
        return $newRegistryValueData
    }
    catch {
        Write-Host "Assigning value data to registry value $($registryValueObj.Name) failed.: $($Global:Error[0])"
        return $null
    
    }

    #}
}

Function Remove-RegistryValue {
    Param (
        $RegistryKey,
        $ValueName
    )

    $PSDrive = Assert-RootKeyPSDrive -RegistryKey $RegistryKey
    if (!$PSDrive) { return $null }

    if (!(Test-Path -Path $RegistryKey)) {
        Write-Log -LogType ERROR -Message "Registry key $RegistryKey not found."
    }

    try {
        Remove-ItemProperty $RegistryKey -Name $ValueName -Force -ErrorAction Stop
        Write-Log -LogType INFO "$ValueName value has been removed from $RegistryKey."
    }
    catch {
        Write-Log -LogType ERROR "Error removing value $ValueName in $RegistryKey : $($Global:Error[0])"
        return $null
    }   
}

Function Get-PreInstallScriptBlock {
    Param($ProgramName)

    switch -regex($ProgramName) {
        'Adobe Digital Editions' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName'
                ScriptBlock = {
                    $N360RegistryKeys = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\N360', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\N360'
                    foreach ($key in $N360RegistryKeys) {
                        Write-Log -LogType INFO -Message "Creating registry key $key to skip Norton installation."
                        Add-RegistryKey -RegistryKey $key | Out-Null
                    }
                }
            }

            return $scriptBlockObj
        }
        'CutePDF Writer | Putty | Digisign' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'programName'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $programName = $($preInstallInputParams.programName)

                    $installedProgram = Get-InstalledProgram $programName
                    if ($installedProgram) {
                        Write-Log -LogType INFO -Message "The older version of $programName will be uninstalled."
    
                        # Gets uninstall command
                        Write-Log -LogType INFO -Message "Retrieving uninstall command..."
                        $uninstallCommand = Get-UninstallCommand -InstalledProgram $installedProgram
                        if ($null -eq $uninstallCommand) { 
                            Write-Log -LogType ERROR -Message "Unable to retrieve uninstall command."
                            exit 1 
                        }
                        Write-Log -LogType INFO -Message "Uninstall command $uninstallCommand retrieved."
        
                        # Uninstalls the program
                        Write-Log -LogType INFO "Uninstalling $programName..."
                        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
                        if ($uninstallationResult -ne 0) {
                            Write-Log -LogType ERROR "Uninstallation failed. Process returned $uninstallationResult."
                            exit 1
                        }
                        
                        Write-Log -LogType INFO "Script will resume in 10 seconds to allow program to cleanly uninstall."
                        Start-Sleep 10
                    }
                    else {
                        Write-Log -LogType INFO "No old instance of $programName is installed."
                    }
                }
            }
            return $scriptBlockObj
        }
        'HP Support Assistant' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {

                    Param (
                        $preInstallInputParams
                    )

                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)

                    # Extracts installer
                    $extractCommand = '"{0}" /s /e /f "{1}"'
                    Write-Log -LogType INFO -Message "Extracting installer to $savePath..."
                    cmd /c $($extractCommand -f $installerLocation, $savePath)
                    Start-Sleep -Seconds 30

                    $installerFileName = 'InstallHPSA.exe'
                    $installerLocation = $savePath + "\$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'LegalAid Templates' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = (Get-ChildItem -Path $SavePath -Filter 'LegalAid Templates * .msi').Name
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'LOLComponents' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $installerFileName = 'LOLComponents.msi'
                    $installerLocation = $savePath + "\$installerFilename"
                    $preInstallOutputObject = [PSCustomObject]@{
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'Microsoft 365' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath', 'architecture', 'installCommand'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )

                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
                    $architecture = $($preInstallInputParams.architecture)
                    $installCommand = $($preInstallInputParams.installCommand)

                    $diskName = 'C'
                    $RequiredSpace = 30
                    $ConfigFileName = 'OfficeAppsConfiguration.xml'
                    
                    $architecture = if ( $architecture ) { $architecture }
                    else { 'x64' }

                    if ($architecture -eq 'x64') { $architecture = '64' }
                    else { $architecture = '32' }

                    Function Confirm-DiskSpaceRequirement {
                        Param (
                            $DiskName,
                            $RequiredSpace
                        )
                    
                        $disk = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($DiskName):'" | Select-Object -Property DeviceID, @{'Name' = 'FreeGB'; Expression = { [int]($_.FreeSpace / 1GB) } }
                    
                        if ($disk.FreeGB -lt $RequiredSpace) {
                            return $false
                        }
                        return $true
                    }

                    Function Expand-OfficeDeploymentTool {
                        Param (
                            $ODTPath,
                            $ODTExtractPath
                        )
                    
                        if (Test-Path $ODTExtractPath) {
                            # If folder is existing, delete all contents inside it
                            try {
                                #Write-Host "$ExtractPath is existing. Removing existing contents." -NoNewline
                                Get-ChildItem -Path $ODTExtractPath -Include *.* -File -Recurse | ForEach-Object { $_.Delete() }
                            }
                            catch {
                                Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                                exit 1
                            }
                        }
                        else {
                            # Creates folder
                            try {
                                New-Item $ODTExtractPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
                            }
                            catch {
                                Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                                exit 1
                            }
                        }
                    
                        $cmd = '"{0}" /extract:"{1}" /quiet' -f $ODTPath, $ODTExtractPath
                        cmd /c $cmd
                    
                        return $(Get-Item $ODTExtractPath\setup.exe)
                    }

                    Function Write-OfficeConfigurationFile {
                        Param (
                            $Path,
                            $ConfigFileName,
                            $Architecture
                        )
                    
                        $configFilePath = "$Path\$ConfigFileName"
                        $configuration = @"
<Configuration>
    <Add SourcePath="$Path" OfficeClientEdition="$Architecture">
        <Product ID="O365BusinessRetail">
            <Language ID="en-us"/>
            <Language ID="MatchPreviousMSI"/>
            <ExcludeApp ID="Groove"/>
            <ExcludeApp ID="Lync"/>
            <ExcludeApp ID="Bing"/>
        </Product>
    </Add>
    <Property Name="FORCEAPPSHUTDOWN" Value="TRUE"/>
    <Property Name="PinIconsToTaskbar" Value="TRUE" />
    <Property Name="DeviceBasedLicensing" Value="0" />
    <Property Name="SCLCacheOverride" Value="0" />
    <Updates Enabled="TRUE"/>
    <Logging Level="Standard" Path="$Path\OfficeSetupLog.log"/>
    <RemoveMSI />
    <AppSettings>
        <Setup Name="Company" Value="Company" />
    </AppSettings>
</Configuration>
"@

                        
                        try {
                            New-Item $configFilePath -Force -ErrorAction Stop | Out-Null
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    
                        # Sets content to file
                        try {
                            Set-Content $configFilePath $configuration | Out-Null
                        }
                        catch {
                            Write-Log -LogType ERROR -Message "The script ran into an issue: $($Global:Error[0])"
                            return $null
                        }
                    
                        return $(Get-Item $configFilePath)
                    
                    }
                    
                    Write-Log -LogType INFO -Message "Checking disk space requirement..." 
                    $diskSpaceRequirement = Confirm-DiskSpaceRequirement -DiskName $diskName -RequiredSpace $RequiredSpace
                    if (!$diskSpaceRequirement) { 
                        Write-Log -LogType ERROR -Message "Installlation requires $RequiredSpace GB free in disk $DiskName. Please free up some space to continue."
                        exit 1 
                    }
                    Write-Log -LogType INFO -Message "There is sufficient disk space in $diskName to proceed."

                    Write-Log -LogType INFO -Message "Extracting office deployment tool..." 
                    $ODTExtractPath = $($SavePath + "\ODTSetup")
                    $installerLocation = Expand-OfficeDeploymentTool -ODTPath $installerLocation -ODTExtractPath $ODTExtractPath
                    if ($null -eq $installerLocation) { 
                        Write-Log -LogType ERROR -Message "Failed to extract office deployment tool."
                        exit 1 
                    }
                    Write-Log -LogType INFO -Message "Deployment tool extracted at $installerLocation."
                    
                    Write-Log -LogType INFO -Message "Creating Office365 configuration file..." 
                    $configurationFile = Write-OfficeConfigurationFile -Path $ODTExtractPath -ConfigFileName $ConfigFileName -Architecture $architecture
                    if ($null -eq $configurationFile) { 
                        Write-Log -LogType ERROR -Message "Failed to create configuration file."
                        exit 1 
                    }
                    Write-Log -LogType INFO -Message "Configuration file created at $configurationFile."

                    $cmd = '"{0}" /download "{1}\{2}"' -f $installerLocation, $ODTExtractPath, $ConfigFileName
                    Write-Log -LogType INFO "Downloading installation files using the command $cmd."
                    cmd /c $cmd
                    Write-Log -LogType INFO "Execution completed with exit code $LASTEXITCODE"
                    if ($LASTEXITCODE -ne 0) {
                        Write-Log -LogType ERROR "Unable to download installation files."
                        Remove-InstallerFolder -Path $SavePath -CleanupDelay $cleanupDelay
                        exit 1
                    }

                    $installCommand = $installCommand -f '{0}', $ODTExtractPath, $ConfigFileName #, $ConfigFileName
                    #$installerFileName = 'setup.exe'

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $ODTExtractPath
                        installerFileName = [System.IO.Path]::GetFileName($installerLocation)
                        installerLocation = $installerLocation
                        installCommand    = $installCommand
                    }

                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj

        }

        'Synology Active Backup for Business Agent' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installCommand', 'address', 'username', 'password'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
                    

                    $installCommand = $($preInstallInputParams.installCommand)
                    $address = $($preInstallInputParams.address)
                    $username = $($preInstallInputParams.username)
                    $password = $($preInstallInputParams.password)

                    $installCommand = $installCommand -f '{0}', $address, $username, $password
                    $preInstallOutputObject = [PSCustomObject]@{
                        installCommand = $installCommand
                    }

                    Write-Log -LogType INFO "Crafted install string based on NAS Address, username, and password."
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }
        'UniPrint' {
            $scriptBlockObj = [PSCustomOBject]@{
                Params      = 'installerLocation', 'savePath'
                ScriptBlock = {
                    Param (
                        $preInstallInputParams
                    )
        
                    $installerLocation = $($preInstallInputParams.installerLocation)
                    $savePath = $($preInstallInputParams.savePath)
        
                    #Expand
                    Write-Log -LogType INFO -Message "Extracting installer..." -NoNewline
                    try {
                        Add-Type -Assembly "System.IO.Compression.FileSystem"
                        [System.IO.Compression.ZipFile]::ExtractToDirectory($installerLocation, $SavePath)
                        # Expand-Archive -Path $installerLocation -DestinationPath "$savePath\"
                    }
                    catch {
                        Write-Warning "The script ran into an issue: $($Global:Error[0])"
                        exit 1
                    }
                    
                    $savePath = $savePath + "\Admin"
                    $installerFileName = (Get-ChildItem -Path $savePath -Filter "UniPrintClient_*.msi").Name
                    $installerLocation = $savePath + "\Admin" + "\$installerFilename"

                    $preInstallOutputObject = [PSCustomObject]@{
                        savePath          = $savePath
                        installerFileName = $installerFileName
                        installerLocation = $installerLocation
                    }
                    Write-Log -LogType INFO -Message "Extraction Complete."
        
                    return $preInstallOutputObject
                }
            }
            return  $scriptBlockObj
        }

    }
}

Function Get-ProcessToTerminate {
    Param (
        $ProgramName
    )

    $ProcessesToTerminate = @{
        '7-Zip'                 = '*7z*'
        'Adobe Acrobat'         = '*Acrobat*'
        'Audacity'              = '*Audacity*'
        'BitWarden'             = '*Bitwarden*'
        'Citrix Workspace'      = '*Citrix*', '*Receiver*', '*SelfService*'
        'CutePDF Writer'        = '*CutePDF*'
        'Digisign'              = '*Digisign*'
        'Dropbox'               = '*Dropbox*'
        'Microsoft Edge'        = '*msedge*'
        'Filezilla'             = '*filezilla*'
        'Foxit PDF Reader'      = '*Foxit*'
        'Google Drive'          = '*GoogleDrive*'
        'GPL Ghostscript'       = '*gswin*'
        'Google Chrome'         = '*chrome*'
        'IrfanView'             = '*i_view*'
        'Jabra Direct'          = '*jabra-direct*'
        'LOLComponents'         = '*LOLComponents*'
        'LegalAid Templates'    = '*LegalAid Templates*'
        'Mozilla Firefox'       = '*firefox*'
        'Notepad++'             = '*notepad++*'
        'PDFCreator'            = '*PDFCreator*'
        'Putty'                 = '*putty*'
        'Python'                = '*Python*'
        'Synology Drive Client' = '*cloud-drive-ui.exe*'
        'TreeSize Free'         = '*TreeSizeFree*'
        'UniPrint'              = '*UPC*'
        'VLC'                   = '*VLC*'
        'Wireshark'             = '*Wireshark*'
        'Zoom'                  = '*Zoom*'
    }

    $ProcessesToTerminate = $ProcessesToTerminate.$ProgramName
    if (!$ProcessesToTerminate) { return $null }
    else { return $ProcessesToTerminate }
}

Function Get-InstallationsArchitecture {
    Param (
        $InstalledPrograms
    )

    foreach ($program in $InstalledPrograms) {
        $installedArchitecture = $program | Get-ProgramArchitecture

        $program | Add-Member -Name "Architecture" -Value $installedArchitecture -MemberType NoteProperty
    }

    return $InstalledPrograms

}
Function Invoke-ResolveArchitectureConflicts {
    Param (
        $ProgramName,
        $InstalledPrograms
    )

    $registryDisplayName = Get-ProgramRegistryDisplayRegex -ProgramName $programName

    # Gets current installations of program in a different architecture
    $installationsOnDifferentArchitecture = $installedPrograms | Where-Object { ($_.Architecture -ne $Global:ArchitectureUsed) -and ($null -ne $_.Architecture) }
    if (!$installationsOnDifferentArchitecture) {
        Write-Log -LogType INFO -Message "No installations of $programName on a different architecture have been detected."
        return
    }
 
    # If there are, uninstall them
    Write-Log -LogType INFO -Message "Installations of $programName on a different architecture have been detected."
             
    # Loops through each installation and uninstalls.
    :OuterLoop foreach ($installation in $installationsOnDifferentArchitecture) {
        Write-Log -LogType INFO -Message "$($installation.DisplayName) ($($installation.Architecture)) will be uninstalled."
        Write-Log -LogType INFO -Message "Retrieving uninstall command."
        $uninstallCommand = $installation | Get-UninstallCommand # -UseStringsExe
 
        # Uninstalls the program
        Write-Log -LogType INFO "Uninstalling $($installation.DisplayName)..."
        $uninstallationResult = Uninstall-Program -UninstallCommand $uninstallCommand
        if ($uninstallationResult -ne 0) {
            Write-Log -LogType ERROR "Uninstallation failed."
        }
        else {
 
            # After uninstalling the script will do another check if there are still installations on a different architecture existing.
            $delay = 10
            Write-Log -LogType INFO -Message "Performing another check after uninstalling in $delay seconds."
            Start-Sleep -Second $delay
 
            $installedPrograms_recheck = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All | Where-Object { ($_.Architecture -ne $Global:ArchitectureUsed) -and ($null -ne $_.Architecture) }
         
            # If duplicates are still found, it will continue to try uninstalling the remaining installations
            if (!$installedPrograms_recheck) {
                Write-Log -LogType INFO -Message "Duplicates removed."
                return     
            }
                 
            Write-Log -LogType INFO -Message "Duplicates still found."
        }
    }
}

Function Approve-Installation {
    Param (
        $ProgramName,
        $registryDisplayName,
        $Force
    )
    
    $installedPrograms = Get-InstalledProgram -RegistryDisplayName $registryDisplayName -All

    # If installation requires the architecture to be specified, look for installations on the same architecture
    if ($Global:ArchitectureUsed) {
        $installationsOnTheSameArchitecture = $installedPrograms | Where-Object { ($_.Architecture -eq $Global:ArchitectureUsed) -and ($null -ne $_.Architecture) }
    }
    # If not, include all current installations
    else {
        $installationsOnTheSameArchitecture = $installedPrograms
    }

    if ($global:scriptName -like "Install*") {
        # If there's no installation found on the same architecture, resolve possible architecture conflicts then install
        if (!$installationsOnTheSameArchitecture) {
            Write-Log -LogType INFO -Message "No existing installation of $programName $Global:ArchitectureUsed found."
            
            # Resolve architecture conflicts
            if ($Global:ArchitectureUsed) { Invoke-ResolveArchitectureConflicts -ProgramName $programName -InstalledPrograms $installedPrograms }

            return $true
        }

        # If there are found, evaluate if installation should proceed based on the Force switch
        Write-Log -LogType INFO -Message "$programName $($installationsOnTheSameArchitecture[0].DisplayVersion) ($($installationsOnTheSameArchitecture[0].Architecture)) is already installed in this system."
        
        # If force is not specified or set to false, installation will not proceed
        if ((!$force) -or ($force -eq 'false')) {
            Write-Log -LogType INFO -Message "Installation will not proceed."
            return $false
        }
        else {
            # If force is True, resolve possilble architecture conflicts then install
            Write-Log -LogType INFO -Message "Force switch is set to True. Installation will proceed."
            
            # Resolve architecture conflicts
            if ($Global:ArchitectureUsed) { Invoke-ResolveArchitectureConflicts -ProgramName $programName -InstalledPrograms $installedPrograms }

            return $true
        }
        
    }
    elseif ($global:scriptName -like "Update*") {
        
        # If there are installations found on the same architecture, resolve possible architecture conflicts then update
        if ($installationsOnTheSameArchitecture) {
            Write-Log -LogType INFO -Message "$programName $Global:ArchitectureUsed is installed in this system."
            
            $installedVersion = $installationsOnTheSameArchitecture[0].DisplayVersion
            $isProgramForUpdate = Confirm-Update -ProgramName $programName -InstalledVersion $installedVersion -VersionMatchRegex $versionMatchRegex

            if (!$isProgramForUpdate) {

                if ((!$force) -or ($force -eq 'false')) {
                    Write-Log -LogType INFO -Message "Update will not proceed."
                    return $false
                }

                Write-Log -LogType INFO -Message "Force switch is set to True. Update will proceed."
            }

            # Resolve architecture conflicts
            if ($Global:ArchitectureUsed) { Invoke-ResolveArchitectureConflicts -ProgramName $programName -InstalledPrograms $installedPrograms }

            return $true
        }
        
        # If there are found, evaluate if installation should proceed based on the Force switch
        Write-Log -LogType INFO -Message "No existing installation of $programName $Global:ArchitectureUsed found."

        # If force is not specified or set to false, installation will not proceed
        if ((!$force) -or ($force -eq 'false')) {
            Write-Log -LogType INFO -Message "Nothing to update."
            return $false
        }
        else {
            # If force is True, resolve possilble architecture conflicts then update
            Write-Log -LogType INFO -Message "Force switch is set to True. Update will proceed."
            
            # Since force is True in this scope, update will proceed regardless of the current version vs latest version retrieved

            # Resolve architecture conflicts
            if ($Global:ArchitectureUsed) { Invoke-ResolveArchitectureConflicts -ProgramName $programName -InstalledPrograms $installedPrograms }
            
            return $true
        }
    }
}

Function Get-M365SupportedVersion {
    Param (
        $UpdateChannel
    )

    $UpdateChannel = $([Regex]::Escape($UpdateChannel)) 
    
    $HTML = Invoke-RestMethod 'https://learn.microsoft.com/en-us/officeupdates/update-history-microsoft365-apps-by-date' -ErrorAction Stop
    $Pattern = '<td style=\"text-align: left;\">{0}<br/></td>\n<td style=\"text-align: left;\">.+?<br/></td>\n<td style=\"text-align: left;\">(?<version>.*)<br/></td>' -f $UpdateChannel
    $AllMatches = ([regex]$Pattern).Matches($HTML)
    $latestVersion = ($AllMatches[0].Groups.Where{ $_.Name -like 'version' }).Value
    return $latestVersion
}

Function Get-M365UpdateChannel {
    $RegistryKey = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration'
    $ValueName = 'AudienceID'

    $UpdateChannels = @{
        '492350f6-3a01-4f97-b9c0-c7c6ddf67d60' = 'Current Channel'
        '55336b82-a18d-4dd6-b5f6-9e5095c314a6' = 'Monthly Enterprise Channel'
        'b8f9b850-328d-4355-9145-c59439a0c4cf' = 'Semi-Annual Enterprise Channel (Preview)'
        '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114' = 'Semi-Annual Enterprise Channel'
    }

    $audienceID = Read-RegistryValueData -RegistryKey $RegistryKey -ValueName $ValueName
    if (!$audienceID) {
        return $null
    }

    $updateChannel = $UpdateChannels.$audienceID
    if (!$updateChannel) {
        Write-Log -LogType ERROR "Unable to find corresponding update channel for AudienceID $audienceID."
        return $null
    }

    return $updateChannel
}

Export-ModuleMember -Function 'Test-WebRequest'
Export-ModuleMember -Function 'Get-Installer'
Export-ModuleMember -Function 'Add-InstallerFolder'
Export-ModuleMember -Function 'Remove-InstallerFolder'
Export-ModuleMember -Function 'Get-InstalledProgram'
Export-ModuleMember -Function 'Get-MultipleInstalledProgram'
Export-ModuleMember -Function 'Get-InstalledService'
Export-ModuleMember -Function 'Install-Program'
Export-ModuleMember -Function 'Confirm-ProgramInstallation'
Export-ModuleMember -Function 'Confirm-ServiceInstallation'
Export-ModuleMember -Function 'Confirm-Update'
Export-ModuleMember -Function 'Set-RegistryItem'
Export-ModuleMember -Function 'Get-ProgramArchitecture'
Export-ModuleMember -Function 'Send-Keys'
Export-ModuleMember -Function 'Invoke-ModuleForUpdate'
Export-ModuleMember -Function 'Get-DownloadLink'
Export-ModuleMember -Function 'Get-LatestVersionNumber'
Export-ModuleMember -Function 'Confirm-InstallerValidity'
Export-ModuleMember -Function 'Set-AgentRefresh'
Export-ModuleMember -Function 'Confirm-LogFolder'
Export-ModuleMember -Function 'Write-Log'
Export-ModuleMember -Function 'Disable-IEFirstRunCustomization'
Export-ModuleMember -Function 'Set-Alert'
Export-ModuleMember -Function 'Get-UninstallCommand'
Export-ModuleMember -Function 'Uninstall-Program'
Export-ModuleMember -Function 'Get-ProgramRegistryDisplayRegex'
Export-ModuleMember -Function 'Get-InstallCommand'
Export-ModuleMember -Function 'Approve-SelectedProgram'
Export-ModuleMember -Function 'Find-RootKeyFromPath'
Export-ModuleMember -Function 'Assert-RootKeyPSDrive'
Export-ModuleMember -Function 'Read-RegistryValueData'
Export-ModuleMember -Function 'Add-RegistryKey'
Export-ModuleMember -Function 'Add-RegistryValue'
Export-ModuleMember -Function 'Remove-RegistryValue'
Export-ModuleMember -Function 'Get-PreInstallScriptBlock'
Export-ModuleMember -Function 'Get-InstallationsArchitecture'
Export-ModuleMember -Function 'Get-ProcessToTerminate'
Export-ModuleMember -Function 'Approve-Installation'
Export-ModuleMember -Function 'Invoke-ResolveArchitectureConflicts'
Export-ModuleMember -Function 'Get-IgnoreExitCodes'
Export-ModuleMember -Function 'Get-M365SupportedVersion'
Export-ModuleMember -Function 'Get-M365UpdateChannel'