package main

// Doppler tracking — walking the radio through a pass.
//
// The hard part of satellite tuning is not the arithmetic, it is deciding who
// owns the dial. A tracker that simply forces both frequencies fights the
// operator every time they turn the knob to follow a station across a linear
// transponder, and one that never touches the receiver leaves them chasing a
// signal that slides 9 kHz across a 70 cm pass.
//
// So: the operator owns the receiver, and the tracker follows them. Every tick
// it asks the radio where the receiver actually is. If that is where the tracker
// put it, nothing has changed and it keeps correcting from the same NOMINAL
// frequency. If it has moved further than a dial-turn's tolerance, the operator
// has chosen a new station: the tracker converts what they landed on back into a
// nominal frequency and carries on from there. The transmitter is derived from
// the nominal and never argued with — which is exactly the division of labour on
// a linear bird, where the operator listens and the radio does the sums.

import (
	"fmt"
	"math"
	"strings"
	"sync"
	"time"

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

	"hamlog/internal/applog"
	"hamlog/internal/cat"
	"hamlog/internal/qso"
	"hamlog/internal/sat"
)

// satTickEvery is how often the radio is re-pointed. One second: at the middle
// of a 70 cm pass the downlink moves about 60 Hz a second, which is audible on
// SSB within two or three of them and inaudible within one.
const satTickEvery = time.Second

// satDialTolerance is how far the receiver may differ from where the tracker put
// it before that difference is read as the operator tuning.
//
// 200 Hz is comfortably more than the rounding and the round-trip lag between
// setting a frequency and reading it back, and comfortably less than the
// smallest deliberate move anybody makes hunting a station on a transponder.
const satDialTolerance = 200

// satLightKmS is the speed of light in km/s, for turning a heard frequency back
// into a nominal one. The same constant internal/sat corrects with.
const satLightKmS = 299792.458

type satTracker struct {
	name string
	tp   int

	mu sync.Mutex
	// nominalDown is where the operator is, expressed as if the satellite were
	// standing still. Everything else is derived from it, and it is the only
	// thing a dial movement changes.
	nominalDown int64
	lastDown    int64 // what was last sent to the radio
	lastUp      int64
	status      SatTrackStatus
	fails       int

	// The az/el rotator, built once at the start of the pass so a serial port is
	// opened once rather than on every command. nil when none is configured.
	rot       satRotator
	rotStep   float64
	rotMinE   float64
	rotPark   bool
	rotAz     float64 // last commanded, so a step smaller than the beamwidth costs nothing
	rotEl     float64
	rotSent   bool
	rotReadAt time.Time // when the controller was last asked where it is

	stop chan struct{}
	done chan struct{}
}

// SatTrackStatus is what the tracker is doing, for the panel.
type SatTrackStatus struct {
	On          bool    `json:"on"`
	Name        string  `json:"name"`
	Transponder string  `json:"transponder"`
	Mode        string  `json:"mode"`
	NominalDown int64   `json:"nominal_down"`
	NominalUp   int64   `json:"nominal_up"`
	DownHz      int64   `json:"down_hz"`
	UpHz        int64   `json:"up_hz"`
	Az          float64 `json:"az"`
	El          float64 `json:"el"`
	Visible     bool    `json:"visible"`
	Radio       string  `json:"radio"` // what the rig is doing: "sat", "downlink-only", ""
	Error       string  `json:"error"`

	// Where the antenna is. RotLive distinguishes a reading from the controller
	// from the last position it was TOLD to go to — a stuck rotator must not be
	// able to hide behind a command it never carried out.
	RotOn   bool    `json:"rot_on"`
	RotAz   float64 `json:"rot_az"`
	RotEl   float64 `json:"rot_el"`
	RotLive bool    `json:"rot_live"`
}

