STOXR.psm1

#requires -version 3
#$VerbosePreference = 'Continue'
<#
Copyright (c) 2015-2017, Svendsen Tech, Joakim Borger Svendsen. All rights reserved.
 
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>

$ErrorActionPreference = 'Stop'
#Set-StrictMode -Version Latest
function GetFuzzyCurrencyMatch {
    [CmdletBinding()]
    param([string] $Currency)
    # Hashtable for partial hits, and possibly complete currency text hits (System.String.Contains()).
    $Hits = @{}
    <#foreach ($Curr in $Script:CurrencyHash.GetEnumerator()) {
        if ($Curr.Name -eq $Currency) {
            Write-Verbose -Message "Got an exact hit for ${Currency}: $($Curr.Value + ' (' + $Curr.Name + ')')."
            # Return direct (unique currency abbreviation) hit as (the expected) hashtable.
            #$Script:Fuzzy = $false
            return @{ $Curr.Name = $Curr.Value }
        }
    }#>

    # Optimized with no drawbacks, I think...
    if ($Script:CurrencyHash.ContainsKey($Currency)) {
        return @{ $Currency = $Script:CurrencyHash[$Currency] }
    }
    # An extra loop first to see if the possibly partial description matches uniquely.
    if (@($Script:CurrencyHash.Values | Where-Object { $_ -like "*${Currency}*" }).Count -eq 1) {
        foreach ($Curr in $Script:CurrencyHash.GetEnumerator()) {
            if ($Curr.Value -like "*${Currency}*") {
                Write-Verbose -Message "Only one instance of this value text, so it's uniquely identifying ($($Curr.Value) ($($Curr.Name)))."
                # Only one instance, so it's unique.
                #$Script:Fuzzy = $false
                return @{ $Curr.Name = $Curr.Value }
            }
        }
    }
    foreach ($Curr in $Script:CurrencyHash.GetEnumerator()) {
        # Now handling this in a separate loop first.
        <#if ($Curr.Name -eq $Currency) {
            Write-Verbose -Message "Got an exact hit for ${Currency}: $($Curr.Value + ' (' + $Curr.Name + ')')."
            # Return direct (unique currency abbreviation) hit as (the expected) hashtable.
            $Script:Fuzzy = $false
            return @{ $Curr.Name = $Curr.Value }
        }#>

        # .Contains($Currency.ToLower())) { # abandoning this in favor of -like to allow for wildcards as input
        if ($Curr.Value.ToLower() -like "*${Currency}*") {
            Write-Verbose -Message "Got a (possibly) partial hit for ${Currency}: $($Curr.Value + ' (' + $Curr.Name + ')')."
            $Hits[$Curr.Name] = $Curr.Value
            # The uniquely identifying part above makes this prone to errors rather than being helpful.
            # Returning possible currencies instead.
            <# If the length is n % or more of the total length, stop looking, and assume it's exact.
            # This isn't perfect, but seems to be an improvement.
            # In addition add that if the length of the string is greater than 14, then 55 % is enough ... scientific numbers, bro.
            if ($Currency.Length / $Curr.Value.Length -gt 0.7 -or `
                ($Curr.Value.Length -gt 18 -and ($Currency.Length / $Curr.Value.Length -gt 0.62))) {
                # Return one hit only, assume it's good ...
                write-Verbose -Message "Returning single hit since it overlaps more than 70 % with the compared description (or 62 % if length of string greater than 18). $($Curr.Value + ' (' + $Curr.Name + ')')."
                #$Script:Fuzzy = $true
                return @{ $Curr.Name = $Curr.Value }
                #break
            }
            else {
                continue
            }#>

        }
        # Final fuzzy step. Split into "words" and check each one.
        # Cache -split operation for an ever so slight performance gain.
        $Values = $Curr.Value -split '\s+'
        # Get matches for each word.
        foreach ($e in @($Currency -split '\s+')) {
            foreach ($TmpValue in $Values) {
                if ($TmpValue -like "*$e*") {
                    Write-Verbose -Message "Got a partial hit for ${Currency}: $($Curr.Value + ' (' + $Curr.Name + ')')."
                    $Hits[$Curr.Name] = $Curr.Value
                    continue
                }
            }
        }
    }
    # If we get here we've got something fuzzy going on.
    #$Script:Fuzzy = $true
    return $Hits
}

function GetCurrencyString {
    param(
        [float] $Amount,
        [string] $From,
        [string] $ToCurr,
        [int] $Precision = 4)
    # USD is special since everything is calculated based on that rate.
    if ($From -ieq "USD") {
        "{0:N$Precision} {1} ({2}) is {3:N$Precision} {4} ({5}, {6:N$Precision}/USD). Date: {7}." -f `
            [float]$Amount, $From.ToUpper(), $CurrencyHash.$From,
            [float]($Amount / $Script:ExchangeRates.Data.rates.$From * $Script:ExchangeRates.Data.rates.$ToCurr), $ToCurr.ToUpper(), $CurrencyHash.$ToCurr,
            $Script:ExchangeRates.Data.rates.$ToCurr, $Script:ExchangeRates.Date.ToString('yyyy-MM-dd HH:mm:ss')
    }
    elseif ($ToCurr -ieq "USD") {
        "{0:N$Precision} {1} ({2} at {3:N$Precision}/USD) is {4:N$Precision} {5} ({6}). Date: {7}." -f `
            [float]$Amount, $From.ToUpper(), $CurrencyHash.$From, $Script:ExchangeRates.Data.rates.$From,
            [float]($Amount / $Script:ExchangeRates.Data.rates.$From * $Script:ExchangeRates.Data.rates.$ToCurr), $ToCurr.ToUpper(),
            $CurrencyHash.$ToCurr, $Script:ExchangeRates.Date.ToString('yyyy-MM-dd HH:mm:ss')
    }
    else {
        "{0:N$Precision} {1} ({2} at {3:N$Precision}/USD) is {4:N$Precision} {5} ({6}, {7:N$Precision}/USD). Date: {8}." -f `
            [float]$Amount, $From.ToUpper(), $CurrencyHash.$From, $Script:ExchangeRates.Data.rates.$From,
            [float]($Amount / $Script:ExchangeRates.Data.rates.$From * $Script:ExchangeRates.Data.rates.$ToCurr), $ToCurr.ToUpper(), $CurrencyHash.$ToCurr,
            $Script:ExchangeRates.Data.rates.$ToCurr, $Script:ExchangeRates.Date.ToString('yyyy-MM-dd HH:mm:ss')
    }
}

