package main

// Auto-call — the wiring around internal/autocall.
//
// The DECISION is in that package, alone and tested. This file does the three
// things it cannot do for itself: cut the decode stream into periods, tell it
// what the log still needs from each station, and carry out what it decides.
//
// It lives in the backend rather than in the panel because it keys a
// transmitter: it must behave identically whether the FT decodes tab is open,
// behind another tab, or the window is minimised — and because every rule it
// applies is then a Go test rather than something only the air can check.

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	wruntime "github.com/wailsapp/wails/v2/pkg/runtime"

	"hamlog/internal/applog"
	"hamlog/internal/autocall"
)

const (
	keyAutoCallOn       = "autocall.enabled"
	keyAutoCallOnly     = "autocall.only"
	keyAutoCallAttempts = "autocall.attempts"
	keyAutoCallWatched  = "autocall.watched_attempts"
	keyAutoCallMisses   = "autocall.misses"
	keyAutoCallRounds   = "autocall.max_rounds"
	// Periods, not minutes — see autocall.Settings.RestPeriods. A new key rather
	// than a reinterpreted one: the old value was in minutes, and reading "2" as
	// two periods or as two minutes are eight periods apart.
	keyAutoCallRest     = "autocall.rest_periods"
	keyAutoCallOnScreen = "autocall.on_screen_only"
	keyAutoCallTrace    = "autocall.trace"
)

// AutoCallSettings is the panel's shape. Durations are in minutes because that
// is what the operator is asked for.
type AutoCallSettings struct {
	Enabled bool   `json:"enabled"`
	Only    string `json:"only"`
	// Attempts / WatchedAttempts: how many calls one station gets before it is
	// released. The larger allowance is for a callsign on the watch list.
	Attempts        int `json:"attempts"`
	WatchedAttempts int `json:"watched_attempts"`
	// Misses: periods in which the station itself transmits, with no decode of
	// it, before it is given up on.
	Misses int `json:"misses"`
	// MaxRounds: how many series of calls one station gets in a session, and
	// RestPeriods the pause between two of them, counted in the station's own
	// transmit periods.
	MaxRounds   int `json:"max_rounds"`
	RestPeriods int `json:"rest_periods"`
	// OnScreenOnly: call only what the decodes panel is showing, so its filters
	// steer the transmitter as well as the eye.
	OnScreenOnly bool `json:"on_screen_only"`
	// Trace writes one line per period to the log: what was on the air, why
	// each station was refused, and what was decided. For diagnosing "it is not
	// calling anything" — and it is a line every fifteen seconds, so it is off
	// unless asked for.
	Trace bool `json:"trace"`
}

// intOr reads a stored integer, keeping a stored ZERO — which num() cannot,
// since it treats zero as "nothing set".
func intOr(v string, def int) int {
	n, err := strconv.Atoi(strings.TrimSpace(v))
	if err != nil || n < 0 {
		return def
	}
	return n
}

func (a *App) GetAutoCallSettings() AutoCallSettings {
	d := autocall.Defaults()
	num := func(key string, def int) int {
		n, err := strconv.Atoi(strings.TrimSpace(a.settingOr(key, "")))
		if err != nil || n <= 0 {
			return def
		}
		return n
	}
	return AutoCallSettings{
		// Never on from a stored value alone — see startAutoCall.
		Enabled:         a.settingOr(keyAutoCallOn, "0") == "1",
		Only:            strings.ToUpper(strings.TrimSpace(a.settingOr(keyAutoCallOnly, ""))),
		Attempts:        num(keyAutoCallAttempts, d.Attempts),
		WatchedAttempts: num(keyAutoCallWatched, d.WatchedAttempts),
		// On by default: the filters are in front of the operator, and a station
		// they have hidden is one they have said they do not want.
		OnScreenOnly: a.settingOr(keyAutoCallOnScreen, "1") == "1",
		Trace:        a.settingOr(keyAutoCallTrace, "0") == "1",
		Misses:       num(keyAutoCallMisses, d.Misses),
		MaxRounds:    num(keyAutoCallRounds, d.MaxRounds),
		// Zero is meaningful (call it again next period), so it is read directly
		// rather than through num(), which treats zero as "unset".
		RestPeriods: intOr(a.settingOr(keyAutoCallRest, ""), d.RestPeriods),
	}
}

