// Package sat is where a satellite is, where it will be, and what that does to
// a frequency.
//
// Three things live here and nothing else: the orbital elements a station keeps
// (Store), the sky as seen from that station (Track, Passes), and the Doppler
// shift the motion imposes (Shift). The radio, the rotator and the screen are
// all somebody else's business — they are handed numbers by the app layer.
//
// The propagation itself is SGP4 from github.com/akhenakh/sgp4 (Apache-2.0,
// pure Go): the model everyone in this hobby uses, and the one the TLEs are
// built for. Writing it again would be writing it worse.
package sat

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

	"github.com/akhenakh/sgp4"
)

// speedOfLightKmS is the constant every Doppler correction here is built on.
const speedOfLightKmS = 299792.458

// Observer is the ground station: where the antenna is, in degrees and metres.
type Observer struct {
	Lat, Lon float64
	AltM     float64
}

// Position is a satellite seen from the ground at one instant.
//
// The two halves answer different questions and both are wanted: where the
// thing IS (for the map) and where to POINT (for the rotator and the Doppler).
type Position struct {
	Name string    `json:"name"`
	At   time.Time `json:"at"`

	// Sub-satellite point and height — the map's half.
	Lat       float64 `json:"lat"`
	Lon       float64 `json:"lon"`
	AltKm     float64 `json:"alt_km"`
	Footprint float64 `json:"footprint_km"` // radius of the visibility circle

	// Look angles — the station's half.
	Az        float64 `json:"az"`
	El        float64 `json:"el"`
	RangeKm   float64 `json:"range_km"`
	RangeRate float64 `json:"range_rate"` // km/s, positive = receding
}

// Visible reports whether the satellite is above the horizon.
//
// Zero degrees, not a courtesy margin: an operator with a clear take-off works
// a pass from the moment it rises, and a station in a valley knows its own
// horizon better than this package ever will.
func (p Position) Visible() bool { return p.El > 0 }

// Pass is one crossing of the sky, from rise to set.
type Pass struct {
	Name     string    `json:"name"`
	AOS      time.Time `json:"aos"`
	LOS      time.Time `json:"los"`
	AOSAz    float64   `json:"aos_az"`
	LOSAz    float64   `json:"los_az"`
	MaxEl    float64   `json:"max_el"`
	MaxElAz  float64   `json:"max_el_az"`
	MaxElAt  time.Time `json:"max_el_at"`
	Duration float64   `json:"duration_s"`
}

// Element is one satellite's orbital elements, as they were published.
//
// The raw lines are kept beside the parsed form because they are what gets
// written to the cache and what an operator pastes in by hand for a bird that
// is not in any feed yet — a freshly launched one, above all, which is exactly
// when everybody wants to hear it.
type Element struct {
	Name  string `json:"name"`
	NORAD int    `json:"norad"`
	Line1 string `json:"line1"`
	Line2 string `json:"line2"`
	// Epoch is when these elements were computed. Their accuracy falls away
	// from it, which is why the store knows how old they are.
	Epoch time.Time `json:"epoch"`

	tle *sgp4.TLE
}

// Age is how long ago these elements were computed.
func (e Element) Age() time.Duration {
	if e.Epoch.IsZero() {
		return 0
	}
	return time.Since(e.Epoch)
}

// ParseElement reads one satellite from its two or three TLE lines.
func ParseElement(name, line1, line2 string) (Element, error) {
	name = strings.TrimSpace(name)
	line1 = strings.TrimSpace(line1)
	line2 = strings.TrimSpace(line2)
	if line1 == "" || line2 == "" {
		return Element{}, fmt.Errorf("sat: %q has no orbital elements", name)
	}
	raw := line1 + "\n" + line2
	if name != "" {
		raw = name + "\n" + raw
	}
	t, err := sgp4.ParseTLE(raw)
	if err != nil {
		return Element{}, fmt.Errorf("sat: %q: %w", name, err)
	}
	if name == "" {
		name = strings.TrimSpace(t.Name)
	}
	return Element{
		Name:  name,
		NORAD: t.SatelliteNumber,
		Line1: line1,
		Line2: line2,
		Epoch: tleEpoch(t),
		tle:   t,
	}, nil
}