function GetCurrencyObject {
    param(
        [decimal] $Amount,
        [string] $FromCurrency,
        [string] $ToCurrency,
        [int] $Precision = 4)
    [pscustomobject] @{
        FromAmount = "{0:N$Precision}" -f $Amount
        FromAmountNumerical = $Amount
        FromCurrency = $FromCurrency.ToUpper()
        FromCurrencyText = $Script:CurrencyHash.$FromCurrency
        ToAmount = "{0:N$Precision}" -f [float]($Amount / $Script:ExchangeRates.Data.rates.$FromCurrency * $Script:ExchangeRates.Data.rates.$ToCurrency)
        ToAmountNumerical = $Amount / $Script:ExchangeRates.Data.rates.$FromCurrency * $Script:ExchangeRates.Data.rates.$ToCurrency
        ToCurrency = $ToCurrency.ToUpper()
        ToCurrencyText = $Script:CurrencyHash.$ToCurrency
        FromCurrencyPerUSD = "{0:N$Precision}" -f $Script:ExchangeRates.Data.rates.$FromCurrency
        ToCurrencyPerUSD = "{0:N$Precision}" -f $Script:ExchangeRates.Data.rates.$ToCurrency
        Date = $Script:ExchangeRates.Date
        Precision = $Precision
        #Fuzzy = $Script:Fuzzy
    }
}

<#
.SYNOPSIS
Get currency exchange data cache interval in seconds.
This is the time between each refresh of currency exchange data.
The first time the module is imported, it is set to 30 minutes (60 seconds * 30).
Change it with Set-STOXRCacheInterval.
#>

function Get-STOXRCacheInterval {
    [CmdletBinding()]
    param()
    $PersistentFilePath = "$PSScriptRoot\STOXRCacheInterval.xml"
    if (-not (Get-Variable -Scope Script -Name CacheInterval -ErrorAction SilentlyContinue)) {
        if (-not (Test-Path -LiteralPath $PersistentFilePath)) {
            Write-Warning -Message "Performing first-run operation of setting currency exchange data refresh interval to 30 * 60 seconds (30 minutes). Use Set-STOXRCacheInterval to change it."
            [int] $Script:CacheInterval = 30 * 60
            $Script:CacheInterval | Export-Clixml -LiteralPath $PersistentFilePath
        }
        else {
            Write-Verbose -Message "Reading cache interval from '$PersistentFilePath'. Use Set-STOXRCacheInterval to change it."
            [int] $Script:CacheInterval = Import-Clixml -LiteralPath $PersistentFilePath
        }
    }
    # Return currently set value (possibly (just) initialized above).
    return $Script:CacheInterval
}

