using System.Globalization;
using OpenNest;
namespace OpenNest.Data;
/// One set member as released: its drawing's current record and the multiplied quantity.
public sealed class SetReleaseItem
{
internal SetReleaseItem(DrawingSetMember member, int quantity)
{
Member = member;
Drawing = member.Drawing!;
Quantity = quantity;
}
public DrawingSetMember Member { get; }
/// The drawing's current library record, as joined by the server when the set was read.
public DrawingLibraryRecord Drawing { get; }
/// The member quantity times the release multiplier.
public int Quantity { get; }
/// The drawing is archived and was ticked back in.
public bool Archived => Drawing.Archived;
}
///
/// The members that can share one nest: the same material name, grade, thickness, units
/// and cut type (S3). Each group becomes one nest when the set is released.
///
public sealed class SetReleaseGroup
{
internal SetReleaseGroup(string key, DrawingLibraryRecord first, bool unassigned,
IReadOnlyList items, IReadOnlyList warnings, string refusal)
{
Key = key;
MaterialName = (first.MaterialName ?? "").Trim();
MaterialGrade = (first.MaterialGrade ?? "").Trim();
Thickness = DrawingLibraryRules.NormalizeThickness(first.Thickness);
Units = first.Units;
CutType = first.CutType;
IsUnassigned = unassigned;
Items = items;
Warnings = warnings;
Refusal = refusal;
}
/// Identifies the group across rebuilds of the plan (for example after the multiplier changes).
public string Key { get; }
/// The material as the group's first member spells it.
public string MaterialName { get; }
public string MaterialGrade { get; }
/// Thickness in , rounded to four decimals.
public double Thickness { get; }
public Units Units { get; }
public CutType CutType { get; }
///
/// The members whose drawings have no material or no thickness. They cannot be released
/// until the library drawings are given one, so the group is always refused.
///
public bool IsUnassigned { get; }
/// The members, in set order.
public IReadOnlyList Items { get; }
/// Sum of the released quantities.
public int TotalQuantity => Items.Sum(i => i.Quantity);
/// Operator-facing notes that do not stop the release.
public IReadOnlyList Warnings { get; }
/// Why this group cannot be released; empty when it can.
public string Refusal { get; }
public bool CanRelease => Refusal.Length == 0;
}
///
/// How a drawing set revision splits into nests, before anything is downloaded or created.
/// Pure: it reads the members' joined drawing records only. Members are grouped by
/// material name and grade (compared by ), thickness
/// (rounded as the library stores it), units and cut type, and every quantity is
/// multiplied. Archived drawings are skipped unless included (S5). A group is refused when
/// two of its drawings share a name, because a nest keys drawings by name (S6).
///
public sealed class SetReleasePlan
{
private SetReleasePlan(DrawingSetRecord set, int multiplier, bool includeArchived,
IReadOnlyList groups, IReadOnlyList skippedArchived)
{
Set = set;
Multiplier = multiplier;
IncludeArchived = includeArchived;
Groups = groups;
SkippedArchived = skippedArchived;
}
/// The set revision being released, as read from the server.
public DrawingSetRecord Set { get; }
public int Multiplier { get; }
public bool IncludeArchived { get; }
///
/// Groups ordered by material, grade, thickness, units and cut type, with the
/// unassigned group last.
///
public IReadOnlyList Groups { get; }
/// Members left out because their drawing is archived and archived drawings were not included.
public IReadOnlyList SkippedArchived { get; }
/// A set read from the server, so every member carries its drawing's current record.
public static SetReleasePlan Build(DrawingSetRecord set, int multiplier, bool includeArchived)
{
ArgumentNullException.ThrowIfNull(set);
if (multiplier < 1 || multiplier > SetReleaseRules.MaxMultiplier)
throw new ArgumentOutOfRangeException(nameof(multiplier), multiplier,
$"The multiplier must be between 1 and {SetReleaseRules.MaxMultiplier}.");
if (set.Id == Guid.Empty || set.Revision < 1)
throw new ArgumentException("Only a set read from the server can be released.", nameof(set));
var members = set.Members ?? throw new ArgumentException("The set has no member list.", nameof(set));
if (members.Any(m => m?.Drawing is null || m.Drawing.Id != m.DrawingId))
throw new ArgumentException("A set member is missing its library drawing. Reload the set.", nameof(set));
var skipped = new List();
var items = new List<(string Key, bool Unassigned, SetReleaseItem Item)>();
foreach (var member in members)
{
if (member.Drawing!.Archived && !includeArchived)
{
skipped.Add(member);
continue;
}
var unassigned = IsUnassigned(member.Drawing);
items.Add((unassigned ? UnassignedKey : GroupKey(member.Drawing), unassigned,
new SetReleaseItem(member, member.Quantity * multiplier)));
}
var groups = items
.GroupBy(i => i.Key, StringComparer.Ordinal)
.Select(g => BuildGroup(g.Key, g.First().Unassigned, g.Select(i => i.Item).ToList()))
.OrderBy(g => g.IsUnassigned)
.ThenBy(g => SharedListNames.Key(g.MaterialName), StringComparer.Ordinal)
.ThenBy(g => SharedListNames.Key(g.MaterialGrade), StringComparer.Ordinal)
.ThenBy(g => g.Thickness)
.ThenBy(g => g.Units)
.ThenBy(g => g.CutType)
.ToList();
return new SetReleasePlan(set, multiplier, includeArchived, groups, skipped);
}
/// The group key of a drawing that has a material and thickness.
public static string GroupKey(DrawingLibraryRecord drawing)
{
ArgumentNullException.ThrowIfNull(drawing);
return string.Join('|',
SharedListNames.Key(drawing.MaterialName),
SharedListNames.Key(drawing.MaterialGrade),
DrawingLibraryRules.NormalizeThickness(drawing.Thickness).ToString("0.####", CultureInfo.InvariantCulture),
drawing.Units.ToString(),
drawing.CutType.ToString());
}
/// The key of the group holding every drawing without a material or thickness.
public const string UnassignedKey = "unassigned";
private static bool IsUnassigned(DrawingLibraryRecord drawing) =>
SharedListNames.Key(drawing.MaterialName).Length == 0
|| !(DrawingLibraryRules.NormalizeThickness(drawing.Thickness) > 0);
private static SetReleaseGroup BuildGroup(string key, bool unassigned, IReadOnlyList items)
{
var warnings = items.Where(i => i.Archived)
.Select(i => $"'{i.Drawing.Name}' is archived in the library.")
.ToList();
var refusal = "";
if (unassigned)
{
refusal = $"{Names(items)} {(items.Count == 1 ? "has" : "have")} no material or thickness. "
+ "Set them on the library drawings, then release again.";
}
else
{
// Matches LibraryDrawingInsert, which refuses a second drawing with the same name.
var duplicate = items.GroupBy(i => i.Drawing.Name, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
if (duplicate != null)
refusal = $"{duplicate.Count()} drawings in this group are named '{duplicate.Key}'. "
+ "A nest keys drawings by name, so rename all but one in the library first.";
}
return new SetReleaseGroup(key, items[0].Drawing, unassigned, items, warnings, refusal);
}
private static string Names(IReadOnlyList items)
{
var names = items.Take(3).Select(i => $"'{i.Drawing.Name}'").ToList();
if (items.Count > 3)
names.Add($"{items.Count - 3} more");
return string.Join(", ", names);
}
}