//go:build windows

package audio

import (
	"fmt"
	"sync"
	"time"
)

// Manager owns the DVK record/playback lifecycle: at most one recording and
// one playback at a time. Device ids are passed per call so the host can route
// recording to the mic and playback to the rig (or the preview speakers).
type Manager struct {
	mu      sync.Mutex
	recStop chan struct{}
	recDone chan recResult
	// Said once each: "the audio is reaching the speakers" and "it is arriving
	// with nobody listening". Both are answers to the same evening — sound
	// switched on, nothing out of the speakers — and neither is worth a line per
	// packet at fifty packets a second.
	gotAudioOnce sync.Once
	noSinkOnce   sync.Once
	// monGainPct scales what the RX monitor plays, 100 = as captured.
	//
	// The "From radio" slider used to reach only the QSO recorder, so an
	// operator listening through OpsLog heard no difference between 10% and
	// 150% — the setting looked broken because it was, for the thing they were
	// listening to.
	monGainPct int
	playStop   chan struct{}
	// playDone closes when the playback goroutine has returned and the audio
	// device is free again. Without it, the next Play raced the old one for the
	// device and lost.
	playDone chan struct{}
	monStop  chan struct{} // RX monitor passthrough (capture → render)
	monRing  *pcmRing      // live audio hand-off, also fed by the network stream
	txStop   chan struct{} // TX audio passthrough (mic → rig)
	onChange func()        // fired on any record/playback state transition
}

type recResult struct {
	pcm []byte
	err error
}

// NewManager creates a DVK manager. onChange (optional) is called whenever the
// recording/playback state changes, so the host can push an audio:status event.
func NewManager(onChange func()) *Manager { return &Manager{onChange: onChange} }

func (m *Manager) notify() {
	if m.onChange != nil {
		m.onChange()
	}
}

// StartRecording begins capturing from deviceID into memory. Finish with
// StopRecording (which writes the WAV) or CancelRecording (which discards it).
func (m *Manager) StartRecording(deviceID string) error {
	m.mu.Lock()
	if m.recStop != nil {
		m.mu.Unlock()
		return fmt.Errorf("already recording")
	}
	stop := make(chan struct{})
	done := make(chan recResult, 1)
	m.recStop, m.recDone = stop, done
	m.mu.Unlock() // release BEFORE notify — onChange re-enters via IsRecording()
	go func() {
		pcm, err := recordPCM(deviceID, stop)
		done <- recResult{pcm, err}
	}()
	m.notify()
	return nil
}

// StopRecording ends the capture and writes it to path as a WAV file.
func (m *Manager) StopRecording(path string) error {
	m.mu.Lock()
	stop, done := m.recStop, m.recDone
	m.recStop, m.recDone = nil, nil
	m.mu.Unlock()
	if stop == nil {
		return fmt.Errorf("not recording")
	}
	close(stop)
	res := <-done
	m.notify()
	if res.err != nil {
		return res.err
	}
	if len(res.pcm) == 0 {
		return fmt.Errorf("captured no audio (check the recording device)")
	}
	return writeWAV(path, res.pcm)
}

// CancelRecording aborts a recording without saving.
func (m *Manager) CancelRecording() {
	m.mu.Lock()
	stop, done := m.recStop, m.recDone
	m.recStop, m.recDone = nil, nil
	m.mu.Unlock()
	if stop != nil {
		close(stop)
		<-done
		m.notify()
	}
}

func (m *Manager) IsRecording() bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.recStop != nil
}

func (m *Manager) IsPlaying() bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.playStop != nil
}

