using Nonemm.Contests; namespace Nonemm.Session; /// What the operator has typed: the callsign, a value per exchange box, and /// which box the cursor is in. The callsign box is field 0. public sealed class EntryFields { private readonly List values; public EntryFields(IReadOnlyList exchange) { Exchange = exchange; values = [.. Enumerable.Repeat("", exchange.Count)]; } public IReadOnlyList Exchange { get; } /// Raised when the callsign box goes from empty to holding something. That /// is where N1MM takes the serial number for the contact, so the number is /// out on the network before the contact is worked. public event EventHandler? CallStarted; public string Call { get; set { bool wasEmpty = field.Trim().Length == 0; field = value; if (wasEmpty && field.Trim().Length > 0) { CallStarted?.Invoke(this, EventArgs.Empty); } } } = ""; /// 0 is the callsign box; 1 and up are the exchange boxes in order. public int Focus { get; private set; } public int FieldCount => Exchange.Count + 1; public string this[int field] { get => field == 0 ? Call : values[field - 1]; set { if (field == 0) { Call = value; } else { values[field - 1] = value; } } } public string ValueOf(ExchangeSlot slot) { for (int at = 0; at < Exchange.Count; at++) { if (Exchange[at].Slot == slot) { return values[at]; } } return ""; } public void Set(ExchangeSlot slot, string value) { for (int at = 0; at < Exchange.Count; at++) { if (Exchange[at].Slot == slot) { values[at] = value; } } } public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1); /// What space and tab do: move to the next box the operator types in, and /// round to the callsign again from the last one. The report boxes and the /// sent serial are stepped over, because nobody types 599 or the number /// they are sending — N1MM steps over the same three and fills them in. public void Advance() => Focus = Next(1); public void Retreat() => Focus = Next(-1); private int Next(int by) { int at = Focus; for (int step = 0; step < FieldCount; step++) { at = (at + by + FieldCount) % FieldCount; if (at == 0 || !IsFilledIn(Exchange[at - 1])) { return at; } } return Focus; } private static bool IsFilledIn(ExchangeField field) => field.IsSent || field.Kind == ExchangeFieldKind.Report; /// True when the operator has typed nothing. The boxes holding what this /// station sends are filled in by the program, so they do not count. public bool IsEmpty => Call.Trim().Length == 0 && Exchange.Where((f, at) => !f.IsSent && values[at].Trim().Length > 0).Any() == false; /// True when everything the contest asks for has been typed. public bool IsComplete => Call.Trim().Length > 0 && Exchange.Where((f, at) => f.IsRequired && values[at].Trim().Length == 0).Any() == false; public void Clear() { Call = ""; for (int at = 0; at < values.Count; at++) { values[at] = ""; } Focus = 0; } }