pub use smallvec;

pub mod draw;
pub mod scratch;
pub mod triple_buffer;
pub mod vec;

pub use draw::{DisplaySlot, Draw, DrawContext, Image, ImageId, ProcessContext};
pub use scratch::{Scratch, ScratchPool};
pub use vec::{BiquadState, EnvelopeMode, EnvelopeState, FilterType, OnePoleState, Waveform};

use std::cell::UnsafeCell;

/// A parameter definition for a Sesh audio plugin
///
/// Declares a named parameter with a value range and default. The host reads these via
/// `sesh_params` and provides runtime values as `&[f32]` buffers in `process()`.
#[repr(C)]
pub struct Param {
    /// Null-terminated UTF-8 name, e.g. "Drive", "Mix", "Decay"
    pub name: [u8; 64],
    pub min: f32,
    pub max: f32,
    pub default: f32,
}

impl Param {
    /// Create a new parameter definition.
    ///
    /// # Example
    /// ```rust
    /// use sesh_sdk::Param;
    /// let p = Param::new("Depth", 0.0, 1.0, 0.5);
    /// ```
    pub fn new(name: &str, min: f32, max: f32, default: f32) -> Self {
        let mut p = Param {
            name: [0u8; 64],
            min,
            max,
            default,
        };
        let bytes = name.as_bytes();
        let len = bytes.len().min(63);
        p.name[..len].copy_from_slice(&bytes[..len]);
        p
    }
}

/// Param list returned by `sesh_params`
///
/// Points into the plugin instance's owned `Vec<Param>` -- stable because Vec heap-allocates.
#[repr(C)]
pub struct ParamList {
    pub count: u32,
    pub params: *const Param,
}

/// Trait for Sesh audio plugins
///
/// Implement this trait on your plugin struct, then pass the type to `sesh_plugin!`.
///
/// # Example
///
/// ```rust
/// use sesh_sdk::{sesh_plugin, Plugin, Param, Scratch, ProcessContext};
///
/// struct MyPlugin {
///     scratch: Scratch,
/// }
///
/// impl Plugin for MyPlugin {
///     fn new() -> Self {
///         Self { scratch: Scratch::new() }
///     }
///
///     fn params() -> Vec<Param> {
///         vec![
///             Param::new("Gain", 0.0, 1.0, 0.5),
///         ]
///     }
///
///     fn process(&mut self, ctx: &mut ProcessContext<'_, '_>) {
///         let gain = ctx.params[0]; // &[f32], one value per frame
///         for ch in ctx.channels.iter_mut() {
///             sesh_sdk::vec::vec_mul_assign(*ch, gain);
///         }
///     }
/// }
///
/// sesh_plugin!(MyPlugin);
/// ```
pub trait Plugin {
    fn new() -> Self
    where
        Self: Sized;
    fn params() -> Vec<Param>
    where
        Self: Sized;
    fn process(&mut self, ctx: &mut ProcessContext<'_, '_>);
}

/// Wrapper around a plugin instance and its param definitions
///
/// Fields use `UnsafeCell` so that `sesh_process` (audio thread) and `sesh_draw` (GUI thread)
/// can both hold `&PluginInstance` from the same raw pointer without aliasing `&mut`.
pub struct PluginInstance<T: Plugin> {
    pub plugin: UnsafeCell<T>,
    /// Backing storage for param_list.params pointer. Never read directly.
    #[allow(dead_code)]
    params: Vec<Param>,
    param_list: ParamList,
    pub display: UnsafeCell<Option<Box<dyn DisplaySlot>>>,
}

/// SAFETY: The host ensures `sesh_process` and `sesh_draw` run on their
/// respective threads (audio/GUI). Interior mutability fields are accessed
/// through the SPSC triple buffer protocol or are thread-private.
unsafe impl<T: Plugin> Sync for PluginInstance<T> {}

impl<T: Plugin> PluginInstance<T> {
    pub fn new() -> Self {
        let plugin = T::new();
        let params = T::params();
        let param_list = ParamList {
            count: params.len() as u32,
            params: params.as_ptr(),
        };
        Self {
            plugin: UnsafeCell::new(plugin),
            params,
            param_list,
            display: UnsafeCell::new(None),
        }
    }

    pub fn param_list_ptr(&self) -> *const ParamList {
        &self.param_list
    }
}

