package cat

// Sending audio TO the radio over TCI.
//
// Three transmissions on a real SunSDR settled how this works, and none of it
// was guessable from the documentation:
//
//  1. The radio asks for audio only when the transmission is the CLIENT'S. With
//     the operator keying the microphone it sent 282 receive frames and nothing
//     else, over six seconds.
//  2. It asks only when its TRANSMIT AUDIO SOURCE is TCI rather than the
//     microphone. This first read as "digital modes only" — SSB produced
//     nothing four times over, DIGU answered at once — but the mode was a
//     coincidence: ExpertSDR3 keeps that source setting per mode, and it was on
//     the microphone in SSB. Which is why nothing is refused on the strength of
//     the mode: the radio is asked, and it answers by asking or by staying
//     quiet.
//  3. The chrono is a REQUEST, not a clock to follow. It carries no payload —
//     the message itself is the ask — and it names the size it wants in the
//     header's length field: 2048 samples, two channels interleaved, arriving
//     47 times a second. Which is 1024 sample-pairs at 48 kHz, exactly real
//     time, measured rather than assumed.
//
// So audio is sent in ANSWER to chrono, never on a timer of our own. A timer
// was the first attempt and the radio ignored every frame of it: 234 sent, none
// used. Answering the request is what makes the difference, and it also means
// the radio sets the pace — no drift, no buffer to tune.
//
// All of it was established with a tone probe — key the radio, push a sine,
// watch — which is gone now that it has served its purpose: it answered the
// three questions above, confirmed 80 W out on a real SunSDR, and had no
// business in front of an operator once the voice keyer worked. What is left is
// the exchange it discovered, with tci_tx_play.go supplying the message.

import (
	"encoding/binary"
	"fmt"
	"time"

	"github.com/gorilla/websocket"
)

// sendBinaryFrame writes one TCI binary frame: the 16-word header the radio's
// own frames carry, then the payload.
func (t *TCI) sendBinaryFrame(stype, rx, rate, length int, payload []byte) error {
	t.mu.Lock()
	c := t.conn
	t.mu.Unlock()
	if c == nil {
		return fmt.Errorf("tci: not connected")
	}
	buf := make([]byte, tciHeaderBytes+len(payload))
	le := binary.LittleEndian
	le.PutUint32(buf[0:], uint32(rx))
	le.PutUint32(buf[4:], uint32(rate))
	// format=3, codec=0: mirrored from what this radio SENDS. The field is
	// documented as an enumeration whose numbering did not survive contact with
	// the firmware — the receive stream answers 3 for four-byte floats — so the
	// only defensible choice is to speak back exactly what was spoken to us.
	le.PutUint32(buf[8:], 3)
	le.PutUint32(buf[12:], 0)
	le.PutUint32(buf[16:], 0) // crc — the radio sends 0 and does not check ours
	le.PutUint32(buf[20:], uint32(length))
	le.PutUint32(buf[24:], uint32(stype))
	copy(buf[tciHeaderBytes:], payload)

	t.wmu.Lock()
	defer t.wmu.Unlock()
	_ = c.SetWriteDeadline(time.Now().Add(3 * time.Second))
	return c.WriteMessage(websocket.BinaryMessage, buf)
}

// serveChrono answers one request for transmit audio.
//
// Called from the reader goroutine, so it does the least it can: take the
// frame from whatever is feeding, and write it. A feed that has run out returns
// nil and the request is counted rather than answered with silence — silence
// would be indistinguishable from a working stream on a meter.
func (t *TCI) serveChrono(rate, samples int) {
	t.audio.mu.Lock()
	feed := t.audio.txFeed
	t.audio.mu.Unlock()
	if feed == nil {
		return
	}
	if samples <= 0 {
		samples = 2048
	}
	payload := feed(samples)
	if payload == nil {
		// Answered with silence rather than left unanswered. TCI 2.0 §3.4: "the
		// client may not send a response or may send a signal with zero counts,
		// which corresponds to no signal - this option is preferable."
		payload = make([]byte, samples*4)
		t.audio.mu.Lock()
		t.audio.txShort++
		t.audio.mu.Unlock()
	}
	if rate <= 0 {
		rate = 48000
	}
	if err := t.sendBinaryFrame(tciStreamTXAudio, 0, rate, samples, payload); err != nil {
		debugLog.Printf("TCI: could not send transmit audio: %v", err)
		return
	}
	t.audio.mu.Lock()
	t.audio.txSent++
	t.audio.mu.Unlock()
}

// setTXFeed installs (or clears) the source of transmit audio.
func (t *TCI) setTXFeed(fn func(samples int) []byte) {
	t.audio.mu.Lock()
	t.audio.txFeed = fn
	t.audio.txSent, t.audio.txShort = 0, 0
	t.audio.mu.Unlock()
}
