// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright © 2024 Mike Robeson [dijksterhuis]

//! Minimal signal processing module for making useful changes to an
//! [`AudioFile`]

use super::{AudioErrors, AudioFile, R};
use log::{trace, warn};
use thiserror::Error;

/// Errors when applying DSP to [`AudioFile`]s.
#[derive(Debug, Error)]
pub enum DspErrors {
    #[error("invalid percentage parameter, must be between 0.0 and 1.0: value={value}")]
    FadePercentageOOB { value: f32 },
}

/// Normalize an [`AudioFile`]'s samples between 0 and 1
pub fn normalize(audio: &mut AudioFile) -> R<()> {
    trace!("Normalizing audio.");
    let max_abs = audio
        .samples
        .iter()
        .map(|x| f32::abs(*x))
        .reduce(f32::max)
        .unwrap_or(1.0_f32);

    let normd = audio
        .samples
        .iter()
        .map(|x| *x * (1.0 / max_abs))
        .collect::<Vec<f32>>();

    audio.samples = normd;
    Ok(())
}

// really hacky and simple time stretching
/// Time stretch an [`AudioFile`] by resampling, inserts duplicate samples to
/// mimic slowing the audio down and removes samples to mimic speeding it up.
/// WARNING: Speeding up via resampling is a lossy procedure.
pub fn timestrech_resampling(audio: &mut AudioFile, stretch: i8) -> R<()> {
    trace!("Resampling WAV file with time stretch factor: {stretch}");
    let mut resampled: Vec<f32> = vec![];

    let n_channels = audio.spec.channels as usize;

    // extending to mimic slowing own the audio
    if stretch < 0 {
        // audio samples are interleaved by channels
        for chn_sample in audio.samples.chunks(n_channels) {
            for _ in 0..=stretch.abs() {
                for x in chn_sample {
                    resampled.push(*x);
                }
            }
        }
    }

    // remove entries to mimic speed up
    // WARNING: this is lossy
    if stretch > 0 {
        warn!("Timestretch resampling with positive factors is a lossy operation!");
        for (idx, chn_sample) in audio.samples.chunks(n_channels).enumerate() {
            if idx.rem_euclid((stretch + 1) as usize) == 0 {
                for x in chn_sample {
                    resampled.push(*x);
                }
            }
        }
    }

    audio.len = resampled.len() as u32;
    audio.samples = resampled;
    Ok(())
}

/// Linear fade in for an [`AudioFile`]
///
/// ```text
/// x_{n} = x_n.k_n
/// forall n < K, n >= 0
/// where k_n = k_{n-1} + 1/K; k_0 = 0
/// ```
pub fn fade_in_linear(audio: &mut AudioFile, percent: f32) -> R<()> {
    // floats have no concept of a 'step' in ranges
    #[allow(clippy::manual_range_contains)]
    if percent > 1.0 || percent < 0.0 {
        return Err(AudioErrors::Dsp(DspErrors::FadePercentageOOB { value: percent }).into());
    }
    trace!("Creating linear fade in for WAV file");

    let mut buf: Vec<f32> = vec![];
    let n_samples = audio.samples.len() as f32;
    let n_channels = audio.spec.channels as f32;

    // length of fade out in samples
    // note: interleaved so need to divide by n channels
    let n_fade_samples = ((n_samples / n_channels) * percent) as usize;

    // start amplitude factor
    let mut k = 0.0;

    // difference in fade amount per sample step
    let d = 1.0 / n_fade_samples as f32;

    for (idx, chan_samples) in audio.samples.chunks(n_channels as usize).enumerate() {
        // if at the relevant % start of the sample, start applying fade in
        if idx <= n_fade_samples {
            for x in chan_samples {
                buf.push(*x * k);
            }
            k += d;
        } else {
            for x in chan_samples {
                buf.push(*x * k);
            }
        };
    }

    audio.samples = buf;
    Ok(())
}

/// Linear fade out for an [`AudioFile`]
///
/// ```text
/// x_{n} = x_n.k_n
/// forall n > N - K, n >= 0
/// where k_n = k_{n-1} - 1/K; k_0 = 1
/// ```
pub fn fade_out_linear(audio: &mut AudioFile, percent: f32) -> R<()> {
    // floats have no concept of a 'step' in ranges
    #[allow(clippy::manual_range_contains)]
    if percent > 1.0 || percent < 0.0 {
        return Err(AudioErrors::Dsp(DspErrors::FadePercentageOOB { value: percent }).into());
    }
    trace!("Creating linear fade out for WAV file");

    let mut buf: Vec<f32> = vec![];
    let n_samps = audio.samples.len();
    let n_chans = audio.spec.channels as usize;

    // deinterleaved sample length
    let n_deint = n_samps / audio.spec.channels as usize;

    // length of fade out in deinterleaved samples
    let n_fade_deint = (n_deint as f32 * percent) as usize;

    // start amplitude factor
    let mut k = 1.0;

    // difference in fade amount per sample step
    let d = 1.0 / n_fade_deint as f32;

    for (idx, chan_samples) in audio.samples.chunks(n_chans).enumerate() {
        // if at the relevant % end of the sample, start applying fade out
        if idx >= n_deint - n_fade_deint {
            for x in chan_samples {
                buf.push(*x * k);
            }
            k -= d;
        } else {
            for x in chan_samples {
                buf.push(*x);
            }
        };
    }

    audio.samples = buf;
    Ok(())
}

/// Naive upmix of mono signal to an interleaved stereo signal
/// (duplicate channels, attenuate by 0.5)
// TODO: test
pub fn mono_to_stereo_interleaved(audio: &mut AudioFile) -> R<()> {
    let mut buf: Vec<f32> = vec![];
    for sample in &audio.samples {
        buf.push(sample * 0.5);
        buf.push(sample * 0.5);
    }
    audio.len = buf.len() as u32;
    audio.samples = buf;
    audio.spec.channels = 2;
    Ok(())
}

// todo: Nx channel to Nx channel interleaving (up and down)
