VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "YAML"
Attribute VB_Base = "0{FCFB3D2A-A0FA-1068-A738-08002B3371B5}"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Attribute VB_TemplateDerived = False
Attribute VB_Customizable = False
Option Explicit

' Const yamlPath As String = "\\Mac\iCloud\Development\cv\cv.yml"

Public yamlPath As String

Private Function RemoveEmptyStrings(arr() As String) As String()
    Dim tempArray() As String
    Dim i As Integer, j As Integer: j = 0
    ReDim tempArray(LBound(arr) To UBound(arr))
    j = 0
    For i = LBound(arr) To UBound(arr)
        If Len(arr(i)) > 0 Then
            tempArray(j) = arr(i)
            j = j + 1
        End If
    Next i
    ReDim Preserve tempArray(0 To j - 1)
    RemoveEmptyStrings = tempArray
End Function


Private Function RegexMatch(inputString As String, pattern As String, Optional isGlobal As Boolean = True) As Boolean
    ' checks for regex match without instantiating 80 gazillion objects
    
    ' parameters
    ' isGlobal: whether the regex check is global
    
    Dim Regex As Object: Set Regex = CreateObject("VBScript.RegExp")
    Regex.pattern = pattern
    Regex.Global = isGlobal
    RegexMatch = Regex.Test(inputString)
End Function

Private Function RegexSplit(inputString As String, pattern As String, Optional onlyFirst As Boolean = False, Optional splitBefore As Boolean = False) As String()
    ' splits array at any pattern that matches a regex
    
    ' parameters
    ' onlyFirst: if true, only splits the first instance of the match, creating an array of length 2
    ' splitBefore: if true, preserves the actual instance of the match
    
    Dim Regex As Object: Set Regex = CreateObject("VBScript.RegExp")
    Dim matches As Object
    Dim match As Object
    Dim splitParts() As String: ReDim splitParts(0 To 0)
    Dim pos As Integer
    Dim lastPos As Integer: lastPos = 1
    Dim i As Integer: i = 0
    
    ' set regex flags
    Regex.Global = True
    Regex.IgnoreCase = False
    Regex.pattern = pattern
    
    Set matches = Regex.Execute(inputString)
    
    ' lastPos = 1
    ' i = 0
    
    For Each match In matches
        pos = match.FirstIndex + 1
        ReDim Preserve splitParts(i)
        splitParts(i) = Mid(inputString, lastPos, pos - lastPos)
        If splitBefore Then
            lastPos = pos
        Else
            lastPos = pos + Len(match.Value)
        End If
        i = i + 1
        If onlyFirst Then Exit For
    Next match
    
    If lastPos <= Len(inputString) Then
        ReDim Preserve splitParts(i)
        splitParts(i) = Mid(inputString, lastPos)
    End If
    
    ' retvrn
    RegexSplit = RemoveEmptyStrings(splitParts)
End Function

Private Function RegexSubstitute(inputString As String, pattern As String, Optional substitution As String = "")
    ' does what it says on the tin
    Dim Regex As Object: Set Regex = CreateObject("VBScript.RegExp")
    Regex.pattern = pattern
    Regex.IgnoreCase = False
    Regex.Global = True
    RegexSubstitute = Regex.Replace(inputString, substitution)
End Function

' YAML Layer Parser Pseudocode
' ====
' function GetYAMLLayerAsCollection(String fromYAML) {
'    Collection mainDictionary = New Collection();
'    if (fromYAML.containsRegex(/\n[A-Za-z]/)) {
'      // is a dictionary
'      String[] temporaryArray = fromYAML.split(/\n[A-Za-z]/);
'      for each x in temporaryArray {
'        x.splitByFirstInstanceOf(':\n');
'        x[1].replaceAllInstancesOf('
'        mainDictionary.add(x[0], x[1]);
'      }
'    } else if (fromYAML.containsRegex(/\n-/)) {
'      // if array, process the array and return it as "self"
'      String[] temporaryArray = fromYAML.splitBy('\n-');
'      for each x in temporaryArray {
'        x.removeAllInstancesOf('\n- ');
'        x.replaceAllInstancesOf('\n  ', '\n');
'        mainDictionary.add("self", temporaryArray);
'      }
'    } else if (fromYAML.startsWith('"')) {
'      mainDictionary.add("self", removeQuotes(fromYAML));
'    } else {
'      MsgBox("Processing error: neither array, dictionary, nor string");
'    }
' }


