Attribute VB_Name = "z_CCVsoExtension"
' ============== Module for additional functionality for CIHT CONCEPT ====================
' Shared module version: 20220714
' Tested in: TestVisio
' Depends on: CDS_Factorizator, z_VsoUtilities, CDS_Graph, API_VsoWrapper, z_VsoGraph
' Required reference: Scripting
Option Explicit

Private Const CC_DEFAULT_CONNECTOR_NAME = "Соединитель"

Public Sub CC_DispatchCommand(sCommand$)
  Select Case sCommand
    Case "WidthAdd10":        Call CC_WidthAdd10
    Case "WidthSub10":        Call CC_WidthSub10
    Case "TopAlignment":      Call CC_TopAlignment
    Case "LeftAlignment":     Call CC_LeftAlignment
    Case "ExpandStraight":    Call CC_ExpandStraight
    Case "ExpandReverse":     Call CC_ExpandReverse
    Case "GotoLinkBegin":     Call CC_GotoLinkBegin
    Case "GotoLinkEnd":       Call CC_GotoLinkEnd
    Case "IterateSimilar":    Call CC_IterateSimilar
    Case "RedirectConnects":  Call CC_RedirectConnectors
    Case "CreateConnects":    Call CC_CreateConnectors
    Case "Convert1251":       Call CC_Convert1251
  End Select
End Sub

Public Sub CC_SelectByID()
  Dim target As Visio.Shape
  Dim sName$: sName = VBA.InputBox("Input ID")
  If sName = vbNullString Then _
    Exit Sub
  
  If Not IsNumeric(sName) Then
    Call MsgBox("Invalid ID", vbExclamation)
    Exit Sub
  End If
  
  On Error Resume Next
  Set target = Visio.Application.ActivePage.Shapes.ItemFromID(CLng(sName))
  On Error GoTo 0
  
  If target Is Nothing Then
    Call MsgBox("Invalid ID", vbExclamation)
    Exit Sub
  End If
  
  Dim iSelection As Visio.Selection: Set iSelection = Visio.Application.ActiveWindow.Selection
  Call iSelection.DeselectAll
  Call iSelection.Select(target, visSelect)
  Visio.Application.ActiveWindow.Selection = iSelection
  Call Visio.Application.ActiveWindow.CenterViewOnShape(target, visCenterViewSelectShape)
End Sub

Public Sub CC_GotoLinkBegin()
  Dim iConnector As Visio.Shape: Set iConnector = GetSelectedConnector
  If iConnector Is Nothing Then _
    Exit Sub
  Dim iTarget As Visio.Shape: Set iTarget = VsoGetConnectedShape(iConnector, bBegin:=True)
  Call VsoCenterViewOn(iTarget)
End Sub

Public Sub CC_GotoLinkEnd()
  Dim iConnector As Visio.Shape: Set iConnector = GetSelectedConnector
  If iConnector Is Nothing Then _
    Exit Sub
  Dim iTarget As Visio.Shape: Set iTarget = VsoGetConnectedShape(iConnector, bBegin:=False)
  Call VsoCenterViewOn(iTarget)
End Sub

Public Sub CC_WidthAdd10()
  If ActiveWindow.Selection.Count = 0 Then
    Call MsgBox("Выделите фигуру", vbExclamation)
    Exit Sub
  End If
  Call GlobalUndo.BeginScope("Увеличение ширины")
  ThisDocument.DiagramServicesEnabled = visServiceStructureBasic
  
  Call VsoIncrementWidth(ActiveWindow.Selection(1), 10)
  
  ThisDocument.DiagramServicesEnabled = 0
  Call GlobalUndo.EndScope
End Sub

Public Sub CC_WidthSub10()
  If ActiveWindow.Selection.Count = 0 Then
    Call MsgBox("Выделите фигуру", vbExclamation)
    Exit Sub
  End If
  Call GlobalUndo.BeginScope("Уменьшение ширины")
  ThisDocument.DiagramServicesEnabled = visServiceStructureBasic
  
  Call VsoIncrementWidth(ActiveWindow.Selection(1), -10)
  
  ThisDocument.DiagramServicesEnabled = 0
  Call GlobalUndo.EndScope
End Sub