// tleEpoch turns the two-digit year and fractional day of a TLE into a time.
//
// The pivot is the one the format itself defines: 57 and above is the twentieth
// century, below it the twenty-first. It matters for the AGE of the elements,
// which is how an operator knows whether to trust a prediction.
func tleEpoch(t *sgp4.TLE) time.Time {
	if t == nil || t.EpochDay <= 0 {
		return time.Time{}
	}
	year := t.EpochYear
	switch {
	case year >= 57 && year <= 99:
		year += 1900
	case year < 57:
		year += 2000
	}
	start := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
	return start.Add(time.Duration((t.EpochDay - 1) * float64(24*time.Hour)))
}

// Store holds the elements a station tracks. Safe for concurrent use: the app
// refreshes it from a feed while the tracking loop reads it several times a
// second.
type Store struct {
	mu    sync.RWMutex
	byKey map[string]Element
	order []string // insertion order, so a listing reads like the feed
	fetch time.Time
}

func NewStore() *Store { return &Store{byKey: map[string]Element{}} }

// key is how a satellite is addressed. Case and spacing vary between feeds and
// between the operator's typing; the NORAD number would be exact but is not
// what anybody says out loud.
func key(name string) string { return strings.ToUpper(strings.TrimSpace(name)) }

// Put adds or replaces one satellite's elements.
func (s *Store) Put(e Element) {
	if e.tle == nil || e.Name == "" {
		return
	}
	k := key(e.Name)
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, had := s.byKey[k]; !had {
		s.order = append(s.order, k)
	}
	s.byKey[k] = e
}

// Get returns one satellite's elements.
func (s *Store) Get(name string) (Element, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	e, ok := s.byKey[key(name)]
	return e, ok
}

// Names lists what the store holds, in the order it arrived.
func (s *Store) Names() []string {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]string, 0, len(s.order))
	for _, k := range s.order {
		out = append(out, s.byKey[k].Name)
	}
	return out
}

// Len is how many satellites are known.
func (s *Store) Len() int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return len(s.byKey)
}

// FetchedAt is when the elements were last loaded from a feed, zero if never.
func (s *Store) FetchedAt() time.Time {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.fetch
}

