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

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

  [int]$FrameCount = 12,

  [double]$StartSeconds = 0,

  [double]$EndSeconds = -1
)

$ErrorActionPreference = "Stop"
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase

function Invoke-DispatcherWait {
  param([int]$Milliseconds)

  $frame = [System.Windows.Threading.DispatcherFrame]::new()
  $timer = [System.Windows.Threading.DispatcherTimer]::new(
    [System.Windows.Threading.DispatcherPriority]::Background
  )
  $timer.Interval = [TimeSpan]::FromMilliseconds($Milliseconds)
  $timer.Add_Tick({
    $timer.Stop()
    $frame.Continue = $false
  })
  $timer.Start()
  [System.Windows.Threading.Dispatcher]::PushFrame($frame)
}

$resolvedVideo = (Resolve-Path -LiteralPath $VideoPath).Path
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputDirectory)
[System.IO.Directory]::CreateDirectory($resolvedOutput) | Out-Null

$player = [System.Windows.Media.MediaPlayer]::new()
$player.ScrubbingEnabled = $true
$player.Open([Uri]::new($resolvedVideo))
$player.Play()

for ($attempt = 0; $attempt -lt 100 -and $player.NaturalVideoWidth -eq 0; $attempt++) {
  Invoke-DispatcherWait -Milliseconds 100
}

if ($player.NaturalVideoWidth -eq 0 -or -not $player.NaturalDuration.HasTimeSpan) {
  $player.Close()
  throw "Unable to decode video: $resolvedVideo"
}

$width = $player.NaturalVideoWidth
$height = $player.NaturalVideoHeight
$duration = $player.NaturalDuration.TimeSpan.TotalSeconds
$rangeStart = [Math]::Min([Math]::Max($StartSeconds, 0), $duration)
$rangeEnd = if ($EndSeconds -lt 0) {
  $duration
} else {
  [Math]::Min([Math]::Max($EndSeconds, $rangeStart), $duration)
}
$metadata = [ordered]@{
  path = $resolvedVideo
  width = $width
  height = $height
  durationSeconds = [Math]::Round($duration, 3)
  frames = @()
}

$player.Stop()
$player.Position = [TimeSpan]::Zero
$player.SpeedRatio = 16
$player.Play()

for ($index = 0; $index -lt $FrameCount; $index++) {
  $ratio = if ($FrameCount -eq 1) { 0.5 } else { $index / ($FrameCount - 1) }
  $seconds = $rangeStart + (($rangeEnd - $rangeStart) * $ratio)
  $seconds = [Math]::Min([Math]::Max($seconds, 0), [Math]::Max($duration - 0.08, 0))
  $deadline = [DateTime]::UtcNow.AddSeconds(45)
  while ($player.Position.TotalSeconds -lt $seconds -and [DateTime]::UtcNow -lt $deadline) {
    Invoke-DispatcherWait -Milliseconds 40
  }
  Invoke-DispatcherWait -Milliseconds 80

  $visual = [System.Windows.Media.DrawingVisual]::new()
  $context = $visual.RenderOpen()
  $context.DrawVideo(
    $player,
    [Windows.Rect]::new(0, 0, $width, $height)
  )
  $context.Close()

  $bitmap = [System.Windows.Media.Imaging.RenderTargetBitmap]::new(
    $width,
    $height,
    96,
    96,
    [System.Windows.Media.PixelFormats]::Pbgra32
  )
  $bitmap.Render($visual)

  $milliseconds = [long][Math]::Round($seconds * 1000)
  $fileName = "frame-{0:D2}-{1:D6}ms.png" -f ([int]($index + 1)), $milliseconds
  $filePath = Join-Path $resolvedOutput $fileName
  $encoder = [System.Windows.Media.Imaging.PngBitmapEncoder]::new()
  $encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
  $stream = [System.IO.File]::Open($filePath, [System.IO.FileMode]::Create)
  try {
    $encoder.Save($stream)
  } finally {
    $stream.Dispose()
  }

  $metadata.frames += [ordered]@{
    file = $fileName
    seconds = [Math]::Round($seconds, 3)
    actualSeconds = [Math]::Round($player.Position.TotalSeconds, 3)
  }
}

$player.Close()
$metadata | ConvertTo-Json -Depth 4
