#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
///
/// One preferred sequence of owned contour choices for a holed part: every remaining hole
/// gets exactly one lead-feasible entry, resolved BACKWARD from the already-chosen
/// perimeter entry — the last hole faces the perimeter's actual emitted pierce, each earlier
/// hole faces the next hole's actual emitted pierce — so the cut chain flows toward the
/// outside start. This is a deterministic recommended v1 proposal, not an optimal joint
/// tour and not an installed program: the search still certifies every rapid, lead and
/// crossing. A blocked preferred rapid is the search's problem (S12), never ignored here.
///
internal static class PreferredContourEntries
{
/// Proposed hole choices in cut order, or an explicit no-preference verdict.
internal sealed record Proposal(
IReadOnlyList HoleChoices,
ContourSelectionShortfall Shortfall,
int? BlockedContour,
string? Reason)
{
public bool IsPreferred => Shortfall != ContourSelectionShortfall.Incomplete
&& HoleChoices.Count > 0 && BlockedContour is null;
}
///
/// Resolves one preferred entry per hole on (cut order,
/// from ), ending at .
/// Arrival proxies avoid circularity: the actual (local) part arrival for the first
/// hole, otherwise the previous hole's center. Facing target (the downstream actual
/// pierce) and tier still rank first. is the S07 adapter
/// verdict; candidates it refuses or leaves uncertain are not preferred.
///
internal static Proposal TryPlan(
PreparedContours prepared,
ContourChoice perimeterChoice,
IReadOnlyList holeRoute,
IReadOnlyList centersByOrdinal,
Vector arrival,
Func evaluate,
CancellationToken token = default)
{
if (prepared == null)
throw new ArgumentException("Prepared contours are required.", nameof(prepared));
if (perimeterChoice == null || !ReferenceEquals(perimeterChoice.Owner, prepared))
throw new ArgumentException("The perimeter choice must belong to this preparation.", nameof(perimeterChoice));
if (holeRoute == null || centersByOrdinal == null || evaluate == null)
throw new ArgumentException("Hole route, centers and evaluation are required.");
token.ThrowIfCancellationRequested();
if (holeRoute.Count == 0)
return new(Array.Empty(), ContourSelectionShortfall.Exhausted, null, null);
// The downstream target is where the tool ACTUALLY arrives next: the emitted
// contour's first cut/lead motion start (native rounding/clamping included).
var target = Pierce(prepared, perimeterChoice, token);
var choices = new ContourChoice[holeRoute.Count];
for (var index = holeRoute.Count - 1; index >= 0; index--)
{
token.ThrowIfCancellationRequested();
var contour = holeRoute[index];
var arrivalProxy = index == 0 ? arrival : centersByOrdinal[holeRoute[index - 1]];
// The S04 merged catalog: preferred kinds when present, fallback kinds
// (compass points, arc midpoints, ...) otherwise — pure circles included.
var catalog = prepared.AutomaticEntryCandidatesWithFallbacks(contour, target, token);
var ranked = catalog.RankTowardNextCut(target, arrivalProxy).DepartingClear(prepared, target, token);
var selection = ContourEntrySelection.Select(ranked, evaluate, 1, token);
if (selection.Choices.Count == 0
&& selection.Shortfall is ContourSelectionShortfall.Exhausted or ContourSelectionShortfall.Incomplete)
{
var eroded = ContourEntrySelection.Select(ErodedPierces(prepared, contour, ranked, 1, token), evaluate, 1, token);
if (eroded.Choices.Count != 0)
selection = eroded;
}
if (selection.Choices.Count == 0)
return new(Array.Empty(), selection.Shortfall, contour,
$"Hole contour {contour} has no preferred lead-feasible entry: {selection.Reason}");
choices[index] = selection.Choices[0];
target = Pierce(prepared, choices[index], token);
}
if (choices.Any(c => c == null!))
throw new InvalidOperationException("Preferred hole resolution left a gap.");
return new(choices, ContourSelectionShortfall.None, null, null);
}
///
/// The fallback for a cutout none of whose entries has a configured
/// lead-in that fits: up to entries pierced where eroding the cutout
/// leaves room, nearest each ranked entry in turn (see
/// ). Proposals only: the lead checks decide.
///
internal static IReadOnlyList ErodedPierces(PreparedContours prepared, int contour,
IReadOnlyList ranked, int max, CancellationToken token) =>
prepared.ErodedPierceEntries(contour, ranked.Select(c => c.Choice).ToArray(), max, token)
.Select(choice => new ContourEntryCandidate(choice, AutomaticEntryKind.ErodedPierce)).ToArray();
///
/// Stable reordering of one hole's ranked entries: those whose straight rapid to
/// (the next cut's pierce, local) clears the hole itself come
/// first. With no lead-out the tool leaves a hole where it entered it, so from an entry
/// whose way out turns back into the hole (a tiny corner arc's midpoint, whose tangent
/// leaves at 45 degrees, ranks as a two-side corner) the rapid onward re-enters the hole
/// just cut and is refused. Order only: every entry stays and every rapid is still checked.
///
internal static IReadOnlyList DepartingClear(this IReadOnlyList ranked,
PreparedContours prepared, Vector target, CancellationToken token)
{
if (ranked.Count < 2)
return ranked;
IReadOnlyList contour;
try
{
// Any entry's probe cuts the same contour (a tab gap aside); scribes and leads are not part of it.
var execution = ExecutionMotionReader.Read(prepared.EmitCandidateForValidation(ranked[0].Choice),
Vector.Zero, null, token);
contour = execution.Motions
.Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display && m.Curve != null)
.Select(m => m.Curve).ToArray();
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or ArithmeticException or NotSupportedException)
{
return ranked; // An unemittable probe is the lead checks' finding, not the ranker's.
}
return ranked.OrderBy(c => PostVerificationGeometry.Crosses(c.Choice.Point, target, contour, token) ? 1 : 0).ToList();
}
///
/// The actual emitted pierce of one owned choice in local coordinates: the start of the
/// first non-rapid motion (lead-in when present — that is where the next rapid must
/// arrive), which reflects native rounding and clamping of a nominal entry.
///
internal static Vector Pierce(PreparedContours prepared, ContourChoice choice, CancellationToken token)
{
var program = prepared.EmitCandidateForValidation(choice);
var execution = ExecutionMotionReader.Read(program, Vector.Zero, null, token);
// The diagnostic emission includes the part's scribes before the selected contour.
// Those marks are not the downstream cut's pierce.
var first = execution.Motions.First(m => !m.Rapid && m.Layer != LayerType.Scribe);
return first.Start ?? first.End;
}
}