// StartSatelliteTracking arms the radio and starts following the satellite.
func (a *App) StartSatelliteTracking(name string, transponder int) error {
	if a.cat == nil {
		return fmt.Errorf("CAT is not running")
	}
	_, birds, _ := a.satParts()
	b, ok := birds.Find(name)
	if !ok || len(b.Transponders) == 0 {
		return fmt.Errorf("%s has no frequency plan to tune to", name)
	}
	if transponder < 0 || transponder >= len(b.Transponders) {
		transponder = 0
	}
	a.StopSatelliteTracking()

	t := &satTracker{
		name:        b.Name,
		tp:          transponder,
		nominalDown: b.Transponders[transponder].Centre(),
		stop:        make(chan struct{}),
		done:        make(chan struct{}),
	}
	t.status = SatTrackStatus{On: true, Name: b.Name, Transponder: b.Transponders[transponder].Label, Mode: b.Transponders[transponder].Mode}

	// The rotator, if there is one. A geostationary bird is pointed at once and
	// left alone, so it gets one command rather than a loop.
	set := a.satSettings()
	if set.RotOn {
		r, rerr := newSatRotator(set)
		if rerr != nil {
			applog.Printf("sat: no rotator: %v", rerr)
			t.status.Error = rerr.Error()
		} else {
			t.rot = r
			t.rotStep, t.rotMinE, t.rotPark = float64(set.RotStep), float64(set.RotMinEl), set.RotPark
		}
	}

	// Arm the radio for the pair. A rig that cannot hold one is NOT a failure:
	// it can still be tuned to the downlink, which is most of a receive-heavy
	// pass, and saying so beats refusing to track at all.
	radio := "downlink-only"
	if a.cat.SatCapable() {
		if err := a.cat.SatDo(func(st cat.SatTuner) error { return st.SetSatellite(true) }); err != nil {
			applog.Printf("sat: could not arm satellite mode: %v", err)
			t.status.Error = err.Error()
		} else {
			radio = "sat"
		}
	}
	t.status.Radio = radio

	a.satTrackMu.Lock()
	a.satTrack = t
	a.satTrackMu.Unlock()
	go a.satTrackLoop(t)
	applog.Printf("sat: tracking %s (%s), radio %s", t.name, t.status.Transponder, radio)
	return nil
}

// StopSatelliteTracking hands the radio back.
func (a *App) StopSatelliteTracking() {
	a.satTrackMu.Lock()
	t := a.satTrack
	a.satTrack = nil
	a.satTrackMu.Unlock()
	if t == nil {
		return
	}
	close(t.stop)
	<-t.done
	if a.cat != nil && a.cat.SatCapable() {
		if err := a.cat.SatDo(func(st cat.SatTuner) error { return st.SetSatellite(false) }); err != nil {
			applog.Printf("sat: could not disarm satellite mode: %v", err)
		}
	}
	applog.Printf("sat: tracking stopped (%s)", t.name)
	a.emitSatTrack(SatTrackStatus{})
}

// TestSatelliteRotator opens the configured controller and asks it where it is.
//
// The one question worth asking before a pass: is this port the rotator, and
// does it talk back? A controller that accepts commands silently is a normal,
// working one — so that answer is a success with a caveat, not a failure.
func (a *App) TestSatelliteRotator() (string, error) {
	set := a.satSettings()
	if !set.RotOn {
		return "", fmt.Errorf("the satellite rotator is switched off")
	}
	c, err := newSatRotator(set)
	if err != nil {
		return "", err
	}
	defer c.Close()
	az, el, live, err := c.Heading()
	if err != nil {
		return "", err
	}
	if !live {
		return "The controller accepted the command but does not report its position — normal for many controllers. It will still be driven.", nil
	}
	return fmt.Sprintf("The rotator is at %.1f° azimuth, %.1f° elevation.", az, el), nil
}

