EventViewerTUI.ps1

<#PSScriptInfo
    .VERSION 1.0.9
    .GUID 6ab35451-d419-4ba3-ab5f-40e94ffeb34e
    .AUTHOR Robert
    .COMPANYNAME OAOA-DEV
    .COPYRIGHT
    .TAGS Big TUI Tools PowerShellToolkit
    .LICENSEURI
    .PROJECTURI https://oaoa.dev/tools
    .ICONURI
    .EXTERNALMODULEDEPENDENCIES
    .REQUIREDMODULES
    .EXTERNALSCRIPTDEPENDENCIES
    .RELEASENOTES
    .PRIVATEDATA
    .DESCRIPTION A console TUI application for Windows Event Viewer log inspection.
#>


<#
.SYNOPSIS
    Provides a text user interface (TUI) inside the PowerShell console to inspect event logs.
.DESCRIPTION
    A console TUI application for Windows Event Viewer log inspection.
.PARAMETER
    None
.EXAMPLE
    EventViewerTUI
#>


# --- TUI LAYOUT ENGINE MODULE FUNCTIONS ---
# Generic Reusable PowerShell Console TUI Library
# Author: Antigravity

$ESC = [char]27

# Global Layout Variables (defaults, dynamically updated on resize)
$global:leftWidth = 35
# mainHeight will be dynamically updated
$global:mainHeight = 25

# ANSI Color Utilities
function Get-ANSIColor($name) {
    switch ($name) {
        'Reset'            { "$ESC[0m" }
        'Bold'             { "$ESC[1m" }
        'Inverse'          { "$ESC[7m" }
        'SelectedActive'   { "$ESC[37;44m" } # White on Blue
        'SelectedInactive' { "$ESC[37;100m" } # White on Dark Gray
        'Error'            { "$ESC[91m" }    # Bright Red
        'Warning'          { "$ESC[93m" }    # Bright Yellow
        'Info'             { "$ESC[92m" }    # Bright Green
        'Gray'             { "$ESC[90m" }    # Dark Gray
        'White'            { "$ESC[97m" }    # White
        'Cyan'             { "$ESC[96m" }    # Cyan
        'Blue'             { "$ESC[94m" }    # Blue
        'Header'           { "$ESC[30;47m" } # Black on Light Gray
        'ErrorRow'         { "$ESC[37;41m" } # White on Red background
        'WarningRow'       { "$ESC[30;43m" } # Black on Yellow background
        'GreenRow'         { "$ESC[32m" }    # Green text
        default            { "$ESC[0m" }
    }
}

# Position the cursor
function Set-Cursor($x, $y) {
    [Console]::SetCursorPosition($x, $y)
}

# Write text at specific coordinates
function Write-At($x, $y, $text, $colorName = 'Reset') {
    $color = Get-ANSIColor $colorName
    $reset = Get-ANSIColor 'Reset'
    Set-Cursor $x $y
    [Console]::Write("$color$text$reset")
}

# Initialize console settings for TUI
function Initialize-Console {
    $global:originalCursorVisible = [Console]::CursorVisible
    $global:originalOutputEncoding = [Console]::OutputEncoding
    [Console]::CursorVisible = $false
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
    
    # Enter Alternate Screen Buffer to preserve user scrollback
    [Console]::Write("$ESC[?1049h")
    [Console]::Write("$ESC[?25l") # ANSI Hide Cursor
    [Console]::Write("$ESC[2J") # Clear entire screen
}

# Restore original console settings on exit
function Restore-Console {
    # Exit Alternate Screen Buffer
    [Console]::Write("$ESC[?1049l")
    [Console]::Write("$ESC[?25h") # ANSI Show Cursor
    [Console]::CursorVisible = $global:originalCursorVisible
    [Console]::OutputEncoding = $global:originalOutputEncoding
}

# Generic Borders Drawing
function Draw-Borders {
    param(
        [int]$Width,
        [int]$Height,
        [int]$LeftWidth,
        [int]$MainHeight,
        [int]$FocusArea,
        [string]$LeftTitle = "",
        [string]$RightTopTitle = "",
        [string]$RightBottomTitle = "",
        [bool]$HasVerticalDivider = $true,
        [bool]$HasHorizontalDivider = $true,
        [double]$RightTopWidthPercent = 1.0
    )
    
    $rightWidth = $Width - $LeftWidth - 4
    
    # 1. Draw top border
    $topBorder = "┌" + ("─" * $LeftWidth) + $(if ($HasVerticalDivider) { "┬" } else { "─" }) + ("─" * $rightWidth) + "┐"
    Write-At 0 1 $topBorder 'Gray'
    
    # 2. Draw middle lines
    for ($y = 2; $y -le ($Height - 3); $y++) {
        Write-At 0 $y "│" 'Gray'
        if ($HasVerticalDivider) {
            Write-At ($LeftWidth + 1) $y "│" 'Gray'
        }
        Write-At ($Width - 2) $y "│" 'Gray'
    }
    
    # 3. Draw horizontal divider between Table and Details (right side only, if exists)
    if ($HasHorizontalDivider) {
        $dividerStart = if ($HasVerticalDivider) { $LeftWidth + 1 } else { 0 }
        $rightDivider = $(if ($HasVerticalDivider) { "├" } else { "│" }) + ("─" * $rightWidth) + "┤"
        Write-At $dividerStart ($MainHeight + 1) $rightDivider 'Gray'
    }
    
    # 4. Draw bottom border
    $bottomBorder = "└" + ("─" * $LeftWidth) + $(if ($HasVerticalDivider) { "┴" } else { "─" }) + ("─" * $rightWidth) + "┘"
    Write-At 0 ($Height - 2) $bottomBorder 'Gray'
    
    # 5. Draw secondary vertical divider if RightTopWidthPercent is < 1.0 (for Resource Monitor graph)
    if ($HasVerticalDivider -and $RightTopWidthPercent -lt 1.0 -and $RightTopWidthPercent -gt 0.0) {
        $listWidth = [int]($rightWidth * $RightTopWidthPercent)
        $dividerX = $LeftWidth + 2 + $listWidth
        for ($y = 2; $y -le $MainHeight; $y++) {
            Write-At $dividerX $y "│" 'Gray'
        }
        Write-At $dividerX 1 "┬" 'Gray'
        if ($HasHorizontalDivider) {
            Write-At $dividerX ($MainHeight + 1) "┴" 'Gray'
        }
    }
    
    # 6. Draw highlighted titles to indicate focus
    if ($LeftTitle) {
        $lTitleText = if ($FocusArea -eq 0) { "● $LeftTitle ●" } else { " $LeftTitle " }
        $lTitleX = [int](($LeftWidth - $lTitleText.Length) / 2) + 1
        $lColor = if ($FocusArea -eq 0) { 'SelectedActive' } else { 'Header' }
        Write-At $lTitleX 1 $lTitleText $lColor
    }
    
    if ($RightTopTitle) {
        $rtTitleText = if ($FocusArea -eq 1) { "● $RightTopTitle ●" } else { " $RightTopTitle " }
        $rtWidth = if ($RightTopWidthPercent -lt 1.0) { [int]($rightWidth * $RightTopWidthPercent) } else { $rightWidth }
        $rtTitleX = $LeftWidth + 2 + [int](($rtWidth - $rtTitleText.Length) / 2)
        $rtColor = if ($FocusArea -eq 1) { 'SelectedActive' } else { 'Header' }
        Write-At $rtTitleX 1 $rtTitleText $rtColor
    }
    
    if ($RightBottomTitle -and $HasHorizontalDivider) {
        $rbTitleText = if ($FocusArea -eq 2) { "● $RightBottomTitle ●" } else { " $RightBottomTitle " }
        $rbTitleX = $LeftWidth + 2 + [int](($rightWidth - $rbTitleText.Length) / 2)
        $rbColor = if ($FocusArea -eq 2) { 'SelectedActive' } else { 'Header' }
        Write-At $rbTitleX ($MainHeight + 1) $rbTitleText $rbColor
    }
}

# Generic Menu Bar Drawing
function Draw-Menu {
    param(
        [int]$Width,
        [string[]]$Items
    )
    $menuColor = Get-ANSIColor 'Header'
    $reset = Get-ANSIColor 'Reset'
    
    $menuText = " " + ($Items -join " ")
    $paddedMenu = $menuText.PadRight($Width).Substring(0, $Width)
    Set-Cursor 0 0
    [Console]::Write("$menuColor$paddedMenu$reset")
}