func (a *App) SaveAutoCallSettings(s AutoCallSettings) error {
	a.setSetting(keyAutoCallOn, map[bool]string{true: "1", false: "0"}[s.Enabled])
	a.setSetting(keyAutoCallOnly, strings.ToUpper(strings.TrimSpace(s.Only)))
	a.setSetting(keyAutoCallOnScreen, map[bool]string{true: "1", false: "0"}[s.OnScreenOnly])
	a.setSetting(keyAutoCallTrace, map[bool]string{true: "1", false: "0"}[s.Trace])
	for key, v := range map[string]int{
		keyAutoCallAttempts: s.Attempts, keyAutoCallWatched: s.WatchedAttempts,
		keyAutoCallMisses: s.Misses, keyAutoCallRounds: s.MaxRounds,
	} {
		if v > 0 {
			a.setSetting(key, strconv.Itoa(v))
		}
	}
	// Stored even at zero, unlike the counters above: no rest at all is a
	// setting, not an empty field.
	if s.RestPeriods >= 0 {
		a.setSetting(keyAutoCallRest, strconv.Itoa(s.RestPeriods))
	}
	a.applyAutoCall()
	applog.Printf("autocall: %v (only=%q, %d/%d calls, %d misses, %d rounds)",
		s.Enabled, s.Only, s.Attempts, s.WatchedAttempts, s.Misses, s.MaxRounds)
	return nil
}

// SetAutoCallOnly is the chase-list field in the decodes toolbar.
//
// Its own binding rather than a settings round-trip: the toolbar knows one
// field, and handing back a whole struct it never read is how a Preferences
// window left open somewhere quietly reverts a limit that was just changed.
func (a *App) SetAutoCallOnly(list string) error {
	s := a.GetAutoCallSettings()
	s.Only = list
	return a.SaveAutoCallSettings(s)
}

// SetAutoCall is the toolbar switch above the decodes.
func (a *App) SetAutoCall(on bool) error {
	s := a.GetAutoCallSettings()
	s.Enabled = on
	return a.SaveAutoCallSettings(s)
}

// autoCallEngine returns the engine, built on first use.
func (a *App) autoCallEngine() *autocall.Engine {
	a.acMu.Lock()
	defer a.acMu.Unlock()
	if a.ac == nil {
		a.ac = autocall.New(a.autoCallSettings())
	}
	return a.ac
}

func (a *App) autoCallSettings() autocall.Settings {
	s := a.GetAutoCallSettings()
	return autocall.Settings{
		Enabled: s.Enabled, Only: s.Only, OnScreenOnly: s.OnScreenOnly,
		Attempts: s.Attempts, WatchedAttempts: s.WatchedAttempts,
		Misses: s.Misses, MaxRounds: s.MaxRounds,
		RestPeriods: s.RestPeriods,
	}
}

// applyAutoCall pushes the settings into the engine, and clears its state when
// the feature is switched off — an operator turning it off is entitled to have
// it forget the station it was calling, not resume it half an hour later.
func (a *App) applyAutoCall() {
	e := a.autoCallEngine()
	s := a.autoCallSettings()
	e.SetSettings(s)
	if a.GetAutoCallSettings().Trace {
		e.SetTrace(func(f string, args ...any) { applog.Printf("autocall: "+f, args...) })
	} else {
		e.SetTrace(nil)
	}
	if !s.Enabled {
		e.Reset()
		a.acMu.Lock()
		a.acPeriod, a.acBuf = nil, nil
		a.acJudged, a.acDirty = nil, nil
		a.acMu.Unlock()
	}
	a.emitAutoCall()
}

// ResetAutoCall is the operator's restart after the engine gave up on an
// explicit target: it clears every verdict, including the grey list.
func (a *App) ResetAutoCall() {
	a.autoCallEngine().Reset()
	a.emitAutoCall()
}

// HaltAutoCall is the Halt button while a call is in progress.
//
// It does NOT clear the engine's state, which is what Halt used to do: that
// wiped the rests and the rounds along with everything else, so the station the
// operator had just stopped was eligible again in the same second and the next
// period called it straight back.
func (a *App) HaltAutoCall() {
	call := a.autoCallEngine().Halt()
	if call != "" {
		a.acMu.Lock()
		a.acReason = fmt.Sprintf("%s stopped by the operator — set aside until auto-call is switched off and on", call)
		a.acMu.Unlock()
		applog.Printf("autocall: %s", a.acReason)
	}
	a.emitAutoCall()
}