<#
.SYNOPSIS
Set currency exchange data cache interval in seconds.
This is the time between each refresh of currency exchange data.
The first time the module is imported, it is set to 30 minutes (60 seconds * 30).
Display current value with Get-STOXRCacheInterval.
 
.PARAMETER CacheInterval
Currency exchange data cache time in seconds.
#>

function Set-STOXRCacheInterval {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][ValidateRange(1, 2147483647)][int] $CacheInterval)
    $PersistentFilePath = "$PSScriptRoot\STOXRCacheInterval.xml"
    [int] $Script:CacheInterval = $CacheInterval
    $Script:CacheInterval | Export-Clixml -LiteralPath $PersistentFilePath
}

<#
.SYNOPSIS
Display the currently set Open Exchange Rates App ID (if any).
Change it with Set-STOXRAppID.
#>

function Get-STOXRAppID {
    [CmdletBinding()]
    param()
    $PersistentFilePath = "$PSScriptRoot\STOXRAppID.xml"
    if (-not (Get-Variable -Scope Script -Name STOpenExchAppID -ErrorAction SilentlyContinue)) {
        if (-not (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf)) {
            Write-Warning -Message "No Open Exchange Rates App ID has been set. You need to set it to use this software. This can be obtained at https://openexchangerates.org/ . Use Set-STOXRAppID to set it."
            return
        }
        else {
            Write-Verbose -Message "Reading App ID from '$PersistentFilePath'. Use Set-STOXRAppID if you need to change it."
            [string] $Script:STOpenExchAppID = Import-Clixml -LiteralPath $PersistentFilePath
        }
    }
    # Return currently set value,
    return $Script:STOpenExchAppID
}

<#
.SYNOPSIS
Change or set the Open Exchange Rates API key.
Obtain the App ID / API key at https://openexchangerates.org/ .
 
.PARAMETER AppID
The API key to set.
.PARAMETER Force
Do not prompt to overwrite an existing key (just do it).
 
.EXAMPLE
Set-STOXRAppID -AppID '1234567890abcde'
.EXAMPLE
Set-STOXRAppID -AppID '1234567890abcde' -Force
#>

function Set-STOXRAppID {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string] $AppID,
        [switch] $Force)
    $PersistentFilePath = "$PSScriptRoot\STOXRAppID.xml"
    if (-not $Force -and (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf)) {
        $Answer = Read-Host -Prompt "An existing Open Exchange Rates App ID was found. Are you sure you want to overwrite it? Use -Force to avoid this prompt. Default is 'Y'."
        if ($Answer -match 'n') {
            Write-Verbose -Message "Aborted."
            return
        }
    }
    [string] $Script:STOpenExchAppID = $AppID
    $Script:STOpenExchAppID | Export-Clixml -LiteralPath $PersistentFilePath
}

<#
.SYNOPSIS
Get Svendsen Tech Open Exchange proxy information.
#>

function Get-STOXRProxy {
    [CmdletBinding()]
    param([switch] $IsInternalCommand)
    $PersistentFilePath = "$PSScriptRoot\STOXRProxy.xml"
    if (-not (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf)) {
        if (-not $IsInternalCommand) {
            Write-Warning -Message "No proxy information has been set."
        }
        return
    }
    $ProxyInfo = Import-Clixml -LiteralPath $PersistentFilePath
    <#if ($ProxyInfo | Get-Member -Name ProxyCredential -MemberType NoteProperty -ErrorAction SilentlyContinue) {
        if ($PasswordAsPlainText) {
            $ProxyInfo | Select Proxy, @{ Name = "PasswordAsPlainText"; Expression = { $_.PSCredential.GetNetworkCredential().Password } }
            return
        }
    }#>

    return $ProxyInfo
}

<#
.SYNOPSIS
Set the Svendsen Tech Open Exchange proxy information.
 
.PARAMETER ProxyUri
URI for the proxy to use. Example: http://proxy.internal.company.com:8080
.PARAMETER ProxyCredential
A PSCredential object with credentials to use for the proxy.
.PARAMETER ProxyUseDefaultCredentials
Use default logon credentials for the proxy.
.PARAMETER RemoveProxyInfo
Delete/remove proxy information from disk.
.PARAMETER Force
Use this to not prompt to remove when you use -RemoveProxyInfo, or are
overwriting existing proxy info / credentials.
#>