Public Sub CC_IterateSimilar()
Attribute CC_IterateSimilar.VB_ProcData.VB_Invoke_Func = "F"
  Dim iSelected As Visio.Selection: Set iSelected = Visio.Application.ActiveWindow.Selection
  Dim iShape As Visio.Shape
  Dim nItem&: nItem = 1
  Do
    If nItem > iSelected.Count Then _
      Exit Do
    Set iShape = iSelected.Item(nItem)
    If iShape.OneD Then
      Call iSelected.Select(iShape, visDeselect)
    Else
      nItem = nItem + 1
    End If
  Loop
  
  If iSelected.Count = 0 Then
    Call MsgBox("Select at least one shape", vbExclamation)
    Exit Sub
  End If
  
  Dim iTarget As Visio.Shape: Set iTarget = iSelected.Item(1)
  
  Dim idList As Scripting.Dictionary: Set idList = ExtractShapeIDs(iTarget)
  If idList Is Nothing Then
    Call MsgBox("Selected shape is missing IDs", vbExclamation)
    Exit Sub
  End If
  
  Dim filteredIDs As Collection: Set filteredIDs = FilterPageIDs(Visio.Application.ActivePage, idList)
  
  Call iSelected.DeselectAll
  Dim nStart&: nStart = -1
  For nItem = 1 To filteredIDs.Count Step 1
    Set iShape = filteredIDs.Item(nItem)
    If nStart <> -1 Then _
      Call iSelected.Select(iShape, visSelect)
    If iShape.ID = iTarget.ID Then
      nStart = nItem
      If nStart = filteredIDs.Count Then _
        nStart = 1
    End If
  Next nItem
  
  For nItem = 1 To nStart Step 1
    Call iSelected.Select(filteredIDs.Item(nItem), visSelect)
  Next nItem
  
  Visio.Application.ActiveWindow.Selection = iSelected
  Call Visio.Application.ActiveWindow.CenterViewOnShape(iSelected.Item(1), visCenterViewSelectShape)
End Sub

' Create connections from all selected to last selected
Public Sub CC_CreateConnectors()
  Dim iSelected As Visio.Selection: Set iSelected = ThisDocument.Application.ActiveWindow.Selection
  If iSelected.Count < 2 Then
    Call MsgBox("Select at least two shapes", vbExclamation)
    Exit Sub
  End If
  
  Call GlobalUndo.BeginScope("Создать связи")
  
  Dim shapeTo As Visio.Shape: Set shapeTo = iSelected(iSelected.Count)
  Dim shapeFrom As Visio.Shape
  Dim conShp As Visio.Shape
  Dim iMaster As Visio.Master: Set iMaster = FindMaster(ThisDocument, CC_DEFAULT_CONNECTOR_NAME)
  Dim nItem&
  For nItem = 1 To iSelected.Count - 1 Step 1
    Set shapeFrom = iSelected(nItem)
    If shapeFrom.Connects.Count <> 0 Then _
      GoTo NEXT_ITEM
    If iMaster Is Nothing Then
      Call shapeFrom.AutoConnect(shapeTo, visAutoConnectDirNone)
    Else
      Call shapeFrom.AutoConnect(shapeTo, visAutoConnectDirNone, iMaster)
    End If
NEXT_ITEM:
  Next nItem
  
  Call GlobalUndo.EndScope
End Sub

' Move connectors end to last selected
Public Sub CC_RedirectConnectors()
  Dim iSelected As Visio.Selection: Set iSelected = ThisDocument.Application.ActiveWindow.Selection
  If iSelected.Count < 2 Then
    Call MsgBox("Select at least one connector and one shape", vbExclamation)
    Exit Sub
  End If
  
  Dim shapeTo As Visio.Shape: Set shapeTo = iSelected(iSelected.Count)
  If shapeTo.Connects.Count <> 0 Then
    Call MsgBox("Select valid shape last (not connector!)", vbExclamation)
    Exit Sub
  End If
  
  Call GlobalUndo.BeginScope("Перенести концы")
  
  Dim nItem&
  For nItem = 1 To iSelected.Count - 1 Step 1
    Call iSelected(nItem).CellsU("EndX").GlueTo(shapeTo.CellsU("PinX"))
  Next nItem
  
  Call GlobalUndo.EndScope
End Sub

Public Sub CC_TopAlignment()
  Dim dGap As Double: dGap = PromptAlignGap
  If dGap = 0 Then _
    Exit Sub
  Dim iShapes As Collection: Set iShapes = VsoActiveShapes(ThisDocument.Application)
  If iShapes.Count = 0 Then _
    Exit Sub

  Dim vsoUI As New API_VsoWrapper: Call vsoUI.SetDocument(ThisDocument)
  Call vsoUI.PauseUI
  ThisDocument.DiagramServicesEnabled = visServiceStructureBasic
  Call GlobalUndo.BeginScope("Выравнивание")
  
  Call VsoAlignShapes(iShapes, dGap, bAlignTop:=True)
  
  Call GlobalUndo.EndScope
  ThisDocument.DiagramServicesEnabled = 0
  Call vsoUI.ResumeUI
