using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace OpenNest.Engine.Jobs.Remote;
///
/// The parts of a that are not drawings: the engine, each
/// item's quantity, priority and rotation with the index of its drawing, the stock and the job
/// options. Drawings travel beside it in a .nest archive (OpenNest.Api.NestJobPackage).
/// A remote solve never runs with : it has no
/// time limit, so a slow server finds the same nest a fast one does, later.
///
public sealed class NestJobDocument
{
///
/// Version 2 nests higher priority numbers first. Version 1 jobs, from desktops that nested
/// lower numbers first, still read, with their priorities converted
/// (); an older worker refuses version 2 instead of reversing it.
///
public const int CurrentVersion = 2;
private const int LowerFirstPriorityVersion = 1;
///
/// A version 2 job that also uses common lines ( on
/// with a positive kerf). Written only then, so a worker that cannot make common lines refuses
/// the job rather than solving it without them, which the desktop's replay would reject.
///
public const int CommonLineVersion = 3;
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};
public int Version { get; set; } = CurrentVersion;
public string EngineName { get; set; }
/// Each drawing's name in list order, so a reader can tell the list arrived in that order.
public List DrawingNames { get; set; } = new();
///
/// Each drawing's as sent, in list order (empty
/// for a drawing without supported geometry). A desktop reopening the job in a later session
/// checks its drawings against these (). Null from
/// clients that predate it; workers ignore it.
///
public List DrawingFingerprints { get; set; }
public List Items { get; set; } = new();
public List Stock { get; set; } = new();
public OptionsJson JobOptions { get; set; }
///
/// Describes , naming each item's drawing by its position in
/// , which must hold every item's drawing.
///
public static NestJobDocument From(NestPipelineRequest request, IReadOnlyList drawings)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(drawings);
var document = new NestJobDocument
{
EngineName = request.EngineName,
DrawingNames = drawings.Select(d => d?.Name ?? "").ToList(),
DrawingFingerprints = drawings.Select(RemoteNestSubmission.Fingerprint).ToList(),
Stock = request.Stock.Select(NestStockJson.From).ToList(),
JobOptions = request.Options is { } o ? new OptionsJson
{
PlacementStrategy = o.PlacementStrategy,
MaxPlates = o.MaxPlates,
SalvageRate = o.SalvageRate,
MinimumSalvageDimension = o.MinimumSalvageDimension,
OffcutCut = o.OffcutCut,
CommonLineUsage = o.CommonLineUsage,
CommonLineKerf = o.CommonLineKerf,
} : null,
};
if (request.Options?.UsesCommonLines == true)
document.Version = CommonLineVersion;
foreach (var item in request.Items)
{
var index = item?.Drawing == null ? -1 : IndexOf(drawings, item.Drawing);
if (item != null && index < 0)
throw new ArgumentException("Every item's drawing must be in the drawing list.", nameof(drawings));
document.Items.Add(item == null ? null : new ItemJson
{
Drawing = index,
Quantity = item.Quantity,
Priority = item.Priority,
StepAngle = item.StepAngle,
RotationStart = item.RotationStart,
RotationEnd = item.RotationEnd,
});
}
return document;
}
/// The request this describes, with drawings taken from by position.
public NestPipelineRequest ToRequest(IReadOnlyList drawings)
{
ArgumentNullException.ThrowIfNull(drawings);
if (Version != CurrentVersion && Version != LowerFirstPriorityVersion && Version != CommonLineVersion)
throw new FormatException($"Nest job version {Version} is not supported; this build reads {CommonLineVersion}.");
if (string.IsNullOrWhiteSpace(EngineName))
throw new FormatException("The nest job names no engine.");
if (DrawingNames == null || DrawingNames.Count != drawings.Count
|| DrawingNames.Where((name, i) => !string.Equals(name, drawings[i]?.Name ?? "", StringComparison.Ordinal)).Any())
throw new FormatException("The nest job's drawings do not match the drawings sent with it.");
var items = new List(Items?.Count ?? 0);
foreach (var item in Items ?? new())
{
if (item == null)
{
items.Add(null);
continue;
}
if (item.Drawing < 0 || item.Drawing >= drawings.Count)
throw new FormatException($"A nest job item names drawing {item.Drawing}, which the job does not hold.");
items.Add(new NestItem
{
Drawing = drawings[item.Drawing],
Quantity = item.Quantity,
Priority = Version == LowerFirstPriorityVersion ? LegacyPriority.Convert(item.Priority) : item.Priority,
StepAngle = item.StepAngle,
RotationStart = item.RotationStart,
RotationEnd = item.RotationEnd,
});
}
var options = JobOptions is { } o
? new NestJobOptions(o.PlacementStrategy ?? "Lattice", o.MaxPlates, o.SalvageRate, o.MinimumSalvageDimension,
searchUntilCancelled: false, o.OffcutCut)
{
CommonLineUsage = Enum.IsDefined(o.CommonLineUsage) ? o.CommonLineUsage
: throw new FormatException("The nest job's common-line usage is not one this build knows."),
CommonLineKerf = double.IsFinite(o.CommonLineKerf) && o.CommonLineKerf >= 0 ? o.CommonLineKerf
: throw new FormatException("The nest job's common-line kerf is not a finite, nonnegative number."),
}
: null;
return new NestPipelineRequest(EngineName, items,
(Stock ?? new()).Select(s => s?.ToStock() ?? throw new FormatException("A nest job stock entry is empty.")).ToList(),
options);
}
public string ToJson() => JsonSerializer.Serialize(this, Options);
public static NestJobDocument FromJson(string json)
{
try
{
return JsonSerializer.Deserialize(json, Options)
?? throw new FormatException("The nest job is empty.");
}
catch (JsonException ex) { throw new FormatException("The nest job is not valid JSON.", ex); }
}
private static int IndexOf(IReadOnlyList drawings, Drawing drawing)
{
for (var i = 0; i < drawings.Count; i++)
{
if (ReferenceEquals(drawings[i], drawing))
return i;
}
return -1;
}
public sealed class ItemJson
{
public int Drawing { get; set; }
public int Quantity { get; set; }
public int Priority { get; set; }
public double StepAngle { get; set; }
public double RotationStart { get; set; }
public double RotationEnd { get; set; }
}
public sealed class OptionsJson
{
public string PlacementStrategy { get; set; }
public int? MaxPlates { get; set; }
public double SalvageRate { get; set; }
public double MinimumSalvageDimension { get; set; }
public CutOffAxis? OffcutCut { get; set; }
/// ; absent (off) before version 3.
public CommonLineUsage CommonLineUsage { get; set; }
/// ; absent (zero) before version 3.
public double CommonLineKerf { get; set; }
}
}