# Generic Status Bar Drawing
function Draw-Status {
    param(
        [string]$StatusText,
        [string]$FocusAreaText,
        [string]$HelpText,
        [int]$Width,
        [int]$Height
    )
    $statusColor = Get-ANSIColor 'Header'
    $reset = Get-ANSIColor 'Reset'
    
    $focusPart = if ($FocusAreaText) { "[$FocusAreaText]" } else { "" }
    $helpPart = if ($HelpText) { $HelpText + " │ " } else { "" }
    
    $maxStatusLen = $Width - $focusPart.Length - $helpPart.Length - 10
    $leftText = $StatusText
    if ($leftText.Length -gt $maxStatusLen) {
        $leftText = $leftText.Substring(0, $maxStatusLen - 3) + "..."
    }
    
    $paddedStatusText = " " + $helpPart + $leftText
    # Pad status bar to exactly Width - 2 to prevent console host auto-scroll
    $remainingSpaces = ($Width - 2) - $paddedStatusText.Length - $focusPart.Length
    if ($remainingSpaces -lt 0) { $remainingSpaces = 0 }
    
    $fullStatus = $paddedStatusText + (" " * $remainingSpaces) + $focusPart
    if ($fullStatus.Length -gt ($Width - 2)) {
        $fullStatus = $fullStatus.Substring(0, $Width - 2)
    }
    
    Set-Cursor 0 ($Height - 1)
    [Console]::Write("$statusColor$fullStatus$reset")
}

# Recalculate dimensions
function Update-LayoutDimensions {
    param(
        [int]$Width,
        [int]$Height
    )
    $global:mainHeight = $Height - 16
    if ($global:mainHeight -lt 5) { $global:mainHeight = 5 }
    
    $global:leftWidth = 35
    $maxWidth = [int]($Width * 0.4)
    if ($global:leftWidth -gt $maxWidth) { $global:leftWidth = $maxWidth }
    if ($global:leftWidth -lt 15) { $global:leftWidth = 15 }
}

# --- TREE VIEW COMPONENTS ---
function Get-VisibleNodes($node) {
    $result = [System.Collections.ArrayList]::new()
    
    function Add-Node($n) {
        $result.Add($n) | Out-Null
        if (!$n.IsLeaf -and $n.IsExpanded -and $n.Children) {
            foreach ($child in $n.Children) {
                Add-Node $child
            }
        }
    }
    
    Add-Node $node
    return $result
}

function Draw-TreeView {
    param(
        [System.Collections.ArrayList]$VisibleNodes,
        [int]$SelectedIndex,
        [int]$ScrollOffset,
        [int]$LeftWidth,
        [int]$TreeHeight,
        [bool]$IsFocused = $true
    )
    $startX = 1
    $startY = 2
    $reset = Get-ANSIColor 'Reset'
    
    for ($i = 0; $i -lt $TreeHeight; $i++) {
        $nodeIndex = $ScrollOffset + $i
        $y = $startY + $i
        
        if ($nodeIndex -lt $VisibleNodes.Count) {
            $node = $VisibleNodes[$nodeIndex]
            $prefix = if ($node.IsLeaf) { " " } else { if ($node.IsExpanded) { "- " } else { "+ " } }
            
            $indent = " " * ($node.Level + 1)
            $displayText = $indent + $prefix + $node.Label
            
            if ($displayText.Length -gt $LeftWidth) {
                $displayText = $displayText.Substring(0, $LeftWidth - 3) + "..."
            }
            $paddedText = $displayText.PadRight($LeftWidth)
            
            if ($nodeIndex -eq $SelectedIndex) {
                $color = if ($IsFocused) { Get-ANSIColor 'SelectedActive' } else { Get-ANSIColor 'SelectedInactive' }
            } else {
                $isDisabled = $false
                $isHidden = $false
                if ($node -is [System.Collections.IDictionary]) {
                    if ($node.ContainsKey('IsDisabled')) { $isDisabled = $node['IsDisabled'] }
                    if ($node.ContainsKey('IsHidden')) { $isHidden = $node['IsHidden'] }
                } else {
                    try { $isDisabled = $node.IsDisabled } catch {}
                    try { $isHidden = $node.IsHidden } catch {}
                }
                
                if ($isDisabled) {
                    $color = Get-ANSIColor 'Error'
                } elseif ($isHidden) {
                    $color = Get-ANSIColor 'Gray'
                } else {
                    $color = Get-ANSIColor 'Reset'
                }
            }
            
            Write-At $startX $y $paddedText
            # Set highlight
            Set-Cursor $startX $y
            [Console]::Write("$color$paddedText$reset")
        } else {
            Write-At $startX $y (" " * $LeftWidth)
        }
    }
}

# --- GENERIC TABLE VIEW COMPONENTS ---

# Helper to format bytes
function Format-Bytes {
    param($bytes)
    if ($null -eq $bytes) { return "0 B" }
    try { $bytes = [double]$bytes } catch { return "0 B" }
    if ($bytes -ge 1GB) { return "{0:N2} GB" -f ($bytes / 1GB) }
    if ($bytes -ge 1MB) { return "{0:N2} MB" -f ($bytes / 1MB) }
    if ($bytes -ge 1KB) { return "{0:N2} KB" -f ($bytes / 1KB) }
    return "{0:N0} B" -f $bytes
}

# Helper to format date strings like "20260527" -> "2026-05-27"
function Format-DateString {
    param($str)
    if ($null -eq $str -or $str.Length -ne 8) { return $str }
    return "$($str.Substring(0,4))-$($str.Substring(4,2))-$($str.Substring(6,2))"
}

# Draw generic table header
function Draw-TableHeader {
    param(
        [int]$StartX,
        [int]$StartY,
        [array]$Columns, # @{ Label="Name"; Width=15; Align="Left" }
        [int]$Width
    )
    $reset = Get-ANSIColor 'Reset'
    $headerColor = Get-ANSIColor 'Header'
    
    $headerParts = @()
    foreach ($col in $Columns) {
        $label = $col.Label
        $w = $col.Width
        if ($label.Length -gt $w) { $label = $label.Substring(0, $w) }
        
        $padded = if ($col.Align -eq 'Right') { $label.PadLeft($w) } else { $label.PadRight($w) }
        $headerParts += $padded
    }
    $headerText = $headerParts -join "│"
    $paddedHeader = $headerText.PadRight($Width).Substring(0, $Width)
    
    Set-Cursor $startX $startY
    [Console]::Write("$headerColor$paddedHeader$reset")
}

# Draw generic table rows
function Draw-TableRows {
    param(
        [System.Collections.ArrayList]$Items,
        [int]$SelectedIndex,
        [int]$ScrollOffset,
        [array]$Columns,
        [int]$StartX,
        [int]$StartY,
        [int]$Width,
        [int]$Height,
        [bool]$IsFocused = $true,
        [scriptblock]$RowColorScript = $null
    )
    $reset = Get-ANSIColor 'Reset'
    
    for ($i = 0; $i -lt $Height; $i++) {
        $itemIndex = $ScrollOffset + $i
        $y = $startY + $i
        
        if ($itemIndex -lt $Items.Count) {
            $item = $Items[$itemIndex]
            $rowParts = @()
            
            foreach ($col in $Columns) {
                $val = $null
                if ($null -ne $item) {
                    if ($item -is [hashtable]) {
                        $val = $item[$col.Prop]
                    } else {
                        $val = $item.$($col.Prop)
                    }
                }
                if ($null -eq $val) { $val = "" }
                
                # Format cell values
                $formattedVal = switch ($col.Format) {
                    'Percent'     { "{0:F1} %" -f $val }
                    'MB'          { "{0:N1} MB" -f ($val / 1MB) }
                    'Bytes'       { Format-Bytes $val }
                    'Decimal'     { "{0:F1}" -f $val }
                    'DateTime'    { if ($val -is [DateTime]) { $val.ToString("yyyy-MM-dd HH:mm:ss") } else { $val.ToString() } }
                    'RegistryDate'{ Format-DateString $val }
                    default       { $val.ToString() }
                }
                
                # Strip control characters to prevent cursor shifting
                $formattedVal = $formattedVal -replace "`r", "" -replace "`n", "" -replace "`t", " "
                
                $w = $col.Width
                if ($formattedVal.Length -gt $w) {
                    $formattedVal = if ($w -gt 2) { $formattedVal.Substring(0, $w - 2) + ".." } else { $formattedVal.Substring(0, $w) }
                }
                
                $padded = if ($col.Align -eq 'Right') { $formattedVal.PadLeft($w) } else { $formattedVal.PadRight($w) }
                $rowParts += $padded
            }
            
            $rowText = $rowParts -join "│"
            
            # Select background based on active selection
            if ($itemIndex -eq $SelectedIndex) {
                $color = if ($IsFocused) { Get-ANSIColor 'SelectedActive' } else { Get-ANSIColor 'SelectedInactive' }
            } else {
                if ($null -ne $RowColorScript) {
                    $colorName = & $RowColorScript $item
                    $color = Get-ANSIColor $colorName
                } else {
                    $color = Get-ANSIColor 'Reset'
                }
            }
            
            $paddedRow = $rowText.PadRight($Width).Substring(0, $Width)
            Set-Cursor $StartX $y
            [Console]::Write("$color$paddedRow$reset")
        } else {
            # Empty row
            Set-Cursor $StartX $y
            [Console]::Write(" " * $Width)
        }
    }
}

