lib/ui.ps1

# WinMole Interactive UI Components
# PwshSpectreConsole-based menus and prompts

# Requires: lib/core.ps1 already dot-sourced

# ── Hide/restore cursor (VT100 sequences) ────────────────────────────────────
$script:_ESC = [char]27
function Hide-Cursor { Write-Host "$($script:_ESC)[?25l" -NoNewline }
function Show-Cursor { Write-Host "$($script:_ESC)[?25h" -NoNewline }

function Write-InputTrace {
    param([string]$Message)

    if (-not $env:WINMOLE_TRACE_INPUT) { return }
    $stamp = (Get-Date).ToString('o')
    Add-Content -Path $env:WINMOLE_TRACE_INPUT -Value "$stamp $Message"
}

function Test-KeyAvailable {
    try {
        return [Console]::KeyAvailable
    } catch {
        return $false
    }
}

function Read-ConsoleKeyInfo {
    try {
        return $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown,AllowCtrlC')
    } catch {
        return [Console]::ReadKey($true)
    }
}

function Get-KeyCharacter {
    param([object]$KeyInfo)

    $charValue = Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'KeyChar'
    if ($null -eq $charValue) {
        $charValue = Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'Character'
    }
    if ($null -eq $charValue) { return '' }

    $character = [char]$charValue
    if ([int]$character -eq 0) { return '' }
    if ([int]$character -lt 32) { return '' }
    return [string]$character
}

function Resolve-KeyName {
    param([object]$KeyInfo)

    $keyName = [string](Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'Key' -Default '')
    $modifiers = Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'Modifiers'
    $controlState = [string](Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'ControlKeyState' -Default '')
    switch ($keyName) {
        'UpArrow' { return 'Up' }
        'DownArrow' { return 'Down' }
        'LeftArrow' { return 'Left' }
        'RightArrow' { return 'Right' }
        'Q' { return 'Quit' }
        'Enter' { return 'Enter' }
        'Spacebar' { return 'Space' }
        'Delete' { return 'Delete' }
        'Backspace' { return 'Backspace' }
        'Home' { return 'Home' }
        'End' { return 'End' }
        'PageUp' { return 'PageUp' }
        'PageDown' { return 'PageDown' }
        'Escape' { return 'Escape' }
        'Tab' { return 'Tab' }
    }

    $virtualKeyCode = Get-OptionalPropertyValue -InputObject $KeyInfo -Name 'VirtualKeyCode'
    $isCtrlModified = $false
    if ($null -ne $modifiers -and ($modifiers.ToString() -match 'Control')) {
        $isCtrlModified = $true
    }
    if (-not $isCtrlModified -and $controlState -match 'Ctrl') {
        $isCtrlModified = $true
    }

    if ($isCtrlModified -and ($keyName -eq 'C' -or [int]$virtualKeyCode -eq 67)) {
        return 'Quit'
    }

    if ($null -ne $virtualKeyCode) {
        switch ([int]$virtualKeyCode) {
            38 { return 'Up' }
            40 { return 'Down' }
            37 { return 'Left' }
            39 { return 'Right' }
            81 { return 'Quit' }
            13 { return 'Enter' }
            32 { return 'Space' }
            46 { return 'Delete' }
            8  { return 'Backspace' }
            36 { return 'Home' }
            35 { return 'End' }
            33 { return 'PageUp' }
            34 { return 'PageDown' }
            27 { return 'Escape' }
            9  { return 'Tab' }
        }
    }

    $c = Get-KeyCharacter -KeyInfo $KeyInfo
    switch ($c) {
        'j' { return 'Down' }
        'k' { return 'Up' }
        'h' { return 'Left' }
        'l' { return 'Right' }
        'q' { return 'Quit' }
        'Q' { return 'Quit' }
        default { return $c }
    }
}

function Clear-PendingKeys {
    while (Test-KeyAvailable) {
        [void](Read-ConsoleKeyInfo)
    }
}

function Format-AnalyzeEntryLine {
    param(
        [string]$Pointer,
        [string]$Rank,
        [string]$Bar,
        [double]$Percent,
        [string]$Icon,
        [string]$Name,
        [string]$SizeText,
        [string]$AgeText = ''
    )

    $pctText = ('{0,5:F1}%' -f $Percent)
    return " $Pointer $Rank $Bar $pctText | [grey]$(Esc $Icon)[/] [white]$(Esc $Name)[/] [gold1]$SizeText[/]$AgeText"
}

# ── Read a single keypress (arrow-key aware) ──────────────────────────────────
# Returns: 'Up','Down','Left','Right','Enter','Space','Delete','Backspace',
# 'Home','End','PageUp','PageDown','Escape','Quit','Tab', or the char itself.
function Read-Key {
    $keyInfo = Read-ConsoleKeyInfo
    $resolved = Resolve-KeyName -KeyInfo $keyInfo
    $keyName = [string](Get-OptionalPropertyValue -InputObject $keyInfo -Name 'Key' -Default '')
    $virtualKeyCode = Get-OptionalPropertyValue -InputObject $keyInfo -Name 'VirtualKeyCode' -Default ''
    $char = Get-KeyCharacter -KeyInfo $keyInfo
    $character = Get-OptionalPropertyValue -InputObject $keyInfo -Name 'Character' -Default ''
    $keyChar = Get-OptionalPropertyValue -InputObject $keyInfo -Name 'KeyChar' -Default ''
    $controlState = [string](Get-OptionalPropertyValue -InputObject $keyInfo -Name 'ControlKeyState' -Default '')
    $modifiers = [string](Get-OptionalPropertyValue -InputObject $keyInfo -Name 'Modifiers' -Default '')
    Write-InputTrace "resolved=[$resolved] key=[$keyName] vk=[$virtualKeyCode] char=[$char] rawChar=[$character] keyChar=[$keyChar] ctrl=[$controlState] modifiers=[$modifiers] type=[$($keyInfo.GetType().FullName)]"
    return $resolved
}