function Set-STOXRProxy {
    [CmdletBinding()]
    param(
        [string] $ProxyUri,
        [pscredential] $ProxyCredential = $null,
        [switch] $ProxyUseDefaultCredentials,
        [switch] $RemoveProxyInfo,
        [switch] $Force)
    $PersistentFilePath = "$PSScriptRoot\STOXRProxy.xml"
    if ($RemoveProxyInfo) {
        if (-not $Force -and (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf)) {
            $Answer = Read-Host "Are you sure you want to remove proxy information? Default is 'Y'. Use -Force to avoid this prompt."
            if ($Answer -match 'n') {
                return
            }
        }
        Remove-Item -LiteralPath $PersistentFilePath -ErrorAction SilentlyContinue
        Write-Verbose -Message "Cleared proxy data if it existed ($PersistentFilePath)."
        return
    }
    if (-not $Force -and (Test-Path -LiteralPath $PersistentFilePath)) {
        $Answer = Read-Host "Proxy credentials already exist. Are you sure you want to overwrite? Default is 'Y'. Use -Force to avoid this prompt."
        if ($Answer -match 'n') {
            return
        }
    }
    if ($ProxyCredential) {
        [pscustomobject] @{
            Proxy = $ProxyUri
            Username = $ProxyCredential.Username
            Password = $ProxyCredential.GetNetworkCredential().SecurePassword
        } | Export-Clixml -LiteralPath $PersistentFilePath
    }
    elseif ($ProxyUseDefaultCredentials) {
        [pscustomobject] @{
            Proxy = $ProxyUri
            UseDefaultCredentials = $true
        } | Export-Clixml -LiteralPath $PersistentFilePath
    }
    else {
        [pscustomobject] @{
            Proxy = $ProxyUri
        } | Export-Clixml -LiteralPath $PersistentFilePath
    }
}

function Get-STOXRCurrencyList {
    [CmdletBinding()]
    param([switch] $AutoSizeTable)
    if ($AutoSizeTable) {
        $Script:CurrencyHash.GetEnumerator() |
            Sort-Object Name |
            Select-Object Name, @{
                Name = 'Description'
                Expression = { $_.Value }
            } |
            Format-Table -AutoSize
    }
    else {
        $Script:CurrencyHash.GetEnumerator() |
            Sort-Object Name |
            Select-Object Name, @{
                Name = 'Description'
                Expression = { $_.Value }
            }
    }
}

function Get-STOXRTrialCurrency {
    [CmdletBinding()]
    param()
    if ($Script:RegisteredProduct) {
        Write-Warning -Message "Your product is registered, so you have access to convert between all currencies.`nUse Get-STOXRCurrencyList to list them all."
        #return
    }
    $Script:TrialCurrency.ToUpper()
}

<#
.SYNOPSIS
Change the one allowed currency to convert to and from in the trial version of the software.
There's a limit on changing the currency frequently, so you can only change it
once every 12 hours. Buy and register the product with Register-STOXR to avoid this limitation.
 
.PARAMETER Currency
Currency code for the currency to use.
#>

function Set-STOXRTrialCurrency {
    [CmdletBinding()]
    param([Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string] $Currency)
    if ($Script:RegisteredProduct) {
        Write-Warning -Message "Your product is registered, so you do not need to change this currency value. Use Get-STOXRCurrencyList to list all available currencies."
        return
    }
    $PersistentFilePath = "$PSScriptRoot\STOXRTrialCurrency.xml"
    if ($Script:CurrencyHash.ContainsKey($Currency)) {
        if (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf) {
            $TrialCurrencyXml = Import-Clixml -LiteralPath $PersistentFilePath
            if ($TrialCurrencyXml.Date -lt [datetime]::Now.AddHours(-12)) {
                $Script:TrialCurrency = $Currency
                [pscustomobject] @{
                    Currency = $Script:TrialCurrency
                    Date = [datetime]::Now
                } | Export-Clixml -LiteralPath $PersistentFilePath -ErrorAction Stop
                "Changed trial currency to '$($Currency.ToUpper())'. It cannot be changed again until 12 hours have passed. If you register the product, this limitation disappears."
            }
            else {
                Write-Warning -Message "Sorry, but 12 hours have not yet passed since the trial currency was last changed."
                $o = [datetime]::Now - $TrialCurrencyXml.Date
                Write-Warning -Message ("Time left before you can change: " + (12 - $o.Hours - 1) + " hours and " + (60 - $o.Minutes - 1) + " minutes (" + (12 * 60 - $o.TotalMinutes).ToString('N2') + " minutes).")
            }
        }
    }
    else {
        Write-Warning -Message "Unsupported currency abbreviation entered. Use Get-STOXRCurrencyList to list them all."
    }
}

