namespace OpenNest.Data.MaterialLedger;
/// A plate's item code that is no longer the nest's to use, and why.
/// The plate's 1-based position in the nest.
public sealed record LedgerCodeConflict(int PlateNumber, string ItemCode, string Reason);
///
/// The save and post re-check of a nest's : finds codes the
/// ledger no longer has free for this nest or that other open nests are coded with, and takes
/// them off the plates when the programmer chooses to. Pure; the caller fetches the inputs.
///
public static class LedgerCodeCheck
{
/// True when any plate has a coded sheet.
public static bool HasCodes(IEnumerable plates) =>
(plates ?? throw new ArgumentNullException(nameof(plates))).Any(plate => plate.StockItemCodes.Count > 0);
/// The nest's plates in order.
/// The ledger's free stock for the nest's material, after every hold.
///
/// The ledger's holds for this nest: an open one keeps its sheet for this nest, so the
/// sheet counts as available here although the ledger no longer lists it as free.
///
/// Other open nests' codes; an unknown answer checks the ledger alone.
/// One conflict per plate and code, in plate order then the plate's code order.
public static IReadOnlyList Find(IList plates, IEnumerable freeItems,
IEnumerable? ownHolds, LedgerCodesInUse inUse)
{
ArgumentNullException.ThrowIfNull(plates);
ArgumentNullException.ThrowIfNull(freeItems);
ArgumentNullException.ThrowIfNull(inUse);
var available = new Dictionary(StringComparer.Ordinal);
foreach (var item in freeItems.Where(item => item != null && item.Quantity > 0))
available[item.ItemCode] = available.GetValueOrDefault(item.ItemCode) + item.Quantity;
foreach (var hold in (ownHolds ?? Array.Empty()).Where(hold => hold != null && LedgerHoldStatuses.IsOpen(hold.Status)))
available[hold.ItemCode] = available.GetValueOrDefault(hold.ItemCode) + 1;
// A bundle's code repeats once per sheet across every plate it is used on.
var needed = new Dictionary(StringComparer.Ordinal);
foreach (var code in plates.SelectMany(plate => plate.StockItemCodes))
needed[code] = needed.GetValueOrDefault(code) + 1;
var reasons = new Dictionary(StringComparer.Ordinal);
foreach (var (code, count) in needed)
{
var parts = new List();
var free = available.GetValueOrDefault(code);
if (free == 0)
parts.Add("not free in MaterialLedger");
else if (free < count)
parts.Add($"only {free} of the {count} sheets needed free in MaterialLedger");
var nests = inUse.NestsUsing(code);
if (nests.Count > 0)
parts.Add((nests.Count == 1 ? "on nest " : "on nests ") + string.Join(", ", nests));
if (parts.Count > 0)
reasons[code] = Capitalize(string.Join("; ", parts));
}
var conflicts = new List();
for (var i = 0; i < plates.Count; i++)
{
foreach (var code in plates[i].StockItemCodes.Distinct(StringComparer.Ordinal))
{
if (reasons.TryGetValue(code, out var reason))
conflicts.Add(new LedgerCodeConflict(i + 1, code, reason));
}
}
return conflicts;
}
///
/// Takes every copy of off the plates, keeping the layout and the
/// other codes in order; the sheets left uncoded are matched by size in ML. Returns how
/// many sheets lost their code.
///
public static int Remove(IEnumerable plates, IEnumerable codes)
{
ArgumentNullException.ThrowIfNull(plates);
ArgumentNullException.ThrowIfNull(codes);
var remove = codes.ToHashSet(StringComparer.Ordinal);
var removed = 0;
foreach (var plate in plates)
{
var kept = plate.StockItemCodes.Where(code => !remove.Contains(code)).ToList();
if (kept.Count == plate.StockItemCodes.Count)
continue;
removed += plate.StockItemCodes.Count - kept.Count;
plate.StockItemCodes = kept;
}
return removed;
}
private static string Capitalize(string text) => text.Length == 0 ? text : char.ToUpperInvariant(text[0]) + text[1..];
}