pub use smallvec;

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

pub use draw::{DisplaySlot, Draw, DrawContext, 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
    }
}

/// 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() {
                const SR: usize = 44100;
                const DURATION: usize = SR * 10;
                const CHUNK: usize = 128;

                let param_defs = <super::$ty as $crate::Plugin>::params();
                let mut instance = $crate::PluginInstance::<super::$ty>::new();

                let mut left = vec![0.0f32; DURATION];
                let mut right = vec![0.0f32; DURATION];
                left[0] = 1.0;
                right[0] = 1.0;

                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_defs
                        .iter()
                        .map(|p| vec![p.default; chunk_len])
                        .collect();
                    let param_refs: ::std::vec::Vec<&[f32]> =
                        param_bufs.iter().map(|b| b.as_slice()).collect();
                    let mut display: ::std::option::Option<
                        ::std::boxed::Box<dyn $crate::DisplaySlot>,
                    > = None;
                    let mut ctx =
                        $crate::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()));

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

                // Check for DC offset: detect output that has settled to a
                // constant non-zero value. A reverb tail still decaying is
                // fine; the problem is when the signal stops moving entirely
                // while sitting away from zero (leaky integrator, stuck
                // state, missing DC blocker).
                //
                // Method: look at the last 1024 samples. If the variance is
                // near zero (signal is flat) but the mean is not (it's flat
                // at a non-zero value), that's a DC offset.
                let n = 1024usize;
                let tail_l = &left[DURATION - n..];
                let tail_r = &right[DURATION - n..];
                for (ch, tail) in [("left", tail_l), ("right", tail_r)] {
                    let mean = tail.iter().sum::<f32>() / n as f32;
                    let variance =
                        tail.iter().map(|s| (s - mean) * (s - mean)).sum::<f32>() / n as f32;
                    let is_flat = variance < 1e-10;
                    let is_offset = mean.abs() > 0.001;
                    assert!(
                        !(is_flat && is_offset),
                        "{ch} channel settled to DC offset: mean {mean:.6}, \
                         variance {variance:.2e}. The output has stopped \
                         changing but is not at zero. Check for leaky \
                         integrators, uninitialized state, or missing \
                         DC blocking.",
                    );
                }
            }
        }
    };
}