End Sub

Public Sub CC_LeftAlignment()
  Dim dGap As Double: dGap = PromptAlignGap
  If dGap = 0 Then _
    Exit Sub
  Dim iShapes As Collection: Set iShapes = VsoActiveShapes(ThisDocument.Application)
  If iShapes.Count = 0 Then _
    Exit Sub

  Dim vsoUI As New API_VsoWrapper: Call vsoUI.SetDocument(ThisDocument)
  Call vsoUI.PauseUI
  ThisDocument.DiagramServicesEnabled = visServiceStructureBasic
  Call GlobalUndo.BeginScope("Выравнивание")
  
  Call VsoAlignShapes(iShapes, dGap, bAlignTop:=False)
  
  Call GlobalUndo.EndScope
  ThisDocument.DiagramServicesEnabled = 0
  Call vsoUI.ResumeUI
End Sub

Public Sub CC_ExpandReverse()
  If ThisDocument.Application.ActiveWindow.Selection.Count = 0 Then
    Call MsgBox("Выделите Элемент", vbExclamation)
    Exit Sub
  End If
  
  Dim iSelected As Collection: Set iSelected = VsoActiveShapes(ThisDocument.Application)
  Dim iGraph As CDS_Graph: Set iGraph = ScanGraph(iSelected, bReverseLinks:=True)
  Call VsoSelectShapesIDs(iGraph.Nodes, ThisDocument.Application.ActivePage)
End Sub

Public Sub CC_ExpandStraight()
  If ThisDocument.Application.ActiveWindow.Selection.Count = 0 Then
    Call MsgBox("Выделите Элемент", vbExclamation)
    Exit Sub
  End If
  
  Dim iSelected As Collection: Set iSelected = VsoActiveShapes(ThisDocument.Application)
  Dim iGraph As CDS_Graph: Set iGraph = ScanGraph(iSelected, bReverseLinks:=False)
  Call VsoSelectShapesIDs(iGraph.Nodes, ThisDocument.Application.ActivePage)
End Sub

Public Sub CC_Convert1251()
  Dim iShapes As Collection: Set iShapes = VsoActiveShapes(ThisDocument.Application)
  If iShapes.Count = 0 Then _
    Exit Sub
    
  Dim aShape As Visio.Shape
  For Each aShape In iShapes
    Dim sText$: sText = aShape.Text
    Dim sNewText$: sNewText = Convert1251(aShape.Text)
    If sText <> sNewText Then _
      aShape.Text = sNewText
  Next aShape
End Sub

