//! Chrome/Blink-compatible audio fingerprint for `OfflineAudioContext` tests.
//!
//! Renders an oscillator → DynamicsCompressor → destination pipeline.

/// Audio fingerprint result.
#[derive(Debug, Clone)]
pub struct AudioFingerprint {
    pub sample_rate: u32,
    pub channel_count: u32,
    pub length: u32,
    pub data: Vec<f32>,
}

/// Oscillator wave type.
#[derive(Debug, Clone, Copy)]
pub enum WaveType {
    Sine,
    Triangle,
    Square,
    Sawtooth,
}

/// Audio fingerprint parameters.
#[derive(Debug, Clone)]
pub struct AudioParams {
    pub sample_rate: u32,
    pub length: u32,
    pub frequency: f64,
    pub wave_type: WaveType,
    pub threshold: f64,
    pub knee: f64,
    pub ratio: f64,
    pub attack: f64,
    pub release: f64,
}

impl Default for AudioParams {
    fn default() -> Self {
        Self {
            sample_rate: 44100,
            length: 5000,
            frequency: 10000.0,
            wave_type: WaveType::Triangle,
            threshold: -24.0,
            knee: 30.0,
            ratio: 12.0,
            attack: 0.003,
            release: 0.25,
        }
    }
}

impl AudioFingerprint {
    pub fn from_seed(seed: u64) -> Self {
        Self::from_params(seed, AudioParams::default())
    }

    pub fn default_fingerprint() -> Self {
        Self::from_seed(0)
    }

    pub fn from_params(seed: u64, params: AudioParams) -> Self {
        let sample_rate = params.sample_rate;
        let length = params.length as usize;

        let seed_f = seed as f32;
        let seed_phase_offset =
            ((seed_f * 0.123_456_79).sin() + (seed_f * 0.987_654_3).cos()) * 1.0e-7;
        let seed_gain = 1.0 + ((seed_f * 0.246_813_6).sin()) * 1.0e-6;

        let padded_len = length.div_ceil(N_DIVISION_FRAMES) * N_DIVISION_FRAMES;
        let mut input = vec![0.0f32; padded_len];

        const BLINK_OSCILLATOR_SCALE: f32 = 0.47624;

        let wave = crate::canvas::periodic_wave::PeriodicWave::new(
            match params.wave_type {
                WaveType::Sine => crate::canvas::periodic_wave::StandardWaveType::Sine,
                WaveType::Triangle => crate::canvas::periodic_wave::StandardWaveType::Triangle,
                WaveType::Square => crate::canvas::periodic_wave::StandardWaveType::Square,
                WaveType::Sawtooth => crate::canvas::periodic_wave::StandardWaveType::Sawtooth,
            },
            sample_rate as f32,
        );
        let frequency = params.frequency as f32;
        let phase_increment = frequency / sample_rate as f32;
        let mut phase: f32 = 0.0;
        for slot in input.iter_mut().take(length) {
            let effective_phase = (phase + seed_phase_offset).rem_euclid(1.0);
            *slot = wave.sample(effective_phase, frequency) * BLINK_OSCILLATOR_SCALE * seed_gain;
            phase += phase_increment;
            if phase >= 1.0 {
                phase -= 1.0;
            }
        }

        let mut kernel = DynamicsCompressorKernel::new(sample_rate as f32, 1);
        let mut output = vec![0.0f32; padded_len];
        kernel.process(
            &[&input],
            &mut [&mut output],
            padded_len,
            params.threshold as f32,
            params.knee as f32,
            params.ratio as f32,
            params.attack as f32,
            params.release as f32,
            0.006,
            0.0,
            1.0,
            0.09,
            0.16,
            0.42,
            0.98,
        );
        output.truncate(length);

        Self {
            sample_rate,
            channel_count: 1,
            length: params.length,
            data: output,
        }
    }
}

const MAX_PRE_DELAY_FRAMES: usize = 1024;
const MAX_PRE_DELAY_FRAMES_MASK: usize = MAX_PRE_DELAY_FRAMES - 1;
const DEFAULT_PRE_DELAY_FRAMES: usize = 256;
const METERING_RELEASE_TIME_CONSTANT: f32 = 0.325;
const N_DIVISION_FRAMES: usize = 32;
const K_SPACING_DB: f32 = 5.0;
const SAT_RELEASE_TIME: f32 = 0.0025;
const UNINITIALIZED: f32 = -1.0;

struct DynamicsCompressorKernel {
    sample_rate: f32,
    detector_average: f32,
    compressor_gain: f32,
    metering_release_k: f32,
    metering_gain: f32,
    pre_delay_buffers: Vec<Vec<f32>>,
    pre_delay_read_index: usize,
    pre_delay_write_index: usize,
    last_pre_delay_frames: usize,
    max_attack_compression_diff_db: f32,
    ratio: f32,
    slope: f32,
    linear_threshold: f32,
    db_threshold: f32,
    db_knee: f32,
    knee_threshold: f32,
    knee_threshold_db: f32,
    ykneee_threshold_db: f32,
    k_cached: f32,
}

