Private/Shared/Read-MenuSelection.ps1

function Read-MenuSelection {
    <#
    .SYNOPSIS
    Displays a menu of options and prompts the user to select one.
    .DESCRIPTION
    This function displays a numbered list of options and prompts the user to select one.
    It validates the selection and returns the selected option value or the value property if objects are provided.
    .PARAMETER Title
    The title/prompt to display before the menu options. If null/empty, no title is shown.
    .PARAMETER HelpText
    An array of help text lines to display after the title.
    .PARAMETER OptionsTitle
    Optional line of text to render before the options list.
    .PARAMETER Options
    An array of option values to display. Can be simple strings/values, or objects with 'label' and 'value' properties.
    When objects with label/value are provided, the label is displayed and the value is returned.
    .PARAMETER DefaultIndex
    The zero-based index of the default option (default: 0).
    .PARAMETER DefaultValue
    Alternative to DefaultIndex - specify the default value directly. If both are provided, DefaultValue takes precedence.
    .PARAMETER AllowManualEntry
    When set, adds an option [0] to allow manual entry.
    .PARAMETER ManualEntryPrompt
    The prompt to display when manual entry is selected (default: "Enter value").
    .PARAMETER ManualEntryValidator
    A script block that validates manual entry input. Should return $true if valid, $false otherwise.
    The input value is passed as $args[0].
    .PARAMETER ManualEntryErrorMessage
    The error message to display when manual entry validation fails.
    .PARAMETER IsRequired
    When set, the user must provide a value (cannot be empty).
    .PARAMETER RequiredMessage
    The error message to display when a required field is left empty.
    .PARAMETER EmptyMessage
    Message to display when the options array is empty. If set and options are empty, shows this message and falls back to manual entry.
    .PARAMETER Type
    The expected data type for validation: 'string' (default), 'number', 'guid', 'boolean', or 'array'.
    When AllowManualEntry is used, input will be validated against this type.
    For 'array', comma-separated input is parsed into an array.
    .PARAMETER IsSensitive
    When set, input is read securely using Read-Host -AsSecureString and the value is masked in display.
    .OUTPUTS
    Returns the selected option value.
    .EXAMPLE
    $selection = Read-MenuSelection -Title "Select IaC type:" -Options @("terraform", "bicep") -DefaultIndex 0
    .EXAMPLE
    $subscriptions = @(
        @{ label = "Subscription 1 (sub-id-1)"; value = "sub-id-1" },
        @{ label = "Subscription 2 (sub-id-2)"; value = "sub-id-2" }
    )
    $selection = Read-MenuSelection -Title "Select subscription:" -Options $subscriptions -AllowManualEntry -ManualEntryPrompt "Enter subscription ID"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string] $Title,

        [Parameter(Mandatory = $false)]
        [string[]] $HelpText = @(),

        [Parameter(Mandatory = $false)]
        [string] $OptionsTitle = $null,

        [Parameter(Mandatory = $false)]
        [array] $Options = @(),

        [Parameter(Mandatory = $false)]
        [int] $DefaultIndex = 0,

        [Parameter(Mandatory = $false)]
        $DefaultValue = $null,

        [Parameter(Mandatory = $false)]
        [switch] $AllowManualEntry,

        [Parameter(Mandatory = $false)]
        [string] $ManualEntryPrompt = "Enter value",

        [Parameter(Mandatory = $false)]
        [scriptblock] $ManualEntryValidator = $null,

        [Parameter(Mandatory = $false)]
        [string] $ManualEntryErrorMessage = "Invalid input. Please try again.",

        [Parameter(Mandatory = $false)]
        [switch] $IsRequired,

        [Parameter(Mandatory = $false)]
        [string] $RequiredMessage = "This field is required. Please enter a value.",

        [Parameter(Mandatory = $false)]
        [string] $EmptyMessage = $null,

        [Parameter(Mandatory = $false)]
        [ValidateSet("string", "number", "guid", "boolean", "array")]
        [string] $Type = "string",

        [Parameter(Mandatory = $false)]
        [switch] $IsSensitive
    )

    # Built-in type validators
    $typeValidators = @{
        "guid"    = {
            param($value)
            return $value -match "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
        }
        "number"  = {
            param($value)
            $intResult = 0
            return [int]::TryParse($value, [ref]$intResult)
        }
        "boolean" = {
            param($value)
            $validBooleans = @('true', 'false', 'yes', 'no', '1', '0', 'y', 'n', 't', 'f')
            return $validBooleans -contains $value.ToString().ToLower()
        }
        "string"  = {
            param($value)
            return $true
        }
        "array"   = {
            param($value)
            return $true  # Arrays are always valid as input
        }
    }

    $typeErrorMessages = @{
        "guid"    = "Invalid GUID format. Please enter a valid GUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
        "number"  = "Invalid format. Please enter an integer number."
        "boolean" = "Invalid format. Please enter true or false."
        "string"  = "Invalid input."
        "array"   = "Invalid input."
    }

    # Function to convert value to appropriate type
    function ConvertTo-TypedValue {
        param($Value, $TargetType, $DefaultValue = $null)
        if ([string]::IsNullOrWhiteSpace($Value)) {
            return $DefaultValue
        }
        switch ($TargetType) {
            "number" {
                $intResult = 0
                if ([int]::TryParse($Value, [ref]$intResult)) {
                    return $intResult
                }
                return $Value
            }
            "boolean" {
                $valueStr = $Value.ToString().ToLower()
                return $valueStr -in @('true', 'yes', '1', 'y', 't')
            }
            "array" {
                # Parse comma-separated values into an array
                return @($Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            }
            default {
                return $Value
            }
        }
    }

    # Get the effective validator - use ManualEntryValidator if provided, otherwise use type validator
    $effectiveValidator = if ($null -ne $ManualEntryValidator) {
        $ManualEntryValidator
    } elseif ($Type -ne "string") {
        $typeValidators[$Type]
    } else {
        $null
    }

    # Get the effective error message
    $effectiveErrorMessage = if (-not [string]::IsNullOrWhiteSpace($ManualEntryErrorMessage) -and $ManualEntryErrorMessage -ne "Invalid input. Please try again.") {
        $ManualEntryErrorMessage
    } elseif ($Type -ne "string") {
        $typeErrorMessages[$Type]
    } else {
        $ManualEntryErrorMessage
    }

    # Helper function to read input (handles sensitive vs normal input)
    function Read-InputValue {
        param($Prompt, $Sensitive)
        if ($Sensitive) {
            $secureValue = Read-Host $Prompt -AsSecureString
            return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureValue)
            )
        } else {
            return Read-Host $Prompt
        }
    }

    # Helper function to mask sensitive values
    function Get-MaskedValue {
        param($Value)
        if ([string]::IsNullOrWhiteSpace($Value)) {
            return "(empty)"
        }
        $valueStr = $Value.ToString()
        if ($valueStr.Length -ge 8) {
            return $valueStr.Substring(0, 3) + "***" + $valueStr.Substring($valueStr.Length - 3)
        } else {
            return "********"
        }
    }

    # Helper function to get the value from an option (handles both simple values and label/value objects)
    function Get-OptionValue {
        param($Option)
        if ($Option -is [hashtable] -and $Option.ContainsKey('value')) {
            return $Option.value
        } elseif ($Option -is [PSCustomObject] -and $null -ne $Option.PSObject.Properties['value']) {
            return $Option.value
        }
        return $Option
    }

    # Helper function to get the label from an option
    function Get-OptionLabel {
        param($Option)
        if ($Option -is [hashtable] -and $Option.ContainsKey('label')) {
            return $Option.label
        } elseif ($Option -is [PSCustomObject] -and $null -ne $Option.PSObject.Properties['label']) {
            return $Option.label
        }
        return $Option.ToString()
    }

    # Determine if we have options to display
    $hasOptions = $null -ne $Options -and $Options.Count -gt 0

    # If DefaultValue is provided and we have options, find its index
    if ($null -ne $DefaultValue -and $hasOptions) {
        for ($i = 0; $i -lt $Options.Count; $i++) {
            if ((Get-OptionValue -Option $Options[$i]) -eq $DefaultValue) {
                $DefaultIndex = $i
                break
            }
        }
    }

    # Display title if provided
    if (-not [string]::IsNullOrWhiteSpace($Title)) {
        Write-ToConsoleLog $Title -IsPrompt
    }

    # Display help text if provided
    foreach ($helpLine in $HelpText) {
        if (-not [string]::IsNullOrWhiteSpace($helpLine)) {
            Write-ToConsoleLog $helpLine -IsSelection
        }
    }

    # Display default value and required status
    if ($null -ne $DefaultValue -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) {
        $displayDefault = if ($IsSensitive.IsPresent) { Get-MaskedValue -Value $DefaultValue } else { $DefaultValue }
        Write-ToConsoleLog "Default: $displayDefault" -Color Cyan -IsSelection
    }
    if ($IsRequired.IsPresent) {
        Write-ToConsoleLog "Required: Yes" -Color Yellow -IsSelection
    }

    # If no options, go directly to manual entry
    if (-not $hasOptions) {
        if (-not [string]::IsNullOrWhiteSpace($EmptyMessage)) {
            Write-ToConsoleLog $EmptyMessage -IsWarning
        }

        $result = $null
        do {
            $manualInput = Read-InputValue -Prompt $ManualEntryPrompt -Sensitive $IsSensitive.IsPresent
            if ([string]::IsNullOrWhiteSpace($manualInput)) {
                # For arrays, return empty array or default; for others return default
                if ($Type -eq "array") {
                    if ($null -ne $DefaultValue -and $DefaultValue -is [System.Collections.IList]) {
                        $result = $DefaultValue
                    } else {
                        $result = @()
                    }
                } elseif ($null -ne $DefaultValue -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) {
                    $result = ConvertTo-TypedValue -Value $DefaultValue -TargetType $Type
                }
            } else {
                # Validate and convert
                if ($null -ne $effectiveValidator -and -not [string]::IsNullOrWhiteSpace($manualInput)) {
                    if (-not (& $effectiveValidator $manualInput)) {
                        Write-ToConsoleLog $effectiveErrorMessage -IsError
                        $result = $null
                        continue
                    }
                }
                $result = ConvertTo-TypedValue -Value $manualInput -TargetType $Type -DefaultValue $DefaultValue
            }
            # Check required - for arrays, check if empty
            if ($IsRequired.IsPresent) {
                if ($Type -eq "array") {
                    if ($null -eq $result -or $result.Count -eq 0) {
                        Write-ToConsoleLog $RequiredMessage -IsError
                        $result = $null
                    }
                } elseif ([string]::IsNullOrWhiteSpace($result)) {
                    Write-ToConsoleLog $RequiredMessage -IsError
                    $result = $null
                }
            }
        } while ($IsRequired.IsPresent -and $null -eq $result)
        return $result
    }

    # Display options title if provided
    if (-not [string]::IsNullOrWhiteSpace($OptionsTitle)) {
        Write-ToConsoleLog $OptionsTitle -IsSelection
    }

    # Display options
    for ($i = 0; $i -lt $Options.Count; $i++) {
        $option = $Options[$i]
        $label = Get-OptionLabel -Option $option
        $value = Get-OptionValue -Option $option
        $isCurrent = ($null -ne $DefaultValue -and $value -eq $DefaultValue) -or ($null -eq $DefaultValue -and $i -eq $DefaultIndex)
        $currentMarker = if ($isCurrent) { " (current)" } else { "" }

        if ($isCurrent) {
            Write-ToConsoleLog "[$($i + 1)] $label$currentMarker" -IsSelection -Color Green -IndentLevel 1
        } else {
            Write-ToConsoleLog "[$($i + 1)] $label" -IsSelection -IndentLevel 1
        }
    }

    # Show manual entry option if allowed
    if ($AllowManualEntry.IsPresent) {
        Write-ToConsoleLog "[0] Enter manually" -IsSelection -IndentLevel 1
    }

    # Build prompt text
    $promptText = "Enter selection (1-$($Options.Count)"
    if ($AllowManualEntry.IsPresent) {
        $promptText += ", 0 for manual entry"
    }
    $promptText += ", default: $($DefaultIndex + 1))"

    # Get selection
    $result = $null
    do {
        $selection = Read-InputValue -Prompt $promptText -Sensitive $IsSensitive.IsPresent

        if ([string]::IsNullOrWhiteSpace($selection)) {
            # Use default
            $result = Get-OptionValue -Option $Options[$DefaultIndex]
        } elseif ($AllowManualEntry.IsPresent -and $selection -eq "0") {
            # Manual entry
            do {

                $manualInput = Read-InputValue -Prompt $ManualEntryPrompt -Sensitive $IsSensitive.IsPresent
                if ([string]::IsNullOrWhiteSpace($manualInput) -and -not [string]::IsNullOrWhiteSpace($DefaultValue)) {
                    $result = $DefaultValue
                    break
                }
                if ($null -ne $effectiveValidator -and -not [string]::IsNullOrWhiteSpace($manualInput)) {
                    if (-not (& $effectiveValidator $manualInput)) {
                        Write-ToConsoleLog $effectiveErrorMessage -IsError
                        continue
                    }
                }
                $result = ConvertTo-TypedValue -Value $manualInput -TargetType $Type
                break
            } while ($true)
        } else {
            $selectedIndex = [int]$selection - 1
            if ($selectedIndex -ge 0 -and $selectedIndex -lt $Options.Count) {
                $result = Get-OptionValue -Option $Options[$selectedIndex]
            } else {
                Write-ToConsoleLog "Invalid selection, please try again." -IsWarning
                continue
            }
        }

        # Check required
        if ($IsRequired.IsPresent -and [string]::IsNullOrWhiteSpace($result)) {
            Write-ToConsoleLog $RequiredMessage -IsError
            $result = $null
        }
    } while ($null -eq $result -and $IsRequired.IsPresent)

    return $result
}

