Source/Classes/NodeParser.ps1

<#
.SYNOPSIS
    Class to support Object Graph Tools
.DESCRIPTION
    This class provides general properties and method to recursively
    iterate through to PowerShell Object Graph nodes.
 
    For details, see:
 
    * [PowerShell Object Parser][1] for details on the `[PSNode]` properties and methods.
    * [Extended-Dot-Notation][2] for details on path selectors.
 
.LINK
    [1]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/ObjectParser.md "PowerShell Object Parser"
    [2]: https://github.com/iRon7/ObjectGraphTools/blob/main/Docs/Xdn.md "Extended Dot Notation"
#>


using module .\..\..\..\ObjectGraphTools

using namespace System.Collections
using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace System.Linq.Expressions
using namespace System.Reflection

enum PSNodeStructure { Leaf; List; Map }
enum PSNodeOrigin { Root; List; Map }

Class PSNodePath {
    hidden [PSNode[]]$Nodes
    hidden [String]$_String

    hidden PSNodePath($Nodes) { $this.Nodes = [PSNode[]]$Nodes }

    static [String] op_Addition([PSNodePath]$Path, [String]$String) {
        return "$Path" + $String
    }

    [Bool] Equals([Object]$Path) {
        if ($Path -is [PSNodePath]) {
            if ($this.Nodes.Count -ne $Path.Nodes.Count) { return $false }
            $Index = 0
            foreach( $Node in $this.Nodes) {
                if ($Node.NodeOrigin -ne $Path.Nodes[$Index].NodeOrigin -or
                    $Node.Name       -ne $Path.Nodes[$Index].Name
                ) { return $false }
                $Index++
            }
            return $true
        }
        elseif ($Path -is [String]) {
            return $this.ToString() -eq $Path
        }
        return $false
    }

    [String] ToString() {
        if ($Null -eq $this._String) {
            $Count = $this.Nodes.Count
            $this._String = if ($Count -gt 1) { $this.Nodes[-2].Path.ToString() }
            $Node = $this.Nodes[-1]
            $this._String += # Copy the new path into the current node
                if ($Node.NodeOrigin -eq 'List') {
                    "[$($Node._Name)]"
                }
                elseif ($Node.NodeOrigin -eq 'Map') {
                    $KeyExpression = [PSKeyExpression]$Node._Name
                    if ($Count -le 2) { $KeyExpression } else { ".$KeyExpression" }
                }
        }
        return $this._String
    }

}