#[inline]
fn linear_to_decibels(linear: f32) -> f32 {
    20.0 * linear.log10()
}

#[inline]
fn decibels_to_linear(db: f32) -> f32 {
    10.0f32.powf(0.05 * db)
}

#[inline]
fn discrete_time_constant_for_sample_rate(tc: f32, sample_rate: f32) -> f32 {
    1.0 - (-1.0 / (sample_rate * tc)).exp()
}

#[inline]
fn flush_denormal(x: f32) -> f32 {
    if x.abs() < f32::MIN_POSITIVE { 0.0 } else { x }
}

impl DynamicsCompressorKernel {
    fn new(sample_rate: f32, number_of_channels: usize) -> Self {
        let metering_release_k =
            discrete_time_constant_for_sample_rate(METERING_RELEASE_TIME_CONSTANT, sample_rate);
        let mut k = Self {
            sample_rate,
            detector_average: 0.0,
            compressor_gain: 1.0,
            metering_release_k,
            metering_gain: 1.0,
            pre_delay_buffers: (0..number_of_channels)
                .map(|_| vec![0.0; MAX_PRE_DELAY_FRAMES])
                .collect(),
            pre_delay_read_index: 0,
            pre_delay_write_index: DEFAULT_PRE_DELAY_FRAMES,
            last_pre_delay_frames: DEFAULT_PRE_DELAY_FRAMES,
            max_attack_compression_diff_db: -1.0,
            ratio: UNINITIALIZED,
            slope: UNINITIALIZED,
            linear_threshold: UNINITIALIZED,
            db_threshold: UNINITIALIZED,
            db_knee: UNINITIALIZED,
            knee_threshold: UNINITIALIZED,
            knee_threshold_db: UNINITIALIZED,
            ykneee_threshold_db: UNINITIALIZED,
            k_cached: UNINITIALIZED,
        };
        k.reset();
        k
    }

    fn reset(&mut self) {
        self.detector_average = 0.0;
        self.compressor_gain = 1.0;
        self.metering_gain = 1.0;
        for buf in &mut self.pre_delay_buffers {
            for s in &mut *buf {
                *s = 0.0;
            }
        }
        self.pre_delay_read_index = 0;
        self.pre_delay_write_index = DEFAULT_PRE_DELAY_FRAMES;
        self.max_attack_compression_diff_db = -1.0;
    }

    fn set_pre_delay_time(&mut self, pre_delay_time: f32) {
        let mut pre_delay_frames = (pre_delay_time * self.sample_rate) as usize;
        if pre_delay_frames > MAX_PRE_DELAY_FRAMES - 1 {
            pre_delay_frames = MAX_PRE_DELAY_FRAMES - 1;
        }
        if self.last_pre_delay_frames != pre_delay_frames {
            self.last_pre_delay_frames = pre_delay_frames;
            for buf in &mut self.pre_delay_buffers {
                for s in &mut *buf {
                    *s = 0.0;
                }
            }
            self.pre_delay_read_index = 0;
            self.pre_delay_write_index = pre_delay_frames;
        }
    }

    fn knee_curve(&self, x: f32, k: f32) -> f32 {
        if x < self.linear_threshold {
            return x;
        }
        self.linear_threshold + (1.0 - (-k * (x - self.linear_threshold)).exp()) / k
    }

    fn saturate(&self, x: f32, k: f32) -> f32 {
        if x < self.knee_threshold {
            self.knee_curve(x, k)
        } else {
            let x_db = linear_to_decibels(x);
            let y_db = self.ykneee_threshold_db + self.slope * (x_db - self.knee_threshold_db);
            decibels_to_linear(y_db)
        }
    }

    fn slope_at(&self, x: f32, k: f32) -> f32 {
        if x < self.linear_threshold {
            return 1.0;
        }
        let x2 = x * 1.001;
        let x_db = linear_to_decibels(x);
        let x2_db = linear_to_decibels(x2);
        let y_db = linear_to_decibels(self.knee_curve(x, k));
        let y2_db = linear_to_decibels(self.knee_curve(x2, k));
        (y2_db - y_db) / (x2_db - x_db)
    }

    fn k_at_slope(&self, desired_slope: f32) -> f32 {
        let x_db = self.db_threshold + self.db_knee;
        let x = decibels_to_linear(x_db);

        let mut min_k = 0.1_f32;
        let mut max_k = 10_000.0_f32;
        let mut k = 5.0_f32;

        for _ in 0..15 {
            let slope = self.slope_at(x, k);
            if slope < desired_slope {
                max_k = k;
            } else {
                min_k = k;
            }
            k = (min_k * max_k).sqrt();
        }
        k
    }

