param([string]$AssemblyPath = (Join-Path $PSScriptRoot '..\bin\Debug\netstandard2.0\ClumsyPilot.dll'))
$ErrorActionPreference = 'Stop'
$assembly = [Reflection.Assembly]::LoadFrom((Resolve-Path $AssemblyPath))

function Assert-True($Actual, [string]$Message) {
    if (-not $Actual) { throw $Message }
}

function Assert-False($Actual, [string]$Message) {
    if ($Actual) { throw $Message }
}

function Assert-Near([double]$Expected, [double]$Actual, [string]$Message) {
    if ([Math]::Abs($Expected - $Actual) -gt 0.000001) { throw "$Message Expected=$Expected Actual=$Actual" }
}

function New-TestMap {
    $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.'
    $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true)
    $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true)
    $rectangleType = $assembly.GetType($mapping + 'AxisAlignedRectangleObstacle', $true)
    $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true)
    $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true)
    $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true)
    $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true)
    $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000))
    $obstacle = [Activator]::CreateInstance($rectangleType, @([single]1500, [single]1550, [single]1200, [single]1800))
    $obstacles = [Array]::CreateInstance($obstacleType, 1)
    $obstacles.SetValue($obstacle, 0)
    $source = [Activator]::CreateInstance($manualType, @('manual', [long]1, $true, $obstacles))
    $sources = [Array]::CreateInstance($sourceType, 1)
    $sources.SetValue($source, 0)
    $request = [Activator]::CreateInstance($requestType)
    $request.Bounds = $bounds
    $request.ResolutionMm = [single]50
    $request.ObstacleSources = $sources
    $request.AllowExplicitEmptyMap = $false
    $result = [Activator]::CreateInstance($factoryType).Create($request)
    Assert-True $result.Succeeded 'Test map must be created.'
    Assert-True $result.Map.PlanningReady 'Test map must be ready.'
    return $result.Map
}

function New-DiagonalCellMap {
    $mapping = 'MultiWheelC.TrajectoryPlanning.Mapping.'
    $boundsType = $assembly.GetType($mapping + 'MapBoundsMm', $true)
    $obstacleType = $assembly.GetType($mapping + 'IMapObstacle', $true)
    $circleType = $assembly.GetType($mapping + 'CircleObstacle', $true)
    $sourceType = $assembly.GetType($mapping + 'IMapObstacleSource', $true)
    $manualType = $assembly.GetType($mapping + 'ManualObstacleSource', $true)
    $requestType = $assembly.GetType($mapping + 'PlanningMapRequest', $true)
    $factoryType = $assembly.GetType($mapping + 'PlanningMapFactory', $true)
    $bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]3000, [single]0, [single]3000))
    $obstacle = [Activator]::CreateInstance($circleType, @([single]1525, [single]1525, [single]0))
    $obstacles = [Array]::CreateInstance($obstacleType, 1)
    $obstacles.SetValue($obstacle, 0)
    $source = [Activator]::CreateInstance($manualType, @('diagonal-cell', [long]1, $true, $obstacles))
    $sources = [Array]::CreateInstance($sourceType, 1)
    $sources.SetValue($source, 0)
    $request = [Activator]::CreateInstance($requestType)
    $request.Bounds = $bounds
    $request.ResolutionMm = [single]50
    $request.ObstacleSources = $sources
    $request.AllowExplicitEmptyMap = $false
    $result = [Activator]::CreateInstance($factoryType).Create($request)
    Assert-True $result.Succeeded 'Diagonal cell map must be created.'
    Assert-True $result.Map.PlanningReady 'Diagonal cell map must be ready.'
    Assert-True $result.Map.IsOccupied(30, 30) 'Diagonal cell obstacle must occupy its 50mm cell.'
    return $result.Map
}

$map = New-TestMap
$vehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters
$vehicle.LengthMeters = 0.20
$vehicle.WidthMeters = 0.20
$vehicle.SafetyMarginMeters = 0.0
$checker = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.FootprintCollisionChecker

$diagonalCellMap = New-DiagonalCellMap
$diagonalCellVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters
$diagonalCellVehicle.LengthMeters = 0.10
$diagonalCellVehicle.WidthMeters = 0.10
$diagonalCellVehicle.SafetyMarginMeters = 0.0
$diagonalCellPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.299, 1.299, 0.0)
$diagonalCellClearance = 0.0
$diagonalCellSafe = $checker.IsPoseCollisionFree($diagonalCellPose, $diagonalCellMap, $diagonalCellVehicle, 0.20, [ref]$diagonalCellClearance)
Assert-False $diagonalCellSafe 'Expanded diagonal footprint must not bypass an occupied 50mm cell.'

$edgePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.35, 1.50, 0.0)
$edgeClearance = 0.0
$edgeSafe = $checker.IsPoseCollisionFree($edgePose, $map, $vehicle, 0.0, [ref]$edgeClearance)
Assert-False $edgeSafe 'Touching an occupied cell must be a collision.'

$farPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.50, 0.50, 0.0)
$farClearance = 0.0
$farSafe = $checker.IsPoseCollisionFree($farPose, $map, $vehicle, 0.0, [ref]$farClearance)
Assert-True $farSafe 'Strict distance field clearance must allow the distant pose.'
Assert-True ($farClearance -gt 0.0) 'Distant pose must report positive body clearance.'

$from = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.20, 1.50, 0.0)
$to = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.80, 1.50, 0.0)
$sweepClearance = 0.0
$sweepSafe = $checker.IsSweptMotionCollisionFree($from, $to, $map, $vehicle, 0.50, [ref]$sweepClearance)
Assert-False $sweepSafe 'A swept vehicle must not pass through an occupied cell.'

$gridCenterPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.475, 1.475, 0.0)
$gridCenterClearance = 0.0
Assert-False $checker.IsPoseCollisionFree($gridCenterPose, $map, $vehicle, 0.0, [ref]$gridCenterClearance) 'A pose at an occupied grid center must collide.'

$subGridPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.481, 1.463, 0.37)
$subGridClearance = 0.0
Assert-False $checker.IsPoseCollisionFree($subGridPose, $map, $vehicle, 0.0, [ref]$subGridClearance) 'An arbitrary sub-grid heading must collide with the thin obstacle.'

$diagonalPose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(1.32, 1.50, ([Math]::PI / 4.0))
$diagonalClearance = 0.0
Assert-False $checker.IsPoseCollisionFree($diagonalPose, $map, $vehicle, 0.0, [ref]$diagonalClearance) 'A 45 degree footprint overlap must collide.'

$outsidePose = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.Pose2D(0.05, 0.05, 0.0)
$outsideClearance = 0.0
Assert-False $checker.IsPoseCollisionFree($outsidePose, $map, $vehicle, 0.0, [ref]$outsideClearance) 'Any footprint corner outside the map must collide.'

$curveVehicle = New-Object MultiWheelC.TrajectoryPlanning.CoarsePath.VehicleParameters
$curveVehicle.MaximumCurvaturePerMeter = 0.80
$curveVehicle.MinimumTurningRadiusMeters = 2.0
$maximumCurvature = 0.0
$hasMaximumCurvature = [MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle.VehicleKinematics]::TryGetMaximumCurvaturePerMeter($curveVehicle, [ref]$maximumCurvature)
Assert-True $hasMaximumCurvature 'Vehicle curvature constraints must be accepted.'
Assert-Near 0.50 $maximumCurvature 'Both curvature constraints must use the conservative minimum.'

Write-Output 'Coarse path collision checks passed.'
