package main

// Which decoder the entry field belongs to, when several are running.
//
// A station running WSJT-X, JTDX and MSHV at once has three programs sending
// Status once a second each. Click a call in one of them and only that one has
// a DX Call; the other two are idle and say so. Both statements are true, and
// both arrive — so the entry field is filled by the program the operator is
// working and emptied by the two that are not, once a second, and the map
// zooms in and out with it.
//
// So the first program to announce a station is FOCUSED, and until it lets go
// the others cannot touch the entry field. That is the operator's own answer:
// "if I call on one program, keep that one's UDP for the duration of the QSO".
//
// Focus is released when the focused program clears its own DX Call, when it
// stops sending altogether (it was closed), or when a QSO is logged — never on
// a timer that could hand the field to another program mid-over.

import (
	"strings"
	"sync"
	"time"

	"hamlog/internal/applog"
)

// udpFocusIdle is how long a focused program may go silent before the focus is
// given up.
//
// Generous on purpose: a decoder sends Status every second, so anything above a
// few seconds means it has been closed or has lost its network. Thirty is long
// enough to survive a machine that stutters and short enough that a program
// closed mid-QSO does not lock the entry field for the rest of the evening.
const udpFocusIdle = 30 * time.Second

type udpFocus struct {
	mu   sync.Mutex
	inst string
	at   time.Time
	// told marks that the log already carries the line explaining why another
	// program's callsign is being ignored. Once per focus, not once a second.
	told map[string]bool
}

// claim records that inst is announcing a station, and reports whether inst is
// the program the entry field currently belongs to.
func (f *udpFocus) claim(inst string) bool {
	if inst == "" {
		return true // a sender with no id: nothing to arbitrate between
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.inst == "" || f.inst == inst || time.Since(f.at) > udpFocusIdle {
		if f.inst != inst {
			applog.Printf("udp: the entry field follows %s while it is calling", inst)
			f.told = nil
		}
		f.inst, f.at = inst, time.Now()
		return true
	}
	return false
}

// holds reports whether inst may act on the entry field, without claiming it.
// Used for the clear: a program that is not focused clearing its own DX Call
// says nothing about the QSO in progress somewhere else.
func (f *udpFocus) holds(inst string) bool {
	if inst == "" {
		return true
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.inst == "" || time.Since(f.at) > udpFocusIdle {
		return true
	}
	return f.inst == inst
}

// release gives the field up — the focused program cleared its call, or a QSO
// was logged and the next station may come from anywhere.
func (f *udpFocus) release(why string) {
	f.mu.Lock()
	had := f.inst
	f.inst, f.at, f.told = "", time.Time{}, nil
	f.mu.Unlock()
	if had != "" {
		applog.Printf("udp: the entry field is free again (%s let go: %s)", had, why)
	}
}

// noteIgnored logs, once per focused program, that another one's callsign was
// not applied. Without it the behaviour is invisible: an operator whose second
// decoder "stopped filling the call" has nothing to read.
func (f *udpFocus) noteIgnored(inst, call string) {
	f.mu.Lock()
	if f.told == nil {
		f.told = map[string]bool{}
	}
	first := !f.told[inst]
	f.told[inst] = true
	holder := f.inst
	f.mu.Unlock()
	if first {
		applog.Printf("udp: [%s] %q not applied — %s has the entry field while it is calling",
			inst, strings.ToUpper(call), holder)
	}
}