# --- DETAILS VIEW COMPONENTS ---
function Get-DetailPropertyLine($lbl1, $val1, $lbl2, $val2, $colWidth) {
    $lblW = 12
    $valW = $colWidth - $lblW
    if ($valW -lt 5) { $valW = 5 }
    
    $lbl1Str = $lbl1.PadRight($lblW).Substring(0, $lblW)
    $val1Str = if ($null -ne $val1) { $val1.ToString() } else { "" }
    if ($val1Str.Length -gt $valW) {
        $val1Str = $val1Str.Substring(0, $valW - 3) + "..."
    }
    $col1Text = ($lbl1Str + $val1Str).PadRight($colWidth)
    
    $lbl2Str = $lbl2.PadRight($lblW).Substring(0, $lblW)
    $val2Str = if ($null -ne $val2) { $val2.ToString() } else { "" }
    if ($val2Str.Length -gt $valW) {
        $val2Str = $val2Str.Substring(0, $valW - 3) + "..."
    }
    $col2Text = ($lbl2Str + $val2Str).PadRight($colWidth)
    
    return "$col1Text │ $col2Text"
}

# Wrap text lines helper
function Wrap-Text($text, $maxLength) {
    if ([string]::IsNullOrEmpty($text)) {
        return @("")
    }
    
    $paragraphs = $text.Split(@("`r`n", "`n"), [System.StringSplitOptions]::None)
    $lines = [System.Collections.ArrayList]::new()
    
    foreach ($para in $paragraphs) {
        if ($para.Length -le $maxLength) {
            $lines.Add($para) | Out-Null
            continue
        }
        
        $words = $para.Split(' ')
        $currentLine = ""
        
        foreach ($word in $words) {
            if ($currentLine.Length -eq 0) {
                $currentLine = $word
            } elseif (($currentLine.Length + 1 + $word.Length) -le $maxLength) {
                $currentLine += " " + $word
            } else {
                $lines.Add($currentLine) | Out-Null
                $currentLine = $word
            }
        }
        if ($currentLine.Length -gt 0) {
            $lines.Add($currentLine) | Out-Null
        }
    }
    
    return $lines
}

function Draw-Details {
    param(
        [array]$HeaderLines,
        [array]$MessageLines,
        [int]$ScrollOffset,
        [int]$StartX,
        [int]$StartY,
        [int]$Width,
        [int]$Height
    )
    
    $hasHeader = ($null -ne $HeaderLines -and $HeaderLines.Count -gt 0)
    
    if ($hasHeader) {
        # Draw the header lines (up to 4)
        $headerCount = $HeaderLines.Count
        for ($i = 0; $i -lt 4; $i++) {
            $y = $StartY + $i
            $lineText = if ($i -lt $headerCount) { $HeaderLines[$i] } else { "" }
            if ($lineText.Length -gt $Width) {
                $lineText = $lineText.Substring(0, $Width)
            }
            $lineText = $lineText -replace "`r", "" -replace "`n", "" -replace "`t", " "
            Set-Cursor $StartX $y
            [Console]::Write($lineText.PadRight($Width))
        }
        
        # Draw divider line
        $yDivider = $StartY + 4
        $reset = Get-ANSIColor 'Reset'
        $gray = Get-ANSIColor 'Gray'
        Set-Cursor $StartX $yDivider
        [Console]::Write("$gray" + ("─" * $Width) + "$reset")
        
        $scrollAreaHeight = $Height - 5
        $bodyStartY = $StartY + 5
    } else {
        $scrollAreaHeight = $Height
        $bodyStartY = $StartY
    }
    
    # Draw scrollable body message lines
    $messageCount = if ($null -eq $MessageLines) { 0 } else { $MessageLines.Count }
    for ($i = 0; $i -lt $scrollAreaHeight; $i++) {
        $lineIndex = $ScrollOffset + $i
        $y = $bodyStartY + $i
        
        $lineText = if ($lineIndex -lt $messageCount) { $MessageLines[$lineIndex] } else { "" }
        if ($lineText.Length -gt $Width) {
            $lineText = $lineText.Substring(0, $Width)
        }
        $lineText = $lineText -replace "`r", "" -replace "`n", "" -replace "`t", " "
        Set-Cursor $StartX $y
        [Console]::Write($lineText.PadRight($Width))
    }
    
    # Fill remaining space
    for ($y = $bodyStartY + $scrollAreaHeight; $y -lt $StartY + $Height; $y++) {
        Set-Cursor $StartX $y
        [Console]::Write(" " * $Width)
    }
}

# --- TEXT DIALOG INPUT BOX ---
function Show-InputBox {
    param(
        [string]$Prompt,
        [string]$Title,
        [int]$Width,
        [int]$Height
    )
    $boxW = 60
    $boxH = 5
    $boxX = [int](($Width - $boxW) / 2)
    $boxY = [int](($Height - $boxH) / 2)
    
    $reset = Get-ANSIColor 'Reset'
    $borderC = Get-ANSIColor 'White'
    $titleC = Get-ANSIColor 'SelectedActive'
    
    # 1. Draw outer frames of the box
    $topB = "╔" + ("═" * ($boxW - 2)) + "╗"
    $midB = "║" + (" " * ($boxW - 2)) + "║"
    $botB = "╚" + ("═" * ($boxW - 2)) + "╝"
    
    Write-At $boxX $boxY $topB 'White'
    Write-At $boxX ($boxY + 1) $midB 'White'
    Write-At $boxX ($boxY + 2) $midB 'White'
    Write-At $boxX ($boxY + 3) $midB 'White'
    Write-At $boxX ($boxY + 4) $botB 'White'
    
    # Draw title
    $titleText = " $Title "
    $titleX = $boxX + [int](($boxW - $titleText.Length) / 2)
    Write-At $titleX $boxY $titleText 'SelectedActive'
    
    # Draw prompt text
    Write-At ($boxX + 2) ($boxY + 1) $Prompt
    
    $inputText = ""
    $inputX = $boxX + 2
    $inputY = $boxY + 2
    $maxInputLen = $boxW - 4
    
    # Temporarily show cursor
    [Console]::Write("$ESC[?25h")
    [Console]::CursorVisible = $true
    
    while ($true) {
        # Render current text
        Set-Cursor $inputX $inputY
        $displayText = $inputText
        if ($displayText.Length -gt $maxInputLen) {
            $displayText = "..." + $displayText.Substring($displayText.Length - $maxInputLen + 3)
        }
        [Console]::Write($displayText.PadRight($maxInputLen))
        
        $cursorX = $inputX + [Math]::Min($displayText.Length, $maxInputLen)
        Set-Cursor $cursorX $inputY
        
        $key = [Console]::ReadKey($true)
        
        if ($key.Key -eq 'Enter') {
            break
        }
        if ($key.Key -eq 'Escape') {
            $inputText = $null
            break
        }
        if ($key.Key -eq 'Backspace') {
            if ($inputText.Length -gt 0) {
                $inputText = $inputText.Substring(0, $inputText.Length - 1)
            }
        } else {
            if ($key.KeyChar -ge 32 -and $key.KeyChar -le 126) {
                $inputText += $key.KeyChar
            }
        }
    }
    
    [Console]::Write("$ESC[?25l")
    [Console]::CursorVisible = $false
    return $inputText
}