/// Result from [`test_impulse_stability`].
pub struct StabilityResult {
    /// Peak absolute amplitude across both channels.
    pub peak: f32,
    /// DC offset detected in the tail (flat signal sitting away from zero).
    pub dc_offset: Option<DcOffset>,
}

/// DC offset detected in one channel's tail.
pub struct DcOffset {
    pub channel: &'static str,
    pub mean: f32,
    pub variance: f32,
}

/// Run a plugin for a given duration with specific parameter values, feeding
/// a unit impulse at the start. Returns peak amplitude and DC offset status.
///
/// ```rust,ignore
/// #[test]
/// fn high_feedback_stability() {
///     let r = sesh_sdk::test_impulse_stability::<MyPlugin>(
///         &[30.0, 100.0, 0.0, 0.0, 100.0, 100.0], 10.0,
///     );
///     assert!(r.peak < 10.0, "blew up: peak {:.2}", r.peak);
///     assert!(r.dc_offset.is_none(), "DC offset detected");
/// }
/// ```
pub fn test_impulse_stability<T: Plugin>(
    param_values: &[f32],
    duration_secs: f32,
) -> StabilityResult {
    let sr = 44100usize;
    let duration = (sr as f32 * duration_secs) as usize;
    let mut left = std::vec![0.0f32; duration];
    let mut right = std::vec![0.0f32; duration];
    left[0] = 1.0;
    right[0] = 1.0;
    run_and_measure::<T>(param_values, &mut left, &mut right)
}

/// Run a plugin for a given duration with specific parameter values, feeding
/// continuous noise at -12 dBFS. Returns peak amplitude and DC offset status.
///
/// Use this to test plugins with feedback paths that may only blow up under
/// sustained input (e.g. shimmer reverbs, resonant filters).
pub fn test_sustained_stability<T: Plugin>(
    param_values: &[f32],
    duration_secs: f32,
) -> StabilityResult {
    let sr = 44100usize;
    let duration = (sr as f32 * duration_secs) as usize;
    let mut left = std::vec![0.0f32; duration];
    let mut right = std::vec![0.0f32; duration];
    let mut seed: u32 = 12345;
    for i in 0..duration {
        seed ^= seed << 13;
        seed ^= seed >> 17;
        seed ^= seed << 5;
        left[i] = (seed as f32 / u32::MAX as f32) * 2.0 - 1.0;
        seed ^= seed << 13;
        seed ^= seed >> 17;
        seed ^= seed << 5;
        right[i] = (seed as f32 / u32::MAX as f32) * 2.0 - 1.0;
    }
    run_and_measure::<T>(param_values, &mut left, &mut right)
}

/// Feed an impulse and measure the RT60 (time for output to drop 60dB below
/// peak). Returns the decay time in seconds, or `None` if the output never
/// reaches -60dB within the duration (infinite sustain or very long tail).
///
/// Useful for verifying that reverbs/delays actually produce meaningful tails
/// at high decay settings.
pub fn test_impulse_rt60<T: Plugin>(param_values: &[f32], duration_secs: f32) -> Option<f32> {
    let sr = 44100usize;
    let duration = (sr as f32 * duration_secs) as usize;
    let mut left = std::vec![0.0f32; duration];
    let mut right = std::vec![0.0f32; duration];
    left[0] = 1.0;
    right[0] = 1.0;

    let chunk = 128usize;
    let mut instance = PluginInstance::<T>::new();
    for start in (0..duration).step_by(chunk) {
        let end = (start + chunk).min(duration);
        let mut channels: [&mut [f32]; 2] = [&mut left[start..end], &mut right[start..end]];
        let chunk_len = end - start;
        let param_bufs: std::vec::Vec<std::vec::Vec<f32>> = param_values
            .iter()
            .map(|&v| std::vec![v; chunk_len])
            .collect();
        let param_refs: std::vec::Vec<&[f32]> = param_bufs.iter().map(|b| b.as_slice()).collect();
        let mut display: Option<Box<dyn DisplaySlot>> = None;
        let mut ctx = ProcessContext::new(&mut channels, &param_refs, &mut display);
        instance.plugin.get_mut().process(&mut ctx);
    }

    // Find peak across both channels
    let peak = left
        .iter()
        .chain(right.iter())
        .fold(0.0f32, |m, s| m.max(s.abs()));

    if peak < 1e-10 {
        return Some(0.0);
    }

    // -60dB below peak
    let threshold = peak * 0.001;

    // Scan in windows of 8192 samples (~186ms) to smooth out momentary dips in the tail
    let window = 8192usize;
    for start in (0..duration).step_by(window) {
        let end = (start + window).min(duration);
        let rms_l: f32 = (left[start..end].iter().map(|s| s * s).sum::<f32>()
            / (end - start) as f32)
            .sqrt();
        let rms_r: f32 = (right[start..end].iter().map(|s| s * s).sum::<f32>()
            / (end - start) as f32)
            .sqrt();
        let rms = rms_l.max(rms_r);
        if rms < threshold {
            return Some(start as f32 / sr as f32);
        }
    }

    None // didn't decay within duration
}

