#include-once

Global $VARIABLE_UDF_COMPILE
If @Compiled And Not $VARIABLE_UDF_COMPILE Then MsgBox(0x30, 'Variable.au3 UDF', 'You have compiled your application with the Variable UDF included.' & @LF & 'If this was intentional, declare "Global $VARIABLE_UDF_COMPILE = True" above the #include.')

; #INDEX# =======================================================================================================================
; Title .........: Variable
; AutoIt Version : 3.2.13.5+
; Language ......: English
; Description ...: Functions for dumping variable information.
; Author(s) .....: Rob Saunders (rksaunders (at) gmail (dot) com)
; ===============================================================================================================================

; #CURRENT# =====================================================================================================================
; _VarDump
; ===============================================================================================================================

; #INTERNAL_USE_ONLY#============================================================================================================
; _VarDumpArray
; _VarDumpStruct
; ===============================================================================================================================

; #FUNCTION# ====================================================================================================================
; Name...........: _VarDump
; Description ...: This function returns information about a variable. Does not reset @error or @extended.
; Syntax.........: _VarDump(ByRef Const $vVar, $vDumpType = Default)
; Parameters ....: $vVar         - The variable to analyze.
;                  $vDumpType    - How to display the information. See Notes for keywords.
; Return values .: String: Information about the variable. This is returned regardless of the $vDumpType parameter.
; Author ........: Rob Saunders (rksaunders (at) gmail (dot) com)
; Remarks .......: Use one of the following keywords for the $vDumpType parameter:
;                      1, ConsoleWrite, Console, CW    = ConsoleWrite()
;                      2, MsgBox, Msg, Box, MB         = MsgBox()
;                      3, ToolTip, Tip, TT             = ToolTip()
;                      4, Clipboard, Clip, CB          = ClipPut()
;                      5, GUI, Window, Text            = Display in an edit control in a GUI window created on the fly
;                  You can add an asterisk "*" to the keyword to also return @ScriptLineNumber, @error, and @extended values. ie: "cw*"
; ===============================================================================================================================
Func _VarDump(ByRef Const $vVar, $vDumpType = '', $sIndent = '', $iLineNum = @ScriptLineNumber, $iError = @error, $iExtended = @extended)
    Local $sVarDump, $sVarInfo
    $vDumpType = StringReplace($vDumpType, '*', '')
    If @extended Then
        $sVarInfo = '*** Line: ' & $iLineNum & ', @error: ' & $iError & ', @extended: ' & $iExtended
    EndIf
    Local $sVarType = VarGetType($vVar)
    $sVarDump &= $sVarType
    Switch $sVarType
        Case 'Array'
            $sVarDump &= '(' & @CRLF & _VarDumpArray($vVar, $sIndent & @TAB) & @CRLF & $sIndent & ')'
        Case 'DllStruct'
            $sVarDump &= '(' & @CRLF & _VarDumpStruct($vVar, $sIndent & @TAB) & @CRLF & $sIndent & ')'
        Case 'Binary'
            $sVarDump &= '(' & BinaryLen($vVar) & ') ' & $vVar
        Case 'Object'
            $sVarDump &= '(' & ObjName($vVar) & ')'
        Case 'String'
            $sVarDump &= '(' & StringLen($vVar) & ') ' & $vVar
        Case Else
            If IsHWnd($vVar) Then $sVarDump &= '/HWnd'
            $sVarDump &= '(' & $vVar & ')'
    EndSwitch

    If $sVarInfo And StringRegExp($sVarDump, '[\r\n]') Then
        $sVarDump = $sVarInfo & @CRLF & $sVarDump
    ElseIf $sVarInfo Then
        $sVarDump = $sVarDump & ' ' & $sVarInfo
    EndIf

    Switch $vDumpType
        Case 1, 'ConsoleWrite', 'Console', 'CW'
            ConsoleWrite($sVarDump & @CRLF)
        Case 2, 'MsgBox', 'Msg', 'Box', 'MB'
            MsgBox(0x2040, 'VarDump', $sVarDump)
        Case 3, 'ToolTip', 'Tip', 'TT'
            ToolTip($sVarDump)
        Case 4, 'Clipboard', 'Clip', 'CB'
            ClipPut($sVarDump)
        Case 5, 'GUI', 'Window', 'Text'
            Local $iGUIOnEventMode = Opt('GUIOnEventMode', 0)
            Local $guiVarDump = GUICreate('VarDump', 300, 200, Default, Default, 0xCF0000) ; $WS_OVERLAPPEDWINDOW
            GUICtrlCreateEdit($sVarDump, 0, 0, 300, 200)
                GUICtrlSetResizing(-1, 102) ; $GUI_DOCKBORDERS
            GUISetState(@SW_SHOW, $guiVarDump)
            Do
            Until GUIGetMsg() = -3 ; $GUI_EVENT_CLOSE
            GUIDelete($guiVarDump)
            Opt('GUIOnEventMode', $iGUIOnEventMode)
    EndSwitch

    Return SetError($iError, $iExtended, StringReplace($sVarDump, Chr(0), ' '))