# --- SCROLLABLE CHECKLIST DIALOG WITH SEARCH FILTERING ---
function Show-CheckListDialog {
    param(
        [string]$Prompt,
        [string]$Title,
        [System.Collections.ArrayList]$Items, # Array of @{ Label = 'Name'; Checked = $true/$false }
        [int]$Width,
        [int]$Height,
        [switch]$ReadOnly,
        [switch]$HideSearch
    )
    
    $boxW = 80
    if ($boxW -gt ($Width - 4)) { $boxW = $Width - 4 }
    if ($boxW -lt 40) { $boxW = 40 }
    
    $boxH = 18
    $boxX = [int](($Width - $boxW) / 2)
    if ($boxX -lt 0) { $boxX = 0 }
    
    $boxY = [int](($Height - $boxH) / 2)
    if ($boxY -lt 1) { $boxY = 1 }
    
    $reset = Get-ANSIColor 'Reset'
    
    $topB = "╔" + ("═" * ($boxW - 2)) + "╗"
    $midB = "║" + (" " * ($boxW - 2)) + "║"
    $botB = "╚" + ("═" * ($boxW - 2)) + "╝"
    
    $searchQuery = ""
    $selectedIndex = 0
    $scrollOffset = 0
    $listHeight = if ($HideSearch) { $boxH - 5 } else { $boxH - 8 }
    $focusedControl = if ($HideSearch) { 'List' } else { 'Search' }
    
    while ($true) {
        # 1. Draw outer frame
        Write-At $boxX $boxY $topB 'White'
        for ($i = 1; $i -lt ($boxH - 1); $i++) {
            Write-At $boxX ($boxY + $i) $midB 'White'
        }
        Write-At $boxX ($boxY + $boxH - 1) $botB 'White'
        
        # Title
        $titleText = " $Title "
        $titleX = $boxX + [int](($boxW - $titleText.Length) / 2)
        Write-At $titleX $boxY $titleText 'SelectedActive'
        
        # Prompt
        Write-At ($boxX + 2) ($boxY + 1) $Prompt
        
        if (-not $HideSearch) {
            # Search Box label and input field
            $searchLabel = "Search Filter: "
            $inputW = $boxW - 22
            if ($inputW -lt 10) { $inputW = 10 }
            
            $dispQuery = $searchQuery
            if ($dispQuery.Length -gt ($inputW - 2)) {
                $dispQuery = $dispQuery.Substring($dispQuery.Length - ($inputW - 2))
            }
            
            if ($focusedControl -eq 'Search') {
                Write-At ($boxX + 2) ($boxY + 2) $searchLabel 'White'
                $fieldVal = " $dispQuery" + "_"
                $fieldVal = $fieldVal.PadRight($inputW)
                Write-At ($boxX + 17) ($boxY + 2) $fieldVal 'SelectedActive'
            } else {
                Write-At ($boxX + 2) ($boxY + 2) $searchLabel 'Gray'
                $fieldVal = " $dispQuery"
                $fieldVal = $fieldVal.PadRight($inputW)
                Write-At ($boxX + 17) ($boxY + 2) $fieldVal 'SelectedInactive'
            }
            Write-At ($boxX + 2) ($boxY + 3) ("─" * ($boxW - 4)) 'Gray'
        }
        
        # Filter items based on search query
        $filteredItems = [System.Collections.ArrayList]::new()
        foreach ($item in $Items) {
            if ([string]::IsNullOrEmpty($searchQuery) -or $item.Label.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
                $filteredItems.Add($item) | Out-Null
            }
        }
        
        # Bounds check
        if ($selectedIndex -ge $filteredItems.Count) {
            $selectedIndex = [Math]::Max(0, $filteredItems.Count - 1)
        }
        if ($selectedIndex -lt 0) { $selectedIndex = 0 }
        
        if ($selectedIndex -lt $scrollOffset) {
            $scrollOffset = $selectedIndex
        }
        if ($selectedIndex -ge ($scrollOffset + $listHeight)) {
            $scrollOffset = $selectedIndex - $listHeight + 1
        }
        if ($scrollOffset -lt 0) { $scrollOffset = 0 }
        
        # Draw Checklist items
        for ($i = 0; $i -lt $listHeight; $i++) {
            $itemIdx = $scrollOffset + $i
            $y = if ($HideSearch) { $boxY + 2 + $i } else { $boxY + 4 + $i }
            
            if ($itemIdx -lt $filteredItems.Count) {
                $item = $filteredItems[$itemIdx]
                $text = if ($ReadOnly) {
                    " $($item.Label) "
                } else {
                    $chk = if ($item.Checked) { "[x]" } else { "[ ]" }
                    " $chk $($item.Label) "
                }
                if ($text.Length -gt ($boxW - 6)) {
                    $text = $text.Substring(0, $boxW - 9) + "..."
                }
                $paddedText = $text.PadRight($boxW - 6)
                
                if ($itemIdx -eq $selectedIndex) {
                    $colorName = if ($focusedControl -eq 'List') { 'SelectedActive' } else { 'SelectedInactive' }
                    $color = Get-ANSIColor $colorName
                } else {
                    $color = Get-ANSIColor 'Reset'
                }
                
                Set-Cursor ($boxX + 3) $y
                [Console]::Write("$color$paddedText$reset")
            } else {
                Write-At ($boxX + 3) $y (" " * ($boxW - 6))
            }
        }
        
        Write-At ($boxX + 2) ($boxY + $boxH - 3) ("─" * ($boxW - 4)) 'Gray'
        
        # Show shortcut guidelines dynamically based on focus
        if ($focusedControl -eq 'Search') {
            $shortcuts = " [Tab] List | [Backspace] Delete | [Enter] OK | [Esc] Cancel"
        } else {
            $shortcutParts = @()
            if (-not $HideSearch) { $shortcutParts += "[Tab] Search" }
            if (-not $ReadOnly) { $shortcutParts += "[Space] Toggle" }
            $shortcutParts += "[Arrows] Move"
            $shortcutParts += "[Enter] OK"
            $shortcutParts += "[Esc] Cancel"
            $shortcuts = " " + ($shortcutParts -join " | ")
        }
        $paddedShortcuts = $shortcuts.PadRight($boxW - 4)
        Write-At ($boxX + 2) ($boxY + $boxH - 2) $paddedShortcuts 'Header'
        
        # Read Key
        $key = [Console]::ReadKey($true)
        
        if ($key.Key -eq 'Escape') {
            return $null
        }
        if ($key.Key -eq 'Enter') {
            return $Items
        }
        if ($key.Key -eq 'Tab') {
            if (-not $HideSearch) {
                if ($focusedControl -eq 'Search') {
                    $focusedControl = 'List'
                } else {
                    $focusedControl = 'Search'
                }
            }
        }
        elseif ($focusedControl -eq 'Search') {
            if ($key.Key -eq 'Backspace') {
                if ($searchQuery.Length -gt 0) {
                    $searchQuery = $searchQuery.Substring(0, $searchQuery.Length - 1)
                    $selectedIndex = 0
                    $scrollOffset = 0
                }
            }
            elseif ($key.Key -eq 'Space') {
                $searchQuery += " "
                $selectedIndex = 0
                $scrollOffset = 0
            }
            else {
                if ($key.KeyChar -ge 32 -and $key.KeyChar -le 126) {
                    $searchQuery += $key.KeyChar
                    $selectedIndex = 0
                    $scrollOffset = 0
                }
            }
        }
        elseif ($focusedControl -eq 'List') {
            if ($key.Key -eq 'Space') {
                if (-not $ReadOnly) {
                    if ($filteredItems.Count -gt 0) {
                        $selectedItem = $filteredItems[$selectedIndex]
                        $selectedItem.Checked = -not $selectedItem.Checked
                    }
                }
            }
            elseif ($key.Key -eq 'UpArrow') {
                if ($selectedIndex -gt 0) { $selectedIndex-- }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($selectedIndex -lt ($filteredItems.Count - 1)) { $selectedIndex++ }
            }
            elseif ($key.Key -eq 'PageUp') {
                $selectedIndex = [Math]::Max(0, $selectedIndex - $listHeight)
            }
            elseif ($key.Key -eq 'PageDown') {
                $selectedIndex = [Math]::Min($filteredItems.Count - 1, $selectedIndex + $listHeight)
            }
        }
    }
}

# --- LIST SELECTION DIALOG (NO SEARCH) ---
function Show-ListSelectionDialog {
    param(
        [string]$Prompt,
        [string]$Title,
        [System.Collections.ArrayList]$Items, # Array of strings or PSCustomObjects with a Label property
        [int]$SelectedIndex = 0,
        [int]$Width,
        [int]$Height
    )
    $boxW = 50
    $boxH = 6 + $Items.Count
    if ($boxH -lt 8) { $boxH = 8 }
    $boxX = [int]((($Width - $boxW) / 2))
    $boxY = [int]((($Height - $boxH) / 2))
    if ($boxX -lt 0) { $boxX = 0 }
    if ($boxY -lt 1) { $boxY = 1 }
    
    $reset = Get-ANSIColor 'Reset'
    
    while ($true) {
        # Draw outer frames of the box
        $topB = "╔" + ("═" * ($boxW - 2)) + "╗"
        $midB = "║" + (" " * ($boxW - 2)) + "║"
        $botB = "╚" + ("═" * ($boxW - 2)) + "╝"
        
        Write-At $boxX $boxY $topB 'White'
        for ($i = 1; $i -lt ($boxH - 1); $i++) {
            Write-At $boxX ($boxY + $i) $midB 'White'
        }
        Write-At $boxX ($boxY + $boxH - 1) $botB 'White'
        
        # Draw title
        $titleText = " $Title "
        $titleX = $boxX + [int](($boxW - $titleText.Length) / 2)
        Write-At $titleX $boxY $titleText 'SelectedActive'
        
        # Draw prompt text
        Write-At ($boxX + 2) ($boxY + 1) $Prompt
        Write-At ($boxX + 2) ($boxY + 2) ("─" * ($boxW - 4)) 'Gray'
        
        # Draw items
        for ($i = 0; $i -lt $Items.Count; $i++) {
            $item = $Items[$i]
            $itemLabel = if ($item -is [PSCustomObject] -or $item -is [hashtable]) { $item.Label } else { $item }
            $text = " $itemLabel "
            if ($text.Length -gt ($boxW - 6)) {
                $text = $text.Substring(0, $boxW - 9) + "..."
            }
            $paddedText = $text.PadRight($boxW - 6)
            
            $y = $boxY + 3 + $i
            if ($i -eq $SelectedIndex) {
                Write-At ($boxX + 3) $y $paddedText 'SelectedActive'
            } else {
                Write-At ($boxX + 3) $y $paddedText 'Reset'
            }
        }
        
        Write-At ($boxX + 2) ($boxY + $boxH - 3) ("─" * ($boxW - 4)) 'Gray'
        
        # Draw help bar at the bottom
        $shortcuts = " [Arrows] Move │ [Enter] Select │ [Esc] Cancel"
        $paddedShortcuts = $shortcuts.PadRight($boxW - 4)
        Write-At ($boxX + 2) ($boxY + $boxH - 2) $paddedShortcuts 'Header'
        
        $key = [Console]::ReadKey($true)
        if ($key.Key -eq 'Escape') {
            return $null
        }
        if ($key.Key -eq 'Enter') {
            return $SelectedIndex
        }
        if ($key.Key -eq 'UpArrow') {
            if ($SelectedIndex -gt 0) { $SelectedIndex-- }
        }
        elseif ($key.Key -eq 'DownArrow') {
            if ($SelectedIndex -lt ($Items.Count - 1)) { $SelectedIndex++ }
        }
    }
}