Class PSNode : IComparable {
    hidden static PSNode() { Use-ClassAccessors }

    static [int]$DefaultMaxDepth = 20

    hidden $_Name
    [Int]$Depth
    hidden $_Value
    hidden [Int]$_MaxDepth = [PSNode]::DefaultMaxDepth
    [PSNode]$ParentNode
    [PSNode]$RootNode = $this
    hidden [Dictionary[String,Object]]$Cache = [Dictionary[String,Object]]::new()
    hidden [DateTime]$MaxDepthWarningTime            # Warn ones per item branch

    static [PSNode] ParseInput($Object, $MaxDepth) {
        $Node =
            if ($Object -is [PSNode]) { $Object }
            else {
                if     ($Null -eq $Object)                                  { [PSLeafNode]::new($Object) }
                elseif ($Object -is [Management.Automation.PSCustomObject]) { [PSObjectNode]::new($Object) }
                elseif ($Object -is [Collections.IDictionary])              { [PSDictionaryNode]::new($Object) }
                elseif ($Object -is [Specialized.StringDictionary])         { [PSDictionaryNode]::new($Object) }
                elseif ($Object -is [Collections.ICollection])              { [PSListNode]::new($Object) }
                elseif ($Object -is [ValueType])                            { [PSLeafNode]::new($Object) }
                elseif ($Object -is [String])                               { [PSLeafNode]::new($Object) }
                elseif ($Object -is [ScriptBlock])                          { [PSLeafNode]::new($Object) }
                elseif ($Object.PSObject.Properties)                        { [PSObjectNode]::new($Object) }
                else                                                        { [PSLeafNode]::new($Object) }
            }
        $Node.RootNode = $Node
        if ($MaxDepth -gt 0) { $Node._MaxDepth = $MaxDepth }
        return $Node
    }

    static [PSNode] ParseInput($Object) { return [PSNode]::parseInput($Object, 0) }

    static [int] Compare($Left, $Right) {
        return [ObjectComparer]::new().Compare($Left, $Right)
    }
    static [int] Compare($Left, $Right, [String[]]$PrimaryKey) {
        return [ObjectComparer]::new($PrimaryKey, 0, [CultureInfo]::CurrentCulture).Compare($Left, $Right)
    }
    static [int] Compare($Left, $Right, [String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) {
        return [ObjectComparer]::new($PrimaryKey, $ObjectComparison, [CultureInfo]::CurrentCulture).Compare($Left, $Right)
    }
    static [int] Compare($Left, $Right, [String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison, [CultureInfo]$CultureInfo) {
       return [ObjectComparer]::new($PrimaryKey, $ObjectComparison, $CultureInfo).Compare($Left, $Right)
    }

    hidden [object] get_Value() { return ,$this._Value }

    hidden set_Value($Value) {
        $this.Cache.Remove('ChildNodes')
        $this._Value = $Value
        if ($Null -ne $this.ParentNode) { $this.ParentNode.SetValue($this._Name,  $Value) }
        if ($this.GetType() -ne [PSNode]::ParseInput($Value).GetType()) { # The root node is of type PSNode (always false)
            Write-Warning "The supplied value has a different PSNode type than the existing $($this.Path). Use .ParentNode.SetValue() method and reload its child item(s)."
        }
    }

    hidden [Object] get_Name() { return ,$this._Name }

    hidden [Object] get_MaxDepth() { return $this.RootNode._MaxDepth }

    hidden set_MaxDepth($MaxDepth) {
        if (-not $this.ChildType) {
            $this._MaxDepth = $MaxDepth
        }
        else {
            Throw 'The MaxDepth can only be set at the root node: [PSNode].RootNode.MaxDepth = <Maximum Depth>'
        }
    }

    hidden [PSNodeStructure] get_NodeStructure()  {
        if ($this -is [PSListNode]) { return 'List' } elseif ($this -is [PSMapNode]) { return 'Map' } else { return 'Leaf' }
    }

    hidden [PSNodeOrigin] get_NodeOrigin()  {
        if ($this.ParentNode -is [PSListNode]) { return 'List' } elseif ($this.ParentNode -is [PSMapNode]) { return 'Map' } else { return 'Root' }
    }

    hidden [Type] get_ValueType() {
        if ($Null -eq $this._Value) { return $Null }
        else { return $this._Value.getType() }
    }

    [Int]GetHashCode() { return $this.GetHashCode($false) } # Ignore the case of a string value

    hidden [Object] get_Path() {
        if (-not $this.Cache.ContainsKey('Path')) {
            if ($this.ParentNode) {
                $this.Cache['Path'] = [PSNodePath]($this.ParentNode.get_Path().Nodes + $this)
            }
            else {
                $this.Cache['Path'] = [PSNodePath]$this
            }
        }
        return $this.Cache['Path']
    }

    [String] GetPathName($VariableName) {
        $PathName = $this.get_Path().ToString()
        if (-not $PathName) { return $VariableName }
        elseif ($PathName.StartsWith('.')) { return "$VariableName$PathName" }
        else { return "$VariableName.$PathName" }
    }

    [String] GetPathName() { return $this.get_Path().ToString() }

    hidden [String] get_Expression() { return [PSSerialize]$this }

    Remove() {
        if ($null -eq $this.ParentNode) { Throw "The root node can't be removed." }
        $this.ParentNode.RemoveAt($this.Name)
        $this.Cache.Remove('ChildNodes')
    }

    [Bool] Equals($Object)  {  # https://learn.microsoft.com/dotnet/api/system.globalization.compareoptions
        if ($Object -is [PSNode]) { $Node = $Object }
        else { $Node = [PSNode]::ParseInput($Object) }
        $ObjectComparer = [ObjectComparer]::new()
        return $ObjectComparer.IsEqual($this, $Node)
    }

    [int] CompareTo($Object)  {
        if ($Object -is [PSNode]) { $Node = $Object }
        else { $Node = [PSNode]::ParseInput($Object) }
        $ObjectComparer = [ObjectComparer]::new()
        return $ObjectComparer.Compare($this, $Node)
    }

    hidden CollectNodes($NodeTable, [XdnPath]$Path, [Int]$PathIndex) {
        $Entry = $Path.Entries[$PathIndex]
        $NextIndex = if ($PathIndex -lt $Path.Entries.Count -1) { $PathIndex + 1 }
        $NextEntry = if ($NextIndex) { $Path.Entries[$NextIndex] }
        $Equals    = if ($NextEntry -and $NextEntry.Key -eq 'Equals') {
            $NextEntry.Value
            $NextIndex = if ($NextIndex -lt $Path.Entries.Count -1) { $NextIndex + 1 }
        }
        switch ($Entry.Key) {
            Root {
                $Node = $this.RootNode
                if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) }
                else { $NodeTable[$Node.getPathName()] = $Node }
            }
            Ancestor {
                $Node = $this
                for($i = $Entry.Value; $i -gt 0 -and $Node.ParentNode; $i--) { $Node = $Node.ParentNode }
                if ($i -eq 0) { # else: reached root boundary
                    if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) }
                    else { $NodeTable[$Node.getPathName()] = $Node }
                }
            }
            Index {
                if ($this -is [PSListNode] -and [Int]::TryParse($Entry.Value, [Ref]$Null)) {
                    $Node = $this.GetChildNode([Int]$Entry.Value)
                    if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) }
                    else { $NodeTable[$Node.getPathName()] = $Node }
                }
            }
            Default { # Child, Descendant
                if ($this -is [PSListNode]) { # Member access enumeration
                    foreach ($Node in $this.get_ChildNodes()) {
                        $Node.CollectNodes($NodeTable, $Path, $PathIndex)
                    }
                }
                elseif ($this -is [PSMapNode]) {
                    $Found = $False
                    foreach ($Value in $Entry.Value) {
                        $Name = $Value._Value
                        if ($Value.ContainsWildcard()) {
                            foreach ($Node in $this.ChildNodes) {
                                if ($Node.Name -like $Name -and (-not $Equals -or ($Node -is [PSLeafNode] -and $Equals -eq $Node._Value))) {
                                    $Found = $True
                                    if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) }
                                    else { $NodeTable[$Node.getPathName()] = $Node }
                                }
                            }
                        }
                        elseif ($this.Contains($Name)) {
                            $Node = $this.GetChildNode($Name)
                            if (-not $Equals -or ($Node -is [PSLeafNode] -and $Equals -eq $Node._Value)) {
                                $Found = $True
                                if ($NextIndex) { $Node.CollectNodes($NodeTable, $Path, $NextIndex) }
                                else { $NodeTable[$Node.getPathName()] = $Node }
                            }
                        }
                    }
                    if (-not $Found -and $Entry.Key -eq 'Descendant') {
                        foreach ($Node in $this.ChildNodes) {
                            $Node.CollectNodes($NodeTable, $Path, $PathIndex)
                        }
                    }
                }
            }
        }
    }

    [Object] GetNode([XdnPath]$Path) {
        $NodeTable = [system.collections.generic.dictionary[String, PSNode]]::new() # Case sensitive (case insensitive map nodes use the same name)
        $this.CollectNodes($NodeTable, $Path, 0)
        if ($NodeTable.Count -eq 0) { return @() }
        if ($NodeTable.Count -eq 1) { return $NodeTable[$NodeTable.Keys] }
        else                        { return [PSNode[]]$NodeTable.Values }
    }
}