// Play renders a WAV file to deviceID. Any current playback is stopped first.
// Returns immediately; playback runs in the background.
// Play sends a recorded message to a device, amplified by gainPct (100 = as
// recorded).
//
// The gain exists because nothing else could raise the level: the message went
// out exactly as captured, so a mic recorded quietly drove the rig quietly and
// the operator had no control anywhere in OpsLog — only the radio's own USB
// input level, buried in its menus, and the Windows mixer.
func (m *Manager) Play(deviceID, path string, gainPct int) error {
	pcm, rate, ch, bits, err := readWAV(path)
	if err != nil {
		return err
	}
	if gainPct > 0 && gainPct != 100 && bits == 16 {
		g := float64(gainPct) / 100
		// In place: the buffer is this call's own copy of the file.
		for i := 0; i+1 < len(pcm); i += 2 {
			v := int16(uint16(pcm[i]) | uint16(pcm[i+1])<<8)
			v = scalePCM(v, g)
			pcm[i], pcm[i+1] = byte(uint16(v)), byte(uint16(v)>>8)
		}
	}
	// Waits for any previous playback to have RELEASED THE DEVICE.
	//
	// It used to only signal the old one to stop and start a new one at once.
	// The old goroutine still held the WASAPI render client for a moment, so the
	// new client could not start: it returned immediately, the PTT was keyed and
	// released a tenth of a second later, and nothing came out. On the air that
	// looked like "press play again and you must wait the whole length of the
	// message before it will play at all".
	m.StopPlayback()
	stop := make(chan struct{})
	done := make(chan struct{})
	m.mu.Lock()
	m.playStop = stop
	m.playDone = done
	m.mu.Unlock()
	go func() {
		// The error was discarded. A device that refuses to start returns here
		// instantly, the PTT is released 120 ms later, and NOTHING says why —
		// which is exactly what a station heard as "it plays once, then never
		// again": the call succeeded, the sound did not.
		play := func() error { return playPCM(deviceID, pcm, rate, ch, bits, stop) }
		if deviceID == NetworkDeviceID {
			// Straight to the radio over its own link. Decided HERE rather than
			// inside playPCM because there is no Windows endpoint to open: asked
			// for one, the system complains about a missing device instead of
			// saying the true thing, which is that no radio is connected.
			fn := networkPlayer()
			play = func() error {
				if fn == nil {
					return errNoNetworkRadio
				}
				return fn(pcm, rate, ch, bits, stop)
			}
		}
		if err := play(); err != nil {
			LogSink("audio: playback on %q failed: %v", DeviceName(deviceID), err)
		}
		m.mu.Lock()
		if m.playStop == stop {
			m.playStop = nil
		}
		if m.playDone == done {
			m.playDone = nil
		}
		m.mu.Unlock()
		close(done) // the device is free from here
		m.notify()
	}()
	m.notify()
	return nil
}

// StopPlayback halts any in-progress playback.
func (m *Manager) StopPlayback() {
	m.mu.Lock()
	stop := m.playStop
	done := m.playDone
	m.playStop = nil
	m.mu.Unlock()
	if stop == nil {
		return
	}
	close(stop)
	// Wait for the goroutine to release the device — that is the whole point of
	// stopping before starting again. Bounded: a wedged WASAPI call must not
	// freeze the caller, which here is the operator clicking a button.
	if done != nil {
		select {
		case <-done:
		case <-time.After(1500 * time.Millisecond):
			LogSink("audio: previous playback did not release the device within 1.5 s")
		}
	}
	m.notify()
}

// ---- RX audio monitor (Phase 2: USB codec passthrough) --------------------
//
// StartMonitor pipes live RX audio from inputDev (e.g. the rig's "USB Audio
// CODEC" capture endpoint) to outputDev (your speakers/headset) through a
// latency-bounded ring, so you HEAR the radio inside OpsLog. The very same ring
// is later fed by the network 50003 stream instead of a USB capture — the render
// half is transport-agnostic. inputDev "" = system default capture.
func (m *Manager) StartMonitor(inputDev, outputDev string) error {
	return m.startMonitor(inputDev, outputDev, true)
}