# Export functions

# --- MAIN UTILITY DRIVER EXECUTION ---
# PowerShell TUI Event Viewer Main Driver
# Run this script to start the Event Viewer console interface.
# Author: Antigravity

$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path

# Helper to map levels
function Get-EventLevelText($level) {
    switch ($level) {
        1 { "● Critical" }
        2 { "● Error" }
        3 { "▲ Warning" }
        4 { "○ Info" }
        5 { "○ Verbose" }
        0 { "○ LogAlways" }
        default { "○ Info" }
    }
}

# --- TREE CONSTRUCTION LOGIC ---
function Build-EventLogTree($logNames) {
    $root = @{
        Id = 'Root'
        Label = 'Event Viewer (Local)'
        IsLeaf = $false
        IsExpanded = $true
        Children = [System.Collections.ArrayList]::new()
        Level = -1
    }
    
    $winLogsFolder = @{
        Id = 'WindowsLogs'
        Label = 'Windows Logs'
        IsLeaf = $false
        IsExpanded = $true
        Children = [System.Collections.ArrayList]::new()
        Level = 0
        Parent = $root
    }
    $root.Children.Add($winLogsFolder) | Out-Null
    
    $appLogsFolder = @{
        Id = 'AppLogs'
        Label = 'Applications and Services Logs'
        IsLeaf = $false
        IsExpanded = $false
        Children = [System.Collections.ArrayList]::new()
        Level = 0
        Parent = $root
    }
    $root.Children.Add($appLogsFolder) | Out-Null
    
    $microsoftFolder = @{
        Id = 'Microsoft'
        Label = 'Microsoft'
        IsLeaf = $false
        IsExpanded = $false
        Children = [System.Collections.ArrayList]::new()
        Level = 1
        Parent = $appLogsFolder
    }
    
    $windowsFolder = @{
        Id = 'Windows'
        Label = 'Windows'
        IsLeaf = $false
        IsExpanded = $false
        Children = [System.Collections.ArrayList]::new()
        Level = 2
        Parent = $microsoftFolder
    }
    $microsoftFolder.Children.Add($windowsFolder) | Out-Null
    
    $classicLogs = @('Application', 'Security', 'System', 'Setup', 'ForwardedEvents')
    
    foreach ($logName in $logNames) {
        if ($classicLogs -contains $logName) {
            $node = @{
                Id = $logName
                Label = $logName
                IsLeaf = $true
                LogName = $logName
                Level = 1
                Parent = $winLogsFolder
            }
            $winLogsFolder.Children.Add($node) | Out-Null
            continue
        }
        
        if ($logName.StartsWith('Microsoft-Windows-', [System.StringComparison]::OrdinalIgnoreCase)) {
            if (-not ($appLogsFolder.Children | Where-Object { $_.Id -eq 'Microsoft' })) {
                $appLogsFolder.Children.Add($microsoftFolder) | Out-Null
            }
            
            $subPath = $logName.Substring(18)
            $parts = $subPath.Split('/')
            $folderName = $parts[0]
            
            $catFolder = $windowsFolder.Children | Where-Object { $_.Label -eq $folderName }
            if ($null -eq $catFolder) {
                $catFolder = @{
                    Id = "MS-Win-$folderName"
                    Label = $folderName
                    IsLeaf = $false
                    IsExpanded = $false
                    Children = [System.Collections.ArrayList]::new()
                    Level = 3
                    Parent = $windowsFolder
                }
                $windowsFolder.Children.Add($catFolder) | Out-Null
            }
            
            if ($parts.Length -gt 1) {
                $channelName = $parts[1]
                $node = @{
                    Id = $logName
                    Label = $channelName
                    IsLeaf = $true
                    LogName = $logName
                    Level = 4
                    Parent = $catFolder
                }
                $catFolder.Children.Add($node) | Out-Null
            } else {
                $catFolder.IsLeaf = $true
                $catFolder.LogName = $logName
            }
            continue
        }
        
        $parts = $logName.Split('/')
        if ($parts.Length -gt 1) {
            $folderName = $parts[0]
            $channelName = $parts[1]
            
            $folder = $appLogsFolder.Children | Where-Object { $_.Label -eq $folderName }
            if ($null -eq $folder) {
                $folder = @{
                    Id = "AppLog-$folderName"
                    Label = $folderName
                    IsLeaf = $false
                    IsExpanded = $false
                    Children = [System.Collections.ArrayList]::new()
                    Level = 1
                    Parent = $appLogsFolder
                }
                $appLogsFolder.Children.Add($folder) | Out-Null
            }
            
            $node = @{
                Id = $logName
                Label = $channelName
                IsLeaf = $true
                LogName = $logName
                Level = 2
                Parent = $folder
            }
            $folder.Children.Add($node) | Out-Null
        } else {
            $node = @{
                Id = $logName
                Label = $logName
                IsLeaf = $true
                LogName = $logName
                Level = 1
                Parent = $appLogsFolder
            }
            $appLogsFolder.Children.Add($node) | Out-Null
        }
    }
    
    function Sort-NodeChildren($n) {
        if ($n.Children -and $n.Children.Count -gt 0) {
            $sorted = $n.Children | Sort-Object { $_.IsLeaf }, { $_.Label }
            $n.Children.Clear()
            foreach ($child in $sorted) {
                $n.Children.Add($child) | Out-Null
                Sort-NodeChildren $child
            }
        }
    }
    
    Sort-NodeChildren $root
    
    if ($appLogsFolder.Children.Count -eq 0) {
        $root.Children.Remove($appLogsFolder) | Out-Null
    }
    
    return $root
}

