VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "API_GraphOrdering"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
' ======= Graph sorting and ordering facilities =====
' Shared module version: 20210422
' Tested in: TestCommons
' Depends on: ex_Collection, CDS_Graph, API_LinkedComponents
' Required reference: Scripting
Option Explicit
 
Private graph_ As CDS_Graph
Private nodes_ As Collection
Private sourceOrder_ As Scripting.Dictionary ' Map of (nodeID, globalIndex)
Private nodeComponents_ As Scripting.Dictionary ' Map of (nodeID, componentIndex)
Private componentCount_ As Long

Private status_ As Scripting.Dictionary
Private ranks_ As Scripting.Dictionary

Private Const MAX_NODE_INDEX& = 100000
Private Const RANK_UNDEF = -1

Private Enum TVisitStatus
  T_STATUS_UNVISITED = 0
  T_STATUS_VISITING = 1
  T_STATUS_VISITED = 2
End Enum

Public Function Init(iGraph As CDS_Graph)
  Call SetGraph(iGraph)
End Function

' Classic topological order (no edges pointing backward in order)
Public Function TopologicalOrder() As Collection
  Set sourceOrder_ = Nothing
  Set TopologicalOrder = RevertCollection(DefaultOrder)
End Function

' Reversed topological order
Public Function ReverseTopologicalOrder() As Collection
  Set sourceOrder_ = Nothing
  Set ReverseTopologicalOrder = DefaultOrder
End Function

' Topological order where linked components are grouped together
Public Function TopologicalComponentOrder() As Collection
  Set sourceOrder_ = Nothing
  Dim iOrder As Collection: Set iOrder = RevertCollection(DefaultOrder)
  Set TopologicalComponentOrder = GroupNodesDefault(iOrder)
End Function

' Reversed topological order where linked components are grouped together
Public Function ReverseTopologicalComponentOrder() As Collection
  Set sourceOrder_ = Nothing
  Dim iOrder As Collection: Set iOrder = DefaultOrder
  Set ReverseTopologicalComponentOrder = GroupNodesDefault(iOrder)
End Function

' Sort topologically using BFS and ordering according to input
' Note: elements not present in the graph are excluded from output
Public Function SortLayers(iSourceOrder As Collection) As Collection
  If iSourceOrder.Count = 0 Then
    Set SortLayers = New Collection
    Exit Function
  End If
  
  Call InitSourceOrder(iSourceOrder)
  Dim iNodesOrder As Collection: Set iNodesOrder = InduceWidthOrder
  Dim iComponents As Collection: Set iComponents = SortComponents(iNodesOrder)
  Set SortLayers = GroupNodesInduced(iNodesOrder, iComponents)
End Function

' Sort topologically using DFS and ordering according to input
' Note: elements not present in the graph are excluded from output
Public Function SortDeep(iSourceOrder As Collection) As Collection
  If iSourceOrder.Count = 0 Then
    Set SortDeep = New Collection
    Exit Function
  End If
  
  Call InitSourceOrder(iSourceOrder)
  Dim iNodesOrder As Collection: Set iNodesOrder = InduceDepthOrder
  Dim iComponents As Collection: Set iComponents = SortComponents(iNodesOrder)
  Set SortDeep = GroupNodesInduced(iNodesOrder, iComponents)
End Function

' ==============
Private Function SetGraph(iGraph As CDS_Graph)
  Set graph_ = iGraph
  Set nodes_ = graph_.Nodes
  Dim iProcessor As New API_LinkedComponents
  Set nodeComponents_ = iProcessor.GetComponents(graph_)
  componentCount_ = iProcessor.CountComponents
End Function