// Replace swaps the whole set — what a feed refresh does. The order of the new
// set is kept, and the fetch time is stamped.
func (s *Store) Replace(els []Element, at time.Time) {
	byKey := make(map[string]Element, len(els))
	order := make([]string, 0, len(els))
	for _, e := range els {
		if e.tle == nil || e.Name == "" {
			continue
		}
		k := key(e.Name)
		if _, had := byKey[k]; !had {
			order = append(order, k)
		}
		byKey[k] = e
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.byKey, s.order, s.fetch = byKey, order, at
}

// Track is where one satellite is, seen from one station, at one instant.
func (s *Store) Track(name string, obs Observer, at time.Time) (Position, error) {
	e, ok := s.Get(name)
	if !ok {
		return Position{}, fmt.Errorf("sat: %q is not in the element set", name)
	}
	return e.Track(obs, at)
}

// Track is the same for elements already in hand.
func (e Element) Track(obs Observer, at time.Time) (Position, error) {
	if e.tle == nil {
		return Position{}, fmt.Errorf("sat: %q has no usable elements", e.Name)
	}
	loc := &sgp4.Location{Latitude: obs.Lat, Longitude: obs.Lon, Altitude: obs.AltM}
	eci, err := e.tle.FindPositionAtTime(at.UTC())
	if err != nil {
		return Position{}, fmt.Errorf("sat: %q: %w", e.Name, err)
	}
	// The state vector carries the position AND the velocity, which is what the
	// look angle needs for the range rate — and the range rate is the whole of
	// the Doppler shift.
	sv := &sgp4.StateVector{
		X: eci.Position.X, Y: eci.Position.Y, Z: eci.Position.Z,
		VX: eci.Velocity.X, VY: eci.Velocity.Y, VZ: eci.Velocity.Z,
	}
	o, err := sv.GetLookAngle(loc, at.UTC())
	if err != nil {
		return Position{}, fmt.Errorf("sat: %q look angle: %w", e.Name, err)
	}
	return Position{
		Name:      e.Name,
		At:        at.UTC(),
		Lat:       o.SatellitePos.Latitude,
		Lon:       o.SatellitePos.Longitude,
		AltKm:     o.SatellitePos.Altitude,
		Footprint: footprintKm(o.SatellitePos.Altitude),
		Az:        o.LookAngles.Azimuth,
		El:        o.LookAngles.Elevation,
		RangeKm:   o.LookAngles.Range,
		RangeRate: o.LookAngles.RangeRate,
	}, nil
}

// earthRadiusKm is the mean radius — the footprint is a circle drawn on a
// sphere, and a metre of flattening does not show at that scale.
const earthRadiusKm = 6371.0

// footprintKm is the radius of the circle from which the satellite is above the
// horizon: the ground distance to where it sits exactly on it.
func footprintKm(altKm float64) float64 {
	if altKm <= 0 {
		return 0
	}
	return earthRadiusKm * math.Acos(earthRadiusKm/(earthRadiusKm+altKm))
}

// Passes lists the crossings of the sky between two instants.
//
// minEl drops the passes not worth waiting for: a bird that scrapes three
// degrees over the horizon is a line in a table that will never be a QSO, and
// on a busy evening those are most of the list.
func (s *Store) Passes(name string, obs Observer, from, to time.Time, minEl float64) ([]Pass, error) {
	e, ok := s.Get(name)
	if !ok {
		return nil, fmt.Errorf("sat: %q is not in the element set", name)
	}
	if !to.After(from) {
		return nil, fmt.Errorf("sat: the window ends before it starts")
	}
	// Thirty seconds: fine enough that the rise and set times are right to a few
	// seconds, coarse enough that a day of predictions for a dozen satellites
	// stays instant.
	details, err := e.tle.GeneratePasses(obs.Lat, obs.Lon, obs.AltM, from.UTC(), to.UTC(), 30)
	if err != nil {
		return nil, fmt.Errorf("sat: %q passes: %w", e.Name, err)
	}
	out := make([]Pass, 0, len(details))
	for _, d := range details {
		if d.MaxElevation < minEl {
			continue
		}
		out = append(out, Pass{
			Name:     e.Name,
			AOS:      d.AOS.UTC(),
			LOS:      d.LOS.UTC(),
			AOSAz:    d.AOSAzimuth,
			LOSAz:    d.LOSAzimuth,
			MaxEl:    d.MaxElevation,
			MaxElAz:  d.MaxElevationAz,
			MaxElAt:  d.MaxElevationTime.UTC(),
			Duration: d.Duration.Seconds(),
		})
	}
	return out, nil
}

// NextPasses is Passes over several satellites at once, in time order — the
// question an operator actually asks: what is coming, and when.
func (s *Store) NextPasses(names []string, obs Observer, from time.Time, window time.Duration, minEl int) []Pass {
	var all []Pass
	for _, n := range names {
		ps, err := s.Passes(n, obs, from, from.Add(window), float64(minEl))
		if err != nil {
			continue // a satellite whose elements are missing is simply not listed
		}
		all = append(all, ps...)
	}
	sort.Slice(all, func(i, j int) bool { return all[i].AOS.Before(all[j].AOS) })
	return all
}

// Shift is the Doppler-corrected pair for one moment.
type Shift struct {
	DownHz int64 `json:"down_hz"` // where to LISTEN for a nominal downlink
	UpHz   int64 `json:"up_hz"`   // where to TRANSMIT for a nominal uplink
}

// Doppler corrects a nominal uplink/downlink pair for the satellite's motion.
//
// Two corrections, opposite in sign, and that is the part worth being careful
// about: the DOWNLINK is what we receive, so it arrives shifted and we tune to
// meet it — approaching (negative range rate) means a higher frequency. The
// UPLINK is what the satellite receives, so we must transmit shifted the other
// way for it to land on the transponder's nominal input.
//
// Zero in, zero out: a satellite with no uplink (a beacon) is not given an
// invented one.
func Doppler(p Position, downHz, upHz int64) Shift {
	f := -p.RangeRate / speedOfLightKmS // fraction, positive when approaching
	var s Shift
	if downHz > 0 {
		s.DownHz = downHz + int64(math.Round(float64(downHz)*f))
	}
	if upHz > 0 {
		s.UpHz = upHz - int64(math.Round(float64(upHz)*f))
	}
	return s
}
