using Nonemm.Contests; using Nonemm.Core; using Nonemm.Core.Calls; using Nonemm.Spotting; namespace Nonemm.Session; /// Answers what the callsign being typed could be, a column per source. public sealed class CheckWindowSources { private readonly OperatingPosition session; private readonly CallDatabase database; private readonly Bandmap bandmap; public CheckWindowSources(OperatingPosition session, CallDatabase database, Bandmap bandmap) { this.session = session; this.database = database; this.bandmap = bandmap; } /// A callsign being typed somewhere other than the entry window. The QTC /// window sets it while the cursor is in one of its callsign boxes, so a /// call taken down in traffic is checked the same way one being worked is. /// Null the rest of the time, and then the entry window's call is read. public string? Elsewhere { get; set; } /// The columns are read off what the operator has typed: the callsign box /// feeds the call columns, and whichever exchange box has the cursor feeds /// the exchange column. public IReadOnlyList Columns(int limit = 24) { string query = (Elsewhere ?? session.Entry.Call).Trim().ToUpperInvariant(); return [ Column(CheckSource.Log, LoggedCalls(), query, limit), new CheckColumn( CheckSource.Database, database.Count, query.Length < PartialMatching.ShortestUsefulQuery ? [] : database.Matches(query, limit).Select(Judged).ToList()), Column(CheckSource.History, HistoryCalls(), query, limit), Column(CheckSource.Bandmap, SpottedCalls(), query, limit), ExchangeColumn(limit), ]; } /// What the exchange box with the cursor in it could hold: the ARRL /// sections, or the states and provinces. A serial number or a report has /// no list behind it, so the column stays empty. private CheckColumn ExchangeColumn(int limit) { int focus = session.Entry.Focus; // a call typed outside the entry window brings no exchange box with it if (Elsewhere is not null || focus == 0) { return new CheckColumn(CheckSource.Exchange, 0, []); } ExchangeField field = session.Entry.Exchange[focus - 1]; IReadOnlyList values = ExchangeValues.For(field.Kind); string query = session.Entry[focus].Trim().ToUpperInvariant(); if (query.Length == 0) { return new CheckColumn(CheckSource.Exchange, values.Count, []); } List found = [.. values .Where(v => v.StartsWith(query, StringComparison.OrdinalIgnoreCase)) .Take(limit) .Select(v => new CheckCandidate(v, PartialMatchQuality.Exact, null))]; return new CheckColumn(CheckSource.Exchange, values.Count, found); } private CheckColumn Column( CheckSource source, IReadOnlyList calls, string query, int limit) { if (query.Length < PartialMatching.ShortestUsefulQuery) { return new CheckColumn(source, calls.Count, []); } List found = []; foreach (string call in calls) { PartialMatchQuality? quality = PartialMatching.Judge(query, call); if (quality is not null) { found.Add(Judged(new PartialMatch(call, quality.Value))); } } return new CheckColumn( source, calls.Count, found.OrderBy(c => c.Quality).ThenBy(c => c.Text, StringComparer.Ordinal).Take(limit).ToList()); } private CheckCandidate Judged(PartialMatch match) => new(match.Call, match.Quality, session.JudgeCall(match.Call)); private IReadOnlyList LoggedCalls() => session.Log.Qsos.Select(q => q.Call.Text).Distinct(StringComparer.Ordinal).ToList(); private IReadOnlyList HistoryCalls() => [.. session.Session.History.Calls]; private IReadOnlyList SpottedCalls() => bandmap.All() .Where(s => s.IsStation) .Select(s => s.Call.Text) .Distinct(StringComparer.Ordinal) .ToList(); }