function InvokeRestMethodWithProxyCheck {
    [CmdletBinding()]
    param(
        [string] $Uri)
    <#$ProxyPath = "$PSScriptRoot\STOXRProxy.xml"
    if (-not (Test-Path -LiteralPath $ProxyPath -PathType Leaf)) {
        Invoke-RestMethod -Uri $Uri
        return
    }#>

    #$ProxyInfo = Import-Clixml -LiteralPath $ProxyPath
    $ProxyInfo = Get-STOXRProxy -IsInternalCommand
    if (-not $ProxyInfo) {
        Invoke-RestMethod -Uri $Uri
        return
    }
    if ($ProxyInfo | Get-Member -Name UseDefaultCredentials -MemberType NoteProperty -ErrorAction SilentlyContinue) {
        Invoke-RestMethod -Uri $Uri -UseDefaultCredentials
        return
    }
    if ($ProxyInfo | Get-Member -Name Username -MemberType NoteProperty -ErrorAction SilentlyContinue) {
        $Credential = [pscredential] $null
        $Credential.Username = $ProxyInfo.Username
        $Credential.Password = $ProxyInfo.Password # ConvertTo-SecureString -AsPlainText -Force -String $ProxyInfo.Password
        Invoke-RestMethod -Uri $Uri -ProxyCredential $Credential
        return
    }
    Invoke-RestMethod -Uri $Uri
}