Class PSLeafNode : PSNode {

    hidden MaxDepthReached() { } # return nothing knowing that leaf nodes terminate the path anyways

    hidden PSLeafNode($Object) {
        if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object }
    }

    [Int]GetHashCode($CaseSensitive) {
        if ($Null -eq $this._Value) { return '$Null'.GetHashCode() }
        if ($CaseSensitive) { return $this._Value.GetHashCode() }
        else {
            if ($this._Value -is [String]) { return $this._Value.ToUpper().GetHashCode() } # Windows PowerShell doesn't have a System.HashCode structure
            else { return $this._Value.GetHashCode() }
        }
    }

    [string]ToString() {
        return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))"
    }
}

Class PSCollectionNode : PSNode {
    hidden static PSCollectionNode() { Use-ClassAccessors }
    hidden [Dictionary[bool,int]]$_HashCode # Unlike the value HashCode, the default (bool = $false) node HashCode is case insensitive
    hidden [Dictionary[bool,int]]$_ReferenceHashCode # if changed, recalculate the (bool = case sensitive) node's HashCode

    hidden [bool]MaxDepthReached() {
        # Check whether the max depth has been reached.
        # Warn if it has, but suppress the warning if
        # it took less then 5 seconds since the last
        # time it reached the max depth.
        $MaxDepthReached = $this.Depth -ge $this.RootNode._MaxDepth
        if ($MaxDepthReached) {
            if (([DateTime]::Now - $this.RootNode.MaxDepthWarningTime).TotalSeconds -gt 5) {
                Write-Warning "$($this.Path) reached the maximum depth of $($this.RootNode._MaxDepth)."
            }
            $this.RootNode.MaxDepthWarningTime = [DateTime]::Now
        }
        return $MaxDepthReached
    }

    hidden WarnSelector ([PSCollectionNode]$Node, [String]$Name) {
        if ($Node -is [PSListNode]) {
            $SelectionName  = "'$Name'"
            $CollectionType = 'list'
        }
        else {
            $SelectionName  = "[$Name]"
            $CollectionType = 'list'
        }
        Write-Warning "Expected $SelectionName to be a $CollectionType selector for: <Object>$($Node.Path)"
    }

    hidden [List[Ast]] GetAstSelectors ($Ast) {
        $List = [List[Ast]]::new()
        if ($Ast -isnot [Ast]) {
            $Ast = [Parser]::ParseInput("`$_$Ast", [ref]$Null, [ref]$Null)
            $Ast = $Ast.EndBlock.Statements.PipeLineElements.Expression
        }
        if ($Ast -is [IndexExpressionAst]) {
            $List.AddRange($this.GetAstSelectors($Ast.Target))
            $List.Add($Ast)
        }
        elseif ($Ast -is [MemberExpressionAst]) {
            $List.AddRange($this.GetAstSelectors($Ast.Expression))
            $List.Add($Ast)
        }
        elseif ($Ast.Extent.Text -ne '$_') {
            Throw "Parse error: $($Ast.Extent.Text)"
        }
        return $List
    }

    [List[PSNode]]GetNodeList($Levels, [Bool]$LeafNodesOnly) {
        $NodeList = [List[PSNode]]::new()
        $Stack = [Stack]::new()
        $Stack.Push($this.get_ChildNodes().GetEnumerator())
        $Level = 1
        While ($Stack.Count -gt 0) {
            $Enumerator = $Stack.Pop()
            $Level--
            while ($Enumerator.MoveNext()) {
                $Node = $Enumerator.Current
                if ($Node.MaxDepthReached() -or ($Levels -ge 0 -and $Level -ge $Levels)) { break }
                if (-not $LeafNodesOnly -or $Node -is [PSLeafNode]) { $NodeList.Add($Node) }
                if ($Node -is [PSCollectionNode]) {
                    $Stack.Push($Enumerator)
                    $Level++
                    $Enumerator = $Node.get_ChildNodes().GetEnumerator()
                }
            }
        }
        return $NodeList
    }
    [List[PSNode]]GetNodeList()             { return $this.GetNodeList(1, $False) }
    [List[PSNode]]GetNodeList([Int]$Levels) { return $this.GetNodeList($Levels, $False) }
    hidden [PSNode[]]get_DescendantNodes()  { return $this.GetNodeList(-1, $False) }
    hidden [PSNode[]]get_LeafNodes()        { return $this.GetNodeList(-1, $True) }

    Sort() { $this.Sort($Null, 0) }
    Sort([ObjectComparison]$ObjectComparison) { $this.Sort($Null, $ObjectComparison) }
    Sort([String[]]$PrimaryKey) { $this.Sort($PrimaryKey, 0) }
    Sort([ObjectComparison]$ObjectComparison, [String[]]$PrimaryKey) { $this.Sort($PrimaryKey, $ObjectComparison) }
    Sort([String[]]$PrimaryKey, [ObjectComparison]$ObjectComparison) {
        # As the child nodes are sorted first, we just do a side-by-side node compare:
        $ObjectComparison = $ObjectComparison -bor [ObjectComparison]'MatchMapOrder'
        $ObjectComparison = $ObjectComparison -band (-1 - [ObjectComparison]'IgnoreListOrder')
        $PSListNodeComparer = [PSListNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison }
        $PSMapNodeComparer = [PSMapNodeComparer]@{ PrimaryKey = $PrimaryKey; ObjectComparison = $ObjectComparison }
        $this.SortRecurse($PSListNodeComparer, $PSMapNodeComparer)
    }

    hidden SortRecurse([PSListNodeComparer]$PSListNodeComparer, [PSMapNodeComparer]$PSMapNodeComparer) {
        $NodeList = $this.GetNodeList()
        foreach ($Node in $NodeList) {
            if ($Node -is [PSCollectionNode]) { $Node.SortRecurse($PSListNodeComparer, $PSMapNodeComparer) }
        }
        if ($this -is [PSListNode]) {
            $NodeList.Sort($PSListNodeComparer)
            if ($NodeList.Count) { $this._Value = @($NodeList.Value) } else { $this._Value = @() }
        }
        else { # if ($Node -is [PSMapNode])
            $NodeList.Sort($PSMapNodeComparer)
            $Properties = [System.Collections.Specialized.OrderedDictionary]::new([StringComparer]::Ordinal)
            foreach($ChildNode in $NodeList) { $Properties[[Object]$ChildNode.Name] = $ChildNode.Value } # [Object] forces a key rather than an index (ArgumentOutOfRangeException)
            if ($this -is [PSObjectNode]) { $this._Value = [PSCustomObject]$Properties } else { $this._Value = $Properties }
        }
    }
}

