param(
    [Parameter(Mandatory = $true)]
    [string]$ExecutablePath,
    [Parameter(Mandatory = $true)]
    [string]$OutputDirectory,
    [string]$TargetMap = '',
    [string[]]$ExpectedLogSubstring = @(),
    [ValidateSet('Startup', 'RuntimeReady')]
    [string]$CaptureTrigger = 'RuntimeReady',
    [int]$TimeoutSeconds = 45,
    [int]$SampleStride = 6,
    [double]$MinimumNonBlackRatio = 0.02,
    [double]$MinimumLuminanceDeviation = 2.5,
    [double]$MinimumLuminanceRange = 20.0
)

$ErrorActionPreference = 'Stop'

function Write-Utf8JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $ParentPath = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($ParentPath))
    {
        New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
    }

    [System.IO.File]::WriteAllText(
        $Path,
        ($Value | ConvertTo-Json -Depth 12),
        (New-Object System.Text.UTF8Encoding($false))
    )
}

function Update-HyperTwistOwnedProcessIds {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.Generic.HashSet[int]]$OwnedProcessIds,
        [Parameter(Mandatory = $true)]
        [string]$ProcessName,
        [int[]]$IgnoredProcessIds = @()
    )

    foreach ($Process in @(Get-Process -Name $ProcessName -ErrorAction SilentlyContinue))
    {
        if ($IgnoredProcessIds -notcontains $Process.Id)
        {
            [void]$OwnedProcessIds.Add([int]$Process.Id)
        }
    }
}

function Measure-HyperTwistBitmap {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [int]$Stride = 6
    )

    Add-Type -AssemblyName System.Drawing
    $Bitmap = [System.Drawing.Bitmap]::FromFile($Path)
    try
    {
        $SafeStride = [Math]::Max($Stride, 1)
        $SampleCount = 0
        $NonBlackCount = 0
        $LuminanceSum = 0.0
        $LuminanceSquaredSum = 0.0
        $MinimumLuminance = 255.0
        $MaximumLuminance = 0.0

        for ($Y = 0; $Y -lt $Bitmap.Height; $Y += $SafeStride)
        {
            for ($X = 0; $X -lt $Bitmap.Width; $X += $SafeStride)
            {
                $Pixel = $Bitmap.GetPixel($X, $Y)
                $Luminance = (0.2126 * $Pixel.R) + (0.7152 * $Pixel.G) + (0.0722 * $Pixel.B)
                $SampleCount += 1
                $LuminanceSum += $Luminance
                $LuminanceSquaredSum += ($Luminance * $Luminance)
                $MinimumLuminance = [Math]::Min($MinimumLuminance, $Luminance)
                $MaximumLuminance = [Math]::Max($MaximumLuminance, $Luminance)
                if ($Luminance -gt 10.0)
                {
                    $NonBlackCount += 1
                }
            }
        }

        if ($SampleCount -le 0)
        {
            throw 'The off-screen frame contained no image samples.'
        }

        $AverageLuminance = $LuminanceSum / $SampleCount
        $Variance = [Math]::Max(
            ($LuminanceSquaredSum / $SampleCount) - ($AverageLuminance * $AverageLuminance),
            0.0
        )

        return [ordered]@{
            width = $Bitmap.Width
            height = $Bitmap.Height
            sampleStride = $SafeStride
            sampleCount = $SampleCount
            nonBlackRatio = $NonBlackCount / $SampleCount
            averageLuminance = $AverageLuminance
            luminanceDeviation = [Math]::Sqrt($Variance)
            minimumLuminance = $MinimumLuminance
            maximumLuminance = $MaximumLuminance
            luminanceRange = $MaximumLuminance - $MinimumLuminance
        }
    }
    finally
    {
        $Bitmap.Dispose()
    }
}

if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
    throw "HyperTwist executable '$ExecutablePath' does not exist."
}

