package dsp

import "math"

// StereoLimiter applies identical gain reduction to L and R channels,
// driven by the peak of max(|L|, |R|). This preserves the stereo image
// while preventing either channel from exceeding the ceiling.
//
// Attack is INSTANTANEOUS — gain is reduced in the same sample that
// exceeds the ceiling. This avoids overshoot entirely, which is critical
// because overshoot causes composite clipping that destroys pilot/RDS.
// Unlike hard clipping, gain scaling preserves the waveform shape and
// does not create harmonics.
//
// Release is smooth (exponential decay) to avoid audible pumping.
type StereoLimiter struct {
	ceiling       float64
	releaseCoeff  float64
	gainReduction float64
}

// NewStereoLimiter creates a stereo-linked limiter with instant attack.
// releaseMs controls how quickly gain recovers after a peak (typ. 50-200ms).
func NewStereoLimiter(ceiling, attackMs, releaseMs, sampleRate float64) *StereoLimiter {
	if ceiling <= 0 {
		ceiling = 1.0
	}
	if releaseMs <= 0 {
		releaseMs = 100
	}
	releaseSamples := releaseMs * sampleRate / 1000

	return &StereoLimiter{
		ceiling:      ceiling,
		releaseCoeff: 1.0 - math.Exp(-1.0/releaseSamples),
	}
}

// Process applies stereo-linked limiting. Both channels receive the
// same gain factor, determined by the louder of the two.
//
// If the peak exceeds ceiling, gain is INSTANTLY reduced (zero overshoot).
// When the signal drops below ceiling, gain recovers smoothly via release.
func (l *StereoLimiter) Process(left, right float64) (float64, float64) {
	peak := math.Max(math.Abs(left), math.Abs(right))

	// Target: how much gain reduction do we need right now?
	targetReduction := 0.0
	if peak > l.ceiling {
		targetReduction = 1.0 - l.ceiling/peak
	}

	// Instant attack: if we need MORE reduction, apply it NOW.
	// Smooth release: if we need LESS reduction, decay slowly.
	if targetReduction > l.gainReduction {
		l.gainReduction = targetReduction // instant
	} else {
		l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction) // smooth
	}

	gain := 1.0 - l.gainReduction
	return left * gain, right * gain
}

// Reset clears the limiter state.
func (l *StereoLimiter) Reset() {
	l.gainReduction = 0
}