// AutoCallStatus is what the toolbar shows.
type AutoCallStatus struct {
	Enabled bool `json:"enabled"`
	// Only is the chase list, carried in the status so the field in the decodes
	// toolbar and the one in Preferences are never two versions of the truth:
	// whichever is typed into, both show it.
	Only   string `json:"only"`
	Target string `json:"target"`
	// Waiting: what it would call if that station were not in a QSO.
	Waiting string `json:"waiting"`
	Calls   int    `json:"calls"`
	Max     int    `json:"max"`
	Misses  int    `json:"misses"`
	MaxMiss int    `json:"max_miss"`
	Stopped bool   `json:"stopped"`
	// Greylisted counts the stations the operator has stopped this session, so
	// the toolbar can say why a station on the air is never called.
	Greylisted int `json:"greylisted"`
	// Reason is the last decision in plain words. An auto-call that is doing
	// nothing on purpose looks exactly like one that is broken.
	Reason string `json:"reason"`
}

func (a *App) GetAutoCallStatus() AutoCallStatus {
	st := a.autoCallEngine().Status()
	a.acMu.Lock()
	reason := a.acReason
	a.acMu.Unlock()
	set := a.GetAutoCallSettings()
	return AutoCallStatus{
		Enabled: set.Enabled, Only: set.Only,
		Target: st.Target, Waiting: st.Waiting, Calls: st.Attempts, Max: st.Max,
		Misses: st.Misses, MaxMiss: st.MaxMiss, Stopped: st.Stopped,
		Greylisted: a.autoCallEngine().Greylisted(),
		Reason:     reason,
	}
}

func (a *App) emitAutoCall() {
	if a.ctx == nil {
		return
	}
	wruntime.EventsEmit(a.ctx, "autocall:status", a.GetAutoCallStatus())
}

// TakeAutoCallTarget adopts the station the operator has just clicked, so a
// manual pick gets the same watchdogs as an automatic one — the click is the
// choice of station, not a decision to call it for ever.
func (a *App) TakeAutoCallTarget(call, band, mode string) {
	if !a.GetAutoCallSettings().Enabled {
		return
	}
	call = strings.ToUpper(strings.TrimSpace(call))
	if call == "" {
		return
	}
	a.autoCallEngine().Take(autocall.Candidate{
		Decode:  autocall.Decode{Call: call, Band: band, Mode: mode, At: time.Now().UTC(), IsNew: true},
		Need:    a.autoCallNeed(call, band, mode),
		Watched: a.autoCallWatched(call),
	})
	a.emitAutoCall()
}

// ── The decode stream, cut into periods ───────────────────────────────────

// acDecode is one decode held until its period is complete.
type acDecode struct {
	d  autocall.Decode
	tx bool // the decode is our own transmission echoed back
}