// GetSatelliteTracking reports what the tracker is doing.
func (a *App) GetSatelliteTracking() SatTrackStatus {
	a.satTrackMu.Lock()
	t := a.satTrack
	a.satTrackMu.Unlock()
	if t == nil {
		return SatTrackStatus{}
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.status
}

// satTrackedNominal is the nominal downlink the tracker is currently working
// from, or 0 when it is not tracking this satellite and transponder.
func (a *App) satTrackedNominal(name string, transponder int) int64 {
	a.satTrackMu.Lock()
	t := a.satTrack
	a.satTrackMu.Unlock()
	if t == nil || t.tp != transponder || !strings.EqualFold(t.name, name) {
		return 0
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.nominalDown
}

func (a *App) emitSatTrack(s SatTrackStatus) {
	if a.ctx != nil {
		wruntime.EventsEmit(a.ctx, "sat:track", s)
	}
}

func (a *App) satTrackLoop(t *satTracker) {
	defer close(t.done)
	defer t.releaseRotator()
	tick := time.NewTicker(satTickEvery)
	defer tick.Stop()
	for {
		a.satTrackStep(t)
		select {
		case <-t.stop:
			return
		case <-tick.C:
		}
	}
}

// satTrackStep is one pass of the loop: read the dial, work out the pair, send
// what changed.
func (a *App) satTrackStep(t *satTracker) {
	_, birds, _ := a.satParts()
	b, ok := birds.Find(t.name)
	if !ok || t.tp >= len(b.Transponders) {
		return
	}
	tp := b.Transponders[t.tp]

	t.mu.Lock()
	nominal := t.nominalDown
	lastDown, lastUp := t.lastDown, t.lastUp
	t.mu.Unlock()

	// Where the satellite is, and how fast it is running away. A geostationary
	// bird is neither: its range rate is zero, so the zero position below gives
	// a zero shift without a special case, and asking for a look angle we do not
	// need would only fail on a station with no locator.
	var pos sat.Position
	visible := true
	if !b.Geostationary {
		obs, err := a.satObserver()
		if err != nil {
			t.setError(err.Error())
			return
		}
		real, ok := a.satResolve(t.name)
		if !ok {
			t.setError(fmt.Sprintf("%s is not in the element set", t.name))
			return
		}
		store, _, _ := a.satParts()
		p, err := store.Track(real, obs, time.Now().UTC())
		if err != nil {
			t.setError(err.Error())
			return
		}
		pos = p
		visible = p.Visible()
	}
	// The fractional shift, positive when the satellite is approaching. Only the
	// dial arithmetic below needs it as a number; the pair itself comes from
	// sat.Doppler, so there is exactly one place where the sign of a correction
	// is decided.
	factor := -pos.RangeRate / satLightKmS

	// Where did the operator leave the receiver? If it is not where the tracker
	// put it, they have moved to another station and that is the new nominal.
	if lastDown > 0 && tp.Linear() {
		if actual, err := a.satReceiveHz(); err == nil && actual > 0 {
			if abs64i(actual-lastDown) > satDialTolerance {
				moved := satNominalFromDial(actual, factor)
				if moved >= tp.DownLo && moved <= tp.DownHi {
					nominal = moved
					t.mu.Lock()
					t.nominalDown = moved
					t.mu.Unlock()
				}
			}
		}
	}

	nomUp := tp.UplinkFor(nominal)
	sh := sat.Doppler(pos, nominal, nomUp)
	down, up := sh.DownHz, sh.UpHz

	t.mu.Lock()
	t.status = SatTrackStatus{
		On: true, Name: b.Name, Transponder: tp.Label, Mode: tp.Mode,
		NominalDown: nominal, NominalUp: nomUp,
		DownHz: down, UpHz: up,
		Az: pos.Az, El: pos.El, Visible: visible,
		Radio: t.status.Radio, Error: t.status.Error,
	}
	t.mu.Unlock()

	t.pointRotator(pos, b.Geostationary)
	t.readRotator()

	t.mu.Lock()
	st := t.status
	t.mu.Unlock()
	a.emitSatTrack(st)

	// Only send what has actually moved. The step is the smallest change worth a
	// command: on SSB a listener hears twenty hertz, on an FM channel nothing
	// under a couple of hundred matters at all.
	step := int64(20)
	if strings.EqualFold(tp.Mode, "FM") {
		step = 200
	}
	if abs64i(down-lastDown) < step && abs64i(up-lastUp) < step {
		return
	}

	mode := tp.Mode
	if lastDown != 0 {
		mode = "" // set once, at the start of the pass — see satMode/satSetMode
	}
	err := a.satTune(down, up, mode, mode)
	t.mu.Lock()
	if err == nil {
		t.lastDown, t.lastUp, t.fails = down, up, 0
		t.status.Error = ""
	} else {
		t.fails++
		t.status.Error = err.Error()
	}
	fails := t.fails
	t.mu.Unlock()
	if err != nil && (fails == 1 || fails%30 == 0) {
		// Once, then once every half minute: a radio that has gone away must be
		// visible in the log without filling it.
		applog.Printf("sat: tuning %s failed (%d in a row): %v", t.name, fails, err)
	}
}

// satNominalFromDial turns a frequency the operator tuned to into the nominal
// one it corresponds to.
//
// The inverse of the downlink correction: what comes out of the transponder at
// nominal arrives at heard = nominal × (1 + f). Doing this is what lets the
// operator hunt across a linear passband without the tracker dragging them back
// — where they land becomes the new truth, and the uplink follows it.
func satNominalFromDial(heardHz int64, factor float64) int64 {
	if heardHz <= 0 || factor <= -1 {
		return heardHz
	}
	return int64(math.Round(float64(heardHz) / (1 + factor)))
}

// pointRotator keeps the antenna on the satellite.
//
// Below the configured elevation the rotator is left alone. Not because the
// numbers stop being right — they are right all the way round the orbit — but
// because a rotator that chases a satellite through the far side of the earth
// spends the whole night turning, and a mast is a mechanical thing with a
// finite number of turns in it.
func (t *satTracker) pointRotator(pos sat.Position, geostationary bool) {
	if t.rot == nil {
		return
	}
	if !geostationary && pos.El < t.rotMinE {
		return
	}
	// A step below the beamwidth is a command for nothing. Compared against what
	// was last COMMANDED rather than where the rotator says it is: a rotator in
	// motion is always somewhere between the two, and comparing against that
	// would order a fresh move on every tick of a slew.
	az, el := pos.Az, pos.El
	if geostationary {
		// A satellite that does not move needs pointing once. Its own az/el were
		// not computed (there is nothing to compute), so leave the rotator where
		// the operator put it.
		if t.rotSent {
			return
		}
	}
	if t.rotSent && math.Abs(az-t.rotAz) < t.rotStep && math.Abs(el-t.rotEl) < t.rotStep {
		return
	}
	if err := t.rot.Point(az, el); err != nil {
		t.setError(err.Error())
		return
	}
	t.rotAz, t.rotEl, t.rotSent = az, el, true
}

// readRotator asks the controller where it actually is, for the display.
//
// Separate from the pointing, and it runs on every tick rather than only when a
// command was sent: watching the antenna crawl towards the bearing is how an
// operator sees a rotator that is slow, stalled, or turning the wrong way. A
// controller that does not answer says so once and is not asked again.
func (t *satTracker) readRotator() {
	if t.rot == nil {
		return
	}
	// Not on every tick. A PstRotator query binds a socket and waits up to a
	// second and a half for an answer, and a held serial port still costs a
	// round trip; three seconds is often enough to watch an antenna slew and
	// rare enough not to sit in the way of the tuning.
	if time.Since(t.rotReadAt) < 3*time.Second {
		return
	}
	t.rotReadAt = time.Now()
	az, el, live, err := t.rot.Heading()
	t.mu.Lock()
	defer t.mu.Unlock()
	if err != nil {
		t.status.RotOn = true
		return
	}
	t.status.RotOn, t.status.RotAz, t.status.RotEl, t.status.RotLive = true, az, el, live
}

// releaseRotator hands the mast back at the end of a pass.
func (t *satTracker) releaseRotator() {
	if t.rot == nil {
		return
	}
	if t.rotPark && t.rotSent {
		// Elevation down first and azimuth to north: a dish or a pair of yagis
		// left pointing at the sky is what a gale takes away.
		if err := t.rot.Point(0, 0); err != nil {
			applog.Printf("sat: could not park the rotator: %v", err)
		}
	}
	t.rot.Close()
	t.rot = nil
}

func (t *satTracker) setError(msg string) {
	t.mu.Lock()
	t.status.Error = msg
	t.mu.Unlock()
}

// satTune sends the pair to whichever radio is connected.
func (a *App) satTune(downHz, upHz int64, downMode, upMode string) error {
	if a.cat == nil {
		return fmt.Errorf("CAT is not running")
	}
	if a.cat.SatCapable() {
		return a.cat.SatDo(func(st cat.SatTuner) error {
			return st.TuneSatellite(downHz, upHz, downMode, upMode)
		})
	}
	// No satellite pair on this backend: the downlink is what it can do, and the
	// operator was told so when tracking started (Radio = "downlink-only").
	if err := a.cat.SetFrequency(downHz); err != nil {
		return err
	}
	if downMode != "" {
		return a.cat.SetMode(downMode)
	}
	return nil
}

// satReceiveHz is where the receiver is, asked of the backend that knows.
func (a *App) satReceiveHz() (int64, error) {
	if a.cat == nil {
		return 0, fmt.Errorf("CAT is not running")
	}
	if a.cat.SatCapable() {
		var hz int64
		err := a.cat.SatDo(func(st cat.SatTuner) error {
			v, e := st.SatReceiveHz()
			hz = v
			return e
		})
		return hz, err
	}
	st := a.cat.State()
	if st.RxFreqHz > 0 {
		return st.RxFreqHz, nil
	}
	return st.FreqHz, nil
}

func abs64i(v int64) int64 {
	if v < 0 {
		return -v
	}
	return v
}

// ── What goes in the log ────────────────────────────────────────────────────

// applySatellite stamps a QSO made through a satellite.
//
// The NOMINAL frequencies are logged, never the Doppler-corrected ones. Two
// stations working each other through a transponder read different numbers off
// their dials at the same instant — that is what Doppler means — and the only
// figure they can both agree on, and the only one that means anything to
// somebody reading the log later, is the transponder's own. LoTW matches on the
// band, so nothing is lost; a log full of 435.847 231 would simply be a record
// of where one radio happened to be.
func (a *App) applySatellite(q *qso.QSO) {
	a.satTrackMu.Lock()
	t := a.satTrack
	a.satTrackMu.Unlock()
	if t == nil {
		return
	}
	t.mu.Lock()
	name, down, up := t.status.Name, t.status.NominalDown, t.status.NominalUp
	az, el := t.status.Az, t.status.El
	t.mu.Unlock()
	if name == "" || down <= 0 {
		return
	}
	// Nothing the operator filled in is overwritten. A QSO edited by hand, or
	// imported, or logged from a second radio while the tracker happened to be
	// running, keeps what it was given.
	if strings.TrimSpace(q.PropMode) == "" {
		q.PropMode = "SAT"
	}
	if q.PropMode != "SAT" {
		return // they said it was something else — meteor scatter, EME
	}
	if strings.TrimSpace(q.SatName) == "" {
		q.SatName = name
	}
	if strings.TrimSpace(q.SatMode) == "" {
		q.SatMode = satModeLetters(up, down)
	}
	// The transmit frequency is the uplink and the receive frequency the
	// downlink — which is the one place a satellite QSO differs from every other
	// kind, and the reason FREQ alone cannot describe one.
	if up > 0 {
		q.FreqHz = &up
		if b := bandForHz(up); b != "" {
			q.Band = b
		}
	}
	d := down
	q.FreqRXHz = &d
	if b := bandForHz(down); b != "" {
		q.BandRX = b
	}
	if q.AntAz == nil && (az != 0 || el != 0) {
		v := az
		q.AntAz = &v
	}
	if q.AntEl == nil && el != 0 {
		v := el
		q.AntEl = &v
	}
}

// satModeLetters is the ADIF SAT_MODE: the uplink band's letter, then the
// downlink's — "U/V" for 435 up, 145 down. The letters are AMSAT's, and they
// are what every satellite operator writes on a QSL card.
func satModeLetters(upHz, downHz int64) string {
	u, d := satBandLetter(upHz), satBandLetter(downHz)
	if u == "" || d == "" {
		return ""
	}
	return u + "/" + d
}

func satBandLetter(hz int64) string {
	switch {
	case hz <= 0:
		return ""
	case hz < 30_000_000:
		return "A" // 10 m — mode A's downlink
	case hz < 148_000_000:
		return "V" // 2 m
	case hz < 450_000_000:
		return "U" // 70 cm
	case hz < 1_300_000_000:
		return "L" // 23 cm
	case hz < 2_500_000_000:
		return "S" // 13 cm
	case hz < 6_000_000_000:
		return "C" // 6 cm
	case hz < 11_000_000_000:
		return "X" // 3 cm
	}
	return "K" // 24 GHz and above
}
