# FireStorm run driver -- launch/drive/screenshot the game (MW4.exe) and mission editor (MW4Ed2.exe).
# Windows PowerShell 5.1. Usage:  powershell -ExecutionPolicy Bypass -File driver.ps1 <command> [arg]
# Commands:
#   status                 list running FireStorm processes + their top-level windows
#   report <MissionName>   HEADLESS: editor loads the mission, writes <MissionName>.report, exits.
#                          Prints the report. (Editor always exits code 1 by design -- not an error.)
#   editor-start           launch the mission editor (windowed), wait for its UI to appear
#   game-start [args]      launch the game (fullscreen 800x600 16-bit -- grabs the display);
#                          optional quoted extra command line, e.g. game-start "-tbaud 115200"
#   shot [name]            screenshot -> shots\<name|timestamp>.png (whole virtual screen;
#                          fullscreen-exclusive DDraw has no window rect worth cropping)
#   windows                enumerate top-level windows (detects the editor's modal dialogs)
#   stop                   kill MW4Ed2 / MW4 / MW4pro
param(
    [Parameter(Position = 0)][string]$Command = 'status',
    [Parameter(Position = 1)][string]$Arg,
    [Parameter(Position = 2)][string]$Arg2
)
$ErrorActionPreference = 'Stop'