Private Function DefaultOrder()
  Dim iOrder As New Collection
  
  Call InitStatus
  
  Dim iStack As Collection
  Dim vNode As Variant
  For Each vNode In nodes_
    If status_(vNode) <> T_STATUS_UNVISITED Then _
      GoTo NEXT_NODE
    Set iStack = New Collection: Call iStack.Add(vNode)
    Do While iStack.Count > 0
      Dim vItem As Variant: vItem = iStack.Item(iStack.Count)
      If status_(vItem) = T_STATUS_UNVISITED Then
        status_(vItem) = T_STATUS_VISITING
        Dim vChild As Variant
        For Each vChild In graph_.nodes_(vItem).outputs_
          If status_(vChild) = T_STATUS_UNVISITED Then _
            Call iStack.Add(vChild)
        Next vChild
      Else
        Call iStack.Remove(iStack.Count)
        If status_(vItem) <> T_STATUS_VISITED Then
          status_(vItem) = T_STATUS_VISITED
          Call iOrder.Add(vItem)
        End If
      End If
    Loop
NEXT_NODE:
  Next vNode
  
  Set DefaultOrder = iOrder
End Function

Private Function InduceWidthOrder() As Collection
  Dim iOrder As New Collection
  
  Call InitRanks
  
  Dim iQueue As Collection: Set iQueue = New Collection
  Dim iKnown As New Scripting.Dictionary
  Dim vNode As Variant
  For Each vNode In CompleteInducedOrder
    If ranks_(vNode) = 1 Then
      Call iQueue.Add(vNode)
      Call iKnown.Add(vNode, 0)
    End If
  Next vNode
    
  Do While iQueue.Count > 0
    vNode = iQueue.Item(1)
    Call iQueue.Remove(1)
    Call iOrder.Add(vNode)
    
    If graph_.nodes_(vNode).outputs_.Count > 0 Then
      Dim vChildren() As Variant: vChildren = ToArray(graph_.nodes_(vNode).outputs_)
      Call SortChildren(vChildren)
      Dim vChild As Variant
      For Each vChild In vChildren
        If Not iKnown.Exists(vChild) Then
          If ranks_(vChild) = ranks_(vNode) + 1 Then
            Call iQueue.Add(vChild)
            Call iKnown.Add(vChild, 0)
          End If
        End If
      Next vChild
    End If
  Loop

  Set InduceWidthOrder = iOrder
End Function

Private Function InduceDepthOrder() As Collection
  Dim iOrder As New Collection
  
  Call InitRanks

  Dim iStack As Collection: Set iStack = New Collection
  Dim iKnown As New Scripting.Dictionary
  Dim vNode As Variant
  For Each vNode In RevertCollection(CompleteInducedOrder)
    If ranks_(vNode) = 1 Then
      Call iStack.Add(vNode)
    End If
  Next vNode
  
  Dim vChildren() As Variant
  Dim vChild As Variant
  Dim nChild&
  Do While iStack.Count > 0
    vNode = iStack.Item(iStack.Count)
    Call iStack.Remove(iStack.Count)
    If iKnown.Exists(vNode) Then _
      GoTo NEXT_ITEM
      
    Call iOrder.Add(vNode)
    Call iKnown.Add(vNode, 0)

    If graph_.nodes_(vNode).outputs_.Count > 0 Then
      vChildren = ToArray(graph_.nodes_(vNode).outputs_)
      Call SortChildren(vChildren)
      For nChild = UBound(vChildren) To LBound(vChildren) Step -1
        vChild = vChildren(nChild)
        If Not iKnown.Exists(vChild) Then _
          Call iStack.Add(vChild)
      Next nChild
    End If
NEXT_ITEM:
  Loop

  Set InduceDepthOrder = iOrder
End Function

Private Function InitStatus()
  Set status_ = New Scripting.Dictionary
  Dim vNode As Variant
  For Each vNode In nodes_
    status_(vNode) = T_STATUS_UNVISITED
  Next vNode
End Function

Private Function InitRanks()
  Call InitStatus

  Set ranks_ = New Scripting.Dictionary
  Dim vNode As Variant
  For Each vNode In CompleteInducedOrder
    ranks_(vNode) = RANK_UNDEF
  Next vNode
  
  For Each vNode In nodes_
    Call PropagateRank(vNode, 1)
  Next vNode
End Function