# --- EVENT LOADING ENGINE ---
function Load-LogEvents($logName, $filterLevels, $filterIds, $filterSources, $maxEvents = 1000) {
    $xpathTerms = @()
    
    # Levels (Critical = 1, Error = 2, Warning = 3, Information = 4, Verbose = 5)
    if ($null -ne $filterLevels -and $filterLevels.Count -gt 0) {
        $levelQueries = @()
        foreach ($lvl in $filterLevels) {
            $levelQueries += "System/Level=$lvl"
        }
        $xpathTerms += "(" + ($levelQueries -join " or ") + ")"
    }
    
    # Event IDs (comma-separated or single)
    if ($null -ne $filterIds -and $filterIds -ne "") {
        $idQueries = @()
        foreach ($id in $filterIds.Split(',')) {
            $trimmedId = $id.Trim()
            if ($trimmedId -match '^\d+$') {
                $idQueries += "System/EventID=$trimmedId"
            }
        }
        if ($idQueries.Count -gt 0) {
            $xpathTerms += "(" + ($idQueries -join " or ") + ")"
        }
    }
    
    # Sources
    if ($null -ne $filterSources -and $filterSources.Count -gt 0) {
        $srcQueries = @()
        foreach ($src in $filterSources) {
            $srcQueries += "System/Provider[@Name='$src']"
        }
        $xpathTerms += "(" + ($srcQueries -join " or ") + ")"
    }
    
    $xpath = "*"
    if ($xpathTerms.Count -gt 0) {
        $xpath = "*[ " + ($xpathTerms -join " and ") + " ]"
    }
    
    $events = [System.Collections.ArrayList]::new()
    try {
        $query = New-Object System.Diagnostics.Eventing.Reader.EventLogQuery($logName, [System.Diagnostics.Eventing.Reader.PathType]::LogName, $xpath)
        $query.ReverseDirection = $true
        $reader = New-Object System.Diagnostics.Eventing.Reader.EventLogReader($query)
        
        for ($i = 0; $i -lt $maxEvents; $i++) {
            $ev = $reader.ReadEvent()
            if ($null -eq $ev) { break }
            
            # Format description safely ahead of time for table
            $levelText = Get-EventLevelText $ev.Level
            $date = if ($null -ne $ev.TimeCreated) { $ev.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss") } else { "" }
            $source = if ($null -eq $ev.ProviderName) { "" } else { $ev.ProviderName }
            $category = if ($null -ne $ev.TaskDisplayName) { $ev.TaskDisplayName } elseif ($null -ne $ev.Task) { $ev.Task.ToString() } else { "None" }
            
            $mapped = [PSCustomObject]@{
                RawEvent = $ev
                Level = $ev.Level
                LevelText = $levelText
                FormattedDate = $date
                ProviderName = $source
                Id = $ev.Id
                TaskDisplayName = $category
            }
            $events.Add($mapped) | Out-Null
        }
    } catch {
        # Gracefully handle exceptions
    }
    
    return $events
}

# --- DETAIL PANEL FORMATTING ---
function Format-EventDetailsMapped($mappedItem, $width) {
    $colWidth = [int](($width - 4) / 2)
    if ($null -eq $mappedItem) {
        return [PSCustomObject]@{
            HeaderLines = @(
                (Get-DetailPropertyLine "Log Name:" "N/A" "Date:" "N/A" $colWidth),
                (Get-DetailPropertyLine "Source:" "N/A" "Event ID:" "N/A" $colWidth),
                (Get-DetailPropertyLine "Level:" "N/A" "User:" "N/A" $colWidth),
                (Get-DetailPropertyLine "Computer:" "N/A" "Category:" "N/A" $colWidth)
            )
            MessageLines = @("No event selected.")
        }
    }
    
    $ev = $mappedItem.RawEvent
    $logName = $ev.LogName
    if ($null -eq $logName) { $logName = "N/A" }
    
    $source = $mappedItem.ProviderName
    $date = $mappedItem.FormattedDate
    $eventId = $mappedItem.Id.ToString()
    $level = $mappedItem.LevelText
    $category = $mappedItem.TaskDisplayName
    
    # Resolve user principal safely
    $user = "N/A"
    if ($null -ne $ev.UserId) {
        try {
            $user = $ev.UserId.Translate([System.Security.Principal.SecurityIdentifier]).Value
        } catch {
            $user = $ev.UserId.ToString()
        }
    }
    
    $computer = $ev.MachineName
    if ($null -eq $computer) { $computer = "N/A" }
    
    $headerLines = [System.Collections.ArrayList]::new()
    $headerLines.Add((Get-DetailPropertyLine "Log Name:" $logName "Date:" $date $colWidth)) | Out-Null
    $headerLines.Add((Get-DetailPropertyLine "Source:" $source "Event ID:" $eventId $colWidth)) | Out-Null
    $headerLines.Add((Get-DetailPropertyLine "Level:" $level "User:" $user $colWidth)) | Out-Null
    $headerLines.Add((Get-DetailPropertyLine "Computer:" $computer "Category:" $category $colWidth)) | Out-Null
    
    # Event Description Message
    $msg = ""
    try {
        $msg = $ev.FormatDescription()
    } catch { }
    if ([string]::IsNullOrEmpty($msg)) {
        $msg = "Description could not be loaded. (The event source resource DLL may not be registered)"
        try {
            if ($null -ne $ev.Properties -and $ev.Properties.Count -gt 0) {
                $msg += "`n`nRaw Event Data:"
                foreach ($prop in $ev.Properties) {
                    if ($null -ne $prop.Value) {
                        $msg += "`n - $($prop.Value.ToString())"
                    }
                }
            }
        } catch { }
    }
    
    # Wrap text
    $messageLines = [System.Collections.ArrayList]::new()
    $messageLines.Add("Description:") | Out-Null
    $wrappedMsg = Wrap-Text $msg ($width - 2)
    foreach ($line in $wrappedMsg) {
        $messageLines.Add($line) | Out-Null
    }
    
    return [PSCustomObject]@{
        HeaderLines = $headerLines
        MessageLines = $messageLines
    }
}

# --- MAIN EXECUTION ---

# Fetch all logs
$logNames = @()
try {
    $logNames = [System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.GetLogNames() | 
        Where-Object { $_ -and $_.Trim() -ne "" } | 
        Select-Object -Unique
} catch {
    $logNames = @('Application', 'Security', 'System', 'Setup')
}

$rootNode = Build-EventLogTree $logNames
$treeVisibleNodes = Get-VisibleNodes $rootNode
$treeSelectedIndex = 0
$treeScrollOffset = 0

# Table State
$loadedEvents = [System.Collections.ArrayList]::new()
$filteredEvents = [System.Collections.ArrayList]::new()
$tableSelectedIndex = 0
$tableScrollOffset = 0
$selectedLogName = ""

# Filter Configuration State
$activeFilterLevels = [System.Collections.ArrayList]::new()
$activeFilterSources = [System.Collections.ArrayList]::new()
$activeFilterIds = ""
$searchQuery = ""

# UI Layout Configuration
$width = [Console]::WindowWidth
$height = [Console]::WindowHeight
Update-LayoutDimensions $width $height
$rightWidth = $width - $global:leftWidth - 4

$detailLines = Format-EventDetailsMapped $null $rightWidth
$detailsScrollOffset = 0

# Focus State: 0 = Tree, 1 = Table, 2 = Details
$global:focusArea = 0

# Setup console
Initialize-Console

# Initial full drawing
[Console]::Write("$ESC[2J")
Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "EVENT LOGS" "LOG ENTRIES" "EVENT DETAILS"
Draw-Menu $width @("Ctrl+1: Filter", "Ctrl+2: Search", "Ctrl+3: Refresh", "Ctrl+Q: Exit")
$statusText = "Ready. Use arrows to browse logs. Press Enter to load."
Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height

# Precompute columns width based on right panel size
$levelW = 12
$dateW = 20
$idW = 9
$dividers = 4
$categoryW = [int](($rightWidth - ($levelW + $dateW + $idW + $dividers)) * 0.5)
$sourceW = $rightWidth - ($levelW + $dateW + $idW + $dividers) - $categoryW
if ($categoryW -lt 10) { $categoryW = 10 }
if ($sourceW -lt 10) { $sourceW = 10 }

$columns = @(
    @{ Label = "Level"; Prop = "LevelText"; Align = "Left"; Width = $levelW }
    @{ Label = "Date and Time"; Prop = "FormattedDate"; Align = "Left"; Width = $dateW }
    @{ Label = "Source"; Prop = "ProviderName"; Align = "Left"; Width = $sourceW }
    @{ Label = "Event ID"; Prop = "Id"; Align = "Right"; Width = $idW }
    @{ Label = "Task Category"; Prop = "TaskDisplayName"; Align = "Left"; Width = $categoryW }
)

$eventRowColorScript = {
    param($item)
    if ($item.Level -eq 1 -or $item.Level -eq 2) { return 'ErrorRow' }
    if ($item.Level -eq 3) { return 'WarningRow' }
    return 'Reset'
}

# Redraw flags
$redrawAll = $false
$needsTreeRedraw = $true
$needsTableRedraw = $true
$needsDetailsRedraw = $true
$needsStatusRedraw = $true

try {
    while ($true) {
        # 1. Resize Check
        $newWidth = [Console]::WindowWidth
        $newHeight = [Console]::WindowHeight
        if ($newWidth -ne $width -or $newHeight -ne $height) {
            $width = $newWidth
            $height = $newHeight
            Update-LayoutDimensions $width $height
            $rightWidth = $width - $global:leftWidth - 4
            
            # Recalculate columns
            $categoryW = [int](($rightWidth - ($levelW + $dateW + $idW + $dividers)) * 0.5)
            $sourceW = $rightWidth - ($levelW + $dateW + $idW + $dividers) - $categoryW
            if ($categoryW -lt 10) { $categoryW = 10 }
            if ($sourceW -lt 10) { $sourceW = 10 }
            $columns[2].Width = $sourceW
            $columns[4].Width = $categoryW
            
            $selectedEvent = if ($filteredEvents.Count -gt 0 -and $tableSelectedIndex -lt $filteredEvents.Count) {
                $filteredEvents[$tableSelectedIndex]
            } else { $null }
            $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
            
            [Console]::Write("$ESC[2J")
            $redrawAll = $true
        }
        
        # 2. Redraw Components
        if ($redrawAll) {
            Draw-Borders $width $height $global:leftWidth $global:mainHeight $global:focusArea "EVENT LOGS" "LOG ENTRIES" "EVENT DETAILS"
            Draw-Menu $width @("Ctrl+1: Filter", "Ctrl+2: Search", "Ctrl+3: Refresh", "Ctrl+Q: Exit")
            $needsTreeRedraw = $true
            $needsTableRedraw = $true
            $needsDetailsRedraw = $true
            $needsStatusRedraw = $true
            $redrawAll = $false
        }
        
        if ($needsTreeRedraw) {
            Draw-TreeView $treeVisibleNodes $treeSelectedIndex $treeScrollOffset $global:leftWidth ($height - 4) ($global:focusArea -eq 0)
            $needsTreeRedraw = $false
        }
        
        if ($needsTableRedraw) {
            Draw-TableHeader ($global:leftWidth + 2) 2 $columns $rightWidth
            $tableHeight = $global:mainHeight - 2
            Draw-TableRows $filteredEvents $tableSelectedIndex $tableScrollOffset $columns ($global:leftWidth + 2) 3 $rightWidth $tableHeight ($global:focusArea -eq 1) $eventRowColorScript
            $needsTableRedraw = $false
        }
        
        if ($needsDetailsRedraw) {
            $detailHeight = $height - $global:mainHeight - 4
            Draw-Details $detailLines.HeaderLines $detailLines.MessageLines $detailsScrollOffset ($global:leftWidth + 2) ($global:mainHeight + 2) $rightWidth $detailHeight
            $needsDetailsRedraw = $false
        }
        
        if ($needsStatusRedraw) {
            $focusTxt = switch ($global:focusArea) {
                0 { "Tree View" }
                1 { "Log Entries" }
                2 { "Event Details" }
            }
            Draw-Status $statusText $focusTxt "Tab: Switch Pane" $width $height
            $needsStatusRedraw = $false
        }
        
        Set-Cursor 0 0
        
        # 3. Read Keyboard Input
        if (-not [Console]::KeyAvailable) {
            Start-Sleep -Milliseconds 20
            continue
        }
        
        $key = [Console]::ReadKey($true)
        $isCtrl = ($key.Modifiers -band [System.ConsoleModifiers]::Control) -eq [System.ConsoleModifiers]::Control
        
        if ($isCtrl) {
            if ($key.Key -eq 'D1') {
                # --- ADVANCED CHECKLIST FILTER DIALOG ---
                if ($selectedLogName -eq "") {
                    $statusText = "Please select a log and press Enter first."
                    $needsStatusRedraw = $true
                    continue
                }
                
                $statusText = "Loading sources for $selectedLogName..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height
                
                # Get unique sources
                $logSources = @()
                try {
                    $logSources = (New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration($selectedLogName)).ProviderNames | Sort-Object
                } catch {}
                if ($logSources.Count -eq 0) {
                    $logSources = @('Application', 'System', 'Security', '.NET Runtime')
                }
                
                # Step 1: Select Event Levels
                $levelItems = [System.Collections.ArrayList]::new(@(
                    [PSCustomObject]@{ Label = "1 - Critical"; Value = 1; Checked = $activeFilterLevels -contains 1 }
                    [PSCustomObject]@{ Label = "2 - Error"; Value = 2; Checked = $activeFilterLevels -contains 2 }
                    [PSCustomObject]@{ Label = "3 - Warning"; Value = 3; Checked = $activeFilterLevels -contains 3 }
                    [PSCustomObject]@{ Label = "4 - Information"; Value = 4; Checked = $activeFilterLevels -contains 4 }
                    [PSCustomObject]@{ Label = "5 - Verbose"; Value = 5; Checked = $activeFilterLevels -contains 5 }
                ))
                
                $resLevels = Show-CheckListDialog "Select Event Levels to include (Space to toggle):" "Filter Levels" $levelItems $width $height
                if ($null -eq $resLevels) {
                    $statusText = "Filter setup cancelled."
                    $redrawAll = $true
                    continue
                }
                
                # Step 2: Select Event Sources
                $sourceItems = [System.Collections.ArrayList]::new()
                foreach ($src in $logSources) {
                    $sourceItems.Add([PSCustomObject]@{ Label = $src; Checked = $activeFilterSources -contains $src }) | Out-Null
                }
                
                $resSources = Show-CheckListDialog "Select Event Sources to include (Type to filter list):" "Filter Sources" $sourceItems $width $height
                if ($null -eq $resSources) {
                    $statusText = "Filter setup cancelled."
                    $redrawAll = $true
                    continue
                }
                
                # Step 3: Event ID Input
                $resIds = Show-InputBox "Filter by Event IDs (e.g. 4624, 7036 - Empty = All):" "Filter Event IDs" $width $height
                if ($null -eq $resIds) {
                    $statusText = "Filter setup cancelled."
                    $redrawAll = $true
                    continue
                }
                
                # Step 4: Message Search Query
                $resSearch = Show-InputBox "Filter by search text (Message content - Empty = None):" "Search Text" $width $height
                if ($null -eq $resSearch) {
                    $statusText = "Filter setup cancelled."
                    $redrawAll = $true
                    continue
                }
                
                # Apply Filters
                $activeFilterLevels.Clear()
                foreach ($item in $resLevels) {
                    if ($item.Checked) { $activeFilterLevels.Add($item.Value) | Out-Null }
                }
                
                $activeFilterSources.Clear()
                foreach ($item in $resSources) {
                    if ($item.Checked) { $activeFilterSources.Add($item.Label) | Out-Null }
                }
                
                $activeFilterIds = $resIds.Trim()
                $searchQuery = $resSearch.Trim()
                
                $statusText = "Loading log events with filters..."
                $needsStatusRedraw = $true
                Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height
                
                $loadedEvents = Load-LogEvents $selectedLogName $activeFilterLevels $activeFilterIds $activeFilterSources
                
                # Apply search filter locally
                $filteredEvents = [System.Collections.ArrayList]::new()
                if ($searchQuery -ne "") {
                    foreach ($ev in $loadedEvents) {
                        $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {}
                        if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or
                            ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) {
                            $filteredEvents.Add($ev) | Out-Null
                        }
                    }
                } else {
                    $filteredEvents = $loadedEvents
                }
                
                $tableSelectedIndex = 0
                $tableScrollOffset = 0
                
                $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null }
                $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                $detailsScrollOffset = 0
                
                $statusText = "Loaded $($filteredEvents.Count) events (filtered)."
                $redrawAll = $true
                continue
            }
            
            if ($key.Key -eq 'D2') {
                # Quick Search
                $searchIn = Show-InputBox "Quick Search (Message/Source, Empty = Clear):" "Search Message" $width $height
                if ($null -ne $searchIn) {
                    $searchQuery = $searchIn.Trim()
                    
                    $filteredEvents = [System.Collections.ArrayList]::new()
                    if ($searchQuery -ne "") {
                        foreach ($ev in $loadedEvents) {
                            $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {}
                            if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or
                                ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) {
                                $filteredEvents.Add($ev) | Out-Null
                            }
                        }
                    } else {
                        $filteredEvents = $loadedEvents
                    }
                    
                    $tableSelectedIndex = 0
                    $tableScrollOffset = 0
                    
                    $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null }
                    $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                    $detailsScrollOffset = 0
                    $statusText = "Quick Search found $($filteredEvents.Count) matches."
                }
                $redrawAll = $true
                continue
            }
            
            if ($key.Key -eq 'D3') {
                # Refresh
                if ($selectedLogName -ne "") {
                    $statusText = "Refreshing events for $selectedLogName..."
                    $needsStatusRedraw = $true
                    Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height
                    
                    $loadedEvents = Load-LogEvents $selectedLogName $activeFilterLevels $activeFilterIds $activeFilterSources
                    
                    $filteredEvents = [System.Collections.ArrayList]::new()
                    if ($searchQuery -ne "") {
                        foreach ($ev in $loadedEvents) {
                            $msg = ""; try { $msg = $ev.RawEvent.FormatDescription() } catch {}
                            if ($ev.ProviderName.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0 -or
                                ($null -ne $msg -and $msg.IndexOf($searchQuery, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) {
                                $filteredEvents.Add($ev) | Out-Null
                            }
                        }
                    } else {
                        $filteredEvents = $loadedEvents
                    }
                    
                    $tableSelectedIndex = 0
                    $tableScrollOffset = 0
                    
                    $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null }
                    $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                    $detailsScrollOffset = 0
                    $statusText = "Refreshed log. Loaded $($filteredEvents.Count) events."
                }
                $redrawAll = $true
                continue
            }
            
            if ($key.Key -eq 'Q') {
                break
            }
        }
        
        # --- FOCUS NAVIGATION ---
        if ($global:focusArea -eq 0) {
            # --- TREE VIEW ---
            $node = $treeVisibleNodes[$treeSelectedIndex]
            
            if ($key.Key -eq 'UpArrow') {
                if ($treeSelectedIndex -gt 0) {
                    $treeSelectedIndex--
                    if ($treeSelectedIndex -lt $treeScrollOffset) { $treeScrollOffset = $treeSelectedIndex }
                    $needsTreeRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($treeSelectedIndex -lt ($treeVisibleNodes.Count - 1)) {
                    $treeSelectedIndex++
                    $treeHeight = $height - 4
                    if ($treeSelectedIndex -ge ($treeScrollOffset + $treeHeight)) {
                        $treeScrollOffset = $treeSelectedIndex - $treeHeight + 1
                    }
                    $needsTreeRedraw = $true
                }
            }
            elseif ($key.Key -eq 'PageUp') {
                $treeHeight = $height - 4
                $treeSelectedIndex = [Math]::Max(0, $treeSelectedIndex - $treeHeight)
                $treeScrollOffset = [Math]::Max(0, $treeScrollOffset - $treeHeight)
                if ($treeSelectedIndex -lt $treeScrollOffset) { $treeSelectedIndex = $treeScrollOffset }
                $needsTreeRedraw = $true
            }
            elseif ($key.Key -eq 'PageDown') {
                $treeHeight = $height - 4
                $treeSelectedIndex = [Math]::Min($treeVisibleNodes.Count - 1, $treeSelectedIndex + $treeHeight)
                $treeScrollOffset = [Math]::Min($treeVisibleNodes.Count - $treeHeight, $treeScrollOffset + $treeHeight)
                if ($treeScrollOffset -lt 0) { $treeScrollOffset = 0 }
                if ($treeSelectedIndex -ge ($treeScrollOffset + $treeHeight)) {
                    $treeSelectedIndex = $treeScrollOffset + $treeHeight - 1
                }
                $needsTreeRedraw = $true
            }
            elseif ($key.Key -eq 'RightArrow') {
                if (-not $node.IsLeaf -and -not $node.IsExpanded) {
                    $node.IsExpanded = $true
                    $treeVisibleNodes = Get-VisibleNodes $rootNode
                    $needsTreeRedraw = $true
                    $statusText = "Expanded $($node.Label)."
                    $needsStatusRedraw = $true
                }
            }
            elseif ($key.Key -eq 'LeftArrow') {
                if (-not $node.IsLeaf -and $node.IsExpanded) {
                    $node.IsExpanded = $false
                    $treeVisibleNodes = Get-VisibleNodes $rootNode
                    $needsTreeRedraw = $true
                    $statusText = "Collapsed $($node.Label)."
                    $needsStatusRedraw = $true
                }
                elseif ($null -ne $node.Parent -and $node.Parent.Id -ne 'Root') {
                    $parentIndex = $treeVisibleNodes.IndexOf($node.Parent)
                    if ($parentIndex -ge 0) {
                        $treeSelectedIndex = $parentIndex
                        if ($treeSelectedIndex -lt $treeScrollOffset) { $treeScrollOffset = $treeSelectedIndex }
                        $needsTreeRedraw = $true
                    }
                }
            }
            elseif ($key.Key -eq 'Enter') {
                if ($node.IsLeaf) {
                    $selectedLogName = $node.LogName
                    $statusText = "Loading log: $selectedLogName..."
                    $needsStatusRedraw = $true
                    Draw-Status $statusText "Tree View" "Tab: Switch Pane" $width $height
                    
                    # Reset filter configs
                    $activeFilterLevels.Clear()
                    $activeFilterSources.Clear()
                    $activeFilterIds = ""
                    $searchQuery = ""
                    
                    $loadedEvents = Load-LogEvents $selectedLogName $null $null $null
                    $filteredEvents = $loadedEvents
                    
                    $tableSelectedIndex = 0
                    $tableScrollOffset = 0
                    
                    $selectedEvent = if ($filteredEvents.Count -gt 0) { $filteredEvents[0] } else { $null }
                    $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                    $detailsScrollOffset = 0
                    
                    $global:focusArea = 1
                    $statusText = "Loaded $($filteredEvents.Count) events from $selectedLogName."
                    $redrawAll = $true
                }
            }
            elseif ($key.Key -eq 'Tab') {
                $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift
                if ($isShift) {
                    $global:focusArea = 2
                } else {
                    $global:focusArea = if ($filteredEvents.Count -gt 0) { 1 } else { 2 }
                }
                $redrawAll = $true
            }
        }
        elseif ($global:focusArea -eq 1) {
            # --- TABLE VIEW ---
            $tableHeight = $global:mainHeight - 2
            
            if ($key.Key -eq 'UpArrow') {
                if ($tableSelectedIndex -gt 0) {
                    $tableSelectedIndex--
                    if ($tableSelectedIndex -lt $tableScrollOffset) { $tableScrollOffset = $tableSelectedIndex }
                    $selectedEvent = $filteredEvents[$tableSelectedIndex]
                    $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                    $detailsScrollOffset = 0
                    $needsTableRedraw = $true
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                if ($tableSelectedIndex -lt ($filteredEvents.Count - 1)) {
                    $tableSelectedIndex++
                    if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) {
                        $tableScrollOffset = $tableSelectedIndex - $tableHeight + 1
                    }
                    $selectedEvent = $filteredEvents[$tableSelectedIndex]
                    $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                    $detailsScrollOffset = 0
                    $needsTableRedraw = $true
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'PageUp') {
                $tableSelectedIndex = [Math]::Max(0, $tableSelectedIndex - $tableHeight)
                $tableScrollOffset = [Math]::Max(0, $tableScrollOffset - $tableHeight)
                if ($tableSelectedIndex -lt $tableScrollOffset) { $tableSelectedIndex = $tableScrollOffset }
                $selectedEvent = $filteredEvents[$tableSelectedIndex]
                $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                $detailsScrollOffset = 0
                $needsTableRedraw = $true
                $needsDetailsRedraw = $true
            }
            elseif ($key.Key -eq 'PageDown') {
                $tableSelectedIndex = [Math]::Min($filteredEvents.Count - 1, $tableSelectedIndex + $tableHeight)
                $tableScrollOffset = [Math]::Min($filteredEvents.Count - $tableHeight, $tableScrollOffset + $tableHeight)
                if ($tableScrollOffset -lt 0) { $tableScrollOffset = 0 }
                if ($tableSelectedIndex -ge ($tableScrollOffset + $tableHeight)) {
                    $tableSelectedIndex = $tableScrollOffset + $tableHeight - 1
                }
                $selectedEvent = $filteredEvents[$tableSelectedIndex]
                $detailLines = Format-EventDetailsMapped $selectedEvent $rightWidth
                $detailsScrollOffset = 0
                $needsTableRedraw = $true
                $needsDetailsRedraw = $true
            }
            elseif ($key.Key -eq 'Escape') {
                $global:focusArea = 0
                $redrawAll = $true
                $statusText = "Focused on event logs tree."
            }
            elseif ($key.Key -eq 'Tab') {
                $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift
                $global:focusArea = if ($isShift) { 0 } else { 2 }
                $redrawAll = $true
            }
        }
        elseif ($global:focusArea -eq 2) {
            # --- DETAILS VIEW ---
            $detailHeight = $height - $global:mainHeight - 4
            $scrollAreaHeight = $detailHeight - 5
            if ($scrollAreaHeight -lt 1) { $scrollAreaHeight = 1 }
            
            if ($key.Key -eq 'UpArrow') {
                if ($detailsScrollOffset -gt 0) {
                    $detailsScrollOffset--
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'DownArrow') {
                $maxOffset = $detailLines.MessageLines.Count - $scrollAreaHeight
                if ($maxOffset -lt 0) { $maxOffset = 0 }
                if ($detailsScrollOffset -lt $maxOffset) {
                    $detailsScrollOffset++
                    $needsDetailsRedraw = $true
                }
            }
            elseif ($key.Key -eq 'PageUp') {
                $detailsScrollOffset = [Math]::Max(0, $detailsScrollOffset - $scrollAreaHeight)
                $needsDetailsRedraw = $true
            }
            elseif ($key.Key -eq 'PageDown') {
                $maxOffset = $detailLines.MessageLines.Count - $scrollAreaHeight
                if ($maxOffset -lt 0) { $maxOffset = 0 }
                $detailsScrollOffset = [Math]::Min($maxOffset, $detailsScrollOffset + $scrollAreaHeight)
                $needsDetailsRedraw = $true
            }
            elseif ($key.Key -eq 'Escape') {
                $global:focusArea = 0
                $redrawAll = $true
                $statusText = "Focused on event logs tree."
            }
            elseif ($key.Key -eq 'Tab') {
                $isShift = ($key.Modifiers -band [System.ConsoleModifiers]::Shift) -eq [System.ConsoleModifiers]::Shift
                $global:focusArea = if ($isShift) { if ($filteredEvents.Count -gt 0) { 1 } else { 0 } } else { 0 }
                $redrawAll = $true
            }
        }
    }
} finally {
    Restore-Console
}