modules/UI/LyricsFormDisplay.psm1

# Lyrics Form Display Module
# Shows synchronized lyrics in a Windows Form window

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

function Show-LyricsForm {
    <#
    .SYNOPSIS
    Display lyrics in a Windows Form window with real-time synchronization
 
    .DESCRIPTION
    Creates a non-blocking Windows Form that displays lyrics with highlighting
    synchronized to the currently playing track. Automatically detects track
    changes and loads new lyrics. Pauses when Spotify is paused.
 
    .PARAMETER LyricsData
    Hashtable containing lyrics data (from LyricsManager)
 
    .PARAMETER InitialPositionMs
    Initial playback position in milliseconds
 
    .EXAMPLE
    Show-LyricsForm -LyricsData $lyricsResult
    #>


    param(
        [Parameter(Mandatory)]
        [hashtable]$LyricsData,

        [int]$InitialPositionMs = 0
    )

    if (-not $LyricsData.Success) {
        Write-Error "Cannot display lyrics: $($LyricsData.Error)"
        return
    }

    # Launch in separate process to avoid blocking
    $scriptPath = $PSScriptRoot
    $modulePath = Split-Path -Parent $scriptPath
    $rootPath = Split-Path -Parent $modulePath

    # Create temp directory for data
    $tempDir = Join-Path $env:TEMP "SpotifyLyrics"
    if (-not (Test-Path $tempDir)) {
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
    }

    # Save lyrics data to temp file
    $dataFile = Join-Path $tempDir "lyrics_$(Get-Date -Format 'yyyyMMddHHmmss').json"
    $LyricsData | ConvertTo-Json -Depth 10 | Out-File -FilePath $dataFile -Encoding UTF8

    # Create background script
    $backgroundScript = @"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
 
# Import necessary modules
Import-Module '$rootPath\modules\Core\ApiClientManager.psm1' -Force -ErrorAction SilentlyContinue
Import-Module '$rootPath\modules\Core\LegacyApiClient.psm1' -Force -ErrorAction SilentlyContinue
Import-Module '$rootPath\modules\Lyrics\LyricsEngine.psm1' -Force -ErrorAction SilentlyContinue
 
# Load lyrics data from temp file
`$script:lyricsData = Get-Content '$dataFile' -Raw | ConvertFrom-Json -AsHashtable
 
# Clean up temp file after loading
Remove-Item '$dataFile' -Force -ErrorAction SilentlyContinue
 
# Create lyrics manager for track changes
`$lyricsConfig = @{
    DataDirectory = Join-Path `$env:APPDATA "SpotifyCLI\Lyrics"
    CacheEnabled = `$true
    CacheTtlDays = 30
}
if (`$env:GENIUS_ACCESS_TOKEN) { `$lyricsConfig.GeniusApiKey = `$env:GENIUS_ACCESS_TOKEN }
if (`$env:MUSIXMATCH_API_KEY) { `$lyricsConfig.MusixmatchApiKey = `$env:MUSIXMATCH_API_KEY }
`$script:lyricsManager = New-LyricsManager -Configuration `$lyricsConfig
 
# Track info
`$artist = if (`$script:lyricsData.TrackId) { `$script:lyricsData.TrackId.Split('-')[0] } else { "Unknown" }
`$track = if (`$script:lyricsData.TrackId) { (`$script:lyricsData.TrackId.Split('-', 2)[1]) } else { "Unknown" }
`$source = `$script:lyricsData.Source
 
