using System.Globalization;
using System.Security.Cryptography;
using OpenNest;
namespace OpenNest.Data;
/// A downloaded library package as the caller read it (with DrawingPackage.Read).
public sealed class LibraryPackage
{
public LibraryPackage(byte[] bytes, Drawing drawing, Units units, IReadOnlyList? warnings = null)
{
ArgumentNullException.ThrowIfNull(bytes);
ArgumentNullException.ThrowIfNull(drawing);
Sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
Drawing = drawing;
Units = units;
Warnings = warnings ?? Array.Empty();
}
/// Lower-case hex SHA-256 of the downloaded bytes.
public string Sha256 { get; }
/// The package's drawing; the insert adopts this instance as the nest's copy.
public Drawing Drawing { get; }
public Units Units { get; }
public IReadOnlyList Warnings { get; }
}
public enum LibraryInsertKind
{
/// The library drawing becomes a new nest drawing.
AddDrawing,
/// The nest already holds this exact revision; only its quantity grows.
AddQuantity,
/// Nothing may change; says why.
Refused,
}
///
/// Decides how one library drawing revision enters a nest, before anything changes:
/// a new exact copy carrying its library link, more quantity on the copy already there,
/// or a refusal. Material, grade or thickness that differ from the nest still allow the
/// add, but the drawing's cut pins are dropped (D3). Different units, a different revision
/// of the same drawing, or another drawing with the same name refuse it.
///
public sealed class LibraryDrawingInsert
{
private bool _applied;
private LibraryDrawingInsert(Nest nest, LibraryInsertKind kind, Drawing? drawing, int quantity,
bool keepPins, IReadOnlyList warnings, string refusal)
{
Nest = nest;
Kind = kind;
Drawing = drawing;
Quantity = quantity;
KeepPins = keepPins;
Warnings = warnings;
Refusal = refusal;
}
public Nest Nest { get; }
public LibraryInsertKind Kind { get; }
/// The new copy () or the existing one; null when refused.
public Drawing? Drawing { get; }
/// The quantity added to the nest's required count.
public int Quantity { get; }
///
/// For a new copy, true when the library drawing's cut pins come with it: the library
/// material, grade and thickness match the nest's. Otherwise the copy has no pins. An
/// added quantity leaves the existing copy as it is.
///
public bool KeepPins { get; }
/// Operator-facing notes to show before the add completes.
public IReadOnlyList Warnings { get; }
/// Why nothing may change; empty unless is .
public string Refusal { get; }
public bool IsRefused => Kind == LibraryInsertKind.Refused;
public static LibraryDrawingInsert Plan(Nest nest, string serverUrl, DrawingLibraryRecord record,
LibraryPackage package, int quantity)
{
ArgumentNullException.ThrowIfNull(nest);
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(package);
ArgumentOutOfRangeException.ThrowIfLessThan(quantity, 1);
var serverKey = ServerUrl.Key(serverUrl);
if (serverKey.Length == 0)
throw new ArgumentException("A library insert needs the server the drawing came from.", nameof(serverUrl));
var label = Describe(record);
if (!string.Equals(package.Sha256, record.PackageSha256, StringComparison.OrdinalIgnoreCase))
return Refuse(nest, $"The downloaded package for {label} does not match the library record. Try again.");
if (package.Units != record.Units)
return Refuse(nest, $"The package for {label} is drawn in {UnitName(package.Units)}, "
+ $"but its library record says {UnitName(record.Units)}.");
if (package.Units != nest.Units)
return Refuse(nest, $"{label} is drawn in {UnitName(package.Units)} and this nest is in "
+ $"{UnitName(nest.Units)}. Converting units is not supported.");
var link = new DrawingLibraryLink(serverKey, record.Id, record.Revision, record.CutType,
record.Thickness, record.MaterialName, record.MaterialGrade, record.Units, record.Notes,
record.PackageSha256.ToLowerInvariant());
var sameRevision = nest.Drawings.FirstOrDefault(d => d.Library?.IsSameRevision(link) == true);
if (sameRevision != null)
return new LibraryDrawingInsert(nest, LibraryInsertKind.AddQuantity, sameRevision, quantity,
keepPins: false, Array.Empty(), "");
var otherRevision = nest.Drawings.FirstOrDefault(d => d.Library is { } l
&& l.Id == link.Id && string.Equals(l.ServerKey, link.ServerKey, StringComparison.Ordinal));
if (otherRevision != null)
return Refuse(nest, $"This nest already uses revision {otherRevision.Library!.Revision} of this "
+ $"library drawing as '{otherRevision.Name}'. Use Update Drawings to change its revision.");
// DrawingCollection is keyed by name, so a second drawing with this name would be lost.
var sameName = nest.Drawings.FirstOrDefault(d => string.Equals(d.Name, record.Name, StringComparison.Ordinal));
if (sameName != null)
return Refuse(nest, $"This nest already has a drawing named '{sameName.Name}' "
+ $"({DescribeExisting(sameName)}), so {label} was not added. Rename one of them first.");
var differences = Differences(nest, record);
var warnings = package.Warnings.ToList();
if (differences.Count > 0)
warnings.Add($"{label} differs from this nest: {string.Join("; ", differences)}. The drawing is "
+ "added, but its drawing cut pins are dropped and the nest's cutting settings apply.");
var drawing = package.Drawing;
drawing.Name = record.Name;
drawing.Customer = record.Customer;
drawing.Material = new Material(record.MaterialName, record.MaterialGrade, drawing.Material?.Density ?? 0);
drawing.Quantity.Required = quantity;
drawing.Library = link;
// Pins were authored for the library material and thickness; elsewhere the nest's
// cutting settings apply (D3). The link still records the revision the copy came from.
if (differences.Count > 0)
drawing.CutPins.Clear();
return new LibraryDrawingInsert(nest, LibraryInsertKind.AddDrawing, drawing, quantity,
keepPins: differences.Count == 0, warnings, "");
}
/// Changes the planned nest; call at most once, and only for an insert that is not refused.
public void Apply()
{
if (IsRefused)
throw new InvalidOperationException(Refusal);
if (_applied)
throw new InvalidOperationException("This library insert was already applied.");
if (Kind == LibraryInsertKind.AddQuantity)
{
if (!Nest.Drawings.Any(d => ReferenceEquals(d, Drawing)))
throw new InvalidOperationException($"'{Drawing!.Name}' is no longer in this nest.");
Drawing!.Quantity.Required += Quantity;
}
else if (!Nest.Drawings.Add(Drawing!))
{
throw new InvalidOperationException($"This nest already has a drawing named '{Drawing!.Name}'.");
}
_applied = true;
}
private static LibraryDrawingInsert Refuse(Nest nest, string reason) =>
new(nest, LibraryInsertKind.Refused, null, 0, keepPins: false, Array.Empty(), reason);
private static List Differences(Nest nest, DrawingLibraryRecord record)
{
var differences = new List();
var material = nest.Material ?? new Material();
if (SharedListNames.Key(record.MaterialName) != SharedListNames.Key(material.Name))
differences.Add($"material {Text(record.MaterialName)} (nest {Text(material.Name)})");
if (SharedListNames.Key(record.MaterialGrade) != SharedListNames.Key(material.Grade))
differences.Add($"grade {Text(record.MaterialGrade)} (nest {Text(material.Grade)})");
var thickness = DrawingLibraryRules.NormalizeThickness(record.Thickness);
var nestThickness = DrawingLibraryRules.NormalizeThickness(nest.Thickness);
if (thickness != nestThickness)
differences.Add($"thickness {Number(thickness)} (nest {Number(nestThickness)})");
return differences;
}
private static string Describe(DrawingLibraryRecord record) =>
string.IsNullOrWhiteSpace(record.Customer)
? $"library drawing '{record.Name}' revision {record.Revision}"
: $"library drawing '{record.Name}' ({record.Customer}) revision {record.Revision}";
private static string DescribeExisting(Drawing drawing) =>
drawing.Library is { } link
? $"library revision {link.Revision} from {link.ServerKey}"
: "not from the drawing library";
private static string Text(string? value) => string.IsNullOrWhiteSpace(value) ? "not set" : value.Trim();
private static string Number(double value) => value.ToString("0.####", CultureInfo.CurrentCulture);
private static string UnitName(Units units) => units.ToString().ToLowerInvariant();
}