Private/UI/Show-OperationSplash.ps1

function Show-OperationSplash {
    <#
    .SYNOPSIS
        Displays a responsive loading splash screen for PIM operations like activation and refresh.
     
    .DESCRIPTION
        Creates a non-blocking loading splash screen that runs in a separate runspace.
        Provides smooth progress animation and status updates for long-running operations.
        Designed for use during role activation and refresh operations.
     
    .PARAMETER Title
        Window title text. Default: "PIM Operation"
     
    .PARAMETER InitialMessage
        Initial status message to display. Default: "Processing..."
     
    .PARAMETER ShowProgressBar
        Whether to show a progress bar. Default: $true
     
    .PARAMETER Width
        Width of the splash window. Default: 450
     
    .PARAMETER Height
        Height of the splash window. Default: 180
     
    .OUTPUTS
        PSCustomObject with UpdateStatus() and Close() methods for controlling the splash screen.
     
    .EXAMPLE
        $splash = Show-OperationSplash -Title "Role Activation" -InitialMessage "Preparing role activation..."
        $splash.UpdateStatus("Activating Global Administrator...", 50)
        $splash.Close()
     
    .EXAMPLE
        $splash = Show-OperationSplash -Title "Refreshing Roles" -InitialMessage "Fetching role data..."
        # Do work...
        $splash.Close()
    #>

    [CmdletBinding()]
    param(
        [string]$Title = "PIM Operation",
        [string]$InitialMessage = "Processing...",
        [bool]$ShowProgressBar = $true,
        [int]$Width = 450,
        [int]$Height = 180
    )

    # Synchronized hashtable for cross-runspace communication
    $syncHash = [hashtable]::Synchronized(@{
        Title = $Title
        Message = $InitialMessage
        Progress = 0
        TargetProgress = 0
        ShouldClose = $false
        Form = $null
        StatusLabel = $null
        ProgressBar = $null
        IsDisposed = $false
        ShowProgressBar = $ShowProgressBar
        Width = $Width
        Height = $Height
    })

    # Create STA runspace for the UI
    $runspace = [runspacefactory]::CreateRunspace()
    $runspace.ApartmentState = "STA"
    $runspace.ThreadOptions = "ReuseThread"
    $runspace.Open()
    $runspace.SessionStateProxy.SetVariable("syncHash", $syncHash)
    
    # Create PowerShell instance
    $powershell = [powershell]::Create()
    $powershell.Runspace = $runspace
    
    # UI creation script
    [void]$powershell.AddScript({
        Add-Type -AssemblyName System.Windows.Forms
        Add-Type -AssemblyName System.Drawing
        
        # Main form
        $form = New-Object System.Windows.Forms.Form -Property @{
            Text = $syncHash.Title
            Size = [System.Drawing.Size]::new($syncHash.Width, $syncHash.Height)
            StartPosition = 'CenterScreen'
            FormBorderStyle = 'FixedDialog'
            BackColor = [System.Drawing.Color]::White
            TopMost = $true
            ShowInTaskbar = $true
            MaximizeBox = $false
            MinimizeBox = $false
            ControlBox = $false  # Hide close button during operations
        }

        # Add icon (optional - uses default if not found)
        try {
            $iconPath = Join-Path $PSScriptRoot "Resources\pim-icon.ico"
            if (Test-Path $iconPath) {
                $form.Icon = [System.Drawing.Icon]::new($iconPath)
            }
        } catch {}

        # Header panel with color
        $headerPanel = New-Object System.Windows.Forms.Panel -Property @{
            Location = [System.Drawing.Point]::new(0, 0)
            Size = [System.Drawing.Size]::new($syncHash.Width, 40)
            BackColor = [System.Drawing.Color]::FromArgb(0, 120, 212)
            Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
        }
        $form.Controls.Add($headerPanel)

        # Title label in header
        $titleLabel = New-Object System.Windows.Forms.Label -Property @{
            Text = $syncHash.Title
            Font = [System.Drawing.Font]::new("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
            ForeColor = [System.Drawing.Color]::White
            Location = [System.Drawing.Point]::new(15, 10)
            Size = [System.Drawing.Size]::new($syncHash.Width - 30, 25)
            BackColor = [System.Drawing.Color]::Transparent
        }
        $headerPanel.Controls.Add($titleLabel)

        # Status label
        $statusLabel = New-Object System.Windows.Forms.Label -Property @{
            Text = $syncHash.Message
            Font = [System.Drawing.Font]::new("Segoe UI", 10)
            Location = [System.Drawing.Point]::new(15, 55)
            Size = [System.Drawing.Size]::new($syncHash.Width - 30, 40)
            TextAlign = 'MiddleCenter'
            ForeColor = [System.Drawing.Color]::FromArgb(32, 31, 30)
            Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
            AutoEllipsis = $true
        }
        $form.Controls.Add($statusLabel)

        # Progress bar (optional)
        if ($syncHash.ShowProgressBar) {
            $progressBar = New-Object System.Windows.Forms.ProgressBar -Property @{
                Location = [System.Drawing.Point]::new(20, 105)
                Size = [System.Drawing.Size]::new($syncHash.Width - 40, 20)
                Style = [System.Windows.Forms.ProgressBarStyle]::Continuous
                Minimum = 0
                Maximum = 100
                Value = 0
                ForeColor = [System.Drawing.Color]::FromArgb(0, 103, 184)
                BackColor = [System.Drawing.Color]::FromArgb(242, 242, 242)
                Anchor = [System.Windows.Forms.AnchorStyles]::Top -bor [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right
            }
            $form.Controls.Add($progressBar)
            $syncHash.ProgressBar = $progressBar
        }

        # Spinner animation (when no progress bar)
        if (-not $syncHash.ShowProgressBar) {
            $spinnerLabel = New-Object System.Windows.Forms.Label -Property @{
                Text = "⚪⚪⚪"
                Font = [System.Drawing.Font]::new("Segoe UI", 14)
                Location = [System.Drawing.Point]::new(($syncHash.Width / 2) - 30, 100)
                Size = [System.Drawing.Size]::new(60, 30)
                TextAlign = 'MiddleCenter'
                ForeColor = [System.Drawing.Color]::FromArgb(0, 120, 212)
            }
            $form.Controls.Add($spinnerLabel)
            
            # Spinner animation timer
            $spinnerStates = @("⚫⚪⚪", "⚪⚫⚪", "⚪⚪⚫", "⚪⚫⚪")
            $spinnerIndex = 0
            $spinnerTimer = New-Object System.Windows.Forms.Timer
            $spinnerTimer.Interval = 200
            $spinnerTimer.Add_Tick({
                $spinnerLabel.Text = $spinnerStates[$spinnerIndex]
                $spinnerIndex = ($spinnerIndex + 1) % $spinnerStates.Count
            })
            $spinnerTimer.Start()
        }
        
        # Store UI references
        $syncHash.Form = $form
        $syncHash.StatusLabel = $statusLabel
        
        $form.Add_FormClosed({ 
            $syncHash.IsDisposed = $true
            if ($spinnerTimer) { 
                $spinnerTimer.Stop()
                $spinnerTimer.Dispose()
            }
        })
        
        # Update timer for smooth animations
        $timer = New-Object System.Windows.Forms.Timer
        $timer.Interval = 20
        $timer.Add_Tick({
            # Update status text
            if ($syncHash.StatusLabel.Text -ne $syncHash.Message) {
                $syncHash.StatusLabel.Text = $syncHash.Message
            }
            
            # Animate progress bar if exists
            if ($syncHash.ProgressBar) {
                $currentValue = $syncHash.ProgressBar.Value
                $targetValue = [Math]::Min($syncHash.TargetProgress, 100)
                
                if ($currentValue -ne $targetValue) {
                    # Smooth animation - move 20% of the way to target each frame
                    $diff = $targetValue - $currentValue
                    $step = [Math]::Max(1, [Math]::Abs($diff * 0.2))
                    
                    if ($diff -gt 0) {
                        $syncHash.ProgressBar.Value = [Math]::Min($currentValue + $step, $targetValue)
                    } elseif ($diff -lt 0) {
                        $syncHash.ProgressBar.Value = [Math]::Max($currentValue - $step, $targetValue)
                    }
                }
            }
            
            if ($syncHash.ShouldClose) {
                $timer.Stop()
                $form.Close()
            }
        })
        $timer.Start()
        
        [void]$form.ShowDialog()
        
        # Cleanup
        $timer.Stop()
        $timer.Dispose()
        $syncHash.IsDisposed = $true
    })
    
    # Start splash screen
    $handle = $powershell.BeginInvoke()
    
    # Wait for form to be created
    $maxWait = 50  # 5 seconds max
    $waited = 0
    while (-not $syncHash.Form -and $waited -lt $maxWait) {
        Start-Sleep -Milliseconds 100
        $waited++
    }
    
    # Control object
    $splashControl = [PSCustomObject]@{
        SyncHash = $syncHash
        PowerShell = $powershell
        Runspace = $runspace
        Handle = $handle
    }
    
    $splashControl | Add-Member -MemberType ScriptProperty -Name IsDisposed -Value {
        $this.SyncHash.IsDisposed
    }
    
    $splashControl | Add-Member -MemberType ScriptMethod -Name UpdateStatus -Value {
        param([string]$Status, [int]$Progress = -1)
        if (-not $this.IsDisposed) {
            $this.SyncHash.Message = $Status
            if ($Progress -ge 0 -and $this.SyncHash.ShowProgressBar) {
                $this.SyncHash.TargetProgress = [Math]::Min($Progress, 100)
            }
        }
    }
    
    $splashControl | Add-Member -MemberType ScriptMethod -Name Close -Value {
        if (-not $this.IsDisposed) {
            if ($this.SyncHash.ShowProgressBar) {
                $this.SyncHash.TargetProgress = 100
                Start-Sleep -Milliseconds 300
            }
            
            $this.SyncHash.ShouldClose = $true
            Start-Sleep -Milliseconds 200
            
            if ($this.PowerShell) {
                $this.PowerShell.Stop()
                $this.PowerShell.Dispose()
            }
            if ($this.Runspace) {
                $this.Runspace.Close()
                $this.Runspace.Dispose()
            }
        }
    }
    
    return $splashControl
}