using OpenNest.Engine.Jobs;
using M = System.Math;
namespace OpenNest.Engine.Gpt6Astra;
/// Independent configuration-space contact packing with bounded stock-plan search.
public sealed class Gpt6AstraNestingEngine : INestingEngine
{
public NestJobResult Solve(NestJob job, IProgress? progress = null,
CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(job);
token.ThrowIfCancellationRequested();
NestJobValidator.Validate(job);
var parts = GeometryPreparation.Prepare(job, token);
var fit = parts.Select(p => job.Plates.Select(s => p.Variants.Any(v =>
v.Width <= s.Size.Length - s.EdgeSpacing.Left - s.EdgeSpacing.Right + 1e-9 &&
v.Height <= s.Size.Width - s.EdgeSpacing.Top - s.EdgeSpacing.Bottom + 1e-9)).ToArray()).ToArray();
var placer = new ContactPlacer(parts, new ContactGeometry(), token);
var initial = new Plan(new int[parts.Length], new int[job.Plates.Count], new List(), 0);
var frontier = new List { initial };
var best = initial;
Plan? complete = IsComplete(initial) ? initial : null;
var trials = new Dictionary(StringComparer.Ordinal);
var priorities = job.Parts.Select(p => p.Priority).Distinct().Order().ToArray();
var evaluated = 0;
var unitCosts = Enumerable.Repeat(double.PositiveInfinity, parts.Length).ToArray();
while (frontier.Count > 0)
{
token.ThrowIfCancellationRequested();
var children = new Dictionary(StringComparer.Ordinal);
foreach (var state in frontier)
{
if (state.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue)) continue;
var lowerBound = LowerBound(state);
if (complete != null && lowerBound >= complete.Cost - 1e-7) continue;
var flexibility = Enumerable.Range(0, parts.Length).Select(p =>
Enumerable.Range(0, job.Plates.Count).Count(s => fit[p][s] && Available(state, s))).ToArray();
for (var s = 0; s < job.Plates.Count; s++)
{
if (!Available(state, s)) continue;
// Search breadth is work-count bounded, never elapsed-time dependent.
// Past this budget, continue filling greedily instead of abandoning demand.
var modes = evaluated < 24 ? 2 : 1;
for (var mode = 0; mode < modes; mode++)
{
token.ThrowIfCancellationRequested();
var key = $"{s}/{mode}/{string.Join(',', state.Counts)}/{string.Join(',', flexibility)}";
if (!trials.TryGetValue(key, out var trial))
{
progress?.Report(new(NestJobStage.EvaluatingCandidate, job.Plates[s].Id,
state.Sheets.Count, 0, 0));
trial = placer.Pack(s, job.Plates[s], state.Counts, flexibility, mode);
if (trials.Count >= 256) trials.Clear();
trials[key] = trial;
evaluated++;
}
if (trial.Shapes.Count == 0) continue;
var sheetCost = job.Plates[s].Size.Length * job.Plates[s].Size.Width;
for (var p = 0; p < parts.Length; p++)
{
var delivered = trial.Counts[p] - state.Counts[p];
if (delivered > 0) unitCosts[p] = M.Min(unitCosts[p], sheetCost / delivered);
}
var used = (int[])state.Used.Clone(); used[s]++;
var sheets = new List(state.Sheets) { trial };
var next = new Plan(trial.Counts, used, sheets,
state.Cost + job.Plates[s].Size.Length * job.Plates[s].Size.Width);
if (BetterFulfillment(next, best)) best = next;
if (IsComplete(next))
{
if (complete == null || next.Cost < complete.Cost - 1e-7 ||
(M.Abs(next.Cost - complete.Cost) < 1e-7 && next.Sheets.Count < complete.Sheets.Count)) complete = next;
continue;
}
var stateKey = $"{string.Join(',', next.Counts)}/{string.Join(',', next.Used)}";
if (!children.TryGetValue(stateKey, out var prior) || next.Cost < prior.Cost)
children[stateKey] = next;
}
}
}
var ranked = children.Values.Where(p => complete == null || LowerBound(p) < complete.Cost - 1e-7)
.OrderBy(Estimate).ThenByDescending(PlacedArea).ThenBy(p => p.Cost).ToList();
frontier = new List();
if (ranked.Count > 0)
{
frontier.Add(ranked[0]);
// A material-only lower bound favors cheap small-sheet prefixes and
// can discard every high-throughput plan. Preserve one progress leader.
var leader = ranked.OrderByDescending(PlacedArea).ThenBy(p => p.Cost).First();
if (!ReferenceEquals(leader, ranked[0])) frontier.Add(leader);
foreach (var candidate in ranked)
{
if (frontier.Count >= (evaluated < 64 ? 3 : 2)) break;
if (!frontier.Contains(candidate)) frontier.Add(candidate);
}
}
}
var selected = complete ?? best;
var counts = new int[parts.Length];
var plates = new List();
foreach (var sheet in selected.Sheets)
{
token.ThrowIfCancellationRequested();
var stock = job.Plates[sheet.StockIndex];
var x = (stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length) + stock.EdgeSpacing.Left;
var y = (stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width) + stock.EdgeSpacing.Bottom;
var placements = sheet.Shapes.Select(p => new NestJobPlacement(job.Parts[p.Variant.Part].Id,
counts[p.Variant.Part]++, x + p.X - p.Variant.OriginX,
y + p.Y - p.Variant.OriginY, p.Variant.Angle)).ToArray();
plates.Add(new(plates.Count, stock, placements));
progress?.Report(new(NestJobStage.PlateCommitted, stock.Id, plates.Count - 1,
plates.Count, counts.Sum()));
}
token.ThrowIfCancellationRequested();
var reason = complete != null ? NestJobStopReason.Completed :
selected.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue) ? NestJobStopReason.PlateLimitReached :
!Enumerable.Range(0, job.Plates.Count).Any(s => Available(selected, s)) ? NestJobStopReason.StockExhausted :
NestJobStopReason.NoPlacementFound;
return new(complete != null ? NestJobStatus.Complete : NestJobStatus.Incomplete, reason, plates,
job.Parts.Select((p, i) => new PartFulfillment(p.Id, p.Quantity, counts[i], p.Quantity - counts[i])),
job.Plates.Select((s, i) => new StockUsage(s.Id, selected.Used[i], s.Quantity - selected.Used[i])));
bool Available(Plan p, int s) => p.Used[s] < (job.Plates[s].Quantity ?? int.MaxValue);
bool IsComplete(Plan p) => parts.Select((part, i) => p.Counts[i] == part.Requirement.Quantity).All(v => v);
double PlacedArea(Plan p) => parts.Select((part, i) => p.Counts[i] * part.Area).Sum();
double LowerBound(Plan p) => p.Cost + parts.Select((part, i) =>
(part.Requirement.Quantity - p.Counts[i]) * part.Area).Sum();
double Estimate(Plan p)
{
var projected = 0.0;
for (var i = 0; i < parts.Length; i++)
if (double.IsFinite(unitCosts[i])) projected = M.Max(projected,
(parts[i].Requirement.Quantity - p.Counts[i]) * unitCosts[i]);
return M.Max(LowerBound(p), p.Cost + projected);
}
bool BetterFulfillment(Plan a, Plan b)
{
foreach (var priority in priorities)
{
var ac = parts.Select((p, i) => p.Requirement.Priority == priority ? a.Counts[i] : 0).Sum();
var bc = parts.Select((p, i) => p.Requirement.Priority == priority ? b.Counts[i] : 0).Sum();
if (ac != bc) return ac > bc;
}
return a.Cost < b.Cost;
}
}
private sealed record Plan(int[] Counts, int[] Used, List Sheets, double Cost);
}