namespace OpenNest.Data;
/// What adding library drawings to a set did.
public sealed class DrawingSetAddResult
{
internal DrawingSetAddResult(IReadOnlyList added, IReadOnlyList alreadyMembers)
{
Added = added;
AlreadyMembers = alreadyMembers;
}
public IReadOnlyList Added { get; }
/// Drawings left out because the set already lists them (S6: one member per drawing).
public IReadOnlyList AlreadyMembers { get; }
}
///
/// Edits one drawing set (new or read from the server): its name, customer and notes and its
/// ordered members, each one library drawing with a quantity. A save of an existing set is
/// guarded by the revision it was read at, so a stale save raises
/// and changes nothing.
///
/// A new set's id is allocated here, before the first save. When that save fails without a
/// definite answer it stays pending: the next save first reads the id, adopts the stored set
/// if it is there (then saves any later change as its next revision), and otherwise creates it
/// again under the same id. A new set is therefore never stored twice.
///
///
public sealed class DrawingSetEditSession
{
private readonly IDrawingSetStore _sets;
private readonly List _members = new();
private bool _pendingCreate;
/// A set read with , or null for a new set.
public DrawingSetEditSession(IDrawingSetStore sets, DrawingSetRecord? current, string savedBy)
{
_sets = sets ?? throw new ArgumentNullException(nameof(sets));
SavedBy = savedBy ?? "";
if (current == null)
{
Id = Guid.NewGuid();
return;
}
if (current.Id == Guid.Empty || current.Revision < 1)
throw new ArgumentException("Choose a set read from the server to edit.", nameof(current));
if (current.Archived)
throw new ArgumentException("An archived set cannot be edited. Restore it first.", nameof(current));
if (current.Members is null || current.Members.Any(m => m?.Drawing is null))
throw new ArgumentException("The set's members are missing their library drawings. Reload the set.", nameof(current));
Id = current.Id;
Load(current);
}
public Guid Id { get; }
/// The store the set is read from and saved to; also reads its history.
public IDrawingSetStore Store => _sets;
public string SavedBy { get; }
/// The set as last read or saved; null until a new set is first saved.
public DrawingSetRecord? Saved { get; private set; }
public bool IsNew => Saved == null;
public string Name { get; set; } = "";
public string Customer { get; set; } = "";
public string Notes { get; set; } = "";
/// The members in order. Each carries the library record it was added or read with.
public IReadOnlyList Members => _members;
/// An earlier save failed without a definite answer; the next save checks it first.
public bool HasPendingSave => _pendingCreate;
///
/// Appends each drawing not already in the set, in order, with .
/// Drawings the set already lists (or listed twice in ) are skipped.
///
public DrawingSetAddResult AddDrawings(IEnumerable drawings, int quantity = 1)
{
ArgumentNullException.ThrowIfNull(drawings);
if (quantity < 1 || quantity > DrawingSetRules.MaxQuantity)
throw new ArgumentOutOfRangeException(nameof(quantity), quantity, $"Quantity must be 1 to {DrawingSetRules.MaxQuantity}.");
var added = new List();
var skipped = new List();
foreach (var drawing in drawings)
{
if (drawing is null || drawing.Id == Guid.Empty)
throw new ArgumentException("Only library drawings can be added to a set.", nameof(drawings));
if (_members.Any(m => m.DrawingId == drawing.Id))
{
skipped.Add(drawing);
continue;
}
_members.Add(new DrawingSetMember { DrawingId = drawing.Id, Quantity = quantity, Drawing = drawing });
added.Add(drawing);
}
return new DrawingSetAddResult(added, skipped);
}
public void RemoveAt(int index)
{
CheckIndex(index);
_members.RemoveAt(index);
}
/// Moves a member by places; false when it is already at that end.
public bool Move(int index, int offset)
{
CheckIndex(index);
var target = index + offset;
if (target < 0 || target >= _members.Count || offset == 0)
return false;
var member = _members[index];
_members.RemoveAt(index);
_members.Insert(target, member);
return true;
}
public void SetQuantity(int index, int quantity)
{
CheckIndex(index);
if (quantity < 1 || quantity > DrawingSetRules.MaxQuantity)
throw new ArgumentOutOfRangeException(nameof(quantity), quantity, $"Quantity must be 1 to {DrawingSetRules.MaxQuantity}.");
_members[index].Quantity = quantity;
}
///
/// Shows , a newer revision of a member's library drawing (for
/// example one just edited), on that member. Members are saved by id, so the set itself is
/// not changed; false when no member lists the drawing or it already shows that revision.
///
public bool UpdateDrawing(DrawingLibraryRecord drawing)
{
ArgumentNullException.ThrowIfNull(drawing);
var member = _members.FirstOrDefault(m => m.DrawingId == drawing.Id);
if (member == null || (member.Drawing != null && member.Drawing.Revision >= drawing.Revision))
return false;
member.Drawing = drawing;
return true;
}
/// The record a save sends.
public DrawingSetRecord Build() => new()
{
Id = Id,
Name = Name ?? "",
Customer = Customer ?? "",
Notes = Notes ?? "",
SavedBy = SavedBy,
Members = _members.Select(m => new DrawingSetMember { DrawingId = m.DrawingId, Quantity = m.Quantity, Drawing = m.Drawing }).ToList(),
};
/// The first rule the set breaks, or null when it may be saved.
public string? GetError() => DrawingSetRules.GetError(Build());
/// True when saving would store something the server does not already hold.
public bool IsChanged => Saved == null || !SameContent(Saved, Build());
///
/// Saves the set: revision 1 for a new set, otherwise the next revision when the one it was
/// read at is still current. On success the session holds the stored record and its members.
///
public async Task SaveAsync(CancellationToken cancellationToken = default)
{
var record = Build();
var error = DrawingSetRules.GetError(record);
if (error != null)
throw new ArgumentException(error);
if (_pendingCreate && await _sets.GetAsync(Id, cancellationToken) is { } stored)
Adopt(stored);
DrawingSetRecord saved;
if (Saved == null)
{
_pendingCreate = true;
try
{
saved = await _sets.CreateAsync(record, cancellationToken);
}
catch (DrawingSetSaveConflictException conflict)
{
// The id is this session's own: an earlier save whose reply was lost was stored.
Adopt(conflict.Current);
if (!IsChanged)
{
Load(conflict.Current);
return conflict.Current;
}
saved = await _sets.UpdateAsync(Id, record, conflict.Current.Revision, cancellationToken);
}
}
else
{
if (!IsChanged)
return Saved;
saved = await _sets.UpdateAsync(Id, record, Saved.Revision, cancellationToken);
}
_pendingCreate = false;
Load(saved);
return saved;
}
private void Adopt(DrawingSetRecord stored)
{
_pendingCreate = false;
Saved = stored;
}
private void Load(DrawingSetRecord record)
{
Saved = record;
Name = record.Name;
Customer = record.Customer;
Notes = record.Notes;
_members.Clear();
_members.AddRange(record.Members.Select(m => new DrawingSetMember { DrawingId = m.DrawingId, Quantity = m.Quantity, Drawing = m.Drawing }));
}
private void CheckIndex(int index)
{
if (index < 0 || index >= _members.Count)
throw new ArgumentOutOfRangeException(nameof(index));
}
// The server's identical-save rule: name, customer, notes and members in order.
private static bool SameContent(DrawingSetRecord a, DrawingSetRecord b) =>
a.Name == b.Name && a.Customer == b.Customer && a.Notes == b.Notes
&& a.Members.Select(m => (m.DrawingId, m.Quantity)).SequenceEqual(b.Members.Select(m => (m.DrawingId, m.Quantity)));
}