Class PSListNode : PSCollectionNode {
    hidden static PSListNode() { Use-ClassAccessors }

    hidden PSListNode($Object) {
        if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object }
    }

    hidden [Object]get_Count() {
        return $this._Value.get_Count()
    }

    hidden [Object]get_Names() {
        if ($this._Value.Length) { return ,@(0..($this._Value.Length - 1)) }
        return ,@()
    }

    hidden [Object]get_Values() {
        return ,@($this._Value)
    }

    hidden [Object]get_CaseMatters() { return $false }

    [Bool]Contains($Index) {
       return $Index -ge 0 -and $Index -lt $this.get_Count()
    }

    [Bool]Exists($Index) {
        return $Index -ge 0 -and $Index -lt $this.get_Count()
    }

    [Object]GetValue($Index) { return $this._Value[$Index] }
    [Object]GetValue($Index, $Default) {
        if (-not $This.Contains($Index)) { return $Default }
        return $this._Value[$Index]
    }

    SetValue($Index, $Value) {
        if ($Value -is [PSNode]) { $Value = $Value.Value }
        $this._Value[$Index] = $Value
    }

    Add($Value) {
        if ($Value -is [PSNode]) { $Value = $Value._Value }
        if ($this._Value.GetType().GetMethod('Add')) { $null = $This._Value.Add($Value) }
        else { $this._Value = ($this._Value + $Value) -as $this._Value.GetType() }
        $this.Cache.Remove('ChildNodes')
    }

    Remove($Value) {
        if ($Value -is [PSNode]) { $Value = $Value.Value }
        if (-not $this.Value.Contains($Value)) { return }
        if ($this.Value.GetType().GetMethod('Remove')) { $null = $this._value.remove($Value) }
        else {
            $cList = [List[Object]]::new()
            $iList = [List[Object]]::new()
            $ceq = $false
            foreach ($ChildNode in $this.get_ChildNodes()) {
                if (-not $ceq -and $ChildNode.Value -ceq $Value) { $ceq = $true } else { $cList.Add($ChildNode.Value) }
                if (-not $ceq -and $ChildNode.Value -ine $Value)                       { $iList.Add($ChildNode.Value) }
            }
            if ($ceq) { $this._Value = $cList -as $this._Value.GetType() }
            else      { $this._Value = $iList -as $this._Value.GetType() }
        }
        $this.Cache.Remove('ChildNodes')
    }

    RemoveAt([Int]$Index) {
        if ($Index -lt 0 -or $Index -ge $this.Value.Count) { Throw 'Index was out of range. Must be non-negative and less than the size of the collection.' }
        if ($this.Value.GetType().GetMethod('RemoveAt')) { $null = $this._Value.removeAt($Index) }
        else {
            $this._Value = $(for ($i = 0; $i -lt $this._Value.Count; $i++) {
                if ($i -ne $index) { $this._Value[$i] }
            }) -as $this.ValueType
        }
        $this.Cache.Remove('ChildNodes')
    }

    [Object]GetChildNode([Int]$Index) {
        if ($this.MaxDepthReached()) { return @() }
        $Count = $this._Value.get_Count()
        if ($Index -lt -$Count -or $Index -ge $Count) { throw "The <Object>$($this.Path) doesn't contain a child index: $Index" }
        if ($Index -lt 0) { $Index = $Count + $Index } # Negative index
        if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = [Dictionary[Int,Object]]::new() }
        if (
            -not $this.Cache.ChildNode.ContainsKey($Index) -or
            -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Index]._Value, $this._Value[$Index])
        ) {
            $Node             = [PSNode]::ParseInput($this._Value[$Index])
            $Node._Name       = $Index
            $Node.Depth       = $this.Depth + 1
            $Node.RootNode    = [PSNode]$this.RootNode
            $Node.ParentNode  = $this
            $this.Cache.ChildNode[$Index] = $Node
        }
        return $this.Cache.ChildNode[$Index]
    }

    hidden [Object[]]get_ChildNodes() {
        if (-not $this.Cache.ContainsKey('ChildNodes')) {
            $ChildNodes = for ($Index = 0; $Index -lt $this._Value.get_Count(); $Index++) { $this.GetChildNode($Index) }
            if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] =  @() }
        }
        return $this.Cache['ChildNodes']
    }

    [Int]GetHashCode($CaseSensitive) {
        # The hash of a list node is equal if all items match the order and the case.
        # The primary keys and the list type are not relevant
        if ($null -eq $this._HashCode) {
            $this._HashCode = [Dictionary[bool,int]]::new()
            $this._ReferenceHashCode = [Dictionary[bool,int]]::new()
        }
        $ReferenceHashCode = $This._value.GetHashCode()
        if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) {
            $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode
            $HashCode = '@()'.GetHashCode() # Empty lists have a common hash that is not 0
            $Index = 0
            foreach ($Node in $this.GetNodeList()) {
                $HashCode = $HashCode -bxor "$Index.$($Node.GetHashCode($CaseSensitive))".GetHashCode()
                $index++
            }
            $this._HashCode[$CaseSensitive] = $HashCode
        }
        return $this._HashCode[$CaseSensitive]
    }

    [string]ToString() {
        return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))"
    }
}