// autoCallFeed takes one decode from the UDP loop.
//
// Decodes arrive one datagram at a time and a decision needs the whole period:
// the best station in it, and whether the target was there at all. They are
// therefore buffered under the period they belong to, and the period is judged
// when the next one starts — or, if the band goes quiet, by the sweeper below,
// which is what makes "not decoded for three of its periods" reachable when the
// answer is that nothing is being decoded at all.
func (a *App) autoCallFeed(d autocall.Decode) {
	if !a.GetAutoCallSettings().Enabled {
		return
	}
	inst := d.Instance
	key := acPeriodKey(d.At, d.TRPeriod)
	a.acMu.Lock()
	if a.acPeriod == nil {
		a.acPeriod, a.acAt, a.acTR, a.acBuf = map[string]string{}, map[string]time.Time{}, map[string]int{}, map[string][]acDecode{}
		a.acFed = map[string]time.Time{}
		a.acJudged, a.acDirty = map[string]string{}, map[string]bool{}
	}
	if prev := a.acPeriod[inst]; prev != "" && prev != key {
		prevAt, prevTR, buf := a.acAt[inst], a.acTR[inst], a.acBuf[inst]
		// Only if something in it has NOT been judged. The buffer now outlives
		// the judgement (see below), so without this the arrival of the next
		// period would judge the previous one a second time on the very same
		// decodes — and act on them, a slot late.
		pending := a.acDirty[inst] || a.acJudged[inst] != prev
		a.acPeriod[inst], a.acAt[inst], a.acTR[inst] = key, d.At, d.TRPeriod
		a.acBuf[inst] = []acDecode{{d: d}}
		a.acFed[inst], a.acDirty[inst] = time.Now(), true
		a.acMu.Unlock()
		// The previous period ends here whatever the sweeper was going to do: a
		// decode stamped with the next slot is proof the old one is over.
		if pending {
			a.autoCallJudge(inst, prev, prevAt, prevTR, buf)
		}
		return
	}
	// APPENDED, never restarted. The buffer survives the period being judged,
	// so a decode that arrives after the others belongs to the same period and
	// is weighed against all of them.
	//
	// It used to be dropped and then judged on its own: the sweeper cleared the
	// buffer, a straggler opened a "new" one under the same key, and the ladder
	// was applied to whatever handful had come late — with the other thirty
	// stations of that period nowhere in sight. A deep decode arriving a second
	// after the burst is exactly the station worth calling.
	a.acPeriod[inst], a.acAt[inst], a.acTR[inst] = key, d.At, d.TRPeriod
	a.acFed[inst], a.acDirty[inst] = time.Now(), true
	a.acBuf[inst] = append(a.acBuf[inst], acDecode{d: d})
	a.acMu.Unlock()
}

// acQuiet is how long a period is left open after its LAST decode arrives.
//
// This is the whole timing budget of the feature. A decoder finishes a period
// and sends its decodes about a second before the next slot opens, so the
// answer has to be back before that boundary — a reply that arrives after it
// makes the decoder start its call several seconds into the slot, which is what
// an operator sees as "it calls late" and what a station on the other end sees
// as a message it cannot decode.
//
// It was a whole slot plus four seconds, measured from the DECODE'S OWN
// TIMESTAMP — the start of the period, not the moment it arrived — so the
// answer left about four seconds INTO the next slot, every time.
//
// 800 ms: long enough for a busy period's decodes to arrive together (measured
// in bursts of a few hundred milliseconds), short enough to answer inside the
// same second they landed.
const acQuiet = 800 * time.Millisecond

// autoCallSweep closes the periods nothing has closed for us. Called on a timer.
//
// Per receiver, because with two decoders one may fall silent while the other
// is busy — and it is the silent one's period that has to close for a missed
// period to be counted at all.
func (a *App) autoCallSweep() {
	if !a.GetAutoCallSettings().Enabled {
		return
	}
	type due struct {
		inst, key string
		at        time.Time
		tr        int
		buf       []acDecode
	}
	var ready []due
	a.acMu.Lock()
	for inst, key := range a.acPeriod {
		if key == "" {
			continue
		}
		tr := a.acTR[inst]
		if tr <= 0 {
			tr = 15
		}
		// Measured from when the last decode ARRIVED, not from the period it
		// belongs to: a decode is stamped with the start of its own slot, so
		// waiting "a slot plus four seconds" from that stamp is waiting until
		// the middle of the NEXT slot. See acQuiet.
		if time.Since(a.acFed[inst]) < acQuiet {
			continue
		}
		// Judged once per period, and again only when something new has come in
		// for it. The period itself is kept open until a decode from the NEXT one
		// arrives, so a straggler is judged with the whole period behind it.
		if a.acJudged[inst] == key && !a.acDirty[inst] {
			continue
		}
		ready = append(ready, due{inst, key, a.acAt[inst], tr, a.acBuf[inst]})
		a.acJudged[inst], a.acDirty[inst] = key, false
	}
	a.acMu.Unlock()
	for _, d := range ready {
		a.autoCallJudge(d.inst, d.key, d.at, d.tr, d.buf)
	}
}