// Logf receives this package's diagnostic lines. Set to applog.Printf by the
// app; a no-op in tests and for anything that vendors the package alone.
var Logf = func(string, ...any) {}

// StartMonitorSink starts ONLY the render side (no USB capture) so an external
// producer — the network 50003 stream — can feed decoded RX PCM via
// PushMonitorAudio. Same output path as StartMonitor, minus the capture goroutine.
func (m *Manager) StartMonitorSink(outputDev string) error {
	return m.startMonitor("", outputDev, false)
}

// startMonitor wires the RX monitor: always a render loop pulling from monRing;
// when capture is true it also captures inputDev into that ring (USB monitor).
// When false the ring is fed only by PushMonitorAudio (network audio).
func (m *Manager) startMonitor(inputDev, outputDev string, capture bool) error {
	m.mu.Lock()
	if m.monStop != nil {
		m.mu.Unlock()
		return fmt.Errorf("monitor already running")
	}
	stop := make(chan struct{})
	ring := newPCMRing(bytesPerSec / 2) // ~500 ms cap — low latency for live monitor
	m.monStop, m.monRing = stop, ring
	m.mu.Unlock()

	if capture {
		// Producer: capture the rig's USB audio into the ring, at the level the
		// operator set. Applied HERE rather than on the render side so the
		// network-fed path (PushMonitorAudio) keeps its own untouched levels —
		// that stream is already scaled by the radio.
		go func() {
			_ = captureStream(inputDev, stop, func(chunk []byte) { ring.Push(m.scaleMonitor(chunk)) })
		}()
	}
	// Consumer: render the ring to the output device at the internal 16 kHz mono.
	//
	// The error was thrown away, and that is the whole of "I turned the sound on
	// and nothing comes out": a Listening device that has been unplugged, renamed
	// by Windows or cannot open at 16 kHz fails here, silently, while everything
	// upstream reports success — the stream is up, the packets arrive, the
	// monitor says it started. Said out loud, the operator knows to look at the
	// device rather than at the radio.
	go func() {
		if err := renderStream(outputDev, sampleRate, channels, bitsPerSample, stop, ring); err != nil {
			Logf("audio: the Listening device could not be opened (%q): %v", outputDev, err)
		}
	}()
	m.notify()
	return nil
}

// SetMonitorGain sets the RX monitor level in percent (100 = as captured).
// Takes effect on the next captured chunk — no need to restart the monitor.
func (m *Manager) SetMonitorGain(pct int) {
	if pct <= 0 {
		pct = 100
	}
	m.mu.Lock()
	m.monGainPct = pct
	m.mu.Unlock()
}

// scaleMonitor applies the monitor level to one captured chunk, returning a
// buffer the ring may keep. At unity it hands the chunk straight back: the
// common case must not pay for a copy 30 times a second.
func (m *Manager) scaleMonitor(chunk []byte) []byte {
	m.mu.Lock()
	pct := m.monGainPct
	m.mu.Unlock()
	if pct == 0 || pct == 100 {
		return chunk
	}
	g := float64(pct) / 100
	out := make([]byte, len(chunk))
	copy(out, chunk)
	for i := 0; i+1 < len(out); i += 2 {
		v := int16(uint16(out[i]) | uint16(out[i+1])<<8)
		v = scalePCM(v, g)
		out[i], out[i+1] = byte(uint16(v)), byte(uint16(v)>>8)
	}
	return out
}

// StopMonitor stops the RX monitor passthrough.
func (m *Manager) StopMonitor() {
	m.mu.Lock()
	stop := m.monStop
	m.monStop, m.monRing = nil, nil
	m.mu.Unlock()
	if stop != nil {
		close(stop)
		m.notify()
	}
}

// MonitorActive reports whether the RX monitor passthrough is running.
func (m *Manager) MonitorActive() bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.monStop != nil
}