    fn update_static_curve_parameters(
        &mut self,
        db_threshold: f32,
        db_knee: f32,
        ratio: f32,
    ) -> f32 {
        if db_threshold != self.db_threshold || db_knee != self.db_knee || ratio != self.ratio {
            self.db_threshold = db_threshold;
            self.linear_threshold = decibels_to_linear(db_threshold);
            self.db_knee = db_knee;
            self.ratio = ratio;
            self.slope = 1.0 / ratio;

            let k = self.k_at_slope(1.0 / ratio);

            self.knee_threshold_db = db_threshold + db_knee;
            self.knee_threshold = decibels_to_linear(self.knee_threshold_db);
            self.ykneee_threshold_db = linear_to_decibels(self.knee_curve(self.knee_threshold, k));
            self.k_cached = k;
        }
        self.k_cached
    }

    #[allow(clippy::needless_range_loop)]
    fn process(
        &mut self,
        source_channels: &[&[f32]],
        destination_channels: &mut [&mut [f32]],
        frames_to_process: usize,
        db_threshold: f32,
        db_knee: f32,
        ratio: f32,
        attack_time: f32,
        release_time: f32,
        pre_delay_time: f32,
        db_post_gain: f32,
        effect_blend: f32,
        release_zone1: f32,
        release_zone2: f32,
        release_zone3: f32,
        release_zone4: f32,
    ) {
        let sample_rate = self.sample_rate;

        let dry_mix = 1.0 - effect_blend;
        let wet_mix = effect_blend;

        let k = self.update_static_curve_parameters(db_threshold, db_knee, ratio);

        let full_range_gain = self.saturate(1.0, k);
        let mut full_range_makeup_gain = 1.0 / full_range_gain;

        let exponent = if db_threshold <= -24.0 {
            0.6 + 0.0739 * ((-db_threshold - 24.0) / 26.0)
        } else {
            0.6
        };
        full_range_makeup_gain = full_range_makeup_gain.powf(exponent);
        let master_linear_gain = decibels_to_linear(db_post_gain) * full_range_makeup_gain;

        let attack_time = attack_time.max(0.001);
        let attack_frames = attack_time * sample_rate;

        let release_frames = sample_rate * release_time;
        let sat_release_frames = SAT_RELEASE_TIME * sample_rate;

        let y1 = release_frames * release_zone1;
        let y2 = release_frames * release_zone2;
        let y3 = release_frames * release_zone3;
        let y4 = release_frames * release_zone4;

        let ka = 0.999_999_999_999_999_8 * y1 + 1.843_221_9e-16 * y2 - 1.937_339_4e-16 * y3
            + 8.824_516e-18 * y4;
        let kb = -1.578_832 * y1 + 2.330_583_8 * y2 - 0.914_119_4 * y3 + 0.162_367_75 * y4;
        let kc = 0.533_414_3 * y1 - 1.272_736_8 * y2 + 0.925_885_6 * y3 - 0.186_563_1 * y4;
        let kd = 0.087_834_634 * y1 - 0.169_416_3 * y2 + 0.085_880_58 * y3 - 0.004_298_914 * y4;
        let ke = -0.042_416_88 * y1 + 0.111_569_38 * y2 - 0.097_646_765 * y3 + 0.028_494_263 * y4;

        self.set_pre_delay_time(pre_delay_time);

        let n_divisions = frames_to_process / N_DIVISION_FRAMES;

        let mut frame_index: usize = 0;
        for _ in 0..n_divisions {
            if self.detector_average.is_nan() {
                self.detector_average = 1.0;
            }
            if self.detector_average.is_infinite() {
                self.detector_average = 1.0;
            }

            let desired_gain = self.detector_average;
            let scaled_desired_gain = desired_gain.asin() / (0.5 * std::f32::consts::PI);

            let is_releasing = scaled_desired_gain > self.compressor_gain;

            let mut compression_diff_db =
                linear_to_decibels(self.compressor_gain / scaled_desired_gain);

            let envelope_rate;
            if is_releasing {
                self.max_attack_compression_diff_db = -1.0;

                if compression_diff_db.is_nan() {
                    compression_diff_db = -1.0;
                }
                if compression_diff_db.is_infinite() {
                    compression_diff_db = -1.0;
                }

                let mut x = compression_diff_db;
                x = x.max(-12.0);
                x = x.min(0.0);
                x = 0.25 * (x + 12.0);

                let x2 = x * x;
                let x3 = x2 * x;
                let x4 = x2 * x2;
                let release_frames_adaptive = ka + kb * x + kc * x2 + kd * x3 + ke * x4;

                let db_per_frame = K_SPACING_DB / release_frames_adaptive;
                envelope_rate = decibels_to_linear(db_per_frame);
            } else {
                if compression_diff_db.is_nan() {
                    compression_diff_db = 1.0;
                }
                if compression_diff_db.is_infinite() {
                    compression_diff_db = 1.0;
                }

                if self.max_attack_compression_diff_db == -1.0
                    || self.max_attack_compression_diff_db < compression_diff_db
                {
                    self.max_attack_compression_diff_db = compression_diff_db;
                }

                let eff_atten_diff_db = self.max_attack_compression_diff_db.max(0.5);

                let x = 0.25 / eff_atten_diff_db;
                envelope_rate = 1.0 - x.powf(1.0 / attack_frames);
            }

            let mut pre_delay_read_index = self.pre_delay_read_index;
            let mut pre_delay_write_index = self.pre_delay_write_index;
            let mut detector_average = self.detector_average;
            let mut compressor_gain = self.compressor_gain;

            for _ in 0..N_DIVISION_FRAMES {
                let mut compressor_input: f32 = 0.0;

                for ch in 0..source_channels.len() {
                    let undelayed_source = source_channels[ch][frame_index];
                    self.pre_delay_buffers[ch][pre_delay_write_index] = undelayed_source;
                    let abs_undelayed = undelayed_source.abs();
                    if compressor_input < abs_undelayed {
                        compressor_input = abs_undelayed;
                    }
                }

                let scaled_input = compressor_input;
                let abs_input = scaled_input.abs();

                let shaped_input = self.saturate(abs_input, k);

                let attenuation = if abs_input <= 0.0001 {
                    1.0
                } else {
                    shaped_input / abs_input
                };

                let mut attenuation_db = -linear_to_decibels(attenuation);
                attenuation_db = attenuation_db.max(2.0);

                let db_per_frame_inner = attenuation_db / sat_release_frames;
                let sat_release_rate = decibels_to_linear(db_per_frame_inner) - 1.0;

                let is_release = attenuation > detector_average;
                let rate = if is_release { sat_release_rate } else { 1.0 };

                detector_average += (attenuation - detector_average) * rate;
                detector_average = detector_average.min(1.0);

                if detector_average.is_nan() {
                    detector_average = 1.0;
                }
                if detector_average.is_infinite() {
                    detector_average = 1.0;
                }

                if envelope_rate < 1.0 {
                    compressor_gain += (scaled_desired_gain - compressor_gain) * envelope_rate;
                } else {
                    compressor_gain *= envelope_rate;
                    compressor_gain = compressor_gain.min(1.0);
                }

                let post_warp_compressor_gain =
                    (0.5 * std::f32::consts::PI * compressor_gain).sin();

                let total_gain = dry_mix + wet_mix * master_linear_gain * post_warp_compressor_gain;

                let db_real_gain = 20.0 * post_warp_compressor_gain.log10();
                if db_real_gain < self.metering_gain {
                    self.metering_gain = db_real_gain;
                } else {
                    self.metering_gain +=
                        (db_real_gain - self.metering_gain) * self.metering_release_k;
                }

                for ch in 0..destination_channels.len() {
                    destination_channels[ch][frame_index] =
                        self.pre_delay_buffers[ch][pre_delay_read_index] * total_gain;
                }

                frame_index += 1;
                pre_delay_read_index = (pre_delay_read_index + 1) & MAX_PRE_DELAY_FRAMES_MASK;
                pre_delay_write_index = (pre_delay_write_index + 1) & MAX_PRE_DELAY_FRAMES_MASK;
            }

            self.pre_delay_read_index = pre_delay_read_index;
            self.pre_delay_write_index = pre_delay_write_index;
            self.detector_average = flush_denormal(detector_average);
            self.compressor_gain = flush_denormal(compressor_gain);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deterministic_output() {
        let a = AudioFingerprint::from_seed(42);
        let b = AudioFingerprint::from_seed(42);
        assert_eq!(a.data, b.data);
    }

    #[test]
    fn different_seeds_different_output() {
        let a = AudioFingerprint::from_seed(0);
        let b = AudioFingerprint::from_seed(1);
        let diffs = a
            .data
            .iter()
            .zip(b.data.iter())
            .filter(|(a, b)| (**a - **b).abs() > 1e-10)
            .count();
        assert!(diffs > 0);
    }

    #[test]
    fn correct_length() {
        let fp = AudioFingerprint::default_fingerprint();
        assert_eq!(fp.data.len(), 5000);
        assert_eq!(fp.sample_rate, 44100);
        assert_eq!(fp.channel_count, 1);
    }

    #[test]
    fn samples_in_range() {
        let fp = AudioFingerprint::default_fingerprint();
        for &s in &fp.data {
            assert!(s.abs() <= 1.5);
        }
    }
}