Class PSMapNode : PSCollectionNode {
    hidden static PSMapNode() { Use-ClassAccessors }

    [Int]GetHashCode($CaseSensitive) {
        # The hash of a map node is equal if all names and items match the order and the case.
        # The map type is not relevant
        if ($null -eq $this._HashCode) {
            $this._HashCode = [Dictionary[bool,int]]::new()
            $this._ReferenceHashCode = [Dictionary[bool,int]]::new()
        }
        $ReferenceHashCode = $This._value.GetHashCode()
        if (-not $this._ReferenceHashCode.ContainsKey($CaseSensitive) -or $this._ReferenceHashCode[$CaseSensitive] -ne $ReferenceHashCode) {
            $this._ReferenceHashCode[$CaseSensitive] = $ReferenceHashCode
            $HashCode = '@{}'.GetHashCode() # Empty maps have a common hash that is not 0
            $Index = 0
            foreach ($Node in $this.GetNodeList()) {
                $Name = if ($CaseSensitive) { $Node._Name } else { $Node._Name.ToUpper() }
                $HashCode = $HashCode -bxor "$Index.$Name=$($Node.GetHashCode())".GetHashCode()
                $Index++
            }
            $this._HashCode[$CaseSensitive] = $HashCode
        }
        return $this._HashCode[$CaseSensitive]
    }
}