// autoCallSilence is the empty period. With the band dead, no decode ever
// arrives to close the next one, and the target's absence would never be
// counted — so a period with nothing in it is still a period.
//
// Only for the receiver the target is being called on: an idle second decoder
// has no periods to miss.
func (a *App) autoCallSilence() {
	if !a.GetAutoCallSettings().Enabled {
		return
	}
	inst, target := a.autoCallEngine().TargetInstance()
	if target == "" {
		return
	}
	a.acMu.Lock()
	// Nothing pending for this receiver, and nothing judged for a while: the
	// band has gone quiet under it.
	quiet := !a.acDirty[inst] && time.Since(a.acLastJudge) > 20*time.Second
	tr := a.acTR[inst]
	a.acMu.Unlock()
	if !quiet {
		return
	}
	now := time.Now().UTC()
	a.autoCallJudge(inst, acPeriodKey(now, tr), now, tr, nil)
}

func acPeriodKey(at time.Time, trSec int) string {
	if trSec <= 0 {
		trSec = 15
	}
	return fmt.Sprintf("%d", at.UTC().Unix()/int64(trSec))
}

// autoCallJudge resolves what the log needs from each station in the period,
// runs the decision, and carries it out.
func (a *App) autoCallJudge(inst, key string, at time.Time, tr int, buf []acDecode) {
	a.acMu.Lock()
	a.acLastJudge = time.Now()
	a.acMu.Unlock()

	// One status call for the whole period. It reads a cached worked-index, but
	// it is still per-callsign work and a busy period is thirty of them.
	seen := map[string]bool{}
	var q []SpotQuery
	var uniq []acDecode
	for _, dd := range buf {
		k := dd.d.Call + "|" + dd.d.Band + "|" + dd.d.Mode
		if seen[k] {
			// The same station twice in one period is one candidate, judged on
			// its most callable line — the engine's bestOf does that, so both
			// decodes are kept; only the status lookup is deduplicated.
			uniq = append(uniq, dd)
			continue
		}
		seen[k] = true
		uniq = append(uniq, dd)
		q = append(q, SpotQuery{Call: dd.d.Call, Band: dd.d.Band, Mode: dd.d.Mode})
	}
	status := map[string]SpotStatus{}
	for _, st := range a.ClusterSpotStatuses(q) {
		status[st.Call+"|"+st.Band+"|"+st.Mode] = st
	}

	chase := a.autoCallChase()
	cands := make([]autocall.Candidate, 0, len(uniq))
	for _, dd := range uniq {
		st := status[dd.d.Call+"|"+dd.d.Band+"|"+dd.d.Mode]
		c := candidateOf(dd.d, st, chase)
		c.Watched = a.autoCallWatched(dd.d.Call)
		c.Hidden = a.autoCallHidden(dd.d.Call)
		cands = append(cands, c)
	}

	tx := a.autoCallTX()

	act := a.autoCallEngine().OnPeriod(autocall.Period{
		Instance: inst, Key: key, At: at, TRPeriod: tr, Decodes: cands, TX: tx,
		MyCall: a.opCall,
	})
	a.autoCallDo(act)
}

// candidateOf turns one decode and the log's verdict on it into a candidate.
//
// Split out and kept pure because ONE line of it was wrong for weeks and
// nothing could catch it: the entity's verdict was read as the station's.
// chaseExtras is what the operator hunts BESIDES entities — the cluster's
// orthogonal markers, from the same switches that decide whether the badges are
// shown at all (Settings → DX Cluster). One answer for the eye and the
// transmitter: a marker withdrawn from the screen is not one to call for.
type chaseExtras struct{ pota, grid, pfx, county, state bool }