$RepoRoot  = (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName   # ...\.claude\skills\run-firestorm -> repo
$GameDir   = Join-Path $RepoRoot 'MW4'
$EditorDir = Join-Path $RepoRoot 'Gameleap\mw4'
$ShotDir   = Join-Path $PSScriptRoot 'shots'
$ProcNames = 'MW4Ed2', 'MW4', 'MW4pro'

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
public class FsWin {
    [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr lp);
    [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
    [DllImport("user32.dll")] static extern int  GetWindowText(IntPtr h, StringBuilder s, int n);
    [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
    [DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT r);
    [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
    [DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
    [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);   // 9 = SW_RESTORE
    [DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint data, UIntPtr extra);
    public static void Click(int x, int y) {
        SetCursorPos(x, y);
        System.Threading.Thread.Sleep(150);
        mouse_event(0x0002, 0, 0, 0, UIntPtr.Zero);   // LEFTDOWN
        System.Threading.Thread.Sleep(80);
        mouse_event(0x0004, 0, 0, 0, UIntPtr.Zero);   // LEFTUP
    }
    public struct RECT { public int L, T, R, B; }
    delegate bool EnumProc(IntPtr h, IntPtr lp);
    public class Info { public IntPtr HWnd; public uint Pid; public string Title; public int X, Y, W, H; }
    public static List<Info> Windows(uint[] pids) {
        var found = new List<Info>();
        EnumWindows((h, lp) => {
            if (!IsWindowVisible(h)) return true;
            uint pid; GetWindowThreadProcessId(h, out pid);
            if (Array.IndexOf(pids, pid) < 0) return true;
            var sb = new StringBuilder(512); GetWindowText(h, sb, 512);
            RECT r; GetWindowRect(h, out r);
            found.Add(new Info { HWnd = h, Pid = pid, Title = sb.ToString(),
                                 X = r.L, Y = r.T, W = r.R - r.L, H = r.B - r.T });
            return true;
        }, IntPtr.Zero);
        return found;
    }
}
'@

function Get-FsProcs { Get-Process -Name $ProcNames -ErrorAction SilentlyContinue }

function Show-Windows {
    $procs = Get-FsProcs
    if (-not $procs) { Write-Output 'No FireStorm processes running.'; return }
    $pids = [uint32[]]($procs | ForEach-Object Id)
    foreach ($w in [FsWin]::Windows($pids)) {
        $p = $procs | Where-Object Id -eq $w.Pid | Select-Object -First 1
        Write-Output ("{0,-8} pid={1,-6} {2}x{3} @({4},{5})  '{6}'" -f $p.Name, $w.Pid, $w.W, $w.H, $w.X, $w.Y, $w.Title)
    }
}

function Wait-ForWindow([string]$TitlePattern, [int]$TimeoutSec = 180) {
    $sw = [Diagnostics.Stopwatch]::StartNew()
    while ($sw.Elapsed.TotalSeconds -lt $TimeoutSec) {
        $procs = Get-FsProcs
        if ($procs) {
            $pids = [uint32[]]($procs | ForEach-Object Id)
            $hit = [FsWin]::Windows($pids) | Where-Object { $_.Title -match $TitlePattern }
            if ($hit) { return $hit | Select-Object -First 1 }
        }
        Start-Sleep -Milliseconds 2000
    }
    return $null
}

switch ($Command) {

    'status' {
        $procs = Get-FsProcs
        if ($procs) { $procs | Select-Object Name, Id, StartTime | Format-Table -AutoSize | Out-String | Write-Output }
        Show-Windows
    }

    'windows' { Show-Windows }

    'report' {
        if (-not $Arg) { throw 'Usage: driver.ps1 report <MissionName>  (a folder name under Gameleap\mw4\Content\Missions)' }
        if (Get-Process -Name 'MW4Ed2' -ErrorAction SilentlyContinue) {
            throw 'MW4Ed2 already running (single-instance mutex would pop a message box). Run: driver.ps1 stop'
        }
        $reportFile = Join-Path $EditorDir "$Arg.report"
        if (Test-Path $reportFile) { Remove-Item $reportFile -Force }
        # -report hides all windows, loads the mission, writes <name>.report to the CWD, then exit(1).
        $p = Start-Process -FilePath (Join-Path $EditorDir 'MW4Ed2.exe') `
                           -ArgumentList "-armorlevel 3 -report $Arg" `
                           -WorkingDirectory $EditorDir -PassThru
        if (-not $p.WaitForExit(300000)) { $p.Kill(); throw 'report: editor did not exit within 5 min (STOP dialog on broken content?). Killed.' }
        Write-Output "editor exit code: $($p.ExitCode) (1 is normal -- it always exit(1)s after -report)"
        if (Test-Path $reportFile) {
            $lines = Get-Content $reportFile
            Write-Output "report: $reportFile ($($lines.Count) lines)"
            Write-Output ($lines | Select-Object -First 12)
            if ($lines.Count -gt 12) { Write-Output "... ($($lines.Count - 12) more lines)" }
            if ($lines -contains 'UnLoadable') { Write-Output '!! Mission failed to load (UnLoadable).' }
        } else {
            Write-Output "!! No report file produced ($reportFile) -- editor died before MakeReport (content STOP?)."
        }
    }

    'editor-start' {
        if (Get-Process -Name 'MW4Ed2' -ErrorAction SilentlyContinue) { throw 'MW4Ed2 already running. Run: driver.ps1 stop' }
        Start-Process -FilePath (Join-Path $EditorDir 'MW4Ed2.exe') -ArgumentList '-armorlevel 3' -WorkingDirectory $EditorDir | Out-Null
        # Editor loads/validates the whole content tree, then presents a modal "Open Mission" dialog.
        $w = Wait-ForWindow 'Open Mission|Mission Editor' 180
        if ($w) { Write-Output "Editor up: '$($w.Title)'"; Show-Windows }
        else    { throw 'Editor window did not appear within 180 s.' }
    }

    'game-start' {
        if (Get-Process -Name 'MW4' -ErrorAction SilentlyContinue) { throw 'MW4 already running. Run: driver.ps1 stop' }
        if ($Arg) {
            Start-Process -FilePath (Join-Path $GameDir 'MW4.exe') -ArgumentList $Arg -WorkingDirectory $GameDir | Out-Null
        } else {
            Start-Process -FilePath (Join-Path $GameDir 'MW4.exe') -WorkingDirectory $GameDir | Out-Null
        }
        # NB: the game window title is 'BattleTech Firestorm' (not MechWarrior/MW4)
        $w = Wait-ForWindow 'Firestorm|FireStorm' 120
        if ($w) { Write-Output "Game up: '$($w.Title)' ($($w.W)x$($w.H))"; Show-Windows }
        else    { throw 'Game window did not appear within 120 s.' }
    }

    { $_ -in 'shot', 'shotwin' } {
        if (-not (Test-Path $ShotDir)) { New-Item -ItemType Directory $ShotDir | Out-Null }
        if ($Command -eq 'shotwin') {
            # shotwin <titleRegex> [name] -- crop to the first matching app window
            if (-not $Arg) { throw 'Usage: driver.ps1 shotwin <titleRegex> [name]' }
            $procs = Get-FsProcs
            if (-not $procs) { throw 'No FireStorm process running.' }
            $pids = [uint32[]]($procs | ForEach-Object Id)
            $w = [FsWin]::Windows($pids) | Where-Object { $_.Title -match $Arg } | Select-Object -First 1
            if (-not $w) { throw "No window title matches '$Arg' (run: driver.ps1 windows)" }
            [FsWin]::SetForegroundWindow($w.HWnd) | Out-Null; Start-Sleep -Milliseconds 400
            $bx = $w.X; $by = $w.Y; $bw = $w.W; $bh = $w.H
            $name = if ($Arg2) { $Arg2 } else { Get-Date -Format 'yyyyMMdd-HHmmss' }
        } else {
            # shot [name] -- primary screen only (the game runs fullscreen on the primary;
            # deliberately NOT the whole virtual desktop, to avoid capturing other monitors)
            $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
            $bx = $b.X; $by = $b.Y; $bw = $b.Width; $bh = $b.Height
            $name = if ($Arg) { $Arg } else { Get-Date -Format 'yyyyMMdd-HHmmss' }
        }
        $file = Join-Path $ShotDir "$name.png"
        $bmp = New-Object System.Drawing.Bitmap($bw, $bh)
        $g = [System.Drawing.Graphics]::FromImage($bmp)
        $g.CopyFromScreen($bx, $by, 0, 0, $bmp.Size)
        $g.Dispose()
        $bmp.Save($file, [System.Drawing.Imaging.ImageFormat]::Png)
        # crude non-blackness check so an agent can tell a dead capture without opening the file
        $sum = 0.0; $n = 0
        for ($x = 50; $x -lt $bmp.Width; $x += [Math]::Max(1, [int]($bmp.Width / 16))) {
            for ($y = 50; $y -lt $bmp.Height; $y += [Math]::Max(1, [int]($bmp.Height / 16))) {
                $c = $bmp.GetPixel($x, $y); $sum += ($c.R + $c.G + $c.B) / 3.0; $n++
            }
        }
        $bmp.Dispose()
        Write-Output ("saved: {0} ({1}x{2}, mean brightness {3:N0}/255)" -f $file, $bw, $bh, ($sum / $n))
    }

    'click' {
        # click <x> <y> -- screen-coordinate left click (game is fullscreen at 0,0, so
        # screen coords == game coords at 800x600). Foregrounds the app window first.
        if (-not $Arg -or -not $Arg2) { throw 'Usage: driver.ps1 click <x> <y>' }
        $procs = Get-FsProcs
        if ($procs) {
            $pids = [uint32[]]($procs | ForEach-Object Id)
            $w = [FsWin]::Windows($pids) | Where-Object { $_.Title -notmatch 'DirectDrawDeviceWnd' } | Select-Object -First 1
            if ($w) {
                # Fullscreen-exclusive DDraw MINIMIZES on focus loss -- must restore before clicking,
                # then wait out the display-mode switch back to 800x600.
                [FsWin]::ShowWindow($w.HWnd, 9) | Out-Null
                [FsWin]::SetForegroundWindow($w.HWnd) | Out-Null
                Start-Sleep -Milliseconds 2500
            }
        }
        [FsWin]::Click([int]$Arg, [int]$Arg2)
        Write-Output "clicked ($Arg,$Arg2)"
    }

    'stop' {
        $procs = Get-FsProcs
        if (-not $procs) { Write-Output 'Nothing to stop.'; break }
        $procs | ForEach-Object { Write-Output "killing $($_.Name) pid=$($_.Id)"; Stop-Process -Id $_.Id -Force }
    }

    default { throw "Unknown command '$Command'. Commands: status report editor-start game-start shot windows stop" }
}