Private Function PropagateRank(vNode As Variant, nRank&)
  If ranks_(vNode) >= nRank Then _
    Exit Function
  ranks_(vNode) = nRank
  
  If graph_.nodes_(vNode).outputs_.Count = 0 Then _
    Exit Function
  
  status_(vNode) = T_STATUS_VISITING
  
  Dim vChildren() As Variant: vChildren = ToArray(graph_.nodes_(vNode).outputs_)
  Call SortChildren(vChildren)
  Dim vChild As Variant
  For Each vChild In vChildren
    If status_(vChild) <> T_STATUS_VISITING Then _
      Call PropagateRank(vChild, nRank + 1)
  Next vChild
  
  status_(vNode) = T_STATUS_VISITED
End Function

Private Function InitSourceOrder(iSourceOrder As Collection)
  Set sourceOrder_ = New Scripting.Dictionary
  Dim aVal As Variant
  Dim nIndex&: nIndex = 1
  For Each aVal In iSourceOrder
    If graph_.HasNode(aVal) Then
      sourceOrder_(aVal) = nIndex
      nIndex = nIndex + 1
    End If
  Next aVal
End Function

Private Function CompleteInducedOrder() As Collection
  Dim iOrder As New Collection
  Dim vItem As Variant
  For Each vItem In sourceOrder_
    Call iOrder.Add(vItem)
  Next vItem
  
  If iOrder.Count = graph_.Size Then
    Set CompleteInducedOrder = iOrder
    Exit Function
  End If
  
  For Each vItem In nodes_
    If Not sourceOrder_.Exists(vItem) Then _
      Call iOrder.Add(vItem)
  Next vItem
  
  Set CompleteInducedOrder = iOrder
End Function

Private Function SortChildren(ByRef target() As Variant)
  If UBound(target) < 1 Then _
    Exit Function
  Dim i&, j&
  Dim vItem As Variant
  For i = LBound(target) + 1 To UBound(target) Step 1
    vItem = target(i)
    Dim nIndex&: nIndex = GetNodeIndex(vItem)
    j = i - 1
    Do While j >= 0
      If GetNodeIndex(target(j)) <= nIndex Then _
        Exit Do
      target(j + 1) = target(j)
      j = j - 1
    Loop
    target(j + 1) = vItem
  Next i
End Function

Private Function GroupNodesDefault(iNodes As Collection) As Collection
  Dim iResult As New Collection
  Dim nComponent&
  For nComponent = 0 To componentCount_ - 1 Step 1
    Dim vNode As Variant
    For Each vNode In iNodes
      If Not sourceOrder_ Is Nothing Then _
        If Not sourceOrder_.Exists(vNode) Then _
          GoTo NEXT_NODE
      If nodeComponents_(vNode) = nComponent Then _
        Call iResult.Add(vNode)
NEXT_NODE:
    Next vNode
  Next nComponent
  Set GroupNodesDefault = iResult
End Function

Private Function GetNodeIndex(vNode As Variant) As Long
  If sourceOrder_.Exists(vNode) Then
    GetNodeIndex = sourceOrder_(vNode)
  Else
    GetNodeIndex = MAX_NODE_INDEX
  End If
End Function

Private Function SortComponents(iNodesOrder As Collection) As Collection
  Dim iOrder As New Collection
  Dim nComponent&
  Dim vNode As Variant
  For Each vNode In iNodesOrder
    nComponent = nodeComponents_(vNode)
    Call SafeAddToCollection(nComponent, VBA.CStr(nComponent), iOrder)
  Next vNode
  Set SortComponents = iOrder
End Function

Private Function GroupNodesInduced(iNodes As Collection, iComponents As Collection) As Collection
  Dim iResult As New Collection
  Dim nComponent As Variant
  For Each nComponent In iComponents
    Dim vNode As Variant
    For Each vNode In iNodes
      If Not sourceOrder_.Exists(vNode) Then _
        GoTo NEXT_NODE
      If nodeComponents_(vNode) = nComponent Then _
        Call iResult.Add(vNode)
NEXT_NODE:
    Next vNode
  Next nComponent
  Set GroupNodesInduced = iResult
End Function

