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 time is tuned for psychoacoustic burst masking (Bonello, JAES 2007):
// A ~2ms attack lets initial transient peaks pass through to the hard
// clipper, where they're clipped for <5ms. The human auditory system
// cannot resolve distortion in bursts shorter than ~5ms (burst masking
// provides up to 36 dB of additional masking). This gives higher average
// loudness without audible distortion, compared to instant attack which
// suppresses transients that were already inaudible.
//
// Release is smooth (exponential decay) to avoid audible pumping.
type StereoLimiter struct {
	ceiling       float64
	attackCoeff   float64
	releaseCoeff  float64
	gainReduction float64
}

// NewStereoLimiter creates a stereo-linked limiter.
// attackMs ~2ms for burst-masking benefit (Bonello). 0 = instant (legacy).
// 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
	}

	var attackCoeff float64
	if attackMs > 0 {
		attackSamples := attackMs * sampleRate / 1000
		attackCoeff = 1.0 - math.Exp(-1.0/attackSamples)
	} else {
		attackCoeff = 1.0 // instant: full step in one sample
	}
	releaseSamples := releaseMs * sampleRate / 1000

	return &StereoLimiter{
		ceiling:      ceiling,
		attackCoeff:  attackCoeff,
		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.
//
// With attackMs > 0: transient peaks that exceed the ceiling are NOT
// instantly suppressed. They pass through to the downstream hard clipper,
// which clips them for a few ms until the limiter catches up. These
// sub-5ms clip artifacts are inaudible due to psychoacoustic burst masking.
func (l *StereoLimiter) Process(left, right float64) (float64, float64) {
	peak := math.Max(math.Abs(left), math.Abs(right))

	targetReduction := 0.0
	if peak > l.ceiling {
		targetReduction = 1.0 - l.ceiling/peak
	}

	if targetReduction > l.gainReduction {
		// Attack: smooth ramp toward target (or instant if attackCoeff=1.0)
		l.gainReduction += l.attackCoeff * (targetReduction - l.gainReduction)
	} else {
		// Release: smooth decay back toward zero reduction
		l.gainReduction += l.releaseCoeff * (targetReduction - l.gainReduction)
	}

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

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