func candidateOf(d autocall.Decode, st SpotStatus, ch chaseExtras) autocall.Candidate {
	c := autocall.Candidate{
		Decode: d,
		// The ENTITY's verdict decides what is still needed…
		Need: autoCallNeedOf(st.Status),
		// …and THIS CALLSIGN on this band and mode decides whether calling it
		// would be a duplicate.
		//
		// Status was used for both. "worked" there means the COUNTRY is in the
		// log on this band and mode, so on a band where the operator has most of
		// them, nearly every station on the air was refused as already worked —
		// a trace of one evening shows twenty decodes out of twenty-one turned
		// away that way, the watched DXpedition among them.
		Worked: st.WorkedSlot,
		// The need exists only because a QSL never came: worth chasing, and worth
		// less than the same need never worked at all.
		Unconfirmed: st.UnconfStatus,
	}
	// NOTHING LEFT ON THE ENTITY, AND STILL WORTH A CALL.
	//
	// A prefix, a square, a county, a state, a park: never worked, orthogonal to
	// the entity's verdict, and exactly what the operator ticked in the chase
	// settings. They ranked at nothing-needed, so auto-call sat through a
	// never-worked WPX prefix calling CQ and did not answer it.
	//
	// Only when the entity has nothing to add — a new band that is ALSO a new
	// prefix is a new band, and says so.
	if c.Need == autocall.NeedNone {
		switch {
		case ch.pfx && st.NewPfx:
			c.Need, c.Extra, c.Unconfirmed = autocall.NeedExtra, "prefix", st.UnconfPfx
		case ch.county && st.NewCounty:
			c.Need, c.Extra, c.Unconfirmed = autocall.NeedExtra, "county", st.UnconfCty
		case ch.state && st.NewState:
			c.Need, c.Extra, c.Unconfirmed = autocall.NeedExtra, "state", st.UnconfState
		case ch.grid && st.NewGrid:
			c.Need, c.Extra, c.Unconfirmed = autocall.NeedExtra, "square", st.GridState == "unconf"
		case ch.pota && st.NewPOTA:
			c.Need, c.Extra = autocall.NeedExtra, "park"
		}
	}
	return c
}

// autoCallChase reads the chase switches the cluster and the decode list use.
//
// Read once per period rather than per decode: they are settings-store reads,
// and the period loop runs over every station on the band.
func (a *App) autoCallChase() chaseExtras {
	on := func(key string) bool {
		if a.settings == nil {
			return true
		}
		v, _ := a.settings.Get(a.ctx, "ui.opslog."+key)
		return v != "0" // unset means on, as it does on the screen
	}
	return chaseExtras{
		pota:   on("chasePota"),
		grid:   on("chaseGrids"),
		pfx:    on("chasePfx"),
		county: on("chaseCounty"),
		state:  on("chaseState"),
	}
}

// autoCallDo carries out a decision and records it.
func (a *App) autoCallDo(act autocall.Action) {
	if act.Reason != "" {
		a.acMu.Lock()
		a.acReason = act.Reason
		a.acMu.Unlock()
		applog.Printf("autocall: %s", act.Reason)
	}
	switch act.Kind {
	case autocall.DoReply:
		d := act.Decode
		if err := a.AnswerDecode(d.Instance, d.Ms, d.SNR, d.DT, d.AudioHz, d.ModeRaw, d.MsgRaw, d.LowConf); err != nil {
			applog.Printf("autocall: the call to %s could not be sent: %v", d.Call, err)
		}
	case autocall.DoHalt:
		// Soft: let the over finish, then stop transmitting. Hard: stop now.
		// The engine decides — see Action.Soft.
		if err := a.HaltDecodeTx("", act.Soft); err != nil {
			applog.Printf("autocall: halt failed: %v", err)
		}
	}
	if act.Kind != autocall.DoNothing || act.Reason != "" {
		a.emitAutoCall()
	}
}

// autoCallNoteTX is the attempt counter, fed from the decoder's own status.
//
// ONCE PER TRANSMIT PERIOD. Status arrives every second and says "transmitting"
// throughout the over, so counting each one would spend the whole allowance of
// seven calls inside a single fifteen-second slot — the counter has to measure
// transmissions, not seconds of carrier.
func (a *App) autoCallNoteTX(tx autocall.TXState) {
	if !a.GetAutoCallSettings().Enabled {
		return
	}
	a.acMu.Lock()
	// Per receiver as well as per period: in a split view both decoders report
	// their own transmissions, and one key for both would let the second one's
	// carrier swallow the first one's count.
	key := tx.Instance + "|" + acPeriodKey(time.Now().UTC(), a.acTR[tx.Instance])
	if a.acTXPeriod == key {
		a.acMu.Unlock()
		return
	}
	a.acTXPeriod = key
	a.acMu.Unlock()
	a.autoCallDo(a.autoCallEngine().NoteTX(tx))
}

// autoCallTX is the last transmit state reported, as the engine wants it.
func (a *App) autoCallTX() autocall.TXState {
	a.acMu.Lock()
	defer a.acMu.Unlock()
	return a.acTX
}

func (a *App) autoCallSetTX(tx autocall.TXState) {
	a.acMu.Lock()
	a.acTX = tx
	a.acMu.Unlock()
}