Public Function VsoAlignShapes(target As Collection, dGap As Double, bAlignTop As Boolean)
  Dim iFactors As New CDS_Factorizator
  Call iFactors.Init(Application.ConvertResult(dGap, "mm", "in"))
  
  Dim sCellPosition$: sCellPosition = IIf(bAlignTop, "PinY", "PinX")
  Dim sDimension$: sDimension = IIf(bAlignTop, "Height", "Width")
  Dim k&: k = IIf(bAlignTop, 1, -1)
  
  Dim aShape As Visio.Shape
  For Each aShape In target
    If VsoIsMovable(aShape) Then _
      Call iFactors.Insert(aShape.ID, k * (aShape.CellsU(sCellPosition) + k * aShape.CellsU(sDimension) / 2#))
  Next aShape
  
  For Each aShape In target
    Dim vFactor As Variant: vFactor = iFactors.FactorFor(aShape.ID)
    If vFactor = Empty Then _
      GoTo NEXT_SHAPE
    If vFactor = aShape.ID Then _
      GoTo NEXT_SHAPE
      
    Dim targetPos As Double: targetPos = iFactors.FactorValueFor(vFactor)
    aShape.CellsU(sCellPosition) = k * (targetPos - aShape.CellsU(sDimension) / 2#)
NEXT_SHAPE:
  Next aShape
End Function

Public Function ExtractShapeIDs(target As Visio.Shape) As Scripting.Dictionary
  Dim sText$: sText = target.Text
  If sText = vbNullString And target.Shapes.Count > 1 Then _
    sText = target.Shapes(1).Text
  If sText = vbNullString Then _
    Exit Function
  
  Dim sPrefix$: sPrefix = ExtractShapePrefix(sText)
  If sPrefix = vbNullString Then
    Set ExtractShapeIDs = New Scripting.Dictionary
    Call ExtractShapeIDs.Add(VBA.Trim(sText), VBA.Trim(sText))
    Exit Function
  End If
  
  Set ExtractShapeIDs = SplitPrefix(sPrefix)
End Function

Public Function FilterPageIDs(iPage As Visio.Page, iFilter As Scripting.Dictionary) As Collection
  Dim iResult As New Collection
  Dim idList As Scripting.Dictionary
  Dim iShape As Visio.Shape
  For Each iShape In iPage.Shapes
    Set idList = ExtractShapeIDs(iShape)
    If Not idList Is Nothing Then
      Dim iKey As Variant
      For Each iKey In idList.Keys
        Dim sValue$: sValue = idList(iKey)
        If iFilter.Exists(sValue) Then
          Call iResult.Add(iShape)
          Exit For
        End If
      Next iKey
    End If
  Next iShape
  Set FilterPageIDs = iResult
End Function

' ========
Private Function GetSelectedConnector() As Visio.Shape
  Dim iSelected As Visio.Selection: Set iSelected = Application.ActiveWindow.Selection
  If iSelected.Count <> 1 Then
    Call MsgBox("Выберите соединитель", vbExclamation)
    Exit Function
  End If
  Dim iConnector As Visio.Shape: Set iConnector = iSelected(1)
  If iConnector.Connects.Count = 0 Then
    Call MsgBox("Выбранный соединитель должен быть прикрелпен к фигурам", vbExclamation)
    Exit Function
  End If
  Set GetSelectedConnector = iConnector
End Function

Private Function PromptAlignGap() As Double
  Dim sInput$: sInput = InputBox("Введите пороговое различие в миллиметрах", Default:="10")
  If sInput = vbNullString Then _
    Exit Function
  If Not IsNumeric(sInput) Then
    Call MsgBox("Введите число", vbExclamation)
    Exit Function
  End If
  PromptAlignGap = CDbl(sInput)
End Function

Private Function Convert1251(sText$) As String
  If sText = "" Then _
    Exit Function
  
  Dim nChr&
  For nChr = 1 To VBA.Len(sText)
    Dim aChr$: aChr = VBA.Mid$(sText, nChr, 1)
    Dim uniChr&: uniChr = VBA.AscW(aChr)
    If uniChr < 256 And uniChr > 127 Then _
      aChr = VBA.Chr(uniChr)
    Convert1251 = Convert1251 & aChr
  Next nChr
End Function

Private Function ExtractShapePrefix(sText$) As String
  Dim nChr&
  For nChr = 1 To VBA.Len(sText) Step 1
    If VBA.Mid$(sText, nChr, 1) Like "[а-яёa-zA-Z]" Then
      nChr = nChr - 1
      Exit For
    End If
  Next nChr
  If nChr = 0 Then _
    Exit Function
  
  Dim sResult$: sResult = VBA.Left(sText, nChr)
  If VBA.InStr(1, sResult, " ", vbTextCompare) = 0 Then _
    Exit Function
  
  ExtractShapePrefix = VBA.Trim(VBA.Left(sResult, VBA.InStrRev(sResult, " ")))
End Function

Private Function SplitPrefix(sPrefix$) As Scripting.Dictionary
  Dim iResult As New Scripting.Dictionary
  If VBA.InStr(1, sPrefix, "(") = 0 Then
    Call iResult.Add(sPrefix, sPrefix)
    Set SplitPrefix = iResult
    Exit Function
  End If
  
  Dim nStart&: nStart = -1
  Dim nChr&
  For nChr = 1 To VBA.Len(sPrefix) Step 1
    Dim aChr$: aChr = VBA.Mid$(sPrefix, nChr, 1)
    If aChr = "(" Then
      nStart = nChr
    ElseIf aChr = ")" Then
      If nStart <> -1 And nStart + 1 < nChr Then
        Dim sID$: sID = VBA.Trim(VBA.Mid$(sPrefix, nStart + 1, nChr - nStart - 1))
        If sID <> vbNullString Then
          On Error Resume Next
          Call iResult.Add(sID, sID)
          On Error GoTo 0
        End If
        nStart = -1
      End If
    End If
  Next nChr
  If iResult.Count = 0 Then _
    Call iResult.Add(sPrefix, sPrefix)
  Set SplitPrefix = iResult
End Function