fn run_and_measure<T: Plugin>(
    param_values: &[f32],
    left: &mut [f32],
    right: &mut [f32],
) -> StabilityResult {
    let chunk = 128usize;
    let duration = left.len();
    let mut instance = PluginInstance::<T>::new();

    for start in (0..duration).step_by(chunk) {
        let end = (start + chunk).min(duration);
        let mut channels: [&mut [f32]; 2] = [&mut left[start..end], &mut right[start..end]];
        let chunk_len = end - start;
        let param_bufs: std::vec::Vec<std::vec::Vec<f32>> = param_values
            .iter()
            .map(|&v| std::vec![v; chunk_len])
            .collect();
        let param_refs: std::vec::Vec<&[f32]> = param_bufs.iter().map(|b| b.as_slice()).collect();
        let mut display: Option<Box<dyn DisplaySlot>> = None;
        let mut ctx = ProcessContext::new(&mut channels, &param_refs, &mut display);
        instance.plugin.get_mut().process(&mut ctx);
    }

    let peak = left
        .iter()
        .chain(right.iter())
        .fold(0.0f32, |m, s| m.max(s.abs()));

    let n = 1024usize.min(duration);
    let mut dc_offset = None;
    for (ch, buf) in [("left", &*left), ("right", &*right)] {
        let tail = &buf[buf.len() - n..];
        let mean = tail.iter().sum::<f32>() / n as f32;
        let variance =
            tail.iter().map(|s| (s - mean) * (s - mean)).sum::<f32>() / n as f32;
        if variance < 1e-10 && mean.abs() > 0.001 {
            dc_offset = Some(DcOffset {
                channel: ch,
                mean,
                variance,
            });
            break;
        }
    }

    StabilityResult { peak, dc_offset }
}