# SIG # Begin signature block
# MIIoUwYJKoZIhvcNAQcCoIIoRDCCKEACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCByMSErcHx/F7v4
# xUVajKa+N197cJ9iUw37zC0UaPY8LqCCDYUwggYDMIID66ADAgECAhMzAAAEhJji
# EuB4ozFdAAAAAASEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM1WhcNMjYwNjE3MTgyMTM1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDtekqMKDnzfsyc1T1QpHfFtr+rkir8ldzLPKmMXbRDouVXAsvBfd6E82tPj4Yz
# aSluGDQoX3NpMKooKeVFjjNRq37yyT/h1QTLMB8dpmsZ/70UM+U/sYxvt1PWWxLj
# MNIXqzB8PjG6i7H2YFgk4YOhfGSekvnzW13dLAtfjD0wiwREPvCNlilRz7XoFde5
# KO01eFiWeteh48qUOqUaAkIznC4XB3sFd1LWUmupXHK05QfJSmnei9qZJBYTt8Zh
# ArGDh7nQn+Y1jOA3oBiCUJ4n1CMaWdDhrgdMuu026oWAbfC3prqkUn8LWp28H+2S
# LetNG5KQZZwvy3Zcn7+PQGl5AgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUBN/0b6Fh6nMdE4FAxYG9kWCpbYUw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwNTM2MjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AGLQps1XU4RTcoDIDLP6QG3NnRE3p/WSMp61Cs8Z+JUv3xJWGtBzYmCINmHVFv6i
# 8pYF/e79FNK6P1oKjduxqHSicBdg8Mj0k8kDFA/0eU26bPBRQUIaiWrhsDOrXWdL
# m7Zmu516oQoUWcINs4jBfjDEVV4bmgQYfe+4/MUJwQJ9h6mfE+kcCP4HlP4ChIQB
# UHoSymakcTBvZw+Qst7sbdt5KnQKkSEN01CzPG1awClCI6zLKf/vKIwnqHw/+Wvc
# Ar7gwKlWNmLwTNi807r9rWsXQep1Q8YMkIuGmZ0a1qCd3GuOkSRznz2/0ojeZVYh
# ZyohCQi1Bs+xfRkv/fy0HfV3mNyO22dFUvHzBZgqE5FbGjmUnrSr1x8lCrK+s4A+
# bOGp2IejOphWoZEPGOco/HEznZ5Lk6w6W+E2Jy3PHoFE0Y8TtkSE4/80Y2lBJhLj
# 27d8ueJ8IdQhSpL/WzTjjnuYH7Dx5o9pWdIGSaFNYuSqOYxrVW7N4AEQVRDZeqDc
# fqPG3O6r5SNsxXbd71DCIQURtUKss53ON+vrlV0rjiKBIdwvMNLQ9zK0jy77owDy
# XXoYkQxakN2uFIBO1UNAvCYXjs4rw3SRmBX9qiZ5ENxcn/pLMkiyb68QdwHUXz+1
# fI6ea3/jjpNPz6Dlc/RMcXIWeMMkhup/XEbwu73U+uz/MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGiQwghogAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAASEmOIS4HijMV0AAAAA
# BIQwDQYJYIZIAWUDBAIBBQCggbAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGnY
# pf36Jd/eoAPithVR8hI2RVg04Mm3XdldZxLBnKCcMEQGCisGAQQBgjcCAQwxNjA0
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEcgBpodHRwczovL3d3dy5taWNyb3NvZnQu
# Y29tIDANBgkqhkiG9w0BAQEFAASCAQBcy9M66pYrmfCqBtfDlSYww1HmlHVhfp/F
# 5MEr7s32MKNZs09HcwZT75O+yD/Me+DNuEQYzS670qFUlEm97BXNb2W31G5Z5IBQ
# t6+QOdXpEjtHqjNLZnOWPNUTdJ3Po7aK1FurW6QhKp73WlZWjblg1stSGxA9rpWL
# jGY9Qu5UVows1VvpNU88Z3Aa4AMsKRoW+lIqumvUOBkwMZHKuMntUXRN0k87hcM+
# nDJFDXbY8jI5mcDSIC4rrd6wItwnGQ+0Ci/Z8JEh01RYUpcwbBaLo7tP7Rt+DoVh
# zG0AIg4EiLMKnJWr5LcET6xzOAzZtguFrUq25mlWHKe+NzNrph7moYIXrDCCF6gG
# CisGAQQBgjcDAwExgheYMIIXlAYJKoZIhvcNAQcCoIIXhTCCF4ECAQMxDzANBglg
# hkgBZQMEAgEFADCCAVkGCyqGSIb3DQEJEAEEoIIBSASCAUQwggFAAgEBBgorBgEE
# AYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIAbw4gvj/oXUizrMqx0IMcFrm6LGEIvc
# rgsG4tjEgb8bAgZpdCqXkawYEjIwMjYwMTMwMjAwNjEwLjk4WjAEgAIB9KCB2aSB
# 1jCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE
# CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQL
# Ex5uU2hpZWxkIFRTUyBFU046NTIxQS0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WgghH7MIIHKDCCBRCgAwIBAgITMwAAAhdx
# +y6lrwEd6gABAAACFzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt
# cCBQQ0EgMjAxMDAeFw0yNTA4MTQxODQ4MjNaFw0yNjExMTMxODQ4MjNaMIHTMQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNy
# b3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGll
# bGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
# AMDPNrBMPt/b2Ee4hgiBQ52DYUTPgcxdgGIquXVKWaSRT0YwnIqUOkVGyPkQGDUQ
# KRMcecoK122lrbsL6rKgFUlm1P1un3nLo4yC5D6p/aZ4LVkZH0IG2IL2lwC5ej2N
# sjh/X+9jA/ugMsUHp0qQDHVM2P0JP47Y7B3RV1QXvrbIZSJEidxmcPFVSqk4NxmR
# Jc7cGyVjba4QRL4X+mp8THoEDCT+7TvPwSeyE7OPwFdw9m7/Hh5PNWPnzpkPJkOt
# Vi6tsCLb5cyE+W83pSvS7xMnH+cdZV8QMBMOWu3aUvgik2p4bb/kNpsHmwMm43/Y
# qOJOPLLU2qta7XKzog8HXalZtUvXvIdU7M1xc4yy7xPRJo5zXyHsLGPkQoVGh52O
# BRcMCRJxL/yUu+qB09KBncu/ietHxjpewNVKFMgJIaW9gY2vksEiIh20OBQ8iYi5
# Wena2WfODKCfOdiUsTNIQYxjuhWZTzvIrjcpOvNA4vFZ20jaSHSTfg4ZXqXk1DAQ
# x6gpndJlVkVmT5tab4Lcvd2rDbfzYOqOJtnZ6KFjnTN8irCWlo8h6onPYH062QTP
# jwl6nk2jtaBImkPoeMNJx1XVfl0ryNptcjeIom6o5uRz2gDtzifjpjS37TVZq+Ge
# lNk+qSHCwcrKCnA1LwqsnF4+Av/p9ShQaQxnQzMPYJrvAgMBAAGjggFJMIIBRTAd
# BgNVHQ4EFgQUUKzfY5cDTqHhOWUKpxecfEtWyUMwHwYDVR0jBBgwFoAUn6cVXQBe
# Yl2D9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNy
# b3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBD
# QSUyMDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0
# cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNV
# HSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQEL
# BQADggIBAEZoBXYQe8SACBxBIOSP+UvoMeu2kekYlvGQMLRN5s/KNHcp/qSD/pYn
# TUdSCkENK/kY2ICPXGepm7wMr4d3tqvwVfK4xxN7nfT8mMqt9nrhYHWd71+G+UF3
# j1paQQGK3c4kdu6x8+lsKR+XWbEsqW1wwW0JFpDZeoPNsk8twGwWyg1wXc8WbBGr
# mjqZWrSuxK+HYYJSgXsfCUNnTEpEmcHgjQ16nfa//VtlXUWbAySj/13OFMkJVbG6
# AaLSrWBElxZI8EdR1bB/kNAuvfzeQj/06t2ICNbm+G/ftzCRSloSVwnCRhWZHC8F
# JmMKBYNVy6OwyyXRo6yB9Y7CNjRVyRUB3n+gHUXtREb2EHqrcqwb8SL0fj//NxTW
# Ms8dOIp1E/UxdgMEzrqggumeu6DcEeKRBCcgCBERL5HYY431nILJP5xk5X2obzWP
# 0jh6zUSqOPSg5XHDc0QiUMkPg+fa+/6nmzUskD038CPfvoxGNEP1FilTS4YqOeRm
# AbHiffbOzc5HcTq/VH7aefexxKL2wOvahpWdzqVBNx9UQRg1afxbzNrl07pvC6zJ
# D18eQKf5GzwErAvY7vaDAntiBFztkng9Wg9yKrwDnRxSh4Nb6Wz1EElm+oNsXwVd
# 9MBXKmj2IM3G8T1j8maivkvJe88OuGjuYZie3kMKDH07tFWsuq+PMIIHcTCCBVmg
# AwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9z
# b2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgy
# MjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ck
# eb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+
# uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4
# bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhi
# JdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD
# 4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKN
# iOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXf
# tnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8
# P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMY
# ctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9
# stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUe
# h17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQID
# AQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4E
# FgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9
# AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsG
# AQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTAD
# AQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0w
# S6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3Rz
# L01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYI
# KwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWlj
# Um9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38
# Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTlt
# uw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99q
# b74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQ
# JL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1
# ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP
# 9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkk
# vnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFH
# qfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g7
# 5LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr
# 4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghi
# f9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCA1YwggI+AgEBMIIBAaGB2aSB
# 1jCB0zELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UE
# CxMkTWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMScwJQYDVQQL
# Ex5uU2hpZWxkIFRTUyBFU046NTIxQS0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAGmygBWirdoW
# lHapB3xWMwM34EjLoIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTAwDQYJKoZIhvcNAQELBQACBQDtJzpiMCIYDzIwMjYwMTMwMTQxMDQyWhgPMjAy
# NjAxMzExNDEwNDJaMHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAO0nOmICAQAwBwIB
# AAICKEEwBwIBAAICErswCgIFAO0oi+ICAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYK
# KwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsF
# AAOCAQEANnr7YpRc+uEPPGll6rALF0kHd93hdy617BxMYHLZ9gL5UJc/1zJkj8g7
# RVUCMFzmlPo0zGoSHDZTfaVKVBjyBvqxJyvsx+HKAi7xQz2Qw0zGops8ylH88Oud
# iwIVmLTmTGcJ/Rh8ao6GA/Qeq6GOTF3OBcNiYEIqQq5whGjsrGfOeiBYvv3XSe5M
# bCsynrKVUY+klMyBTzyAzJRrqDOqXFEN4W7289TeYaWdCkongZsEZUYk5P9/t5q7
# MHAyEblb8HjryX/DpvOmNJ4cVXsgxaYofrc3a7Z5lQuP1dwJ/O8Zbys5ewuzHeXk
# oTisRCF2RXm9Q94fZccAzHtFBz9xgDGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACF3H7LqWvAR3qAAEAAAIXMA0GCWCGSAFl
# AwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcN
# AQkEMSIEIKbT0ld9ookKDwJs6LtLr9zAMNtWpGsbIO90ccrA023oMIH6BgsqhkiG
# 9w0BCRACLzGB6jCB5zCB5DCBvQQg0PJQYD5dt8mdEs1EreaYTiHoBz8sK5DcHr8X
# tpPkWlswgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA
# Ahdx+y6lrwEd6gABAAACFzAiBCBeDeAtGihAD964rKx1xWOSZFBIktw6SyGjxQte
# EzZRSTANBgkqhkiG9w0BAQsFAASCAgAxndAlesfyK7I0X3iYo0Vv+Tc8XK5qKnNY
# 5FLuiGcsuLtfofqCVIoEHGanzD7u4B1ZTZ6EdCgkZAuatcC7QhilmOXIrS+v+E9G
# EAT0fP6OVJErlJZMGcJc5ny5X4BxoveTiAPbtEPkM/OI8QuN6maom+nT6+xfAn8P
# rO0OYPbk/sViIpXqwrs937hQ5zVOBPP82IbqpU7r5MBPCNy99Q7UPTFUYStDG7LM
# qo1nPqo2NrqKFHPZGZ9/3tclTT2uM6Qtseo/y5uK7CMw13P5UByFhNDYhsOvA8pr
# yqb8Yxfdy/c1M++Q6xr4OdfB/KWknNhQuQDUfUtK4BdcqWYMRoORBWpzgsi9aytD
# a8mflw1IJ2ToFNJB3a24okAOCJVm2eOpEsrzj8TYr9xxw5cjpuRugONmncuAZGfG
# g8Ry8yW7VdgCUamJC3sblc/Hnbib/sk6zB87jYWmDxf7YWgH761E/JLtJtqpFQsi
# QFpADUJbZx1nAS3YPDZ6pJOxHDVHzfqfxtHmeu73zLwn01GXWA1oE4uNrUnvmiYC
# yXMhI8DfdtPvs1mxQHZZhXnKmdIAPogLfo83k9VzahdISdc2LPac6LCsMtUO8N1l
# 4UWz0irpIYxkLJoOdAUP6mqOTDM2bLgfJG3D0iBRwKIPI6jyCcerolLvku+wUJoD
# UN31ynRCgA==
# SIG # End signature block