function GetExchangeRate {
    $BaseURL = 'http://openexchangerates.org/api/'
    if ($AppID = Get-STOXRAppID) {
        if (-not (Get-Variable -Scope Script CurrenciesJson -ErrorAction SilentlyContinue)) {
            $Uri = $BaseURL + "currencies.json?app_id=$AppID"
            $Script:CurrenciesJson = InvokeRestMethodWithProxyCheck -Uri $Uri
        }
        if (-not (Get-Variable -Scope Script ExchangeRates -ErrorAction SilentlyContinue) -or `
            $Script:ExchangeRates.Date -lt [DateTime]::Now.AddSeconds((-1 * (Get-STOXRCacheInterval)))) {
            $Uri = $BaseURL + "latest.json?app_id=$AppID"
            $Json = InvokeRestMethodWithProxyCheck -Uri $Uri
            $Script:ExchangeRates = [pscustomobject] @{
                Date = [DateTime]::Now
                Data = $Json
            }
        }
    }
    else {
        Write-Warning -Message "You need to set the Open Exchange Rates app ID with Set-STOXRAppID before using this software."
    }
    if (Get-Variable -Name CurrenciesJson -Scope Script -EA SilentlyContinue) {
        $Script:CurrencyHash = @{}
        foreach ($CurrencyAbbreviation in $Script:CurrenciesJson | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) {
            $Script:CurrencyHash[$CurrencyAbbreviation] = $Script:CurrenciesJson.$CurrencyAbbreviation
        }
    }
}

<#
.SYNOPSIS
Use the license key you bought and received to register the Svendsen Tech Open Exchange Rates module.
This unlocks the full functionality and allows you to convert between any/multiple currencies.
 
.PARAMETER LicenseKey
 
.EXAMPLE
Register-STOXR -LicenseKey "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
#>

function Register-STOXR {
    [CmdletBinding()]
    param(
        [string] $LicenseKey,
        [switch] $RefreshUsingExistingLicense)
    $PersistentFilePath = "$PSScriptRoot\STOXRLicense.txt"
    #'VABoAGUAIABwAHIAbwBkAHUAYwB0ACAAaQBzACAAcgBlAGcAaQBzAHQAZQByAGUAZAAuACAATABvAHIAZQBtACAAaQBwAHMAdQBtACAAYQBuAGQAIABzAHQAdQBmAGYALgA='
    if ($RefreshUsingExistingLicense) {
        if (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf) {
            if ((Get-Content -Raw -LiteralPath $PersistentFilePath).Trim() -eq `
              'VABoAGUAIABwAHIAbwBkAHUAYwB0ACAAaQBzACAAcgBlAGcAaQBzAHQAZQByAGUAZAAuACAATABvAHIAZQBtACAAaQBwAHMAdQBtACAAYQBuAGQAIABzAHQAdQBmAGYALgA=') {
                $Script:RegisteredProduct = $true
                "Found a valid license file and successfully activated full functionality."
                return
            }
            else {
                Write-Warning -Message "Found a license file, but the license appears to be invalid."
                return
            }
        }
        else {
            Write-Warning -Message "Could not find a license file."
            return
        }
        return
    }
    if ([Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($LicenseKey)) -eq `
      'VgBBAEIAbwBBAEcAVQBBAEkAQQBCAHcAQQBIAEkAQQBiAHcAQgBrAEEASABVAEEAWQB3AEIAMABBAEMAQQBBAGEAUQBCAHoAQQBDAEEAQQBjAGcAQgBsAEEARwBjAEEAYQBRAEIAegBBAEgAUQBBAFoAUQBCAHkAQQBHAFUAQQBaAEEAQQB1AEEAQwBBAEEAVABBAEIAdgBBAEgASQBBAFoAUQBCAHQAQQBDAEEAQQBhAFEAQgB3AEEASABNAEEAZABRAEIAdABBAEMAQQBBAFkAUQBCAHUAQQBHAFEAQQBJAEEAQgB6AEEASABRAEEAZABRAEIAbQBBAEcAWQBBAEwAZwBBAD0A') {
        $LicenseKey | Set-Content -Encoding ASCII -LiteralPath $PersistentFilePath -ErrorAction Stop
        "Successfully registered the Svendsen Tech Open Exchange Rates module."
        $Script:RegisteredProduct = $true
    }
    else {
        Write-Warning -Message "Invalid license key."
    }
}

<#
.SYNOPSIS
    Remove the license for the Svendsen Tech Open Exchange Rates module.
    After you do this, it will revert to the trial version mode with
    limited features.
 
.PARAMETER Force
Do not prompt to delete if an existing license is found, just do it.
 
.EXAMPLE
Unregister-STOXR -Force
 
Successfully unregistered the Svendsen Tech Open Exchange Rates module (STOXR).
#>

function Unregister-STOXR {
    [CmdletBinding()]
    param(
        [switch] $Force)
    $PersistentFilePath = "$PSScriptRoot\STOXRLicense.txt"
    if (-not $Force) {
        if (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf) {
            $Answer = Read-Host -Prompt "License found. Are you sure you want to unregister the Svendsen Tech Open Exchange Rates module (STOXR)?"
            if ($Answer -notmatch "n") {
                Write-Verbose -Message "Deleting license."
                Remove-Item -LiteralPath $PersistentFilePath -ErrorAction Stop
                $Script:RegisteredProduct = $false
                "Successfully unregistered the Svendsen Tech Open Exchange Rates module (STOXR)."
                return
            }
            else {
                "User aborted."
                return
            }
        }
        else {
            "The Svendsen Tech Open Exchange Rates module was not registered and could therefore not be unregistered."
            return
        }
    }
    if (Test-Path -LiteralPath $PersistentFilePath -PathType Leaf) {
        Write-Verbose -Message "Deleting license."
        Remove-Item -LiteralPath $PersistentFilePath -ErrorAction Stop
        $Script:RegisteredProduct = $false
        "Successfully unregistered the Svendsen Tech Open Exchange Rates module (STOXR)."
    }
    else {
        "The Svendsen Tech Open Exchange Rates module was not registered and could therefore not be unregistered."
    }
}

<#
.SYNOPSIS
    Display information about the module status.
#>

function Get-STOXRDebugInfo {
    [CmdletBinding()]
    param()
    if (Test-Path -LiteralPath "$PSScriptRoot\STOXRAppID.xml") {
        $AppIDFileFound = $true
        if ((Import-CliXml -LiteralPath "$PSScriptRoot\STOXRAppID.xml") -match '\S') {
            $AppIDStringFound = $true
        }
        else {
            $AppIDStringFound = $false
        }
    }
    else {
        $AppIDFileFound = $false
        $AppIDStringFound = $false
    }
    if (Get-Variable -Name CurrencyHash -Scope Script -ErrorAction SilentlyContinue) {
        $CurrencyHashSet = $true
        $NumberOfCurrencies = $Script:CurrencyHash.Keys.Count
    }
    else {
        $CurrencyHashSet = $false
        $NumberOfCurrencies = "NOT_SET"
    }
    [PSCustomObject] @{
        IsRegistered = $Script:RegisteredProduct
        PSScriptRoot = $PSScriptRoot
        LicenseFileFound = Test-Path -LiteralPath "$PSScriptRoot\STOXRLicense.txt"
        AppIDFileFound = $AppIDFileFound
        AppIDStringFound = $AppIDStringFound
        TrialCurrency = Get-STOXRTrialCurrency
        CurrencyHashSet = $CurrencyHashSet
        NumberOfCurrencies = $NumberOfCurrencies
    }
}

function Initialize {
    [CmdletBinding()]
    param()
    $Script:RegisteredProduct = $false
    #$Script:Fuzzy = $false
    Write-Verbose -Message "PSScriptRoot: $PSScriptRoot"
    $LicensePath = "$PSScriptRoot\STOXRLicense.txt"
    if (Test-Path -LiteralPath $LicensePath -PathType Leaf) {
        if ((Get-Content -Raw -LiteralPath $LicensePath).Trim() -eq `
          'VABoAGUAIABwAHIAbwBkAHUAYwB0ACAAaQBzACAAcgBlAGcAaQBzAHQAZQByAGUAZAAuACAATABvAHIAZQBtACAAaQBwAHMAdQBtACAAYQBuAGQAIABzAHQAdQBmAGYALgA=') {
            Write-Verbose -Message "Found valid license key. Activating full functionality."
            $Script:RegisteredProduct = $true
        }
        else {
            Write-Warning -Message "Found a license file, but the license appears to be invalid. Trial version mode is active. Try setting a valid license with the command Register-STOXR."
        }
    }
    else {
        Write-Warning -Message "This is a trial version of the product. You can only change to or from one single currency (both ways). The currency is 'USD' by default. In the trial software it can be changed once every 12 hours. Change to for example British Pound Sterling with Set-STOXRTrialCurrency -Currency GBP. To display the currently set trial currency, use Get-STOXRTrialCurrency. To list all supported currencies, use Get-STOXRCurrencyList."
    }
    GetExchangeRate
    $TrialCurrencyPath = "$PSScriptRoot\STOXRTrialCurrency.xml"
    if (-not (Test-Path -LiteralPath $TrialCurrencyPath -PathType Leaf)) {
        [string] $Script:TrialCurrency = 'USD'
        [pscustomobject] @{
            Currency = $Script:TrialCurrency
            Date = [datetime]::Now.AddHours(-13) # allow for one change
        } | Export-Clixml -LiteralPath $TrialCurrencyPath
    }
    else {
        [string] $Script:TrialCurrency = (Import-Clixml -LiteralPath $TrialCurrencyPath).Currency
    }
}