/// Define a Sesh audio plugin
///
/// Generates the required WASM exports: `sesh_sdk_version`, `sesh_alloc`, `sesh_free`,
/// `sesh_create`, `sesh_destroy`, `sesh_params`, `sesh_process`, `sesh_draw`, and
/// `sesh_draw_width`. The type must implement the `Plugin` trait.
#[macro_export]
macro_rules! sesh_plugin {
    ($ty:ident) => {
        #[no_mangle]
        pub extern "C" fn sesh_sdk_version() -> u32 {
            4
        }

        #[no_mangle]
        pub extern "C" fn sesh_alloc(size: usize, align: usize) -> *mut u8 {
            let layout =
                ::std::alloc::Layout::from_size_align(size, align).expect("invalid layout");
            unsafe { ::std::alloc::alloc(layout) }
        }

        #[no_mangle]
        pub extern "C" fn sesh_free(ptr: *mut u8, size: usize, align: usize) {
            let layout =
                ::std::alloc::Layout::from_size_align(size, align).expect("invalid layout");
            unsafe { ::std::alloc::dealloc(ptr, layout) }
        }

        #[no_mangle]
        pub extern "C" fn sesh_create() -> *mut u8 {
            let instance = Box::new($crate::PluginInstance::<$ty>::new());
            Box::into_raw(instance) as *mut u8
        }

        #[no_mangle]
        pub extern "C" fn sesh_destroy(instance: *mut u8) {
            if !instance.is_null() {
                unsafe { drop(Box::from_raw(instance as *mut $crate::PluginInstance<$ty>)) };
            }
        }

        #[no_mangle]
        pub extern "C" fn sesh_params(instance: *mut u8) -> *const $crate::ParamList {
            let inst = unsafe { &*(instance as *const $crate::PluginInstance<$ty>) };
            inst.param_list_ptr()
        }

        #[no_mangle]
        pub extern "C" fn sesh_process(
            instance: *mut u8,
            ptr: *mut f32,
            frames: usize,
            channels: usize,
            params_ptr: *const *const f32,
            param_count: usize,
        ) {
            if frames == 0 || channels == 0 {
                return;
            }

            // SAFETY: audio thread only. We access plugin and display through
            // UnsafeCell; the GUI thread never touches plugin, and display
            // access is mediated by the triple buffer's atomic protocol.
            let inst = unsafe { &*(instance as *const $crate::PluginInstance<$ty>) };
            let plugin = unsafe { &mut *inst.plugin.get() };
            let display = unsafe { &mut *inst.display.get() };

            // Reconstruct audio channel slices
            let buffer = unsafe { ::std::slice::from_raw_parts_mut(ptr, frames * channels) };
            let mut chunks: $crate::smallvec::SmallVec<[&mut [f32]; 32]> =
                buffer.chunks_mut(frames).collect();

            // Reconstruct param buffer slices
            let param_ptrs = if param_count > 0 && !params_ptr.is_null() {
                unsafe { ::std::slice::from_raw_parts(params_ptr, param_count) }
            } else {
                &[]
            };
            let param_slices: $crate::smallvec::SmallVec<[&[f32]; 256]> = param_ptrs
                .iter()
                .map(|&p| unsafe { ::std::slice::from_raw_parts(p, frames) })
                .collect();

            {
                let mut ctx = $crate::ProcessContext::new(&mut chunks, &param_slices, display);
                plugin.process(&mut ctx);
            }

            if let Some(ref d) = *display {
                d.publish();
            }
        }

        #[no_mangle]
        pub extern "C" fn sesh_draw(instance: *mut u8) -> i32 {
            // SAFETY: GUI thread only. We access display through UnsafeCell
            // with a shared reference; the audio thread accesses disjoint
            // triple buffer slots via the atomic protocol.
            let inst = unsafe { &*(instance as *const $crate::PluginInstance<$ty>) };
            let display = unsafe { &*inst.display.get() };
            match *display {
                Some(ref d) => {
                    d.draw_gui();
                    1
                }
                None => 0,
            }
        }

        #[no_mangle]
        pub extern "C" fn sesh_draw_width(instance: *mut u8) -> f32 {
            let inst = unsafe { &*(instance as *const $crate::PluginInstance<$ty>) };
            let display = unsafe { &*inst.display.get() };
            match *display {
                Some(ref d) => d.width(),
                None => 0.0,
            }
        }

        #[cfg(test)]
        mod sesh_auto_tests {
            use $crate::Plugin;

            #[test]
            fn impulse_response_safety() {
                let params = <super::$ty as $crate::Plugin>::params();
                let defaults: ::std::vec::Vec<f32> =
                    params.iter().map(|p| p.default).collect();
                let result = $crate::test_impulse_stability::<super::$ty>(&defaults, 10.0);

                assert!(
                    result.peak < 10.0,
                    "impulse response peak {:.1} exceeds 10.0 -- \
                     likely runaway feedback (check feedback gain, \
                     input injection levels, and mixing matrix energy)",
                    result.peak
                );

                if let Some(dc) = result.dc_offset {
                    panic!(
                        "{} channel settled to DC offset: mean {:.6}, \
                         variance {:.2e}. The output has stopped \
                         changing but is not at zero. Check for leaky \
                         integrators, uninitialized state, or missing \
                         DC blocking.",
                        dc.channel, dc.mean, dc.variance
                    );
                }
            }

            #[test]
            fn max_params_stability() {
                let params = <super::$ty as $crate::Plugin>::params();
                let max_values: ::std::vec::Vec<f32> =
                    params.iter().map(|p| p.max).collect();
                let result = $crate::test_impulse_stability::<super::$ty>(&max_values, 10.0);

                assert!(
                    result.peak < 10.0,
                    "impulse at max params: peak {:.1} exceeds 10.0 -- \
                     feedback loop is unstable at maximum parameter values. \
                     Check that feedback gain stays below unity even when \
                     all parameters are at their maximum.",
                    result.peak
                );

                if let Some(dc) = result.dc_offset {
                    panic!(
                        "{} channel DC offset at max params: mean {:.6}, \
                         variance {:.2e}.",
                        dc.channel, dc.mean, dc.variance
                    );
                }
            }
        }
    };
}
