package dsp

import "math"

// BS412Limiter implements ITU-R BS.412 MPX power limiting.
// Measures the rolling 60-second average power of the composite signal
// and reduces audio gain when the power exceeds the threshold.
//
// The threshold is specified in dBr where 0 dBr is the reference power
// of a fully modulated mono signal (composite peak = 1.0, power = 0.5).
//
// Pilot and RDS power are accounted for: the audio power budget is
// reduced by their constant contribution so the total stays within limits.
type BS412Limiter struct {
	enabled       bool
	thresholdPow  float64 // linear power threshold for total MPX
	audioBudget   float64 // = thresholdPow - pilotPow - rdsPow

	// Rolling 60-second power integrator
	powerBuf  []float64 // per-chunk average power values
	bufIdx    int
	bufFull   bool   // true once the buffer has wrapped at least once
	powerSum  float64

	// Slow gain controller
	gain          float64 // current output gain (0..1)
	attackCoeff   float64 // gain reduction speed
	releaseCoeff  float64 // gain recovery speed
}

// NewBS412Limiter creates a BS.412 MPX power limiter.
//
// Parameters:
//   - thresholdDBr: power limit in dBr (0 = standard, +3 = relaxed)
//   - pilotLevel: pilot amplitude in composite (e.g. 0.09)
//   - rdsInjection: RDS amplitude in composite (e.g. 0.04)
//   - chunkDurationSec: duration of each processing chunk (e.g. 0.05 for 50ms)
func NewBS412Limiter(thresholdDBr, pilotLevel, rdsInjection, chunkDurationSec float64) *BS412Limiter {
	// Reference power: 0 dBr = power of mono sine at peak=1.0 = 0.5
	refPower := 0.5
	thresholdPow := refPower * math.Pow(10, thresholdDBr/10)

	// Constant power contributions from pilot and RDS
	pilotPow := pilotLevel * pilotLevel / 2   // sine wave RMS²
	rdsPow := rdsInjection * rdsInjection / 4 // BPSK has ~half the power of a sine

	audioBudget := thresholdPow - pilotPow - rdsPow
	if audioBudget < 0.01 {
		audioBudget = 0.01
	}

	// 60-second window in chunks
	windowSec := 60.0
	bufLen := int(math.Ceil(windowSec / chunkDurationSec))
	if bufLen < 10 {
		bufLen = 10
	}

	// Attack: ~2 seconds (slow, avoids pumping)
	// Release: ~5 seconds (very slow, smooth recovery)
	attackTC := 2.0 / chunkDurationSec   // time constant in chunks
	releaseTC := 5.0 / chunkDurationSec

	return &BS412Limiter{
		enabled:      true,
		thresholdPow: thresholdPow,
		audioBudget:  audioBudget,
		powerBuf:     make([]float64, bufLen),
		gain:         1.0,
		attackCoeff:  1.0 - math.Exp(-1.0/attackTC),
		releaseCoeff: 1.0 - math.Exp(-1.0/releaseTC),
	}
}

// UpdateChunkDuration reconfigures the limiter for a new chunk size.
// Call this from GenerateFrame when the actual chunk duration is known
// (computed as samples/sampleRate) to avoid calibration errors if the
// engine's chunk duration differs from the value passed to NewBS412Limiter.
// Safe to call on every chunk; no-ops when duration has not changed.
func (l *BS412Limiter) UpdateChunkDuration(chunkSec float64) {
	if chunkSec <= 0 {
		return
	}
	windowSec := 60.0
	newBufLen := int(math.Ceil(windowSec / chunkSec))
	if newBufLen < 10 {
		newBufLen = 10
	}
	if newBufLen == len(l.powerBuf) {
		return // no change
	}
	// Resize buffer — drop history to avoid stale power readings from the
	// old window size distorting the rolling average.
	l.powerBuf = make([]float64, newBufLen)
	l.bufIdx = 0
	l.bufFull = false
	l.powerSum = 0
	attackTC := 2.0 / chunkSec
	releaseTC := 5.0 / chunkSec
	l.attackCoeff = 1.0 - math.Exp(-1.0/attackTC)
	l.releaseCoeff = 1.0 - math.Exp(-1.0/releaseTC)
}

// ProcessChunk measures the audio power of a chunk and returns the
// gain factor to apply to the audio composite for BS.412 compliance.
// Call once per chunk with the average audio power of that chunk.
//
// audioPower = (1/N) × Σ sample²  over the chunk's audio composite samples.
func (l *BS412Limiter) ProcessChunk(audioPower float64) float64 {
	if !l.enabled {
		return 1.0
	}

	// Update rolling 60-second power average
	old := l.powerBuf[l.bufIdx]
	l.powerBuf[l.bufIdx] = audioPower
	l.powerSum += audioPower - old
	// BUG-G fix: float64 accumulation over 1200+ chunks can drift slightly
	// negative due to rounding. A negative powerSum → negative avgPower →
	// math.Sqrt of negative → NaN → gain becomes NaN, silently disabling
	// the limiter. Clamp to zero to keep the invariant powerSum >= 0.
	if l.powerSum < 0 {
		l.powerSum = 0
	}
	l.bufIdx++
	if l.bufIdx >= len(l.powerBuf) {
		l.bufIdx = 0
		l.bufFull = true
	}

	// Calculate average power over the window
	var count int
	if l.bufFull {
		count = len(l.powerBuf)
	} else {
		count = l.bufIdx
	}
	if count < 1 {
		return 1.0
	}
	avgPower := l.powerSum / float64(count)

	// Target gain: bring average audio power to budget
	targetGain := 1.0
	if avgPower > l.audioBudget && avgPower > 0 {
		targetGain = math.Sqrt(l.audioBudget / avgPower)
	}

	// Smooth gain changes (slow attack, slower release)
	if targetGain < l.gain {
		l.gain += l.attackCoeff * (targetGain - l.gain)
	} else {
		l.gain += l.releaseCoeff * (targetGain - l.gain)
	}

	// Clamp
	if l.gain < 0.01 {
		l.gain = 0.01
	}
	if l.gain > 1.0 {
		l.gain = 1.0
	}

	return l.gain
}

// CurrentGain returns the current gain factor (0..1).
// Called at the start of each chunk to get the gain to apply.
func (l *BS412Limiter) CurrentGain() float64 {
	return l.gain
}

// CurrentGainDB returns the current gain reduction in dB (negative = reducing).
func (l *BS412Limiter) CurrentGainDB() float64 {
	if l.gain <= 0 {
		return -100
	}
	return 20 * math.Log10(l.gain)
}

// Reset clears the power history and restores unity gain.
func (l *BS412Limiter) Reset() {
	for i := range l.powerBuf {
		l.powerBuf[i] = 0
	}
	l.bufIdx = 0
	l.bufFull = false
	l.powerSum = 0
	l.gain = 1.0
}