Private Function GetYAMLLayerAsDictionary(fromYAML As String) As Scripting.Dictionary
    Dim mainDictionary As Scripting.Dictionary: Set mainDictionary = New Scripting.Dictionary
    ' create regex objects to test for dict, array, and string
    
    'Dim regEx_dict As Object: Set regEx_dict = CreateObject("VBScript.RegExp")
    'Dim regEx_arry As Object: Set regEx_arry = CreateObject("VBScript.RegExp")
    'Dim regEx_strn As Object: Set regEx_strn = CreateObject("VBScript.RegExp")
    
    'regEx_dict.Global = True:  regEx_dict.Pattern = "\n[A-Za-z]"
    'regEx_arry.Global = True:  regEx_arry.Pattern = "\n-\s"
    'regEx_strn.Global = False: regEx_strn.Pattern = "^\s*""(.*?)""\s*$"
    
    Dim parts() As String
    
    If RegexMatch(fromYAML, "(?:\n|\^)\w+:", True) Then
        ' is a dictionary
        parts = RegexSplit(fromYAML, "\n\w+:", False, True)
        Dim part As Variant ' not sure why it can't be as string but whatever billy gates
        Call mainDictionary.Add(ID_TYPE, "Dictionary") ' identify as dict
        For Each part In parts
            Dim keyValue() As String: keyValue = RegexSplit(CStr(part), ":\s", True)
            ' trim trailing \n from category
            If UBound(keyValue) > 0 Then
                keyValue(0) = RegexSubstitute(keyValue(0), "^\n+")
                ' trim 2 spaces off of each line if they're there
                keyValue(1) = RegexSubstitute(keyValue(1), "^\s{2}")
                keyValue(1) = RegexSubstitute(keyValue(1), "\n\s{2}", vbLf)
                Call mainDictionary.Add(keyValue(0), keyValue(1))
            End If
        Next part
    ElseIf RegexMatch(fromYAML, "^-\s", True) Then
        ' is an array
        Call mainDictionary.Add(ID_TYPE, "Array")
        parts = RegexSplit(fromYAML, "(^|\n)-\s", False)
        Dim i As Integer
        For i = LBound(parts) To UBound(parts)
            parts(i) = RegexSubstitute(parts(i), "\n\s{2}", vbLf)
        Next i
        Call mainDictionary.Add(ID_SELF, parts)
    ElseIf RegexMatch(fromYAML, "^\s*""(.*?)""\s*$", True) Then
        ' is a string
        Call mainDictionary.Add(ID_TYPE, "String")
        Call mainDictionary.Add(ID_SELF, RegexSubstitute(fromYAML, """", ""))
    Else
        Call mainDictionary.Add(ID_SELF, "")
        Debug.Print _
        "Neither array, dictionary, nor string:" & _
        vbCrLf & vbCrLf & fromYAML & vbCrLf & vbCrLf & _
        "Make sure all strings are enclosed in double quotes." ', _
        'vbOKOnly, "YAML Error")
    End If
    
    Set GetYAMLLayerAsDictionary = mainDictionary
End Function

' YAML Traverser Pseudocode
' ===
'
' function TraverseYAML(String fromYAML) {
'   Dictionary mainDictionary = GetYAMLLayerAsDictionary(fromYAML);
'   if mainDictionary.___type___ = "Dictionary" {
'     for each entry in mainDictionary {
'       TraverseYAML(entry)
'     }
'     return mainDictionary;
'   } else if mainDictionary.___type___ = "Array" {
'     for each entry in mainDictionary.___self___ {
'       TraverseYAML(entry)
'     }
'     return mainDictionary;
'   } else if mainDictionary.___type___ = "String" {
'     return mainDictionary;
'   } else {
'     MsgBox("Internal YAML Error")
'   }
' }
Private Function GetYAMLAsDictionary(fromYAML As String) As Scripting.Dictionary
    Dim mainDictionary As Object: Set mainDictionary = GetYAMLLayerAsDictionary(fromYAML)
    Dim entry As Variant
    If mainDictionary(ID_TYPE) = "Dictionary" Then
        For Each entry In mainDictionary
            Debug.Print "=== PROCESSING DICTIONARY ENTRY ==="
            Debug.Print entry & " => " & mainDictionary(entry)
            If entry <> ID_TYPE And entry <> ID_SELF Then
                Set mainDictionary(entry) = GetYAMLAsDictionary(mainDictionary(entry))
            End If
        Next entry
    ElseIf mainDictionary(ID_TYPE) = "Array" Then
        Dim i As Integer
        Dim subArray() As Object
        For i = LBound(mainDictionary(ID_SELF)) To UBound(mainDictionary(ID_SELF))
            Debug.Print "=== PROCESSING ARRAY ENTRY ==="
            Debug.Print mainDictionary(ID_SELF)(i)
            'Set subDictionary = GetYAMLAsDictionary(mainDictionary(ID_SELF)(i))
            'Set mainDictionary(ID_SELF)(i) = subDictionary
            ReDim Preserve subArray(i)
            Set subArray(i) = GetYAMLAsDictionary(CStr(mainDictionary(ID_SELF)(i)))
        Next i
        
        mainDictionary(ID_SELF) = subArray
    ElseIf mainDictionary(ID_TYPE) <> "String" Then
        Debug.Print MESSAGE_MALFORMED_TYPE ', vbOKOnly, errIdentifier)
    End If
    Set GetYAMLAsDictionary = mainDictionary
End Function

' YAML Cleaner Pseudocode
' =====
' function YAMLCleaner(Dictionary mainDictionary) {
'   for each entry in mainDictionary {
'     if entry(ID_TYPE) == "Dictionary" {
'       for each secondOrderEntry in entry {
'         YAMLCleaner(secondOrderEntry)
'       }
'     } else if entry(ID_TYPE) == "Array" {
'       for each secondOrderEntry in entry(ID_SELF) {
'         YAMLCleaner(secondOrderEntry)
'       }
'     }
'     if entry(ID_TYPE) != "Dictionary" {
'       mainDictionary(entry) = mainDictionary(entry)(ID_SELF)
'     }
'   }
'   return mainDictionary;
' }

'Function YAMLCleaner(mainDictionary As Dictionary) As Dictionary
'    Dim entry As Variant
'    If mainDictionary(ID_TYPE) = "Array" Then ' go through array and yamlclean it
'        Dim i As Integer
'        Debug.Print JsonConverter.ConvertToJson(mainDictionary)
'        For i = LBound(mainDictionary(ID_SELF)) To UBound(mainDictionary(ID_SELF))
'
'            'If IsObject(mainDictionary(ID_SELF)(i)) Then
'                'Set mainDictionary(ID_SELF)(i) = YAMLCleaner(mainDictionary(ID_SELF)(i))
'            'Else
'            '    Debug.Print "encountered non-object"
'            'End If
'        Next i
'    End If
'    If mainDictionary(ID_TYPE) = "Dictionary" Then 'iterate through dict and yamlclena it
'        For Each entry In mainDictionary
'            If entry <> ID_TYPE Then
'                Set mainDictionary(entry) = YAMLCleaner(mainDictionary(entry))
'            End If
'        Next entry
'    End If
'
'    For Each entry In mainDictionary
'        If mainDictionary(ID_TYPE) = "Dictionary" And mainDictionary(entry)(ID_TYPE) <> "Dictionary" And entry <> ID_TYPE And entry <> ID_SELF Then
'            Debug.Print "processing " & entry & " which is " & mainDictionary(entry)(ID_TYPE)
'            If IsObject(mainDictionary(entry)(ID_SELF)) Then
'                Set mainDictionary(entry) = mainDictionary(entry)(ID_SELF)
'            Else
'                mainDictionary(entry) = mainDictionary(entry)(ID_SELF)
'            End If
'        End If
'    Next entry
'
'    ' destroy type identifier?
'    Set YAMLCleaner = mainDictionary
'End Function

Private Function GetFileAsString(filePath As String) As String
    ' Dim fileContent As String
    Dim line As String
    Dim fileNumber As Integer
    
    'filePath = "\\Mac\iCloud\Development\cv\cv.yml"
    
    fileNumber = FreeFile()
    
    Open filePath For Input As fileNumber
    
    Do While Not EOF(fileNumber)
        Line Input #fileNumber, line
        GetFileAsString = GetFileAsString & line & vbCrLf
    Loop
End Function

Public Property Let path(thePath As String)
    yamlPath = thePath
End Property

Public Property Get path() As String
    path = yamlPath
End Property

Public Property Get props() As Scripting.Dictionary
    Set props = GetYAMLAsDictionary(GetFileAsString(yamlPath))
End Property

' YAML Indexer Pseudocode [implement later]
' =====
' function index(string theIndex) {
'   Variant[] mainArray = theIndex.split(".");
'   Dictionary mainDictionary = yamlProps;
'   for each entry in mainArray {
'     if entry is {
'
'     ....
