using Nonemm.Contests; using Nonemm.Core; using Nonemm.Core.Calls; using Nonemm.Core.Country; using Nonemm.Storage; namespace Nonemm.Session; /// The running contest: the log, the score and what happens when a contact is /// logged. One of these per contest, however many radios the station has. /// What the operator is typing lives in `RadioPosition`, one per radio. /// /// No UI framework is referenced here, so the behaviour an operator judges the /// logger by is tested with plain unit tests. public sealed class ContestSession { private readonly LogStore store; public ContestSession( LogStore store, Contest contest, ContestInstance instance, StationInfo me, CountryFile? countries, CallHistory? history = null) { this.store = store; Countries = countries; History = history ?? CallHistory.Empty; Contest = contest; Instance = instance; Me = me; Log = new ContestLog(contest, me, countries); Log.Restore(store.Qsos(instance.ContestNumber)); Editor = new QsoEditor(contest, me, countries); SentNumber = NextSentNumber(); } public Contest Contest { get; } public ContestInstance Instance { get; } public StationInfo Me { get; } public ContestLog Log { get; } public QsoEditor Editor { get; } public CountryFile? Countries { get; } /// What was published about the stations in this contest before it started. public CallHistory History { get; } /// Serial numbers count up across the station, not per radio. public int SentNumber { get; private set; } public string Operator { get; set; } = ""; public event EventHandler? Changed; public event EventHandler? Logged; /// A contact that was edited, with the call and time it had before. The /// other stations need the old pair to find their copy of it. public event EventHandler? Edited; public event EventHandler? Deleted; /// Scores the contact, writes it and hands back what was stored: the /// timestamp can move by a second when another contact with the same call /// already holds it. public Qso Add(Qso qso) { Qso scored = Log.Add(qso); Qso stored = store.Add(scored); if (stored.TimestampUtc != scored.TimestampUtc) { Log.Replace(stored); } SentNumber = NextSentNumber(); Logged?.Invoke(this, stored); Changed?.Invoke(this, EventArgs.Empty); return stored; } public void Delete(string id) { Qso? gone = Log.Qsos.FirstOrDefault(q => q.Id == id); store.Delete(id); Log.Remove(id); if (gone is not null) { Deleted?.Invoke(this, gone); } Changed?.Invoke(this, EventArgs.Empty); } /// Changes one field of a logged contact. A refused edit leaves the log /// alone and the caller gets the reason back. public QsoEdit Edit(string id, QsoField field, string text) => EditWith(id, before => Editor.Apply(before, field, text)); /// Replaces a logged contact with one the caller has already built and /// checked. The edit contact dialog changes many fields at once and comes /// in this way. public void Update(Qso qso) => EditWith(qso.Id, _ => QsoEdit.Accept(qso)); public void NotifyChanged() => Changed?.Invoke(this, EventArgs.Empty); private QsoEdit EditWith(string id, Func change) { Qso before = Log.Qsos.FirstOrDefault(q => q.Id == id) ?? throw new InvalidOperationException($"no contact with id {id} in the log"); QsoEdit edit = change(before); if (edit.Result is not null) { store.Update(edit.Result); Log.Replace(edit.Result); Edited?.Invoke(this, new QsoChange(edit.Result, before.Call.Text, before.TimestampUtc)); Changed?.Invoke(this, EventArgs.Empty); } return edit; } private int NextSentNumber() => Log.Qsos.Count == 0 ? 1 : Log.Qsos.Max(q => q.SentNumber) + 1; }