# Create form
`$form = New-Object System.Windows.Forms.Form
`$form.Text = "Lyrics - `$artist - `$track"
`$form.Size = New-Object System.Drawing.Size(600, 700)
`$form.StartPosition = 'CenterScreen'
`$form.BackColor = [System.Drawing.ColorTranslator]::FromHtml('#1A1A1A')
`$form.ForeColor = [System.Drawing.Color]::White
`$form.FormBorderStyle = 'SizableToolWindow'
`$form.TopMost = `$true
 
# Header label
`$headerLabel = New-Object System.Windows.Forms.Label
`$headerLabel.Location = New-Object System.Drawing.Point(10, 10)
`$headerLabel.Size = New-Object System.Drawing.Size(560, 60)
`$headerLabel.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)
`$headerLabel.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#4ECDC4')
`$headerLabel.Text = "`$artist`n`$track`nSource: `$source"
`$headerLabel.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
`$form.Controls.Add(`$headerLabel)
 
# Lyrics panel (scrollable)
`$lyricsPanel = New-Object System.Windows.Forms.Panel
`$lyricsPanel.Location = New-Object System.Drawing.Point(10, 80)
`$lyricsPanel.Size = New-Object System.Drawing.Size(560, 580)
`$lyricsPanel.AutoScroll = `$true
`$lyricsPanel.BackColor = [System.Drawing.ColorTranslator]::FromHtml('#2D2D2D')
`$lyricsPanel.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Bottom -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
`$form.Controls.Add(`$lyricsPanel)
 
# Invisible anchor label at the bottom for look-ahead scrolling
`$script:scrollAnchor = New-Object System.Windows.Forms.Label
`$script:scrollAnchor.Size = New-Object System.Drawing.Size(1, 1)
`$script:scrollAnchor.Text = ""
`$lyricsPanel.Controls.Add(`$script:scrollAnchor)
 
# Create labels for each line
`$script:lineLabels = @()
`$yPosition = 10
 
if (`$script:lyricsData.HasSyncedLyrics -and `$script:lyricsData.SyncedLines) {
    foreach (`$line in `$script:lyricsData.SyncedLines) {
        `$label = New-Object System.Windows.Forms.Label
        `$label.Location = New-Object System.Drawing.Point(10, `$yPosition)
        `$label.Size = New-Object System.Drawing.Size(520, 30)
        `$label.Font = New-Object System.Drawing.Font('Segoe UI', 11)
        `$label.ForeColor = [System.Drawing.Color]::White
        `$label.Text = `$line.Text
        `$label.Tag = `$line.Timestamp
        `$label.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
        `$label.AutoSize = `$false
        `$lyricsPanel.Controls.Add(`$label)
        `$script:lineLabels += `$label
        `$yPosition += 35
    }
} else {
    `$textLines = `$script:lyricsData.FullText -split "`n"
    foreach (`$textLine in `$textLines) {
        `$label = New-Object System.Windows.Forms.Label
        `$label.Location = New-Object System.Drawing.Point(10, `$yPosition)
        `$label.Size = New-Object System.Drawing.Size(520, 30)
        `$label.Font = New-Object System.Drawing.Font('Segoe UI', 11)
        `$label.ForeColor = [System.Drawing.Color]::White
        `$label.Text = `$textLine
        `$label.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
        `$label.AutoSize = `$false
        `$lyricsPanel.Controls.Add(`$label)
        `$yPosition += 35
    }
}
 
# State tracking
`$script:currentPositionMs = $InitialPositionMs
`$script:lastUpdate = [DateTime]::UtcNow
`$script:lastApiSync = [DateTime]::UtcNow
`$script:currentHighlightedLabel = `$null
`$script:currentTrackUri = `$null
`$script:isUpdatingTrack = `$false
`$script:isPlaying = `$true
 
# Get initial track state
try {
    `$initPlayback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue
    if (`$initPlayback) {
        if (`$initPlayback.item) { `$script:currentTrackUri = `$initPlayback.item.uri }
        if (`$initPlayback.PSObject.Properties.Name -contains 'is_playing') {
            `$script:isPlaying = `$initPlayback.is_playing
        }
    }
} catch { }
 
# Function to rebuild lyrics labels when track changes
function Rebuild-LyricsLabels {
    param([hashtable]`$NewLyricsData, [int]`$ProgressMs)
 
    `$script:isUpdatingTrack = `$true
    `$script:lyricsData = `$NewLyricsData
 
    `$lyricsPanel.SuspendLayout()
    # Remove old line labels but keep the scroll anchor
    foreach (`$lbl in `$script:lineLabels) {
        `$lyricsPanel.Controls.Remove(`$lbl)
        `$lbl.Dispose()
    }
    `$script:lineLabels = @()
    `$yPos = 10
 
    if (`$NewLyricsData.HasSyncedLyrics -and `$NewLyricsData.SyncedLines) {
        foreach (`$sl in `$NewLyricsData.SyncedLines) {
            `$lbl = New-Object System.Windows.Forms.Label
            `$lbl.Location = New-Object System.Drawing.Point(10, `$yPos)
            `$lbl.Size = New-Object System.Drawing.Size(520, 30)
            `$lbl.Font = New-Object System.Drawing.Font('Segoe UI', 11)
            `$lbl.ForeColor = [System.Drawing.Color]::White
            `$lbl.Text = `$sl.Text
            `$lbl.Tag = `$sl.Timestamp
            `$lbl.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
            `$lbl.AutoSize = `$false
            `$lyricsPanel.Controls.Add(`$lbl)
            `$script:lineLabels += `$lbl
            `$yPos += 35
        }
    } else {
        `$textLines = if (`$NewLyricsData.FullText) { `$NewLyricsData.FullText -split "`n" } else { @("No lyrics found") }
        foreach (`$tl in `$textLines) {
            `$lbl = New-Object System.Windows.Forms.Label
            `$lbl.Location = New-Object System.Drawing.Point(10, `$yPos)
            `$lbl.Size = New-Object System.Drawing.Size(520, 30)
            `$lbl.Font = New-Object System.Drawing.Font('Segoe UI', 11)
            `$lbl.ForeColor = [System.Drawing.Color]::White
            `$lbl.Text = `$tl
            `$lbl.Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
            `$lbl.AutoSize = `$false
            `$lyricsPanel.Controls.Add(`$lbl)
            `$yPos += 35
        }
    }
 
    # Move scroll anchor to end of content
    `$script:scrollAnchor.Location = New-Object System.Drawing.Point(0, `$yPos)
    `$lyricsPanel.ResumeLayout(`$true)
 
    # Scroll to top
    if (`$script:lineLabels.Count -gt 0) {
        `$lyricsPanel.ScrollControlIntoView(`$script:lineLabels[0])
    }
 
    `$script:currentHighlightedLabel = `$null
    `$script:currentPositionMs = `$ProgressMs
    `$script:isUpdatingTrack = `$false
}
 
# Function to update highlighted line
function Update-HighlightedLine {
    param([int]`$positionMs)
 
    if (`$script:isUpdatingTrack) { return }
    if (-not `$script:lyricsData.HasSyncedLyrics -or `$script:lineLabels.Count -eq 0) {
        return
    }
 
    # Find current line index
    `$currentIndex = -1
    for (`$i = `$script:lineLabels.Count - 1; `$i -ge 0; `$i--) {
        if (`$script:lineLabels[`$i].Tag -le `$positionMs) {
            `$currentIndex = `$i
            break
        }
    }
 
    `$currentLabel = if (`$currentIndex -ge 0) { `$script:lineLabels[`$currentIndex] } else { `$null }
 
    # Update highlighting if changed
    if (`$currentLabel -ne `$script:currentHighlightedLabel) {
        for (`$i = 0; `$i -lt `$script:lineLabels.Count; `$i++) {
            if (`$i -lt `$currentIndex) {
                `$script:lineLabels[`$i].ForeColor = [System.Drawing.Color]::DarkGray
                `$script:lineLabels[`$i].Font = New-Object System.Drawing.Font('Segoe UI', 11)
            } elseif (`$i -eq `$currentIndex) {
                `$script:lineLabels[`$i].ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#67cd4e')
                `$script:lineLabels[`$i].Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold)
            } else {
                `$script:lineLabels[`$i].ForeColor = [System.Drawing.Color]::White
                `$script:lineLabels[`$i].Font = New-Object System.Drawing.Font('Segoe UI', 11)
            }
        }
 
        # Scroll: make a label a few lines ahead visible so current line stays in upper area
        if (`$currentIndex -ge 0) {
            `$linesVisible = [Math]::Max(1, [int](`$lyricsPanel.ClientSize.Height / 35))
            `$lookAheadIndex = [Math]::Min(`$script:lineLabels.Count - 1, `$currentIndex + `$linesVisible - 3)
            `$lookAheadIndex = [Math]::Max(`$lookAheadIndex, `$currentIndex)
            `$lyricsPanel.ScrollControlIntoView(`$script:lineLabels[`$lookAheadIndex])
        }
 
        `$script:currentHighlightedLabel = `$currentLabel
    }
}
 
# Timer for updates
`$timer = New-Object System.Windows.Forms.Timer
`$timer.Interval = 100
 
`$timer.Add_Tick({
    try {
        `$now = [DateTime]::UtcNow
 
        # Only advance position if playing
        if (`$script:isPlaying) {
            `$elapsed = (`$now - `$script:lastUpdate).TotalMilliseconds
            `$script:currentPositionMs += [int]`$elapsed
        }
        `$script:lastUpdate = `$now
 
        # Sync with API every 5 seconds
        `$timeSinceApiSync = (`$now - `$script:lastApiSync).TotalMilliseconds
        if (`$timeSinceApiSync -ge 5000) {
            try {
                `$playback = Invoke-SpotifyApi -Method GET -Path '/me/player' -ErrorAction SilentlyContinue
                if (`$playback) {
                    # Update play/pause state
                    if (`$playback.PSObject.Properties.Name -contains 'is_playing') {
                        `$script:isPlaying = `$playback.is_playing
                    }
 
                    if (`$playback.progress_ms) {
                        `$script:currentPositionMs = `$playback.progress_ms
                    }
                    `$script:lastApiSync = `$now
 
                    # Detect track change
                    if (`$playback.item -and `$playback.item.uri -ne `$script:currentTrackUri) {
                        `$script:currentTrackUri = `$playback.item.uri
                        `$newArtist = (`$playback.item.artists | ForEach-Object { `$_.name }) -join ", "
                        `$newTrack = `$playback.item.name
 
                        # Update header
                        `$headerLabel.Text = "`$newArtist`n`$newTrack"
                        `$form.Text = "Lyrics - `$newArtist - `$newTrack"
 
                        # Fetch new lyrics
                        `$newResult = `$script:lyricsManager.GetLyrics(`$newArtist, `$newTrack)
                        if (`$newResult.Success) {
                            `$newLyricsData = `$newResult
                            `$headerLabel.Text = "`$newArtist`n`$newTrack`nSource: `$(`$newResult.Source)"
                        } else {
                            `$newLyricsData = @{
                                Success = `$true
                                HasSyncedLyrics = `$false
                                FullText = "No lyrics found for `$newArtist - `$newTrack"
                            }
                            `$headerLabel.Text = "`$newArtist`n`$newTrack`nNo lyrics found"
                        }
                        Rebuild-LyricsLabels -NewLyricsData `$newLyricsData -ProgressMs ([int]`$playback.progress_ms)
                    }
                }
            } catch {
                # Ignore API errors
            }
        }
 
        # Update highlighted line
        Update-HighlightedLine -positionMs `$script:currentPositionMs
 
    } catch {
        # Silently ignore errors
    }
}.GetNewClosure())
 
# Start timer
`$timer.Start()
 
# Form closing cleanup
`$form.Add_FormClosing({
    `$timer.Stop()
    `$timer.Dispose()
})
 
# Clean up script file on exit
`$form.Add_FormClosed({
    Remove-Item '$scriptFile' -Force -ErrorAction SilentlyContinue
})
 
# Show form
[void]`$form.ShowDialog()
"@


    # Save script to temp file
    $scriptFile = Join-Path $tempDir "lyrics_display_$(Get-Date -Format 'yyyyMMddHHmmss').ps1"
    $backgroundScript | Out-File -FilePath $scriptFile -Encoding UTF8

    # Use ProcessStartInfo to bypass Windows Terminal's process interception
    $psi = [System.Diagnostics.ProcessStartInfo]::new()
    $psi.FileName = (Get-Command pwsh -ErrorAction SilentlyContinue)?.Source ?? 'pwsh.exe'
    $psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$scriptFile`""
    $psi.CreateNoWindow = $true
    $psi.UseShellExecute = $false
    [System.Diagnostics.Process]::Start($psi) | Out-Null

    Write-Host "Lyrics window launched!" -ForegroundColor Green
    Write-Host "Lyrics update automatically when song changes" -ForegroundColor Cyan
}

Export-ModuleMember -Function @('Show-LyricsForm')