param(
    [Parameter(Mandatory = $true)]
    [string]$Executable,

    [Parameter(Mandatory = $true)]
    [string]$Name,

    [Parameter(Mandatory = $true)]
    [string]$OutputDirectory,

    [string]$Arguments = "",

    [string]$WorkingDirectory = "",

    [string]$InitialKeys = "",

    [string]$ScreenshotTool = "",

    [ValidateRange(1, 60)]
    [int]$WaitSeconds = 8
)

$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type @"
using System;
using System.Text;
using System.Runtime.InteropServices;

public static class ScreenshotWindowApi {
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    [StructLayout(LayoutKind.Sequential)]
    public struct Rect {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);

    [DllImport("user32.dll")]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);

    [DllImport("user32.dll")]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out Rect rect);

    [DllImport("dwmapi.dll")]
    public static extern int DwmGetWindowAttribute(IntPtr hWnd, int attribute, out Rect value, int size);

    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool IsIconic(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool IsZoomed(IntPtr hWnd);

    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int command);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr insertAfter, int x, int y, int width, int height, uint flags);

    [DllImport("user32.dll")]
    public static extern bool PostMessage(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);

}
"@

function Close-StaleWindows {
    [ScreenshotWindowApi]::EnumWindows({
        param($handle, $parameter)

        if ([ScreenshotWindowApi]::IsWindowVisible($handle)) {
            $title = [System.Text.StringBuilder]::new(1024)
            [void][ScreenshotWindowApi]::GetWindowText($handle, $title, $title.Capacity)
            if ($title.ToString() -match '^slged\.exe\s+-') {
                [void][ScreenshotWindowApi]::PostMessage($handle, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
            }
        }
        return $true
    }, [IntPtr]::Zero) | Out-Null
}

function Get-ProcessTreeIds {
    param([int]$RootProcessId)

    $ids = [System.Collections.Generic.HashSet[int]]::new()
    [void]$ids.Add($RootProcessId)

    do {
        $changed = $false
        foreach ($process in Get-CimInstance Win32_Process) {
            if ($ids.Contains([int]$process.ParentProcessId) -and $ids.Add([int]$process.ProcessId)) {
                $changed = $true
            }
        }
    } while ($changed)

    return ,$ids
}

function Get-ProcessIds {
    $ids = [System.Collections.Generic.HashSet[int]]::new()
    foreach ($item in Get-Process -ErrorAction SilentlyContinue) {
        [void]$ids.Add([int]$item.Id)
    }
    return ,$ids
}

function Stop-NewProcesses {
    param([System.Collections.Generic.HashSet[int]]$BaselineProcessIds)

    $currentSessionId = (Get-Process -Id $PID).SessionId
    $protectedNames = @(
        "csrss", "dwm", "explorer", "fontdrvhost", "powershell", "sihost",
        "StartMenuExperienceHost", "taskhostw", "TextInputHost", "winlogon"
    )

    foreach ($item in Get-Process -ErrorAction SilentlyContinue) {
        if (
            $item.Id -ne $PID -and
            $item.SessionId -eq $currentSessionId -and
            -not $BaselineProcessIds.Contains([int]$item.Id) -and
            $protectedNames -notcontains $item.ProcessName
        ) {
            taskkill.exe /PID $item.Id /T /F 2>$null | Out-Null
        }
    }
}

function Stop-StaleCatalogProcesses {
    param([string]$ExecutablePath)

    $match = [regex]::Match($ExecutablePath, '^(.*\\work)\\', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    if (-not $match.Success) {
        return
    }

    $workRoot = $match.Groups[1].Value.TrimEnd('\') + '\'
    foreach ($item in Get-CimInstance Win32_Process -ErrorAction SilentlyContinue) {
        if ($item.ExecutablePath -and $item.ExecutablePath.StartsWith($workRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
            taskkill.exe /PID $item.ProcessId /T /F 2>$null | Out-Null
        }
    }
}

function Get-ProgramWindows {
    param([System.Collections.Generic.HashSet[int]]$ProcessIds)

    $windows = [System.Collections.Generic.List[object]]::new()
    [ScreenshotWindowApi]::EnumWindows({
        param($handle, $parameter)

        if (-not [ScreenshotWindowApi]::IsWindowVisible($handle)) {
            return $true
        }

        [uint32]$windowProcessId = 0
        [void][ScreenshotWindowApi]::GetWindowThreadProcessId($handle, [ref]$windowProcessId)
        if (-not $ProcessIds.Contains([int]$windowProcessId)) {
            return $true
        }

        $title = [System.Text.StringBuilder]::new(1024)
        [void][ScreenshotWindowApi]::GetWindowText($handle, $title, $title.Capacity)

        $fullRect = [ScreenshotWindowApi+Rect]::new()
        if (-not [ScreenshotWindowApi]::GetWindowRect($handle, [ref]$fullRect)) {
            return $true
        }

        $rect = [ScreenshotWindowApi+Rect]::new()
        $dwmResult = [ScreenshotWindowApi]::DwmGetWindowAttribute(
            $handle,
            9,
            [ref]$rect,
            [System.Runtime.InteropServices.Marshal]::SizeOf([type][ScreenshotWindowApi+Rect])
        )
        if ($dwmResult -ne 0) {
            $rect = $fullRect
        }

        $width = $rect.Right - $rect.Left
        $height = $rect.Bottom - $rect.Top
        if ($width -le 1 -or $height -le 1) {
            return $true
        }

        $windows.Add([pscustomobject]@{
            Handle = $handle
            ProcessId = [int]$windowProcessId
            Title = $title.ToString()
            Left = $rect.Left
            Top = $rect.Top
            Width = $width
            Height = $height
            Area = $width * $height
            CropLeft = $rect.Left - $fullRect.Left
            CropTop = $rect.Top - $fullRect.Top
        })
        return $true
    }, [IntPtr]::Zero) | Out-Null

    return $windows
}

function Crop-WindowShadow {
    param(
        [string]$Path,
        [int]$Left,
        [int]$Top,
        [int]$Width,
        [int]$Height
    )

    $source = [System.Drawing.Bitmap]::FromFile($Path)
    try {
        if ($Left -eq 0 -and $Top -eq 0 -and $source.Width -eq $Width -and $source.Height -eq $Height) {
            return
        }
        if ($Left -lt 0 -or $Top -lt 0 -or $Left + $Width -gt $source.Width -or $Top + $Height -gt $source.Height) {
            return
        }

        $cropped = [System.Drawing.Bitmap]::new($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
        $graphics = [System.Drawing.Graphics]::FromImage($cropped)
        try {
            $destination = [System.Drawing.Rectangle]::new(0, 0, $Width, $Height)
            $graphics.DrawImage($source, $destination, $Left, $Top, $Width, $Height, [System.Drawing.GraphicsUnit]::Pixel)
            $temporaryPath = "$Path.cropped.png"
            $cropped.Save($temporaryPath, [System.Drawing.Imaging.ImageFormat]::Png)
        }
        finally {
            $graphics.Dispose()
            $cropped.Dispose()
        }
    }
    finally {
        $source.Dispose()
    }

    Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
}

function Clear-LowAlphaBorder {
    param(
        [string]$Path,
        [byte]$AlphaThreshold = 51,
        [int]$BorderWidth = 16
    )

    $bitmap = [System.Drawing.Bitmap]::FromFile($Path)
    try {
        $horizontalBorder = [Math]::Min($BorderWidth, [Math]::Floor($bitmap.Height / 2))
        $verticalBorder = [Math]::Min($BorderWidth, [Math]::Floor($bitmap.Width / 2))
        $rectangle = [System.Drawing.Rectangle]::new(0, 0, $bitmap.Width, $bitmap.Height)
        $bitmapData = $bitmap.LockBits(
            $rectangle,
            [System.Drawing.Imaging.ImageLockMode]::ReadWrite,
            [System.Drawing.Imaging.PixelFormat]::Format32bppArgb
        )
        try {
            $stride = [Math]::Abs($bitmapData.Stride)
            $pixels = [byte[]]::new($stride * $bitmap.Height)
            [System.Runtime.InteropServices.Marshal]::Copy($bitmapData.Scan0, $pixels, 0, $pixels.Length)

            for ($y = 0; $y -lt $bitmap.Height; $y++) {
                $isHorizontalBorder = $y -lt $horizontalBorder -or $y -ge $bitmap.Height - $horizontalBorder
                for ($x = 0; $x -lt $bitmap.Width; $x++) {
                    if (-not $isHorizontalBorder -and $x -ge $verticalBorder -and $x -lt $bitmap.Width - $verticalBorder) {
                        continue
                    }

                    $alphaOffset = $y * $stride + $x * 4 + 3
                    if ($pixels[$alphaOffset] -lt $AlphaThreshold) {
                        $pixels[$alphaOffset] = 0
                    }
                }
            }

            [System.Runtime.InteropServices.Marshal]::Copy($pixels, 0, $bitmapData.Scan0, $pixels.Length)
        }
        finally {
            $bitmap.UnlockBits($bitmapData)
        }

        $temporaryPath = "$Path.alpha.png"
        $bitmap.Save($temporaryPath, [System.Drawing.Imaging.ImageFormat]::Png)
    }
    finally {
        $bitmap.Dispose()
    }

    Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
}

function Save-Result {
    param([hashtable]$Result)

    $Result | ConvertTo-Json -Depth 5 | Set-Content -Encoding UTF8 (Join-Path $OutputDirectory "$Name.json")
}

New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$process = $null
$baselineProcessIds = $null

try {
    Stop-StaleCatalogProcesses -ExecutablePath $Executable
    Close-StaleWindows
    $baselineProcessIds = Get-ProcessIds
    $startParameters = @{
        FilePath = $Executable
        WorkingDirectory = $(if ($WorkingDirectory.Length -gt 0) { $WorkingDirectory } else { Split-Path $Executable })
        PassThru = $true
    }
    if ($Arguments.Length -gt 0) {
        $startParameters.ArgumentList = $Arguments
    }
    $process = Start-Process @startParameters
    $deadline = (Get-Date).AddSeconds($WaitSeconds)
    $windows = @()

    do {
        Start-Sleep -Milliseconds 500
        $processIds = Get-ProcessTreeIds -RootProcessId $process.Id
        $windows = @(Get-ProgramWindows -ProcessIds $processIds)
    } while ($windows.Count -eq 0 -and (Get-Date) -lt $deadline)

    if ($windows.Count -eq 0) {
        $reason = if ($process.HasExited) { "process-exited" } else { "no-window" }
        Save-Result @{
            success = $false
            reason = $reason
            exitCode = $(if ($process.HasExited) { $process.ExitCode } else { $null })
        }
        exit 1
    }

    $settleDeadline = (Get-Date).AddSeconds(3)
    do {
        Start-Sleep -Milliseconds 500
        $processIds = Get-ProcessTreeIds -RootProcessId $process.Id
        $settledWindows = @(Get-ProgramWindows -ProcessIds $processIds)
        if ($settledWindows.Count -gt 0) {
            $windows = $settledWindows
        }
    } while ((Get-Date) -lt $settleDeadline)

    $preferredTitle = ""
    if ($InitialKeys.Length -gt 0) {
        foreach ($initialKey in $InitialKeys.Split(',')) {
            $rawKey = $initialKey.Trim()
            $key = $rawKey.ToUpperInvariant()
            if ($key.StartsWith("SELECT-TITLE=")) {
                $preferredTitle = $rawKey.Substring(13)
            }
            elseif ($key.StartsWith("TEXT=")) {
                $initialWindow = $windows | Sort-Object Area -Descending | Select-Object -First 1
                [void][ScreenshotWindowApi]::SetForegroundWindow($initialWindow.Handle)
                [System.Windows.Forms.SendKeys]::SendWait($rawKey.Substring(5))
            }
            elseif ($key -eq "CLOSE-SMALL") {
                $dialog = $windows | Sort-Object Area | Select-Object -First 1
                [void][ScreenshotWindowApi]::PostMessage($dialog.Handle, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
            }
            elseif ($key -eq "CLOSE-LARGEST") {
                $dialog = $windows | Sort-Object Area -Descending | Select-Object -First 1
                [void][ScreenshotWindowApi]::PostMessage($dialog.Handle, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero)
            }
            else {
                $initialWindow = $windows | Sort-Object Area -Descending | Select-Object -First 1
                [void][ScreenshotWindowApi]::SetForegroundWindow($initialWindow.Handle)
                $sendKeys = switch ($key) {
                    "ALT+C" { "%c" }
                    "ALT+N" { "%n" }
                    "ALT+Y" { "%y" }
                    "ALT+F4" { "%{F4}" }
                    default { "{$key}" }
                }
                [System.Windows.Forms.SendKeys]::SendWait($sendKeys)
            }
            Start-Sleep -Seconds 2
            $processIds = Get-ProcessTreeIds -RootProcessId $process.Id
            $dismissedWindows = @(Get-ProgramWindows -ProcessIds $processIds)
            if ($dismissedWindows.Count -gt 0) {
                $windows = $dismissedWindows
            }
        }
    }

    $window = if ($preferredTitle.Length -gt 0) {
        $windows | Where-Object Title -Like "*$preferredTitle*" | Sort-Object Area -Descending | Select-Object -First 1
    }
    else {
        $windows | Sort-Object Area -Descending | Select-Object -First 1
    }
    if ($null -eq $window) {
        throw "No window matched preferred title: $preferredTitle"
    }
    if ([ScreenshotWindowApi]::IsIconic($window.Handle)) {
        [void][ScreenshotWindowApi]::ShowWindow($window.Handle, 9)
    }
    if (-not [ScreenshotWindowApi]::IsZoomed($window.Handle)) {
        [void][ScreenshotWindowApi]::SetWindowPos($window.Handle, [IntPtr]::Zero, 20, 20, 0, 0, 0x0015)
        Start-Sleep -Milliseconds 300
        $processIds = Get-ProcessTreeIds -RootProcessId $process.Id
        $movedWindows = @(Get-ProgramWindows -ProcessIds $processIds)
        $movedWindow = $movedWindows | Where-Object Handle -eq $window.Handle | Select-Object -First 1
        if ($null -ne $movedWindow) {
            $window = $movedWindow
            $windows = $movedWindows
        }
    }
    [void][ScreenshotWindowApi]::SetForegroundWindow($window.Handle)
    Close-StaleWindows
    Start-Sleep -Milliseconds 500

    $screenshotPath = Join-Path $OutputDirectory "$Name.png"
    $borderInset = 2
    if ($ScreenshotTool.Length -gt 0 -and (Test-Path -LiteralPath $ScreenshotTool)) {
        $screenshotToolName = [System.IO.Path]::GetFileName($ScreenshotTool)
        if ($screenshotToolName -ieq "winapp.exe") {
            $winappExitCode = 1
            $winappOutput = @()
            for ($attempt = 1; $attempt -le 2; $attempt++) {
                Remove-Item -LiteralPath $screenshotPath -Force -ErrorAction SilentlyContinue
                $winappOutput = @(& $ScreenshotTool ui screenshot -w $window.Handle.ToInt64() --output $screenshotPath --json 2>&1)
                $winappExitCode = $LASTEXITCODE
                if ($winappExitCode -eq 0) {
                    break
                }
                Start-Sleep -Milliseconds 500
            }
            if ($winappExitCode -ne 0) {
                Remove-Item -LiteralPath $screenshotPath -Force -ErrorAction SilentlyContinue
                $winappOutput = @(& $ScreenshotTool ui screenshot -a $window.ProcessId --output $screenshotPath --json 2>&1)
                $winappExitCode = $LASTEXITCODE
            }
            if ($winappExitCode -ne 0) {
                $winappDetails = ($winappOutput | Out-String).Trim()
                throw "winapp CLI failed for window '$($window.Title)' with exit code ${winappExitCode}: $winappDetails"
            }
        }
        else {
            [void][ScreenshotWindowApi]::SetForegroundWindow($window.Handle)
            Start-Sleep -Milliseconds 200
            & $ScreenshotTool savescreenshotwin $screenshotPath
        }
        $screenshotDeadline = (Get-Date).AddSeconds(3)
        while (
            (-not (Test-Path -LiteralPath $screenshotPath) -or (Get-Item -LiteralPath $screenshotPath).Length -eq 0) -and
            (Get-Date) -lt $screenshotDeadline
        ) {
            Start-Sleep -Milliseconds 100
        }
        if (-not (Test-Path -LiteralPath $screenshotPath) -or (Get-Item -LiteralPath $screenshotPath).Length -eq 0) {
            throw "Window screenshot tool failed for window '$($window.Title)'."
        }
        if ($screenshotToolName -ieq "winapp.exe") {
            Clear-LowAlphaBorder -Path $screenshotPath
        }
        else {
            Crop-WindowShadow `
                -Path $screenshotPath `
                -Left ($window.CropLeft + $borderInset) `
                -Top ($window.CropTop + $borderInset) `
                -Width ($window.Width - 2 * $borderInset) `
                -Height ($window.Height - 2 * $borderInset)
        }
    }
    else {
        $bitmap = [System.Drawing.Bitmap]::new($window.Width, $window.Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
        $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
        try {
            $graphics.CopyFromScreen($window.Left, $window.Top, 0, 0, $bitmap.Size)
            $bitmap.Save($screenshotPath, [System.Drawing.Imaging.ImageFormat]::Png)
        }
        finally {
            $graphics.Dispose()
            $bitmap.Dispose()
        }
        Crop-WindowShadow `
            -Path $screenshotPath `
            -Left $borderInset `
            -Top $borderInset `
            -Width ($window.Width - 2 * $borderInset) `
            -Height ($window.Height - 2 * $borderInset)
    }

    Save-Result @{
        success = $true
        title = $window.Title
        width = $window.Width
        height = $window.Height
        windows = @($windows | ForEach-Object {
            @{
                processId = $_.ProcessId
                title = $_.Title
                width = $_.Width
                height = $_.Height
            }
        })
    }
}
catch {
    Save-Result @{
        success = $false
        reason = "error"
        error = $_.Exception.Message
    }
    exit 1
}
finally {
    if ($null -ne $process -and -not $process.HasExited) {
        taskkill.exe /PID $process.Id /T /F 2>$null | Out-Null
    }
    if ($null -ne $baselineProcessIds) {
        Stop-NewProcesses -BaselineProcessIds $baselineProcessIds
    }
}