// PushMonitorAudio feeds externally-sourced PCM (16 kHz mono 16-bit) into the
// active monitor's output — the hook the network 50003 audio stream uses to play
// decoded RX through the very same output path a USB capture feeds. No-op when no
// monitor is running. Keeps the unexported ring inside the package.
func (m *Manager) PushMonitorAudio(pcm []byte) {
	m.mu.Lock()
	ring := m.monRing
	m.mu.Unlock()
	if ring == nil {
		// Nothing is listening: the stream is feeding the recorder and the voice
		// keyer only. Said ONCE, because the alternative — silence in the log for
		// silence in the speakers — is what makes this take an evening to find.
		m.noSinkOnce.Do(func() {
			Logf("audio: network RX audio is arriving but no monitor is running — the speakers are off (Listening)")
		})
		return
	}
	m.gotAudioOnce.Do(func() {
		Logf("audio: network RX audio reaching the Listening device (%d-byte chunks)", len(pcm))
	})
	ring.Push(pcm)
}

// ---- TX audio passthrough (Phase 3: live mic → rig over USB) --------------
//
// StartTXAudio pipes your live microphone (micDev) into the rig's audio input
// (toRadioDev — for a USB-connected rig, its "USB Audio CODEC" render endpoint),
// so you talk through the PC. It is the mirror of StartMonitor (same ring +
// capture + render primitives, source/sink swapped). PTT keying is the caller's
// job (the app layer keys PTT before this and unkeys after) so this stays a pure
// audio route. The captured 16 kHz mono stream is also the exact shape the future
// network 50003 TX will encode and send — so Phase 5 reuses this capture side.
func (m *Manager) StartTXAudio(micDev, toRadioDev string) error {
	m.mu.Lock()
	if m.txStop != nil {
		m.mu.Unlock()
		return fmt.Errorf("TX audio already running")
	}
	stop := make(chan struct{})
	ring := newPCMRing(bytesPerSec / 4) // ~250 ms — tighter for live TX latency
	m.txStop = stop
	m.mu.Unlock()

	go func() {
		_ = captureStream(micDev, stop, func(chunk []byte) { ring.Push(chunk) })
	}()
	go func() {
		_ = renderStream(toRadioDev, sampleRate, channels, bitsPerSample, stop, ring)
	}()
	m.notify()
	return nil
}

// StartTXAudioNetwork pipes the live microphone into a SEND function instead
// of a render device — the talk button when the radio is reached over its own
// link. No ring and no pacing goroutine: the microphone delivers in real time,
// and the sender re-frames to the rig's cadence, so the capture callback IS
// the clock.
func (m *Manager) StartTXAudioNetwork(micDev string, send func([]byte) error) error {
	m.mu.Lock()
	if m.txStop != nil {
		m.mu.Unlock()
		return fmt.Errorf("TX audio already running")
	}
	stop := make(chan struct{})
	m.txStop = stop
	m.mu.Unlock()
	go func() {
		if err := captureStream(micDev, stop, func(chunk []byte) { _ = send(chunk) }); err != nil {
			LogSink("audio: network TX capture from %q failed: %v", DeviceName(micDev), err)
		}
	}()
	m.notify()
	return nil
}

// StopTXAudio stops the TX mic→rig passthrough.
func (m *Manager) StopTXAudio() {
	m.mu.Lock()
	stop := m.txStop
	m.txStop = nil
	m.mu.Unlock()
	if stop != nil {
		close(stop)
		m.notify()
	}
}

// TXAudioActive reports whether the TX mic→rig passthrough is running.
func (m *Manager) TXAudioActive() bool {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.txStop != nil
}

// scalePCM applies a gain to one sample, clamping rather than wrapping — an
// overflow that wraps turns loud speech into a burst of noise on the air.
func scalePCM(s int16, g float64) int16 {
	v := float64(s) * g
	if v > 32767 {
		return 32767
	}
	if v < -32768 {
		return -32768
	}
	return int16(v)
}
