using Nonemm.Contests; using Nonemm.Core; using Nonemm.Core.Country; using Nonemm.Storage; namespace Nonemm.Session; /// One radio and what the operator is typing on it. A single-radio station has /// one of these; an SO2R station has two, sharing one `ContestSession` and so /// one log, one score and one run of serial numbers. public sealed class RadioPosition { public RadioPosition(ContestSession session, int radioNumber = 1) { Session = session; RadioNumber = radioNumber; Entry = new EntryFields(session.Contest.ExchangeFieldsFor(session.Me)); } public ContestSession Session { get; } /// 1 or 2. Contacts record which radio made them. public int RadioNumber { get; } public EntryFields Entry { get; } /// With no radio connected this stays where it was last typed. public Frequency Frequency { get; private set; } = Bands.Band20M.Low; /// Where the radio transmits when working split, and zero when it is not. public Frequency TransmitFrequency { get; private set; } = Frequency.Zero; public Mode Mode { get; private set; } = Modes.Cw; /// True while the operator is calling CQ on this radio rather than /// searching. Each radio runs or searches on its own. public bool IsRunning { get; set; } public Contest Contest => Session.Contest; public ContestInstance Instance => Session.Instance; public StationInfo Me => Session.Me; public ContestLog Log => Session.Log; public QsoEditor Editor => Session.Editor; public int SentNumber => Session.SentNumber; public void Tune(Frequency frequency, Mode? mode = null, Frequency? transmitFrequency = null) { Frequency = frequency; Mode = mode ?? Mode; TransmitFrequency = transmitFrequency ?? TransmitFrequency; Session.NotifyChanged(); } /// What the log says about what is typed now. Null when there is no call to /// judge yet. public Verdict? Verdict() { string call = Entry.Call.Trim(); return call.Length == 0 ? null : Log.Judge(BuildQso(call)); } public CountryLookup? Country() => Entry.Call.Trim().Length == 0 ? null : Session.Countries?.Find(Entry.Call.Trim()); /// Every earlier contact with the call being typed, newest first. public IReadOnlyList WorkedBefore() => Entry.Call.Trim().Length == 0 ? [] : Log.WorkedBefore(Entry.Call.Trim()); /// The frequency typed into the callsign box, if that is what it holds. public Frequency? PendingQsy() => FrequencyEntry.Parse(Entry.Call); /// Logs what is typed and clears the entry. Throws when the entry is not /// complete, so a caller that has not checked cannot lose an exchange. public Qso LogContact() { if (!Entry.IsComplete) { throw new InvalidOperationException($"the exchange for {Entry.Call} is not complete"); } Qso stored = Session.Add(BuildQso(Entry.Call.Trim())); Entry.Clear(); return stored; } public void Wipe() { Entry.Clear(); Session.NotifyChanged(); } /// The report an operator sends without thinking about it: 599 on CW and /// digital modes, 59 on phone. public string DefaultReport() => Mode.Category switch { ModeCategory.Cw => "599", ModeCategory.Phone => "59", _ => "599", }; private Qso BuildQso(string call) { Qso qso = new() { Id = Qso.NewId(), TimestampUtc = DateTime.UtcNow.AddTicks(-(DateTime.UtcNow.Ticks % TimeSpan.TicksPerSecond)), Call = Callsign.Parse(call), Frequency = Frequency, QsxFrequency = TransmitFrequency, Mode = Mode, RadioNumber = RadioNumber, ContestName = Contest.Name, ContestNumber = Instance.ContestNumber, SentReport = DefaultReport(), SentNumber = SentNumber, Operator = Session.Operator.Length > 0 ? Session.Operator : Me.Callsign, IsRunQso = IsRunning, }; return ApplyExchange(CountryFields.Apply(qso, Session.Countries)); } private Qso ApplyExchange(Qso qso) { foreach (ExchangeField field in Entry.Exchange) { string value = Entry.ValueOf(field.Slot).Trim(); qso = field.Slot switch { ExchangeSlot.ReceivedReport => qso with { ReceivedReport = value }, ExchangeSlot.SerialNumber => qso with { ReceivedNumber = Number(value) }, ExchangeSlot.Zone => qso with { Zone = Number(value) }, ExchangeSlot.Section => qso with { Section = value.ToUpperInvariant() }, ExchangeSlot.Check => qso with { Check = Number(value) }, ExchangeSlot.Precedence => qso with { Precedence = value.ToUpperInvariant() }, ExchangeSlot.Exchange1 => qso with { Exchange1 = value.ToUpperInvariant() }, ExchangeSlot.MiscText => qso with { MiscText = value.ToUpperInvariant() }, ExchangeSlot.Name => qso with { Name = value }, ExchangeSlot.Qth => qso with { Qth = value }, ExchangeSlot.GridSquare => qso with { GridSquare = value.ToUpperInvariant() }, ExchangeSlot.Power => qso with { Power = value }, ExchangeSlot.Comment => qso with { Comment = value }, _ => qso, }; } return qso; } private static int Number(string value) => int.TryParse(value, out int parsed) ? parsed : 0; }