function GetDataFromCurrencyString {
    [CmdletBinding()]
    param(
        [string] $InputString)
    $FromCurrency, $ToCurrency = $InputString -isplit '\s+(?:in|to)\s+' | ForEach-Object { $_.Trim() }
    if (-not $FromCurrency -or -not $ToCurrency) {
        Write-Warning -Message "The input string needs to be in the format: <FROM_NUMBER> <FROM_CURRENCY> in/to <TO_CURRENCY>. Period as decimal separator. You can use commas as thousand separators for readability/flexibility."
        return
    }
    if ($FromCurrency -match '\s*([\d.,]+)\s*(.+)') {
        $Amount, $FromCurrency = $Matches[1,2]
        $Amount = [float] ($Amount -replace ',')
    }
    else {
        Write-Warning -Message 'Invalid "From" number/currency (that part is validated by the regex "\s*([\d.,]+)\s*(.+)"). The input string needs to be in the format: <FROM_NUMBER> <FROM_CURRENCYE> in/to <TO_CURRENCY>. Period as decimal separator. You can use commas as thousand separators for readability/flexibility.'
        return
    }
    return $Amount, $FromCurrency, $ToCurrency
}

function CurrencyStringOrObject {
    param(
        [switch] $AsString,
        [float] $Amount,
        [string] $FromCurrency,
        [string] $ToCurrency,
        [int] $Precision)
    if ($AsString) {
        GetCurrencyString -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
    }
    else {
        GetCurrencyObject -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
    }
}