EndFunc

; #INTERNAL_USE_ONLY# ===========================================================================================================
; Name...........: _VarDumpArray
; Description ...: Details an array variable.
; Syntax.........: _VarDumpArray(ByRef Const $aArray)
; Parameters ....: $aArray - array to analyze.
; Return values .: String: Structured output of array contents.
; Author ........: Rob Saunders (rksaunders (at) gmail (dot) com)
; ===============================================================================================================================
Func _VarDumpArray(ByRef Const $aArray, $sIndent = '')
    If Not IsArray($aArray) Then
        ; This should only happen if end user calls function directly on non-array
        Return $sIndent & '!! Variable is not array'
    EndIf

    Local $iDimensions = UBound($aArray, 0) ; How many dimensions in array
    If $iDimensions < 1 Then
        ; This should be impossible, but error check for safety
        ; -> As of 3.3.9.6 this (ie: Dim $array[0]) became possible
        Return $sIndent & '!! Array has 0 dimensions'
    EndIf

    Local $aIndexes[$iDimensions] ; Tracker for reading indexes
    Local $aUBounds[$iDimensions] ; List of dimension sizes
    For $i = 0 To $iDimensions - 1
        $aIndexes[$i] = 0
        $aUBounds[$i] = UBound($aArray, $i + 1) ; Store each dimension size
        If $aUBounds[$i] < 1 Then
            Return $sIndent & '!! Array dimension [' & $i + 1 & '] size is ' & $aUBounds[$i]
        EndIf
    Next

    Local $sArrayIndex ; Indexing string for individual item reading (ie: "[1][2][3]")
    Local $sArrayRead ; Value of array item
    Local $bDone ; Indicates if overall array has finished parsing
    Local $sDump ; Output string
    While 1
        $bDone = True ; Default to true, flag false if dimension read is not done

        ; Reset and build indexing string
        $sArrayIndex = ''
        For $i = 0 To $iDimensions - 1
            $sArrayIndex &= '[' & $aIndexes[$i] & ']'
            If $aIndexes[$i] < $aUBounds[$i] - 1 Then $bDone = False ; Indexing has not reached end of dimension: NOT done
        Next

        $sArrayRead = Execute('$aArray' & $sArrayIndex) ; Read the darn value already!
        If @error Then ; Item read failed, cancel the whole listing
            $sDump &= $sIndent & $sArrayIndex & ' !! Error reading index'
            ExitLoop
        Else
            ; Add the index and value to the output string
            $sDump &= $sIndent & $sArrayIndex & ' => ' & _VarDump($sArrayRead, '', $sIndent)

            If $bDone Then
                Return $sDump
            Else
                ; If not done, line break and loop again
                $sDump &= @CRLF
            EndIf
        EndIf

        ; Ok, I didn't comment this before and it took me forever to figure out what was going on so this is going to be verbose.
        ; The reason this FOR loop goes backwards is because it's stepping through the last dimension first and working it's way back.
        ; Just like when you increase a number, you increment the right-most digit first, ie: the "one's place".
        ; So for example, if we have an array of [X][X][X] - it starts with the third dimension [ ][ ][X] and steps through it one at a time,
        ; like this: [0][0][0], [0][0][1], [0][0][2], etc. If the index hasn't hit the dimension's ubound then the FOR loop exits, so all that
        ; gets incremented is that one dimension index. However, if it has met or exceeded that dimension's ubound, then it resets it to 0 and
        ; the FOR loop gets to continue to the next dimension up (ie: [ ][X][ ]), and now we get [0][1][0], [0][1][1], [0][1][2], etc.
        For $i = $iDimensions - 1 To 0 Step -1
            $aIndexes[$i] += 1
            If $aIndexes[$i] >= $aUBounds[$i] Then
                $aIndexes[$i] = 0
                If $i = $iDimensions - 1 Then $sDump &= @CRLF
                ; The above line puts an extra line break between groups of the last dimension. ie:
                ; [0][0][0] => String(0)
                ; [0][0][1] => String(0)
                ; [0][0][2] => String(0)
                ;
                ; [0][1][0] => String(0)
                ; [0][1][1] => String(0)
                ; [0][1][2] => String(0)
            Else
                ExitLoop
            EndIf
        Next
    WEnd
    Return $sDump