Class PSDictionaryNode : PSMapNode {
    hidden static PSDictionaryNode() { Use-ClassAccessors }

    hidden PSDictionaryNode($Object) {
        if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object }
    }

    hidden [Object]get_Count() {
        return $this._Value.get_Count()
    }

    hidden [Object]get_Names() {
        return ,$this._Value.get_Keys()
    }

    hidden [Object]get_Values() {
        return ,$this._Value.get_Values()
    }

    hidden [Object]get_CaseMatters() { #Returns Nullable[Boolean]
        if (-not $this.Cache.ContainsKey('CaseMatters')) {
            $this.Cache['CaseMatters'] = $null # else $Null means that there is no key with alphabetic characters in the dictionary
            foreach ($Key in $this._Value.Get_Keys()) {
                if ($Key -is [String] -and $Key -match '[a-z]') {
                    $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() }
                    $this.Cache['CaseMatters'] = -not $this.Contains($Case) -or $Case -cin $this._Value.Get_Keys()
                    break
                }
            }
        }
        return $this.Cache['CaseMatters']
    }

    [Bool]Contains($Key) {
        if ($this._Value.GetType().GetMethod('ContainsKey')) {
            return $this._Value.ContainsKey($Key)
        }
        else {
            return $this._Value.Contains($Key)
        }
    }
    [Bool]Exists($Key) { return $this.Contains($Key) }

    [Object]GetValue($Key) { return $this._Value[$Key] }
    [Object]GetValue($Key, $Default) {
        if (-not $This.Contains($Key)) { return $Default }
        return $this._Value[$Key]
    }

    SetValue($Key, $Value) {
        if ($Value -is [PSNode]) { $Value = $Value.Value }
        $this._Value[$Key] = $Value
        $this.Cache.Remove('ChildNodes')
    }

    Add($Key, $Value) {
        if ($this.Contains($Key)) { Throw "Item '$Key' has already been added." }
        if ($Value -is [PSNode]) { $Value = $Value.Value }
        $this._Value.Add($Key, $Value)
        $this.Cache.Remove('ChildNodes')
    }

    Remove($Key) {
        $null = $this._Value.Remove($Key)
        $this.Cache.Remove('ChildNodes')
    }

    hidden RemoveAt($Key) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) }
        if (-not $this.Contains($Key)) { Throw "Item '$Key' doesn't exist." }
        $null = $this._Value.Remove($Key)
        $this.Cache.Remove('ChildNodes')
    }

    [Object]GetChildNode([Object]$Key) {
        if ($this.MaxDepthReached()) { return @() }
        if (-not $this.Contains($Key)) { Throw "The <Object>$($this.Path) doesn't contain a child named: $Key" }
        if (-not $this.Cache.ContainsKey('ChildNode')) {
            # The ChildNode cache case sensitivity is based on the current dictionary population.
            # The ChildNode cache is always ordinal, if the contained dictionary is invariant, extra entries might
            # appear in the cache but shouldn't effect the results other than slightly slow down the performance.
            # In other words, do not use the cache to count the entries. Custom comparers are not supported.
            $this.Cache['ChildNode'] = if ($this.get_CaseMatters()) { [HashTable]::new() } else { @{} } # default is case insensitive
        }
        elseif (
            -not $this.Cache.ChildNode.ContainsKey($Key) -or
            -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key]._Value, $this._Value[$Key])
        ) {
            if($null -eq $this.get_CaseMatters()) { # If the case was undetermined, check the new key for case sensitivity
                $this.Cache.CaseMatters = if ($Key -is [String] -and $Key -match '[a-z]') {
                    $Case = if ([Int][Char]($Matches[0]) -ge 97) { $Key.ToUpper() } else { $Key.ToLower() }
                    -not $this._Value.Contains($Case) -or $Case -cin $this._Value.Get_Keys()
                }
                if ($this.get_CaseMatters()) {
                    $ChildNode = $this.Cache['ChildNode']
                    $this.Cache['ChildNode'] = [HashTable]::new() # Create a new cache as it appears to be case sensitive
                    foreach ($Key in $ChildNode.get_Keys()) { # Migrate the content
                        $this.Cache.ChildNode[$Key] = $ChildNode[$Key]
                    }
                }
            }
        }
        if (
            -not $this.Cache.ChildNode.ContainsKey($Key) -or
            -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Key].Value, $this._Value[$Key])
        ) {
            $Node             = [PSNode]::ParseInput($this._Value[$Key])
            $Node._Name       = $Key
            $Node.Depth       = $this.Depth + 1
            $Node.RootNode    = [PSNode]$this.RootNode
            $Node.ParentNode  = $this
            $this.Cache.ChildNode[$Key] = $Node
        }
        return $this.Cache.ChildNode[$Key]
    }

    hidden [Object[]]get_ChildNodes() {
        if (-not $this.Cache.ContainsKey('ChildNodes')) {
            $ChildNodes = foreach ($Key in $this._Value.get_Keys()) { $this.GetChildNode($Key) }
            if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] =  @() }
        }
        return $this.Cache['ChildNodes']
    }

    [string]ToString() {
        return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))"
    }
}