# ── Multi-select checklist menu ───────────────────────────────────────────────
# Items: array of pscustomobject with .Label, .Size (optional), .Tag (optional),
# .Selected (bool), .Disabled (bool, optional)
# Returns: the items array with updated .Selected values, or $null if cancelled.
function Resolve-ChecklistEnterAction {
    param(
        [object[]]$Items,
        [int]$Cursor
    )

    $selectedCount = @(
        $Items | Where-Object {
            [bool](Get-OptionalPropertyValue -InputObject $_ -Name 'Selected' -Default $false)
        }
    ).Count
    if ($selectedCount -gt 0) {
        return 'Confirm'
    }

    if ($Cursor -lt 0 -or $Cursor -ge $Items.Count) {
        return 'NoOp'
    }

    $item = $Items[$Cursor]
    if ([bool](Get-OptionalPropertyValue -InputObject $item -Name 'Disabled' -Default $false)) {
        return 'NoOp'
    }

    $item.Selected = $true
    return 'Toggle'
}

function Show-ChecklistMenu {
    param(
        [string]$Title,
        [object[]]$Items,
        [int]$MaxVisible = 18
    )

    if ($Items.Count -eq 0) { return $Items }
    $cursor = 0
    $offset = 0
    $pageSize = [Math]::Max(5, $MaxVisible)
    $startupGuardUntil = (Get-Date).AddMilliseconds(750)
    $hasUserInteraction = $false

    Hide-Cursor
    try {
        Clear-PendingKeys

        while ($true) {
            [Console]::Clear()
            Write-Host ""
            Write-SpectreHost " [bold deepskyblue1]$(Esc $Title)[/]"
            Write-Host ""

            $end = [Math]::Min($offset + $pageSize, $Items.Count)
            for ($i = $offset; $i -lt $end; $i++) {
                $item = $Items[$i]
                $selected = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Selected' -Default $false)
                $disabled = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Disabled' -Default $false)
                $size = Get-OptionalPropertyValue -InputObject $item -Name 'Size' -Default 0
                $tag = Get-OptionalPropertyValue -InputObject $item -Name 'Tag' -Default ''

                $pointer = if ($i -eq $cursor) { '[deepskyblue1]▶[/]' } else { ' ' }
                $check = if ($selected) { '[green]■[/]' } else { '[grey]□[/]' }
                $line = " $pointer $check [white]$(Esc $item.Label)[/]"
                if ([long]$size -gt 0) { $line += " [gold1]$(Format-Bytes $size)[/]" }
                if ($tag) { $line += " [grey]| $(Esc $tag)[/]" }
                if ($disabled) { $line += " [grey](disabled)[/]" }
                Write-SpectreHost $line
            }

            Write-Host ""
            Write-SpectreHost " [grey]↑↓ Navigate Space Toggle Enter Select/Confirm Esc Cancel[/]"

            $key = Read-Key
            if (-not $key) { continue }

            if (-not $hasUserInteraction -and (Get-Date) -lt $startupGuardUntil -and $key -in @('Enter', 'Escape')) {
                continue
            }

            switch ($key) {
                'Up' {
                    $hasUserInteraction = $true
                    if ($cursor -gt 0) {
                        $cursor--
                        if ($cursor -lt $offset) { $offset = $cursor }
                    }
                }
                'Down' {
                    $hasUserInteraction = $true
                    if ($cursor -lt ($Items.Count - 1)) {
                        $cursor++
                        if ($cursor -ge ($offset + $pageSize)) { $offset++ }
                    }
                }
                'Home' {
                    $hasUserInteraction = $true
                    $cursor = 0
                    $offset = 0
                }
                'End' {
                    $hasUserInteraction = $true
                    $cursor = $Items.Count - 1
                    $offset = [Math]::Max(0, $Items.Count - $pageSize)
                }
                'PageUp' {
                    $hasUserInteraction = $true
                    $cursor = [Math]::Max(0, $cursor - $pageSize)
                    $offset = [Math]::Max(0, $offset - $pageSize)
                }
                'PageDown' {
                    $hasUserInteraction = $true
                    $cursor = [Math]::Min($Items.Count - 1, $cursor + $pageSize)
                    $offset = [Math]::Min([Math]::Max(0, $Items.Count - $pageSize), $offset + $pageSize)
                }
                'Space' {
                    $hasUserInteraction = $true
                    $item = $Items[$cursor]
                    $disabled = [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Disabled' -Default $false)
                    if (-not $disabled) {
                        $item.Selected = -not [bool](Get-OptionalPropertyValue -InputObject $item -Name 'Selected' -Default $false)
                    }
                }
                'Enter' {
                    $action = Resolve-ChecklistEnterAction -Items $Items -Cursor $cursor
                    switch ($action) {
                        'Confirm' { return $Items }
                        'Toggle' { $hasUserInteraction = $true }
                    }
                }
                'Escape' { return $null }
                'Quit' { return $null }
            }
        }
    } finally {
        Show-Cursor
    }
}

# ── Simple single-select menu ─────────────────────────────────────────────────
function Show-SelectMenu {
    param(
        [string]$Title,
        [string[]]$Options,
        [int]$MaxVisible = 20
    )

    try {
        $selected = $Options | Read-SpectreSelection -Title $Title -PageSize $MaxVisible
    } catch {
        return -1
    }

    if (-not $selected) { return -1 }
    return [Array]::IndexOf($Options, $selected)
}