EndFunc

; #INTERNAL_USE_ONLY# ===========================================================================================================
; Name...........: _VarDumpStruct
; Description ...: Details a struct variable.
; Syntax.........: VarDumpStruct(ByRef $tStruct)
; Parameters ....: $tStruct - struct to analyze.
; Return values .: String: Structured output of struct details/contents.
; Author ........: Rob Saunders (rksaunders (at) gmail (dot) com)
; ===============================================================================================================================
Func _VarDumpStruct(ByRef Const $tStruct, $sIndent = '')
    Local $iElement = 1, $vRead, $vTest
    ; Do the initial $sDump contents - Standard fare for any struct
    Local $sDump = _
        $sIndent & '[Size] => ' & DllStructGetSize($tStruct) & @CRLF & _
        $sIndent & '[Ptr] => ' & DllStructGetPtr($tStruct)
    While 1
        $vRead = DllStructGetData($tStruct, $iElement)
        If @error = 2 Then ExitLoop ; If we've overstepped $iElement we're done

        $vTest = VarGetType($vRead)
        If $vTest = 'String' Or $vTest = 'Binary' Then
            ; Test the vartype for String or Binary, in which case we dump now because we already have the full contents
            $sDump &= @CRLF & $sIndent & '[' & $iElement & '] => ' & _VarDump($vRead, '', $sIndent)
        Else
            ; Here we'll test to see if the element is an array by looking for an index of 2.
            DllStructGetData($tStruct, $iElement, 2)
            If @error Then
                ; @error means no index, which means no array, which means we can just dump the stored $vRead from above
                $sDump &= @CRLF & $sIndent & '[' & $iElement & '] => ' & _VarDump($vRead, '', $sIndent)
            Else
                ; If we get no @error then that means that index 2 was valid, so we'll just start from 1 and work our way up til we @error again
                Local $sSubDump, $iIndex = 1, $iCount = 0, $iNonEmpties = 0
                While 1
                    $vRead = DllStructGetData($tStruct, $iElement, $iIndex)
                    If @error Then ExitLoop ; And that's the limit of this array, so we're done.
                    If $vRead Then
                        ; We're skipping empty indices just to cut down on some of the clutter from large structs
                        $sSubDump &= @CRLF & @TAB & $sIndent & '[' & $iIndex & '] => ' & _VarDump($vRead)
                        $iNonEmpties += 1
                    EndIf
                    $iIndex += 1
                WEnd
                $iIndex -= 1
                $sDump &= @CRLF & $sIndent & '[' & $iElement & '] => Array('
                If $iNonEmpties < $iIndex Then
                    $sDump &= @CRLF & $sIndent & @TAB & '> Skipping empty indices (' & ($iIndex-$iNonEmpties) & '/' & $iIndex & ')'
                EndIf
                $sDump &= $sSubDump & @CRLF & $sIndent & ')'
            EndIf
        EndIf
        $iElement += 1
    WEnd
    Return $sDump
EndFunc