Class PSObjectNode : PSMapNode {
    hidden static PSObjectNode() { Use-ClassAccessors }

    hidden PSObjectNode($Object) {
        if ($Object -is [PSNode]) { $this._Value = $Object._Value } else { $this._Value = $Object }
    }

    hidden [Object]get_Count() {
        return @($this._Value.PSObject.Properties).get_Count()
    }

    hidden [Object]get_Names() {
        return ,$this._Value.PSObject.Properties.Name
    }

    hidden [Object]get_Values() {
        return ,$this._Value.PSObject.Properties.Value
    }

    hidden [Object]get_CaseMatters() { return $false }

    [Bool]Contains($Name) {
        return $this._Value.PSObject.Properties[$Name]
    }

    [Bool]Exists($Name) {
        return $this._Value.PSObject.Properties[$Name]
    }

    [Object]GetValue($Name) { return $this._Value.PSObject.Properties[$Name].Value }
    [Object]GetValue($Name, $Default) {
        if (-not $This.Contains($Name)) { return $Default }
        return $this._Value[$Name]
    }

    SetValue($Name, $Value) {
        if ($Value -is [PSNode]) { $Value = $Value.Value }
        if ($this._Value -isnot [PSCustomObject]) {
            $Properties = [Ordered]@{}
            foreach ($Property in $this._Value.PSObject.Properties) { $Properties[$Property.Name] = $Property.Value }
            $Properties[$Name] = $Value
            $this._Value = [PSCustomObject]$Properties
            $this.Cache.Remove('ChildNodes')
        }
        elseif ($this._Value.PSObject.Properties[$Name]) {
            $this._Value.PSObject.Properties[$Name].Value = $Value
        }
        else {
            Add-Member -InputObject $this._Value -Type NoteProperty -Name $Name -Value $Value
            $this.Cache.Remove('ChildNodes')
        }
    }

    Add($Name, $Value) {
        if ($this.Contains($Name)) { Throw "Item '$Name' has already been added." }
        $this.SetValue($Name, $Value)
    }

    Remove($Name) {
        $this._Value.PSObject.Properties.Remove($Name)
        $this.Cache.Remove('ChildNodes')
    }

    hidden RemoveAt($Name) { # General method for: ChildNode.Remove() { $_.ParentNode.Remove($_.Name) }
        if (-not $this.Contains($Name)) { Throw "Item '$Name' doesn't exist." }
        $this._Value.PSObject.Properties.Remove($Name)
        $this.Cache.Remove('ChildNodes')
    }

    [Object]GetChildNode([String]$Name) {
        if ($this.MaxDepthReached()) { return @() }
        if (-not $this.Contains($Name)) { Throw Throw "$($this.GetPathName('<Root>')) doesn't contain a child named: $Name"  }
        if (-not $this.Cache.ContainsKey('ChildNode')) { $this.Cache['ChildNode'] = @{} } # Object properties are case insensitive
        if (
            -not $this.Cache.ChildNode.ContainsKey($Name) -or
            -not [Object]::ReferenceEquals($this.Cache.ChildNode[$Name]._Value, $this._Value.PSObject.Properties[$Name].Value)
        ) {
            $Node             = [PSNode]::ParseInput($this._Value.PSObject.Properties[$Name].Value)
            $Node._Name       = $Name
            $Node.Depth       = $this.Depth + 1
            $Node.RootNode    = [PSNode]$this.RootNode
            $Node.ParentNode  = $this
            $this.Cache.ChildNode[$Name] = $Node
        }
        return $this.Cache.ChildNode[$Name]
    }

    hidden [Object[]]get_ChildNodes() {
        if (-not $this.Cache.ContainsKey('ChildNodes')) {
            $ChildNodes = foreach ($Property in $this._Value.PSObject.Properties) {
                if ($Property.Value -isnot [Reflection.MemberInfo]) { $this.GetChildNode($Property.Name) }
            }
            if ($null -ne $ChildNodes) { $this.Cache['ChildNodes'] = $ChildNodes } else { $this.Cache['ChildNodes'] =  @() }
        }
        return $this.Cache['ChildNodes']
    }

    [string]ToString() {
        return "$([TypeColor][PSSerialize]::new($this, [PSLanguageMode]'NoLanguage'))"
    }
}