// SetAutoCallVisible is the decodes panel saying what it is SHOWING.
//
// The panel owns the filters and therefore owns the answer: reimplementing them
// here would give the screen and the transmitter two definitions of the same
// word, which is how they end up disagreeing. It sends the callsigns that
// survive its filters, and the engine calls nothing else.
//
// An empty list with active=false means "no filtering in force" — the panel was
// closed, or has never been opened this session — and the ladder decides alone.
func (a *App) SetAutoCallVisible(calls []string, active bool) {
	set := make(map[string]bool, len(calls))
	for _, c := range calls {
		if c = strings.ToUpper(strings.TrimSpace(c)); c != "" {
			set[c] = true
		}
	}
	a.acMu.Lock()
	a.acVisible, a.acVisibleOn = set, active
	a.acMu.Unlock()
}

// autoCallHidden reports whether the panel's filters are keeping a station off
// the screen. Unknown when nothing is being published: not hidden.
func (a *App) autoCallHidden(call string) bool {
	a.acMu.Lock()
	defer a.acMu.Unlock()
	if !a.acVisibleOn {
		return false
	}
	return !a.acVisible[strings.ToUpper(strings.TrimSpace(call))]
}

// autoCallNeedOf maps the cluster's own status vocabulary onto the ladder. One
// vocabulary for both, so a station that reads NEW BAND in the decodes list is
// the same NEW BAND the auto-call ranks — two answers to one question is how
// the panel and the caller quietly start disagreeing.
func autoCallNeedOf(status string) autocall.Need {
	switch status {
	case "new":
		return autocall.NeedDXCC
	case "new-band-mode", "new-band":
		// New on both counts is at least a new band, and it is the better catch
		// of the two — it must not fall below a plain new band.
		return autocall.NeedBand
	case "new-mode":
		return autocall.NeedMode
	case "new-slot":
		return autocall.NeedSlot
	}
	return autocall.NeedNone
}

func (a *App) autoCallNeed(call, band, mode string) autocall.Need {
	st := a.ClusterSpotStatuses([]SpotQuery{{Call: call, Band: band, Mode: mode}})
	if len(st) == 0 {
		return autocall.NeedNone
	}
	return autoCallNeedOf(st[0].Status)
}

// autoCallWatched asks the watch list, which is the same list the spot alerts
// and the cluster colouring use.
func (a *App) autoCallWatched(call string) bool {
	if a.watchlist == nil {
		return false
	}
	_, ok := a.watchlist.Match(strings.ToUpper(strings.TrimSpace(call)))
	return ok
}

// startAutoCall starts the engine's loop, with auto-call OFF.
//
// It is never on from a stored value. This is the one setting in the program
// that puts the station on the air by itself, and the operator who left it on
// last night is not necessarily the one at the desk now — nor necessarily at
// the desk at all: OpsLog starts with Windows, and a rig that powers up with it
// would begin calling into an empty shack, on whatever band the radio happens
// to be on, hours after anybody decided that was a good idea.
//
// Arming it is one click, and it is a click somebody has to make.
func (a *App) startAutoCall() {
	a.disarmAutoCall("launch")
	go a.autoCallLoop()
}

// disarmAutoCall switches auto-call off and writes that down.
//
// The setting is what the toolbar and the settings panel both read, so turning
// the engine off without storing it would show a lit switch over a silent
// transmitter — and the operator's next click, meaning "on", would send "off".
func (a *App) disarmAutoCall(why string) {
	if a.settingOr(keyAutoCallOn, "0") == "1" {
		applog.Printf("autocall: off at %s — it is never armed from a stored setting", why)
	}
	a.setSetting(keyAutoCallOn, "0")
	a.applyAutoCall()
}

func (a *App) autoCallLoop() {
	// A quarter of a second. The sweeper is what closes a period, so its tick is
	// part of the same budget as acQuiet: a two-second tick added up to two
	// seconds of its own to every answer, which is most of the margin there is.
	// The work per tick is a map read.
	t := time.NewTicker(250 * time.Millisecond)
	defer t.Stop()
	for range t.C {
		if a.ctx == nil {
			return
		}
		a.autoCallSweep()
		a.autoCallSilence()
	}
}
