Lua.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"

#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [ConvertFrom-LuaTable]
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertFrom-LuaTable] - Importing"
function ConvertFrom-LuaTable {
    <#
        .SYNOPSIS
        Parses a Lua table constructor string into a PowerShell object.

        .DESCRIPTION
        Takes a Lua table constructor string and converts it to PowerShell
        hashtables, arrays, and primitive types. This is the internal parsing
        engine used by ConvertFrom-Lua.
    #>

    [OutputType([object])]
    [CmdletBinding()]
    param(
        # The Lua table string to parse.
        [Parameter(Mandatory)]
        [string] $InputString,

        # Whether to output PSCustomObjects instead of hashtables.
        [Parameter()]
        [switch] $AsPSCustomObject,

        # Maximum allowed nesting depth.
        [Parameter()]
        [int] $MaxDepth = 1024
    )

    begin {}

    process {
        $script:luaString = $InputString
        $script:luaPos = 0
        $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent
        $script:luaMaxDepth = $MaxDepth
        $script:luaCurrentDepth = 0

        Skip-LuaWhitespace

        # Skip optional leading 'return' keyword (common in Lua data files)
        if ($script:luaPos + 6 -le $script:luaString.Length -and
            $script:luaString.Substring($script:luaPos, 6) -ceq 'return') {
            $nextPos = $script:luaPos + 6
            if ($nextPos -ge $script:luaString.Length -or
                $script:luaString[$nextPos] -match '[\s{]') {
                $script:luaPos = $nextPos
                Skip-LuaWhitespace
            }
        }

        $result = Read-LuaValue

        Skip-LuaWhitespace
        if ($script:luaPos -lt $script:luaString.Length) {
            $remainingInput = $script:luaString.Substring($script:luaPos)
            throw "Unexpected trailing content after Lua value at position $($script:luaPos): $remainingInput"
        }

        return $result
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertFrom-LuaTable] - Done"
#endregion [functions] - [private] - [ConvertFrom-LuaTable]
#region [functions] - [private] - [ConvertTo-LuaTable]
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-LuaTable] - Importing"
function ConvertTo-LuaTable {
    <#
        .SYNOPSIS
        Converts a PowerShell object to a Lua table string representation.

        .DESCRIPTION
        Recursively converts a PowerShell object (hashtable, array, PSCustomObject, or primitive)
        into a Lua table constructor string. This is the internal serialization engine used by ConvertTo-Lua.

        Uses fixed 4-space indentation per the Lua community convention.
        Properties with $null values are omitted (Lua nil-means-absent semantics).
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The object to convert to a Lua table string.
        [Parameter(Mandatory)]
        [AllowNull()]
        [object] $InputObject,

        # The current recursion depth.
        [Parameter()]
        [int] $CurrentDepth = 0,

        # Maximum allowed recursion depth.
        [Parameter()]
        [int] $MaxDepth = 2,

        # Whether to compress the output (no newlines or indentation).
        [Parameter()]
        [switch] $Compress,

        # Serialize enum values as their string name instead of numeric value.
        [Parameter()]
        [switch] $EnumsAsStrings
    )

    begin {
        $indent = if ($Compress) { '' } else { ' ' * (4 * $CurrentDepth) }
        $childIndent = if ($Compress) { '' } else { ' ' * (4 * ($CurrentDepth + 1)) }
        $newline = if ($Compress) { '' } else { "`n" }
        $separator = if ($Compress) { ',' } else { ",`n" }
    }

    process {
        if ($null -eq $InputObject) {
            return 'nil'
        }

        if ($InputObject -is [bool]) {
            if ($InputObject) {
                return 'true'
            } else {
                return 'false'
            }
        }

        # Enum handling
        if ($InputObject -is [enum]) {
            if ($EnumsAsStrings) {
                $escaped = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"'
                return "`"$escaped`""
            }
            return ([int]$InputObject).ToString([System.Globalization.CultureInfo]::InvariantCulture)
        }

        if ($InputObject -is [int] -or $InputObject -is [long] -or
            $InputObject -is [int16] -or $InputObject -is [int64] -or
            $InputObject -is [uint16] -or $InputObject -is [uint32] -or
            $InputObject -is [uint64] -or $InputObject -is [byte] -or
            $InputObject -is [sbyte]) {
            return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture)
        }

        if ($InputObject -is [float] -or $InputObject -is [double] -or
            $InputObject -is [decimal] -or $InputObject -is [single]) {
            return $InputObject.ToString([System.Globalization.CultureInfo]::InvariantCulture)
        }

        if ($InputObject -is [string]) {
            $escaped = $InputObject `
                -replace '\\', '\\' `
                -replace '"', '\"' `
                -replace "`0", '\0' `
                -replace "`a", '\a' `
                -replace "`b", '\b' `
                -replace "`f", '\f' `
                -replace "`n", '\n' `
                -replace "`r", '\r' `
                -replace "`t", '\t' `
                -replace "`v", '\v'
            return "`"$escaped`""
        }

        # Depth check for complex types
        if ($CurrentDepth -ge $MaxDepth) {
            Write-Warning "Depth limit ($MaxDepth) exceeded. Serializing remaining object as string."
            $str = $InputObject.ToString() `
                -replace '\\', '\\\\' `
                -replace '"', '\"'
            return "`"$str`""
        }

        if ($InputObject -is [System.Collections.IList]) {
            if ($InputObject.Count -eq 0) {
                return '{}'
            }
            $items = [System.Collections.Generic.List[string]]::new()
            foreach ($item in $InputObject) {
                $childParams = @{
                    InputObject    = $item
                    CurrentDepth   = $CurrentDepth + 1
                    MaxDepth       = $MaxDepth
                    Compress       = $Compress
                    EnumsAsStrings = $EnumsAsStrings
                }
                $value = ConvertTo-LuaTable @childParams
                $items.Add("$childIndent$value")
            }
            return "{$newline$($items -join $separator)$newline$indent}"
        }

        # Handle hashtables and ordered dictionaries
        if ($InputObject -is [System.Collections.IDictionary]) {
            if ($InputObject.Count -eq 0) {
                return '{}'
            }
            $entries = [System.Collections.Generic.List[string]]::new()
            foreach ($key in $InputObject.Keys) {
                $val = $InputObject[$key]
                # Omit $null values per Lua nil-means-absent semantics
                if ($null -eq $val) {
                    continue
                }
                $value = ConvertTo-LuaTable -InputObject $val `
                    -CurrentDepth ($CurrentDepth + 1) `
                    -MaxDepth $MaxDepth `
                    -Compress:$Compress `
                    -EnumsAsStrings:$EnumsAsStrings
                $luaKey = Format-LuaKey -Key ([string]$key)
                $space = if ($Compress) { '' } else { ' ' }
                $entries.Add("$childIndent$luaKey$space=${space}$value")
            }
            if ($entries.Count -eq 0) {
                return '{}'
            }
            return "{$newline$($entries -join $separator)$newline$indent}"
        }

        # Handle PSCustomObject
        if ($InputObject -is [psobject]) {
            $properties = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' }
            if (-not $properties) {
                return '{}'
            }
            $entries = [System.Collections.Generic.List[string]]::new()
            foreach ($prop in $properties) {
                # Omit $null values per Lua nil-means-absent semantics
                if ($null -eq $prop.Value) {
                    continue
                }
                $value = ConvertTo-LuaTable -InputObject $prop.Value `
                    -CurrentDepth ($CurrentDepth + 1) `
                    -MaxDepth $MaxDepth `
                    -Compress:$Compress `
                    -EnumsAsStrings:$EnumsAsStrings
                $luaKey = Format-LuaKey -Key $prop.Name
                $space = if ($Compress) { '' } else { ' ' }
                $entries.Add("$childIndent$luaKey$space=${space}$value")
            }
            if ($entries.Count -eq 0) {
                return '{}'
            }
            return "{$newline$($entries -join $separator)$newline$indent}"
        }

        # Fallback: convert to string
        $escaped = ($InputObject.ToString()) -replace '\\', '\\\\' -replace '"', '\"'
        return "`"$escaped`""
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-LuaTable] - Done"
#endregion [functions] - [private] - [ConvertTo-LuaTable]
#region [functions] - [private] - [Format-LuaKey]
Write-Debug "[$scriptName] - [functions] - [private] - [Format-LuaKey] - Importing"
function Format-LuaKey {
    <#
        .SYNOPSIS
        Formats a string as a valid Lua table key.

        .DESCRIPTION
        Returns the key as a bare identifier if it matches Lua identifier rules
        and is not a reserved word, otherwise wraps it in bracket-quote notation: ["key"].
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The key string to format.
        [Parameter(Mandatory)]
        [string] $Key
    )

    begin {
        # Lua 5.4 reserved words per §3.1
        $reservedWords = @(
            'and', 'break', 'do', 'else', 'elseif', 'end',
            'false', 'for', 'function', 'goto', 'if', 'in',
            'local', 'nil', 'not', 'or', 'repeat', 'return',
            'then', 'true', 'until', 'while'
        )
    }

    process {
        if ($Key -match '^[a-zA-Z_][a-zA-Z0-9_]*$' -and $Key -notin $reservedWords) {
            return $Key
        }
        $escaped = $Key -replace '\\', '\\\\' -replace '"', '\"'
        return "[`"$escaped`"]"
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Format-LuaKey] - Done"
#endregion [functions] - [private] - [Format-LuaKey]
#region [functions] - [private] - [Read-LuaHexFloat]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaHexFloat] - Importing"
function Read-LuaHexFloat {
    <#
        .SYNOPSIS
        Parses a Lua hex float string (e.g. 0x1.fp10) to a double.
    #>

    [OutputType([double])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $HexString
    )

    begin {}

    process {
        $isNegative = $HexString.StartsWith('-')
        $str = if ($isNegative) {
            $HexString.Substring(3)
        } else {
            $HexString.Substring(2)
        }

        $parts = $str -split '[pP]'
        $mantissaStr = $parts[0]
        $exponent = if ($parts.Length -gt 1) {
            [int]$parts[1]
        } else {
            0
        }

        $mantissaParts = $mantissaStr -split '\.'
        $intPart = if ($mantissaParts[0]) {
            [Convert]::ToInt64($mantissaParts[0], 16)
        } else {
            0
        }
        $fracValue = 0.0
        if ($mantissaParts.Length -gt 1 -and $mantissaParts[1]) {
            $fracStr = $mantissaParts[1]
            for ($i = 0; $i -lt $fracStr.Length; $i++) {
                $digitVal = [Convert]::ToInt32(
                    $fracStr[$i].ToString(), 16
                )
                $fracValue += $digitVal * [Math]::Pow(
                    16, - ($i + 1)
                )
            }
        }

        $result = ($intPart + $fracValue) * [Math]::Pow(2, $exponent)
        if ($isNegative) { $result = (-$result) }
        return $result
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaHexFloat] - Done"
#endregion [functions] - [private] - [Read-LuaHexFloat]
#region [functions] - [private] - [Read-LuaMultiLineString]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaMultiLineString] - Importing"
function Read-LuaMultiLineString {
    <#
        .SYNOPSIS
        Reads a multi-line Lua string delimited by long brackets [[ ]], [=[ ]=], [==[ ]==], etc.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param()

    begin {}

    process {
        # Count the number of '=' characters in the opening long bracket
        $script:luaPos++ # skip first [
        $equalsCount = 0
        while ($script:luaPos -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -eq '=') {
            $equalsCount++
            $script:luaPos++
        }
        if ($script:luaPos -ge $script:luaString.Length -or
            $script:luaString[$script:luaPos] -ne '[') {
            throw 'Invalid long bracket string opening.'
        }
        $script:luaPos++ # skip second [

        # Build the closing pattern: ] + N '=' + ]
        $closingBracket = ']' + ('=' * $equalsCount) + ']'
        $closeLen = $closingBracket.Length

        $result = [System.Text.StringBuilder]::new()

        # Per Lua spec, a newline immediately after the opening bracket is ignored
        if ($script:luaPos -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -eq "`n") {
            $script:luaPos++
        } elseif ($script:luaPos + 1 -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -eq "`r" -and
            $script:luaString[$script:luaPos + 1] -eq "`n") {
            $script:luaPos += 2
        }

        while ($script:luaPos -lt $script:luaString.Length) {
            if ($script:luaPos + $closeLen - 1 -lt $script:luaString.Length -and
                $script:luaString.Substring($script:luaPos, $closeLen) -eq $closingBracket) {
                $script:luaPos += $closeLen
                return $result.ToString()
            }
            $null = $result.Append($script:luaString[$script:luaPos])
            $script:luaPos++
        }

        throw 'Unterminated multi-line string.'
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaMultiLineString] - Done"
#endregion [functions] - [private] - [Read-LuaMultiLineString]
#region [functions] - [private] - [Read-LuaNumber]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaNumber] - Importing"
function Read-LuaNumber {
    <#
        .SYNOPSIS
        Reads a Lua number (integer, float, hex, hex float, scientific notation).
    #>

    [OutputType([int])]
    [OutputType([long])]
    [OutputType([double])]
    [CmdletBinding()]
    param()

    begin {}

    process {
        $start = $script:luaPos
        $isFloat = $false
        $isHex = $false

        if ($script:luaString[$script:luaPos] -eq '-') {
            $script:luaPos++
        }

        # Hex number
        if ($script:luaPos + 1 -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -eq '0' -and
            $script:luaString[$script:luaPos + 1] -match '[xX]') {
            $isHex = $true
            $script:luaPos += 2
            while ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -match '[0-9a-fA-F]') {
                $script:luaPos++
            }
            # Hex float fractional part
            if ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -eq '.') {
                $isFloat = $true
                $script:luaPos++
                while ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[0-9a-fA-F]') {
                    $script:luaPos++
                }
            }
            # Hex float exponent (p/P)
            if ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -match '[pP]') {
                $isFloat = $true
                $script:luaPos++
                if ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[+-]') {
                    $script:luaPos++
                }
                while ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[0-9]') {
                    $script:luaPos++
                }
            }
        } else {
            while ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -match '[0-9]') {
                $script:luaPos++
            }
            if ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -eq '.') {
                $isFloat = $true
                $script:luaPos++
                while ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[0-9]') {
                    $script:luaPos++
                }
            }
            # Scientific notation
            if ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -match '[eE]') {
                $isFloat = $true
                $script:luaPos++
                if ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[+-]') {
                    $script:luaPos++
                }
                while ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[0-9]') {
                    $script:luaPos++
                }
            }
        }

        $numStr = $script:luaString.Substring(
            $start, $script:luaPos - $start
        )

        if ($isFloat) {
            if ($isHex) {
                # Hex float like 0x1.fp10 - parse manually
                return [double](Read-LuaHexFloat -HexString $numStr)
            }
            return [double]::Parse(
                $numStr,
                [System.Globalization.CultureInfo]::InvariantCulture
            )
        }
        if ($isHex) {
            $isNegative = $numStr.StartsWith('-')
            $hexPart = if ($isNegative) {
                $numStr.Substring(3)
            } else {
                $numStr.Substring(2)
            }
            $longVal = [Convert]::ToInt64($hexPart, 16)
            if ($isNegative) { $longVal = (-$longVal) }
            if ($longVal -ge [int]::MinValue -and
                $longVal -le [int]::MaxValue) {
                return [int]$longVal
            }
            return $longVal
        }
        $longValue = [long]0
        if ([long]::TryParse($numStr, [ref]$longValue)) {
            if ($longValue -ge [int]::MinValue -and
                $longValue -le [int]::MaxValue) {
                return [int]$longValue
            }
            return $longValue
        }
        return [double]::Parse(
            $numStr,
            [System.Globalization.CultureInfo]::InvariantCulture
        )
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaNumber] - Done"
#endregion [functions] - [private] - [Read-LuaNumber]
#region [functions] - [private] - [Read-LuaString]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaString] - Importing"
function Read-LuaString {
    <#
        .SYNOPSIS
        Reads a quoted Lua string with escape sequence support per Lua 5.4 §3.1.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [char] $QuoteChar
    )

    begin {}

    process {
        $script:luaPos++ # skip opening quote
        $result = [System.Text.StringBuilder]::new()

        while ($script:luaPos -lt $script:luaString.Length) {
            $char = $script:luaString[$script:luaPos]

            if ($char -eq '\') {
                $script:luaPos++
                if ($script:luaPos -ge $script:luaString.Length) {
                    throw 'Unexpected end of string after escape character.'
                }
                $nextChar = $script:luaString[$script:luaPos]
                switch ($nextChar) {
                    'a' {
                        $null = $result.Append([char]7)
                        $script:luaPos++
                    }
                    'b' {
                        $null = $result.Append("`b")
                        $script:luaPos++
                    }
                    'f' {
                        $null = $result.Append([char]12)
                        $script:luaPos++
                    }
                    'n' {
                        $null = $result.Append("`n")
                        $script:luaPos++
                    }
                    'r' {
                        $null = $result.Append("`r")
                        $script:luaPos++
                    }
                    't' {
                        $null = $result.Append("`t")
                        $script:luaPos++
                    }
                    'v' {
                        $null = $result.Append([char]11)
                        $script:luaPos++
                    }
                    '\' {
                        $null = $result.Append('\')
                        $script:luaPos++
                    }
                    '"' {
                        $null = $result.Append('"')
                        $script:luaPos++
                    }
                    "'" {
                        $null = $result.Append("'")
                        $script:luaPos++
                    }
                    'x' {
                        # \xXX - two hex digits
                        $script:luaPos++
                        if ($script:luaPos + 1 -lt $script:luaString.Length) {
                            $hexStr = $script:luaString.Substring(
                                $script:luaPos, 2
                            )
                            $hexVal = [Convert]::ToInt32($hexStr, 16)
                            $null = $result.Append([char]$hexVal)
                            $script:luaPos += 2
                        } else {
                            throw 'Invalid \x escape sequence.'
                        }
                    }
                    'u' {
                        # \u{XXXX} - Unicode code point
                        $script:luaPos++
                        if ($script:luaPos -lt $script:luaString.Length -and
                            $script:luaString[$script:luaPos] -eq '{') {
                            $script:luaPos++
                            $hexStart = $script:luaPos
                            while ($script:luaPos -lt $script:luaString.Length -and
                                $script:luaString[$script:luaPos] -ne '}') {
                                $script:luaPos++
                            }
                            $hexStr = $script:luaString.Substring(
                                $hexStart,
                                $script:luaPos - $hexStart
                            )
                            $codePoint = [Convert]::ToInt32($hexStr, 16)
                            $null = $result.Append(
                                [char]::ConvertFromUtf32($codePoint)
                            )
                            $script:luaPos++ # skip }
                        } else {
                            throw 'Invalid \u escape sequence.'
                        }
                    }
                    "`n" {
                        $null = $result.Append("`n")
                        $script:luaPos++
                        if (
                            $script:luaPos -lt $script:luaString.Length -and
                            $script:luaString[$script:luaPos] -eq "`r"
                        ) {
                            $script:luaPos++
                        }
                    }
                    "`r" {
                        $null = $result.Append("`n")
                        $script:luaPos++
                        if (
                            $script:luaPos -lt $script:luaString.Length -and
                            $script:luaString[$script:luaPos] -eq "`n"
                        ) {
                            $script:luaPos++
                        }
                    }
                    'z' {
                        $script:luaPos++
                        while (
                            $script:luaPos -lt $script:luaString.Length -and
                            [char]::IsWhiteSpace($script:luaString[$script:luaPos])
                        ) {
                            $script:luaPos++
                        }
                    }
                    default {
                        # \ddd - decimal byte sequence (1-3 digits)
                        if ($nextChar -match '[0-9]') {
                            $numStr = $nextChar.ToString()
                            $script:luaPos++
                            for ($d = 0; $d -lt 2; $d++) {
                                if ($script:luaPos -lt $script:luaString.Length -and
                                    $script:luaString[$script:luaPos] -match '[0-9]') {
                                    $numStr += $script:luaString[$script:luaPos]
                                    $script:luaPos++
                                } else {
                                    break
                                }
                            }
                            $null = $result.Append([char][int]$numStr)
                        } else {
                            # Unknown escape - just pass through
                            $null = $result.Append($nextChar)
                            $script:luaPos++
                        }
                    }
                }
                continue
            }

            if ($char -eq $QuoteChar) {
                $script:luaPos++ # skip closing quote
                return $result.ToString()
            }

            $null = $result.Append($char)
            $script:luaPos++
        }

        throw 'Unterminated string literal.'
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaString] - Done"
#endregion [functions] - [private] - [Read-LuaString]
#region [functions] - [private] - [Read-LuaTable]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaTable] - Importing"
function Read-LuaTable {
    <#
        .SYNOPSIS
        Reads a Lua table constructor and returns an array, hashtable,
        or PSCustomObject.
    #>

    [OutputType([object[]])]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param()

    begin {}

    process {
        $script:luaCurrentDepth++
        if ($script:luaCurrentDepth -gt $script:luaMaxDepth) {
            throw "Maximum nesting depth ($($script:luaMaxDepth)) exceeded."
        }

        $script:luaPos++ # skip {
        Skip-LuaWhitespace

        $entries = [System.Collections.Generic.List[object]]::new()
        $arrayValues = [System.Collections.Generic.List[object]]::new()
        $hasStringKeys = $false
        $hasArrayValues = $false

        while ($script:luaPos -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -ne '}') {
            Skip-LuaWhitespace

            if ($script:luaPos -ge $script:luaString.Length -or
                $script:luaString[$script:luaPos] -eq '}') {
                break
            }

            # Check for bracket key: ["key"] = value or [expr] = value
            if ($script:luaString[$script:luaPos] -eq '[' -and
                $script:luaPos + 1 -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos + 1] -ne '[') {
                $script:luaPos++ # skip [
                Skip-LuaWhitespace
                $key = Read-LuaValue
                Skip-LuaWhitespace
                if ($script:luaPos -ge $script:luaString.Length -or
                    $script:luaString[$script:luaPos] -ne ']') {
                    throw "Expected ']' after bracket key in Lua table."
                }
                $script:luaPos++ # skip ]
                Skip-LuaWhitespace
                if ($script:luaPos -ge $script:luaString.Length -or
                    $script:luaString[$script:luaPos] -ne '=') {
                    throw "Expected '=' after bracket key in Lua table."
                }
                $script:luaPos++ # skip =
                Skip-LuaWhitespace
                $value = Read-LuaValue
                $entries.Add(@{ Key = [string]$key; Value = $value })
                $hasStringKeys = $true
            } elseif ($script:luaString[$script:luaPos] -match '[a-zA-Z_]') {
                # Check for identifier key: key = value
                $identStart = $script:luaPos
                while ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') {
                    $script:luaPos++
                }
                $ident = $script:luaString.Substring(
                    $identStart, $script:luaPos - $identStart
                )

                Skip-LuaWhitespace

                if ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -eq '=') {
                    # Key = value pair
                    $script:luaPos++ # skip =
                    Skip-LuaWhitespace
                    $value = Read-LuaValue
                    $entries.Add(@{
                            Key   = $ident
                            Value = $value
                        })
                    $hasStringKeys = $true
                } else {
                    # Bare identifier as keyword value
                    switch ($ident) {
                        'true' { $arrayValues.Add($true) }
                        'false' { $arrayValues.Add($false) }
                        'nil' { $arrayValues.Add($null) }
                        default {
                            throw "Unexpected bare identifier '$ident'."
                        }
                    }
                    $hasArrayValues = $true
                }
            } else {
                # Array value
                $value = Read-LuaValue
                $arrayValues.Add($value)
                $hasArrayValues = $true
            }

            Skip-LuaWhitespace

            # Lua requires a comma or semicolon between fields unless the next token is }
            if ($script:luaPos -lt $script:luaString.Length) {
                if ($script:luaString[$script:luaPos] -eq ',' -or
                    $script:luaString[$script:luaPos] -eq ';') {
                    $script:luaPos++
                } elseif ($script:luaString[$script:luaPos] -ne '}') {
                    throw "Expected ',', ';', or '}' in Lua table constructor."
                }
            }
        }

        if ($script:luaPos -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos] -eq '}') {
            $script:luaPos++ # skip }
        }

        $script:luaCurrentDepth--

        # Pure array (no string keys)
        if ($hasArrayValues -and -not $hasStringKeys) {
            return , [object[]]$arrayValues.ToArray()
        }

        # Empty table
        if (-not $hasArrayValues -and -not $hasStringKeys) {
            if ($script:luaAsPSCustomObject) {
                return [pscustomobject]@{}
            }
            return [ordered]@{}
        }

        # Build ordered hashtable (or PSCustomObject)
        $table = [ordered]@{}
        foreach ($entry in $entries) {
            $table[$entry.Key] = $entry.Value
        }
        # Mixed table: sequential values get integer keys starting at 1
        $arrayIndex = 1
        foreach ($val in $arrayValues) {
            $table[[string]$arrayIndex] = $val
            $arrayIndex++
        }

        if ($script:luaAsPSCustomObject) {
            return [pscustomobject]$table
        }
        return $table
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaTable] - Done"
#endregion [functions] - [private] - [Read-LuaTable]
#region [functions] - [private] - [Read-LuaValue]
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaValue] - Importing"
function Read-LuaValue {
    <#
        .SYNOPSIS
        Reads a single Lua value from the current parser position.
    #>

    [OutputType([object])]
    [OutputType([bool])]
    [OutputType([string])]
    [OutputType([int])]
    [OutputType([long])]
    [OutputType([double])]
    [CmdletBinding()]
    param()

    begin {}

    process {
        Skip-LuaWhitespace

        if ($script:luaPos -ge $script:luaString.Length) {
            return $null
        }

        $char = $script:luaString[$script:luaPos]

        # Table
        if ($char -eq '{') {
            return Read-LuaTable
        }

        # String (double-quoted)
        if ($char -eq '"') {
            return Read-LuaString -QuoteChar '"'
        }

        # String (single-quoted)
        if ($char -eq "'") {
            return Read-LuaString -QuoteChar "'"
        }

        # Multi-line string [[ ... ]] or [=[ ... ]=]
        if ($char -eq '[' -and
            $script:luaPos + 1 -lt $script:luaString.Length -and
            ($script:luaString[$script:luaPos + 1] -eq '[' -or
            $script:luaString[$script:luaPos + 1] -eq '=')) {
            return Read-LuaMultiLineString
        }

        # Number or negative number (including .5 style floats)
        if ($char -match '[0-9]' -or
            ($char -eq '.' -and
            $script:luaPos + 1 -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos + 1] -match '[0-9]') -or
            ($char -eq '-' -and
            $script:luaPos + 1 -lt $script:luaString.Length -and
            $script:luaString[$script:luaPos + 1] -match '[0-9.]')) {
            return Read-LuaNumber
        }

        # Keywords and bare identifiers
        if ($char -match '[a-zA-Z_]') {
            $identStart = $script:luaPos
            while ($script:luaPos -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -match '[a-zA-Z0-9_]') {
                $script:luaPos++
            }
            $ident = $script:luaString.Substring(
                $identStart,
                $script:luaPos - $identStart
            )

            switch ($ident) {
                'true' { return $true }
                'false' { return $false }
                'nil' { return $null }
                default {
                    throw "Unexpected bare identifier '$ident' at position $identStart."
                }
            }
        }

        throw "Unexpected character '$char' at position $($script:luaPos)."
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Read-LuaValue] - Done"
#endregion [functions] - [private] - [Read-LuaValue]
#region [functions] - [private] - [Skip-LuaWhitespace]
Write-Debug "[$scriptName] - [functions] - [private] - [Skip-LuaWhitespace] - Importing"
function Skip-LuaWhitespace {
    <#
        .SYNOPSIS
        Advances the parser position past whitespace and comments.
    #>

    [CmdletBinding()]
    param()

    begin {}

    process {
        while ($script:luaPos -lt $script:luaString.Length) {
            $char = $script:luaString[$script:luaPos]

            # Skip whitespace
            if ($char -match '\s') {
                $script:luaPos++
                continue
            }

            # Skip comments
            if ($script:luaPos + 1 -lt $script:luaString.Length -and
                $script:luaString[$script:luaPos] -eq '-' -and
                $script:luaString[$script:luaPos + 1] -eq '-') {
                $script:luaPos += 2

                # Multi-line comment --[[ ... ]] or --[=[ ... ]=] etc.
                if ($script:luaPos -lt $script:luaString.Length -and
                    $script:luaString[$script:luaPos] -eq '[') {
                    $eqStart = $script:luaPos + 1
                    $eqCount = 0
                    while ($eqStart + $eqCount -lt $script:luaString.Length -and
                        $script:luaString[$eqStart + $eqCount] -eq '=') {
                        $eqCount++
                    }
                    if ($eqStart + $eqCount -lt $script:luaString.Length -and
                        $script:luaString[$eqStart + $eqCount] -eq '[') {
                        # Valid long bracket comment opening
                        $script:luaPos = $eqStart + $eqCount + 1
                        $closePattern = ']' + ('=' * $eqCount) + ']'
                        $closeLen = $closePattern.Length
                        while ($script:luaPos -lt $script:luaString.Length) {
                            if ($script:luaPos + $closeLen - 1 -lt $script:luaString.Length -and
                                $script:luaString.Substring($script:luaPos, $closeLen) -eq $closePattern) {
                                $script:luaPos += $closeLen
                                break
                            }
                            $script:luaPos++
                        }
                    } else {
                        # Not a long bracket - treat as single-line comment
                        while ($script:luaPos -lt $script:luaString.Length -and
                            $script:luaString[$script:luaPos] -ne "`n") {
                            $script:luaPos++
                        }
                    }
                } else {
                    # Single-line comment
                    while ($script:luaPos -lt $script:luaString.Length -and
                        $script:luaString[$script:luaPos] -ne "`n") {
                        $script:luaPos++
                    }
                }
                continue
            }

            break
        }
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [private] - [Skip-LuaWhitespace] - Done"
#endregion [functions] - [private] - [Skip-LuaWhitespace]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [Lua]
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - Processing folder"
#region [functions] - [public] - [Lua] - [ConvertFrom-Lua]
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertFrom-Lua] - Importing"
function ConvertFrom-Lua {
    <#
        .SYNOPSIS
        Converts a Lua table constructor string to a PowerShell object.

        .DESCRIPTION
        Takes a Lua table constructor string and parses it into PowerShell objects.
        By default, Lua tables with string keys become PSCustomObjects and Lua
        sequences become arrays. Use -AsHashtable to get ordered hashtables instead.

        Supports the following Lua to PowerShell type mappings:
        - Lua table (key = value) -> [PSCustomObject] or [ordered] hashtable
        - Lua sequence (array) -> [object[]]
        - Lua double-quoted string -> [string]
        - Lua single-quoted string -> [string]
        - Lua multi-line string [[ ]] -> [string]
        - Lua number (integer) -> [int] or [long]
        - Lua number (float) -> [double]
        - Lua boolean (true/false) -> [bool]
        - nil -> $null
        - Single-line comments (--) -> Ignored
        - Multi-line comments (--[[ ]]) -> Ignored

        .EXAMPLE
        ```powershell
        '{ name = "Alice", age = 30 }' | ConvertFrom-Lua

        name age
        ---- ---
        Alice 30
        ```

        .EXAMPLE
        ```powershell
        ConvertFrom-Lua -InputObject '{ 1, 2, 3 }'

        1
        2
        3
        ```

        .EXAMPLE
        ```powershell
        '{ name = "Alice" }' | ConvertFrom-Lua -AsHashtable

        Name Value
        ---- -----
        name Alice
        ```

        .NOTES
        [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9)

        .LINK
        https://psmodule.io/Lua/Functions/ConvertFrom-Lua/

        .LINK
        https://www.lua.org/manual/5.4/manual.html#3.4.9
    #>

    [OutputType([object])]
    [OutputType([System.Array])]
    [CmdletBinding()]
    param(
        # The Lua table constructor string to convert to a PowerShell object.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string] $InputObject,

        # Output ordered hashtables instead of PSCustomObjects for Lua tables with string keys.
        [Parameter()]
        [switch] $AsHashtable,

        # Max nesting depth allowed in input. Throws a terminating error when exceeded.
        [Parameter()]
        [ValidateRange(0, 1024)]
        [int] $Depth = 1024,

        # Output arrays as a single object instead of enumerating elements through the pipeline.
        [Parameter()]
        [switch] $NoEnumerate
    )

    begin {}

    process {
        $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth
        if ($NoEnumerate) {
            , $result
        } else {
            $result
        }
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertFrom-Lua] - Done"
#endregion [functions] - [public] - [Lua] - [ConvertFrom-Lua]
#region [functions] - [public] - [Lua] - [ConvertTo-Lua]
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertTo-Lua] - Importing"
function ConvertTo-Lua {
    <#
        .SYNOPSIS
        Converts a PowerShell object to a Lua table constructor string.

        .DESCRIPTION
        Takes a PowerShell object (hashtable, PSCustomObject, array, or primitive value) and
        converts it to a Lua table constructor string representation. Nested structures are
        recursively converted with 4-space indentation.

        Supports the following type mappings:
        - [hashtable] / [ordered] -> Lua table with key = value pairs
        - [PSCustomObject] -> Lua table with key = value pairs
        - [array] -> Lua table (sequence)
        - [string] -> Lua double-quoted string with escape sequences
        - [int] / [long] -> Lua integer
        - [float] / [double] -> Lua float
        - [bool] -> Lua boolean (true/false)
        - $null -> omitted (nil means absent in Lua)

        .EXAMPLE
        ```powershell
        @{ name = "Alice"; age = 30 } | ConvertTo-Lua

        {
            age = 30,
            name = "Alice"
        }
        ```

        .EXAMPLE
        ```powershell
        ConvertTo-Lua -InputObject @(1, 2, 3) -Compress

        {1,2,3}
        ```

        .EXAMPLE
        ```powershell
        "hello" | ConvertTo-Lua -AsArray

        {
            "hello"
        }
        ```

        .NOTES
        [Lua 5.4 Reference Manual - Table Constructors](https://www.lua.org/manual/5.4/manual.html#3.4.9)

        .LINK
        https://psmodule.io/Lua/Functions/ConvertTo-Lua/

        .LINK
        https://www.lua.org/manual/5.4/manual.html#3.4.9
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The object to convert to a Lua table constructor string.
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [AllowNull()]
        [object] $InputObject,

        # Max recursion depth for nested object serialization. Emits a warning when exceeded.
        [Parameter()]
        [ValidateRange(0, 100)]
        [int] $Depth = 2,

        # Omit whitespace and indentation.
        [Parameter()]
        [switch] $Compress,

        # Serialize PowerShell enum values as their string name instead of numeric value.
        [Parameter()]
        [switch] $EnumsAsStrings,

        # Always wrap output in a Lua sequence table, even for a single value.
        [Parameter()]
        [switch] $AsArray
    )

    begin {}

    process {
        $objectToConvert = $InputObject
        if ($AsArray -and $InputObject -isnot [System.Collections.IList]) {
            $objectToConvert = @(, $InputObject)
        }
        ConvertTo-LuaTable -InputObject $objectToConvert `
            -CurrentDepth 0 `
            -MaxDepth $Depth `
            -Compress:$Compress `
            -EnumsAsStrings:$EnumsAsStrings
    }

    end {}
}
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - [ConvertTo-Lua] - Done"
#endregion [functions] - [public] - [Lua] - [ConvertTo-Lua]
Write-Debug "[$scriptName] - [functions] - [public] - [Lua] - Done"
#endregion [functions] - [public] - [Lua]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'ConvertFrom-Lua'
        'ConvertTo-Lua'
    )
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter