PsUi.psm1

#requires -Version 5.1

# Module state will be managed by ModuleContext C# class

# Load required WPF and Windows Forms assemblies
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Windows.Forms

# Load the pre-compiled C# backend DLL (framework-specific)
$dllLoaded = $false

# Core loads lib\core, Desktop lib\desktop (net472). Boxes without 4.7.2 (WinPE, pre 1803 Server) drop to lib\net452. Release 461808 is 4.7.2.
if ($PSVersionTable.PSEdition -eq 'Core') {
    $libPath = Join-Path $PSScriptRoot 'lib\core'
}
else {
    $useNet452 = $false
    try {
        $ndpKey = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
        $release = (Get-ItemProperty -Path $ndpKey -ErrorAction Stop).Release
        if ($release -lt 461808) { $useNet452 = $true }
    }
    catch {
        # Registry probe failed (no NDP key, no permission). Assume modern .NET - lib\desktop.
    }
    $libPath = if ($useNet452) { Join-Path $PSScriptRoot 'lib\net452' }
    else { Join-Path $PSScriptRoot 'lib\desktop' }
}

$dllPath = Join-Path $libPath 'PsUi.dll'

if (Test-Path $dllPath) {
    try {
        # Load WebView2 dependencies first (they're referenced by PsUi.dll)
        $webView2Core = Join-Path $libPath 'Microsoft.Web.WebView2.Core.dll'
        $webView2Wpf  = Join-Path $libPath 'Microsoft.Web.WebView2.Wpf.dll'
        
        if (Test-Path $webView2Core) { [System.Reflection.Assembly]::LoadFrom($webView2Core) | Out-Null }
        if (Test-Path $webView2Wpf) {  [System.Reflection.Assembly]::LoadFrom($webView2Wpf) | Out-Null }
        
        # Import the main module DLL
        Import-Module $dllPath -Global -DisableNameChecking -Force
        $dllLoaded = $true
        Write-Verbose "Loaded PsUi backend from: $dllPath"
    }
    catch {  Write-Warning "Failed to load PsUi backend DLL: $_"  }
}
else {
    Write-Warning "PsUi backend DLL not found at: $dllPath"
    Write-Warning "Run Build-PsUi.ps1 from the repository root to compile the C# backend."
}

# Hook up module context now that the DLL is loaded
if ($dllLoaded) {
    [PsUi.ModuleContext]::IsInitialized = $true
    [PsUi.ModuleContext]::ModulePath = $PSScriptRoot
    try { [PsUi.ThemeEngine]::SetModulePath($PSScriptRoot) }
    catch {  Write-Verbose "ThemeEngine module path not set: $_" }
    
    # Clean up orphaned WebView2 temp folders from previous sessions
    try { [PsUi.WebViewHelper]::CleanupOldUserDataFolders() } catch { }
}

# Load icon definitions from JSON resource file
$iconPath = Join-Path $PSScriptRoot 'resources\CharList.json'
if (Test-Path $iconPath) {
    try {
        $iconData = Get-Content $iconPath | ConvertFrom-Json

        # Convert PSCustomObject to Dictionary for C# ModuleContext
        $iconDict = [System.Collections.Generic.Dictionary[string,string]]::new()
        $iconData.PSObject.Properties | ForEach-Object {
            $iconDict.Add($_.Name, $_.Value)
        }

        # Hand off the icons to the C# side
        [PsUi.ModuleContext]::Initialize($iconDict)

        if ([PsUi.ModuleContext]::NoIconFontInstalled) {
            Write-Warning "No icon font found (Segoe MDL2 Assets or Segoe Fluent Icons). Icons will not render correctly."
        }
    }
    catch {  Write-Warning "Failed to load icons: $_"   }
}

# Load all PowerShell function files from private and public folders.
# Concatenating every .ps1 into a single script block (parsed and executed once) is ~10x faster than dot sourcing each file individually. The speedup is dramatic on slow/high latency filesystems (OneDrive reparse points, UNC shares) where 190+ separate file opens dominate module import time.
# Behaviour is identical: these files only define functions at module scope, and the concatenation order matches the previous Get-ChildItem -Recurse enumeration order.
$loaderSb = [System.Text.StringBuilder]::new(524288)
foreach ($folder in @('private', 'public')) {
    $path = Join-Path $PSScriptRoot $folder
    if (Test-Path $path) {
        $files = [System.IO.Directory]::GetFiles($path, '*.ps1', [System.IO.SearchOption]::AllDirectories)
        foreach ($f in $files) {
            [void]$loaderSb.AppendLine([System.IO.File]::ReadAllText($f))
            [void]$loaderSb.AppendLine()
        }
    }
}
if ($loaderSb.Length -gt 0) {
    $loaderBlock = [scriptblock]::Create($loaderSb.ToString())
    . $loaderBlock
}
Remove-Variable -Name loaderSb, loaderBlock -ErrorAction SilentlyContinue

# Register private functions in ModuleContext for injection into async runspaces.
# Private functions work inside button actions because they're injected into each async runspace by AsyncExecutor.Setup.cs. They're NOT exported to the user's console, but ARE available inside -Action scriptblocks.
$privatePath = Join-Path $PSScriptRoot 'private'
$publicPath  = Join-Path $PSScriptRoot 'public'

# Only capture PRIVATE helper functions for injection
$privateFuncs = @{}

if (Test-Path $privatePath) {
    Get-ChildItem $privatePath -Filter '*.ps1' -File -Recurse | ForEach-Object {
        $funcName = $_.BaseName
        $cmd = Get-Command -Name $funcName -CommandType Function -ErrorAction SilentlyContinue
        if ($cmd) {  $privateFuncs[$funcName] = $cmd.Definition  }
    }
}

[PsUi.ModuleContext]::PrivateFunctions = $privateFuncs

# Capture public functions commonly used in async button actions
# These are injected into async runspaces so they're available in background execution
$asyncPublicFuncs = @(
    'Add-UiListItem'
    'Remove-UiListItem'
    'Clear-UiList'
    'Clear-UiStatus'
    'Get-UiListItems'
    'Add-UiDataGridItem'
    'Clear-UiDataGridItems'
    'Set-UiDataGridItems'
    'Set-UiProgress'
    'Set-UiStatusBar'
    'Hide-UiStatusBar'
    'Show-UiStatusBar'
    'Write-Status'
    'Invoke-UiAsync'
    'Get-UiSession'
    'Update-UiChart'
)

$publicFuncs = @{}
foreach ($funcName in $asyncPublicFuncs) {
    $cmd = Get-Command -Name $funcName -CommandType Function -ErrorAction SilentlyContinue
    if ($cmd) {  $publicFuncs[$funcName] = $cmd.Definition  }
}
[PsUi.ModuleContext]::PublicFunctions = $publicFuncs

# OnRemove also fires on Import-Module -Force, because of course it does.
# Skip the static-state nuke if windows are still alive or we yank the rug out from under them.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    try {
        [PsUi.RunspacePoolManager]::Shutdown()
        Write-Verbose "PsUi RunspacePool shutdown complete"
    }
    catch { Write-Debug "[PsUi] RunspacePool shutdown error: $_" }

    # Live sessions = caller did Import-Module -Force, not a real teardown. Leave statics alone.
    $activeSessions = 0
    try { $activeSessions = [PsUi.SessionManager]::ActiveSessionCount } catch { }
    if ($activeSessions -gt 0) {
        Write-Debug "[PsUi] OnRemove: $activeSessions session(s) alive, skipping reset"
        return
    }

    try { [PsUi.ThemeEngine]::Reset() }
    catch { Write-Debug "[PsUi] ThemeEngine reset error: $_" }

    try { [PsUi.SessionManager]::Reset() }
    catch { Write-Debug "[PsUi] SessionManager cleanup error: $_" }

    try {
        [PsUi.ModuleContext]::IsInitialized = $false
        [PsUi.ModuleContext]::PrivateFunctions = $null
        [PsUi.ModuleContext]::PublicFunctions = $null
    }
    catch { Write-Debug "[PsUi] ModuleContext reset error: $_" }

    # Clean up the global session ID marker
    Remove-Variable -Name __PsUiSessionId -Scope Global -ErrorAction SilentlyContinue
}

# Build explicit export list from public folder only (private functions stay internal)
$publicFunctions = @(
    Get-ChildItem $publicPath -Filter '*.ps1' -File -Recurse |
        ForEach-Object { $_.BaseName }
)

Export-ModuleMember -Function $publicFunctions -Cmdlet 'New-UiWindow'