New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ResolvedOutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path
$ExecutableDirectory = Split-Path -Parent $ResolvedExecutablePath
$ExecutableProcessName = [System.IO.Path]::GetFileNameWithoutExtension($ResolvedExecutablePath)
$SavedDirectory = Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved'
$ScreenshotDirectory = if ($CaptureTrigger -eq 'RuntimeReady')
{
    Join-Path $SavedDirectory 'Screenshots\HyperTwistDiagnostics'
}
else
{
    Join-Path $SavedDirectory 'Screenshots\Windows'
}
$RuntimeLogPath = Join-Path $SavedDirectory 'Logs\UnrealHyperTwist.log'
$CapturedFramePath = Join-Path $ResolvedOutputDirectory 'HyperTwist-offscreen-frame.png'
$CapturedLogPath = Join-Path $ResolvedOutputDirectory 'UnrealHyperTwist.log'
$ReportPath = Join-Path $ResolvedOutputDirectory 'HyperTwist-offscreen-visual-report.json'
$ExistingProcessIds = @(
    Get-Process -Name $ExecutableProcessName -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty Id
)
$ExistingScreenshotSignatures = @{}
foreach ($ExistingScreenshot in @(
    Get-ChildItem -LiteralPath $ScreenshotDirectory -File -ErrorAction SilentlyContinue
))
{
    $ExistingScreenshotSignatures[$ExistingScreenshot.FullName.ToLowerInvariant()] =
        '{0}:{1}' -f $ExistingScreenshot.LastWriteTimeUtc.Ticks, $ExistingScreenshot.Length
}
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
$LaunchArguments = @()
if (-not [string]::IsNullOrWhiteSpace($TargetMap))
{
    $LaunchArguments += $TargetMap
}
$LaunchArguments += @(
    '-RenderOffScreen',
    '-windowed',
    '-ResX=1280',
    '-ResY=720'
)
if ($CaptureTrigger -eq 'RuntimeReady')
{
    $LaunchArguments += '-HyperTwistCaptureWhenReady'
}
else
{
    $LaunchArguments += '-ExecCmds="SHOT SHOWUI"'
}

$Result = [ordered]@{
    reportVersion = 'ht-packaged-offscreen-visual/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    executablePath = $ResolvedExecutablePath
    executableSha256 = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
    targetMap = $TargetMap
    captureTrigger = $CaptureTrigger
    launchArguments = @($LaunchArguments)
    expectedLogSubstrings = @($ExpectedLogSubstring)
    matchedLogSubstrings = @()
    screenshotPath = $CapturedFramePath
    screenshotSha256 = $null
    runtimeLogPath = $CapturedLogPath
    metrics = $null
    thresholds = [ordered]@{
        minimumNonBlackRatio = $MinimumNonBlackRatio
        minimumLuminanceDeviation = $MinimumLuminanceDeviation
        minimumLuminanceRange = $MinimumLuminanceRange
    }
    processIds = @()
    terminatedProcessIds = @()
    result = 'failed'
    error = $null
}

$LaunchedProcess = $null