function Convert-STOXRCurrency {
    <#
    .SYNOPSIS
        Convert to and from any of the currently 168 currencies supported by
        the Open Exchange Rates (OXR) API. See http://openexchangerates.org
    .DESCRIPTION
        For now, see the online documentation at
        https://www.powershelladmin.com/wiki/STOXR_-_Currency_Conversion_Software_-_Open_Exchange_Rates_API
         
        This function will be documented more extensively in the built-in help later.
 
 
    #>

    [CmdletBinding()]
    param(
        [decimal] $Amount,
        [string] $FromCurrency,
        [string] $ToCurrency,
        [int] $Precision = 4,
        [string] $FuzzyInput,
        [switch] $AsString)
    # Awesome feature limitation logic. Virtually impossible to circumvent.
    if (-not $Script:RegisteredProduct -and -not ($FromCurrency -eq $Script:TrialCurrency -or $ToCurrency -eq $Script:TrialCurrency)) {
        Write-Warning -Message "This product is not registered, so the limitations apply that either the 'From' or 'To' currency needs to be '$($Script:TrialCurrency.ToUpper())' (default USD) - and that -FuzzyInput does not work. The trial currency can be changed once every 12 hours with the command Set-STOXRTrialCurrency. Register the product to unlock full functionality."
        return
    }
    # Not using parameter sets and explicitly positioned parameters because the error message isn't
    # friendly/comprehensible to those not in the know when you use both -FuzzyInput and {Amount|FromCurrency|ToCurrency}.
    if ($FuzzyInput -and ($Amount -or $FromCurrency -or $ToCurrency)) {
        Write-Warning -Message "You cannot use the FuzzyInput parameter together with any of the following parameters: Amount, FromCurrency, ToCurrency."
        return
    }
    if ($FuzzyInput) {
        $Script:Fuzzy = $false
        $Amount, $FromCurrency, $ToCurrency = GetDataFromCurrencyString -InputString $FuzzyInput
        if (-not $Amount) {
            Write-Warning -Message "Unable to parse FuzzyInput string."
            return
        }
    }
    GetExchangeRate
    if (-not (Get-Variable -Name CurrencyHash -Scope Script -ErrorAction SilentlyContinue)) {
        Write-Warning -Message "Missing currency data. Cannot continue. Do you need to set an app ID with Set-STOXRAppID?"
        return
    }
    if ($Script:CurrencyHash.ContainsKey($FromCurrency)) {
        if ($Script:CurrencyHash.ContainsKey($ToCurrency)) {
            CurrencyStringOrObject -AsString:$AsString -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
        }
        else {
            $Hits = GetFuzzyCurrencyMatch -Currency $ToCurrency
            if ($Hits.Keys.Count -gt 1) {
                Write-Warning -Message "Multiple hits ($($Hits.Count)) for ""to"" currency:`n$(($Hits.GetEnumerator() | Sort-Object -Property Key | ForEach-Object { $_.Key + ', ' + $_.Value }) -join "`n")"
                return
            }
            if ($Hits.Keys.Count -eq 1) {
                $ToCurrency = $Hits.GetEnumerator() | Select-Object -ExpandProperty Key
                # We have both from and to currencies, let's do the math.
                CurrencyStringOrObject -AsString:$AsString -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
            }
            if (-not $Hits.Keys.Count) {
                Write-Warning -Message "No match for `"to`" currency."
                return
            }
        }
    }
    elseif ($Script:CurrencyHash.ContainsKey($ToCurrency)) {
        $Hits = GetFuzzyCurrencyMatch -Currency $FromCurrency
        #'Hits:'; $hits
        if ($Hits.Keys.Count -gt 1) {
            Write-Warning -Message "Multiple hits ($($Hits.Count)) for ""from"" currency:`n$(($Hits.GetEnumerator() | Sort-Object Key | ForEach-Object { $_.Key + ', ' + $_.Value }) -join "`n")"
            return
        }
        if ($Hits.Keys.Count -eq 1) {
            $FromCurrency = $Hits.GetEnumerator() | Select-Object -ExpandProperty Key
            #'Determined "from" currency''s code to be: ' + $FromCurr
            # We have both from and to currencies, let's do the math.
            CurrencyStringOrObject -AsString:$AsString -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
        }

        if (-not $Hits.Keys.Count) {
            Write-Warning -Message "No match for `"from`" currency."
            return
        }
    }
    # Fuzzy match both...
    else {
        $Hits = GetFuzzyCurrencyMatch -Currency $FromCurrency
        if ($Hits.Keys.Count -gt 1) {
            Write-Warning -Message "Multiple hits ($($Hits.Count)) for ""from"" currency:`n$(($Hits.GetEnumerator() | Sort-Object Key | ForEach-Object { $_.Key + ', ' + $_.Value }) -join "`n")"
            return
        }
        if ($Hits.Keys.Count -eq 1) {
            $FromCurrency = $Hits.Keys
            #'Determined "from" currency''s code to be: ' + $FromCurr
            $Hits = GetFuzzyCurrencyMatch -Currency $ToCurrency
            if ($Hits.Keys.Count -gt 1) {
                Write-Warning -Message "Multiple hits ($($Hits.Count)) for ""to"" currency:`n$(($Hits.GetEnumerator() | Sort-Object Key | ForEach-Object { $_.Key + ', ' + $_.Value }) -join "`n")"
                return
            }
            if ($Hits.Keys.Count -eq 1) {
                $ToCurrency = $Hits.Keys
                # We have both from and to currencies, let's do the math.
                CurrencyStringOrObject -AsString:$AsString -Amount $Amount -From $FromCurrency -To $ToCurrency -Precision $Precision
            }
            if (-not $Hits.Keys.Count) {
                Write-Warning -Message 'No match for "to" currency.'
                return
            }
        }
        if (-not $Hits.Keys.Count) {
            Write-Warning -Message "No match for `"from`" currency."
            return
        }
    }
}

Initialize
Export-ModuleMember -Function '*-*'