package main

// The two ways a satellite station points its antenna.
//
// Some operators drive their az/el rotator directly — EasyComm II, what
// SatPC32 and Gpredict speak. Others already run PstRotator, which sits between
// them and a dozen different controllers and handles az AND el; for those,
// OpsLog talking to the controller itself would be a second program fighting
// PstRotator over the same cable.
//
// So both, behind one small interface, chosen in Settings. Neither is more
// "correct" than the other: the right one is whichever the station already has
// working.

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

	"hamlog/internal/rotator/easycomm"
	"hamlog/internal/rotator/pst"
)

// satRotator is what the tracker needs of an antenna: point it, ask where it
// is, and let go of it at the end of the pass.
type satRotator interface {
	Point(az, el float64) error
	// Heading reports where the antenna is. live is false when the answer is
	// the last commanded position rather than a reading — a stuck rotator must
	// not be able to hide behind an order it never carried out.
	Heading() (az, el float64, live bool, err error)
	Close()
}

// The rotator kinds, as stored.
const (
	satRotEasycomm = "easycomm"
	satRotPst      = "pstrotator"
)

// newSatRotator builds the configured controller.
func newSatRotator(s SatSettings) (satRotator, error) {
	switch s.RotType {
	case satRotPst:
		if strings.TrimSpace(s.RotHost) == "" && s.RotPort <= 0 {
			return nil, fmt.Errorf("no address for PstRotator")
		}
		return &pstSatRotator{c: pst.New(s.RotHost, s.RotPstPort), maxAz: s.RotMaxAz}, nil
	default:
		if s.RotTransport == "tcp" {
			if strings.TrimSpace(s.RotHost) == "" {
				return nil, fmt.Errorf("no address for the rotator")
			}
			return easycomm.New(s.RotHost, s.RotPort, s.RotMaxAz), nil
		}
		if strings.TrimSpace(s.RotCOM) == "" {
			return nil, fmt.Errorf("no COM port for the rotator")
		}
		return easycomm.NewSerial(s.RotCOM, s.RotBaud, s.RotMaxAz), nil
	}
}

// pstSatRotator points the antenna through PstRotator.
//
// PstRotator takes whole degrees and does its own overlap handling for a 450°
// rotator — it knows which controller is on the other end, and OpsLog does not.
// So the azimuth is sent plainly, and the 450° logic that EasyComm needs is
// deliberately NOT applied here: two programs each deciding to go the long way
// round is how an antenna ends up unwinding in the middle of a pass.
type pstSatRotator struct {
	c     *pst.Client
	maxAz int

	mu sync.Mutex
	// lastAz/lastEl are what was commanded, for the display when PstRotator
	// does not answer a position query — which is the usual case for the many
	// setups whose controller reports nothing back to it either.
	lastAz, lastEl float64
	commanded      bool
	azSilent       bool // the azimuth query went unanswered; stop asking
	elSilent       bool // likewise for elevation, and far more common
}

func (p *pstSatRotator) Point(az, el float64) error {
	a := math.Mod(az, 360)
	if a < 0 {
		a += 360
	}
	if el < 0 {
		el = 0
	}
	if el > 180 {
		el = 180
	}
	if err := p.c.GoTo(int(math.Round(a)), true, int(math.Round(el))); err != nil {
		return err
	}
	p.mu.Lock()
	p.lastAz, p.lastEl, p.commanded = a, el, true
	p.mu.Unlock()
	return nil
}

func (p *pstSatRotator) Heading() (float64, float64, bool, error) {
	p.mu.Lock()
	azSilent, elSilent, la, le, commanded := p.azSilent, p.elSilent, p.lastAz, p.lastEl, p.commanded
	p.mu.Unlock()

	az, el, live := la, le, false
	if !azSilent {
		if v, _, err := p.c.Heading(); err == nil {
			az, live = float64(v), true
		} else {
			// One silence is enough. Each query binds a socket and waits a second
			// and a half; repeating that every few seconds for a setup that will
			// never answer is a stall per poll for nothing.
			p.mu.Lock()
			p.azSilent = true
			p.mu.Unlock()
		}
	}
	if !elSilent {
		if v, _, err := p.c.Elevation(); err == nil {
			el = float64(v)
		} else {
			p.mu.Lock()
			p.elSilent = true
			p.mu.Unlock()
		}
	}
	if !live && !commanded {
		return 0, 0, false, fmt.Errorf("PstRotator does not report the antenna position")
	}
	return az, el, live, nil
}

// Close: nothing to release. Every PstRotator command is one datagram, and the
// socket lives for the length of a single write.
func (p *pstSatRotator) Close() {}