try
{
    $LaunchStartedAtUtc = [DateTime]::UtcNow
    $LaunchedProcess = Start-Process `
        -FilePath $ResolvedExecutablePath `
        -ArgumentList $LaunchArguments `
        -WorkingDirectory $ExecutableDirectory `
        -PassThru
    [void]$OwnedProcessIds.Add($LaunchedProcess.Id)

    $Deadline = $LaunchStartedAtUtc.AddSeconds([Math]::Max($TimeoutSeconds, 1))
    $FreshScreenshot = $null
    $RuntimeLog = ''
    while ([DateTime]::UtcNow -lt $Deadline)
    {
        Update-HyperTwistOwnedProcessIds `
            -OwnedProcessIds $OwnedProcessIds `
            -ProcessName $ExecutableProcessName `
            -IgnoredProcessIds $ExistingProcessIds

        $FreshScreenshot = Get-ChildItem -LiteralPath $ScreenshotDirectory -File -ErrorAction SilentlyContinue |
            Where-Object {
                $ScreenshotKey = $_.FullName.ToLowerInvariant()
                $CurrentSignature = '{0}:{1}' -f $_.LastWriteTimeUtc.Ticks, $_.Length
                -not $ExistingScreenshotSignatures.ContainsKey($ScreenshotKey) `
                    -or $ExistingScreenshotSignatures[$ScreenshotKey] -ne $CurrentSignature
            } |
            Sort-Object LastWriteTimeUtc -Descending |
            Select-Object -First 1

        if (Test-Path -LiteralPath $RuntimeLogPath)
        {
            $RuntimeLogItem = Get-Item -LiteralPath $RuntimeLogPath
            if ($RuntimeLogItem.LastWriteTimeUtc -ge $LaunchStartedAtUtc.AddSeconds(-1))
            {
                $LoadedRuntimeLog = Get-Content -LiteralPath $RuntimeLogPath -Raw
                $RuntimeLog = if ($null -eq $LoadedRuntimeLog)
                {
                    ''
                }
                else
                {
                    [string]$LoadedRuntimeLog
                }
            }
        }

        $MatchedLogSubstrings = @(
            $ExpectedLogSubstring |
                Where-Object {
                    $RuntimeLog.IndexOf($_, [System.StringComparison]::Ordinal) -ge 0
                }
        )
        $Result.matchedLogSubstrings = @($MatchedLogSubstrings)

        if ($null -ne $FreshScreenshot `
            -and $MatchedLogSubstrings.Count -eq $ExpectedLogSubstring.Count)
        {
            break
        }

        $LiveOwnedProcesses = @(
            $OwnedProcessIds |
                ForEach-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue } |
                Where-Object { $null -ne $_ }
        )
        if ($LiveOwnedProcesses.Count -eq 0)
        {
            throw 'HyperTwist exited before off-screen visual validation completed.'
        }

        Start-Sleep -Milliseconds 500
    }

    if ($null -eq $FreshScreenshot)
    {
        throw "HyperTwist did not write a fresh UI-inclusive screenshot within $TimeoutSeconds seconds."
    }
    if ($Result.matchedLogSubstrings.Count -ne $ExpectedLogSubstring.Count)
    {
        $MissingLogSubstrings = @(
            $ExpectedLogSubstring |
                Where-Object { $Result.matchedLogSubstrings -notcontains $_ }
        )
        throw "The current runtime log omitted required markers: $($MissingLogSubstrings -join '; ')"
    }
    if ($RuntimeLog -match '(?im)Fatal error:|Unhandled Exception|Assertion failed|CreateSwapChainResult failed')
    {
        throw 'The current runtime log contains a fatal startup signature.'
    }

    Copy-Item -LiteralPath $FreshScreenshot.FullName -Destination $CapturedFramePath -Force
    Copy-Item -LiteralPath $RuntimeLogPath -Destination $CapturedLogPath -Force
    $Result.screenshotSha256 = (
        Get-FileHash -LiteralPath $CapturedFramePath -Algorithm SHA256
    ).Hash.ToLowerInvariant()
    $Result.metrics = Measure-HyperTwistBitmap -Path $CapturedFramePath -Stride $SampleStride
    if ($Result.metrics.nonBlackRatio -lt $MinimumNonBlackRatio `
        -or $Result.metrics.luminanceDeviation -lt $MinimumLuminanceDeviation `
        -or $Result.metrics.luminanceRange -lt $MinimumLuminanceRange)
    {
        throw 'The UI-inclusive off-screen frame did not satisfy the configured visual thresholds.'
    }

    $Result.processIds = @($OwnedProcessIds | Sort-Object)
    $Result.result = 'passed'
}
catch
{
    $Result.error = $_.Exception.Message
}
finally
{
    Update-HyperTwistOwnedProcessIds `
        -OwnedProcessIds $OwnedProcessIds `
        -ProcessName $ExecutableProcessName `
        -IgnoredProcessIds $ExistingProcessIds
    foreach ($ProcessId in @($OwnedProcessIds | Sort-Object -Descending))
    {
        $Process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
        if ($null -ne $Process)
        {
            Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
            $Result.terminatedProcessIds += $ProcessId
        }
    }

    Write-Utf8JsonFile -Path $ReportPath -Value $Result
}

if ($Result.result -ne 'passed')
{
    throw $Result.error
}

Write-Output "HyperTwist off-screen visual validation passed: $ReportPath"
