//! Generic VST3 processor wrapper.
//!
//! This module provides [`Vst3Processor`], a generic wrapper that bridges any
//! [`beamer_core::Plugin`] implementation to VST3 COM interfaces.
//!
//! # Architecture
//!
//! Uses the **combined component** pattern where processor and controller are
//! implemented by the same object. This is the modern approach used by most
//! audio plugin frameworks.
//!
//! ```text
//! User Plugin (implements Plugin trait)
//!        ↓
//! Vst3Processor<P> (this wrapper)
//!        ↓
//! VST3 COM interfaces (IComponent, IAudioProcessor, IEditController)
//! ```

use std::cell::UnsafeCell;
use std::ffi::{c_char, c_void};
use std::marker::PhantomData;
use std::slice;

use log::warn;
use vst3::{Class, ComRef, Steinberg::Vst::*, Steinberg::*};

use beamer_core::{
    AuxiliaryBuffers, Buffer, BusType as CoreBusType, ChordInfo,
    FrameRate as CoreFrameRate, MidiBuffer, MidiEvent, MidiEventKind, NoteExpressionInt,
    NoteExpressionText, NoteExpressionValue as CoreNoteExpressionValue, Parameters, Plugin,
    ProcessContext as CoreProcessContext, ScaleInfo, SysEx, Transport,
    MAX_BUSES, MAX_CHANNELS,
    MAX_CHORD_NAME_SIZE, MAX_EXPRESSION_TEXT_SIZE, MAX_SCALE_NAME_SIZE, MAX_SYSEX_SIZE,
};

use crate::factory::ComponentFactory;
use crate::util::{copy_wstring, len_wstring};
use crate::wrapper::PluginConfig;

// VST3 event type constants
const K_NOTE_ON_EVENT: u16 = 0;
const K_NOTE_OFF_EVENT: u16 = 1;
const K_DATA_EVENT: u16 = 2;
const K_POLY_PRESSURE_EVENT: u16 = 3;
const K_NOTE_EXPRESSION_VALUE_EVENT: u16 = 4;
const K_NOTE_EXPRESSION_TEXT_EVENT: u16 = 5;
const K_CHORD_EVENT: u16 = 6;
const K_SCALE_EVENT: u16 = 7;
const K_NOTE_EXPRESSION_INT_VALUE_EVENT: u16 = 8;
const K_LEGACY_MIDI_CC_OUT_EVENT: u16 = 65535;

// LegacyMIDICCOutEvent controlNumber special values
const LEGACY_CC_CHANNEL_PRESSURE: u8 = 128;
const LEGACY_CC_PITCH_BEND: u8 = 129;
const LEGACY_CC_PROGRAM_CHANGE: u8 = 130;

// DataEvent type for SysEx
const DATA_TYPE_MIDI_SYSEX: u32 = 0;

// =============================================================================
// SysEx Output Buffer Pool
// =============================================================================

/// Pool of buffers for SysEx output events.
///
/// VST3's DataEvent requires a pointer to data that must remain valid until
/// the host processes the event. This pool provides stable storage for SysEx
/// data during each process() call.
///
/// The pool is pre-allocated at construction time based on plugin configuration,
/// ensuring no heap allocations occur during audio processing (unless the
/// `sysex-heap-fallback` feature is enabled and the pool overflows).
struct SysExOutputPool {
    /// Pre-allocated buffer slots for SysEx data (Vec of Vecs, but fixed capacity)
    buffers: Vec<Vec<u8>>,
    /// Length of valid data in each slot
    lengths: Vec<usize>,
    /// Maximum number of slots
    max_slots: usize,
    /// Maximum buffer size per slot
    max_buffer_size: usize,
    /// Next available slot index
    next_slot: usize,
    /// Set to true when an allocation fails due to pool exhaustion
    overflowed: bool,
    /// Heap-backed fallback buffer for overflow (only when feature enabled).
    /// Messages stored here are emitted at the start of the next process block.
    #[cfg(feature = "sysex-heap-fallback")]
    fallback: Vec<Vec<u8>>,
}

impl SysExOutputPool {
    /// Create a new pool with the specified capacity.
    ///
    /// Pre-allocates all buffers to avoid heap allocation during process().
    fn with_capacity(slots: usize, buffer_size: usize) -> Self {
        let mut buffers = Vec::with_capacity(slots);
        for _ in 0..slots {
            let buf = vec![0u8; buffer_size];
            buffers.push(buf);
        }
        let lengths = vec![0usize; slots];

        Self {
            buffers,
            lengths,
            max_slots: slots,
            max_buffer_size: buffer_size,
            next_slot: 0,
            overflowed: false,
            #[cfg(feature = "sysex-heap-fallback")]
            fallback: Vec::new(),
        }
    }

    /// Clear the pool for reuse.
    ///
    /// Note: This does NOT clear the fallback buffer, which is drained separately
    /// at the start of the next process block.
    #[inline]
    fn clear(&mut self) {
        self.next_slot = 0;
        self.overflowed = false;
    }

    /// Returns true if any SysEx allocation failed since the last clear.
    #[inline]
    fn has_overflowed(&self) -> bool {
        self.overflowed
    }

    /// Returns the maximum number of slots in this pool.
    #[inline]
    fn capacity(&self) -> usize {
        self.max_slots
    }

    /// Returns true if there are pending fallback messages from a previous overflow.
    #[cfg(feature = "sysex-heap-fallback")]
    #[inline]
    fn has_fallback(&self) -> bool {
        !self.fallback.is_empty()
    }

    /// Take all pending fallback messages, leaving the fallback buffer empty.
    ///
    /// These messages should be emitted at the start of the current process block.
    #[cfg(feature = "sysex-heap-fallback")]
    #[inline]
    fn take_fallback(&mut self) -> Vec<Vec<u8>> {
        std::mem::take(&mut self.fallback)
    }

    /// Allocate a slot and copy SysEx data into it.
    ///
    /// Returns a pointer to the data and its length, or None if the pool is full.
    /// Sets the overflow flag when the pool is exhausted.
    ///
    /// With `sysex-heap-fallback` feature: overflow messages are stored in a
    /// heap-backed fallback buffer instead of being dropped.
    fn allocate(&mut self, data: &[u8]) -> Option<(*const u8, usize)> {
        if self.next_slot >= self.max_slots {
            self.overflowed = true;

            // With heap fallback enabled, store overflow in fallback buffer
            #[cfg(feature = "sysex-heap-fallback")]
            {
                let copy_len = data.len().min(self.max_buffer_size);
                self.fallback.push(data[..copy_len].to_vec());
            }

            return None;
        }

        let slot = self.next_slot;
        self.next_slot += 1;

        let copy_len = data.len().min(self.max_buffer_size);
        self.buffers[slot][..copy_len].copy_from_slice(&data[..copy_len]);
        self.lengths[slot] = copy_len;

        Some((self.buffers[slot].as_ptr(), copy_len))
    }
}

// =============================================================================
// Transport Extraction
// =============================================================================

/// Helper macro for extracting optional fields based on validity flags.
/// Reduces repetitive `if state & FLAG != 0 { Some(value) } else { None }` patterns.
macro_rules! valid_if {
    ($state:expr, $flag:expr, $value:expr) => {
        if $state & $flag != 0 {
            Some($value)
        } else {
            None
        }
    };
}

/// Extract transport information from VST3 ProcessContext.
///
/// Converts VST3's validity flags to Rust's Option<T> idiom.
/// Returns a default Transport if the context pointer is null.
///
/// # Safety
///
/// The caller must ensure `ctx_ptr` is either null or points to a valid
/// ProcessContext struct for the duration of this call.
unsafe fn extract_transport(ctx_ptr: *const ProcessContext) -> Transport {
    if ctx_ptr.is_null() {
        return Transport::default();
    }

    let ctx = &*ctx_ptr;
    let state = ctx.state;

    // VST3 ProcessContext state flags
    const K_PLAYING: u32 = 1 << 1;
    const K_CYCLE_ACTIVE: u32 = 1 << 2;
    const K_RECORDING: u32 = 1 << 3;
    const K_SYSTEM_TIME_VALID: u32 = 1 << 8;
    const K_PROJECT_TIME_MUSIC_VALID: u32 = 1 << 9;
    const K_TEMPO_VALID: u32 = 1 << 10;
    const K_BAR_POSITION_VALID: u32 = 1 << 11;
    const K_CYCLE_VALID: u32 = 1 << 12;
    const K_TIME_SIG_VALID: u32 = 1 << 13;
    const K_SMPTE_VALID: u32 = 1 << 14;
    const K_CLOCK_VALID: u32 = 1 << 15;
    const K_CONT_TIME_VALID: u32 = 1 << 17;

    Transport {
        // Tempo and time signature
        tempo: valid_if!(state, K_TEMPO_VALID, ctx.tempo),
        time_sig_numerator: valid_if!(state, K_TIME_SIG_VALID, ctx.timeSigNumerator),
        time_sig_denominator: valid_if!(state, K_TIME_SIG_VALID, ctx.timeSigDenominator),

        // Position
        project_time_samples: Some(ctx.projectTimeSamples),
        project_time_beats: valid_if!(state, K_PROJECT_TIME_MUSIC_VALID, ctx.projectTimeMusic),
        bar_position_beats: valid_if!(state, K_BAR_POSITION_VALID, ctx.barPositionMusic),

        // Cycle/loop
        cycle_start_beats: valid_if!(state, K_CYCLE_VALID, ctx.cycleStartMusic),
        cycle_end_beats: valid_if!(state, K_CYCLE_VALID, ctx.cycleEndMusic),

        // Transport state (always valid)
        is_playing: state & K_PLAYING != 0,
        is_recording: state & K_RECORDING != 0,
        is_cycle_active: state & K_CYCLE_ACTIVE != 0,

        // Advanced timing
        system_time_ns: valid_if!(state, K_SYSTEM_TIME_VALID, ctx.systemTime),
        continuous_time_samples: valid_if!(state, K_CONT_TIME_VALID, ctx.continousTimeSamples), // Note: VST3 SDK typo
        samples_to_next_clock: valid_if!(state, K_CLOCK_VALID, ctx.samplesToNextClock),

        // SMPTE - use FrameRate::from_raw() for conversion
        smpte_offset_subframes: valid_if!(state, K_SMPTE_VALID, ctx.smpteOffsetSubframes),
        frame_rate: if state & K_SMPTE_VALID != 0 {
            let is_drop = ctx.frameRate.flags & 1 != 0;
            CoreFrameRate::from_raw(ctx.frameRate.framesPerSecond, is_drop)
        } else {
            None
        },
    }
}

/// Conversion buffers for f64→f32 processing when plugin doesn't support native f64.
///
/// Pre-allocated in `setupProcessing()` to avoid heap allocations on the audio thread.
struct ConversionBuffers {
    /// Input conversion buffers: f64 → f32
    /// Outer Vec: per bus, Inner Vec: per channel
    main_input_f32: Vec<Vec<f32>>,
    /// Output conversion buffers: f32 → f64
    main_output_f32: Vec<Vec<f32>>,
    /// Auxiliary input conversion buffers
    aux_input_f32: Vec<Vec<Vec<f32>>>,
    /// Auxiliary output conversion buffers
    aux_output_f32: Vec<Vec<Vec<f32>>>,
}

impl ConversionBuffers {
    fn new() -> Self {
        Self {
            main_input_f32: Vec::new(),
            main_output_f32: Vec::new(),
            aux_input_f32: Vec::new(),
            aux_output_f32: Vec::new(),
        }
    }

    /// Pre-allocate buffers based on bus configuration and max block size.
    fn allocate<P: Plugin>(plugin: &P, max_block_size: usize) -> Self {
        let num_input_buses = plugin.input_bus_count();
        let num_output_buses = plugin.output_bus_count();

        // Main bus (bus 0) channels
        let main_in_channels = plugin.input_bus_info(0).map(|b| b.channel_count as usize).unwrap_or(0);
        let main_out_channels = plugin.output_bus_info(0).map(|b| b.channel_count as usize).unwrap_or(0);

        let main_input_f32: Vec<Vec<f32>> = (0..main_in_channels)
            .map(|_| vec![0.0f32; max_block_size])
            .collect();

        let main_output_f32: Vec<Vec<f32>> = (0..main_out_channels)
            .map(|_| vec![0.0f32; max_block_size])
            .collect();

        // Auxiliary buses (bus 1+)
        let mut aux_input_f32 = Vec::new();
        for bus_idx in 1..num_input_buses {
            if let Some(info) = plugin.input_bus_info(bus_idx) {
                let channels: Vec<Vec<f32>> = (0..info.channel_count)
                    .map(|_| vec![0.0f32; max_block_size])
                    .collect();
                aux_input_f32.push(channels);
            }
        }

        let mut aux_output_f32 = Vec::new();
        for bus_idx in 1..num_output_buses {
            if let Some(info) = plugin.output_bus_info(bus_idx) {
                let channels: Vec<Vec<f32>> = (0..info.channel_count)
                    .map(|_| vec![0.0f32; max_block_size])
                    .collect();
                aux_output_f32.push(channels);
            }
        }

        Self {
            main_input_f32,
            main_output_f32,
            aux_input_f32,
            aux_output_f32,
        }
    }
}

// =============================================================================
// Bus Limit Validation
// =============================================================================

/// Validate that a plugin's bus configuration doesn't exceed compile-time limits.
///
/// Returns `Ok(())` if valid, or `Err` with a descriptive message if limits are exceeded.
fn validate_bus_limits<P: Plugin>(plugin: &P) -> Result<(), String> {
    let num_inputs = plugin.input_bus_count();
    let num_outputs = plugin.output_bus_count();

    // Validate bus counts
    if num_inputs > MAX_BUSES {
        return Err(format!(
            "Plugin declares {} input buses, but MAX_BUSES is {}",
            num_inputs, MAX_BUSES
        ));
    }
    if num_outputs > MAX_BUSES {
        return Err(format!(
            "Plugin declares {} output buses, but MAX_BUSES is {}",
            num_outputs, MAX_BUSES
        ));
    }

    // Validate channel counts for each input bus
    for i in 0..num_inputs {
        if let Some(info) = plugin.input_bus_info(i) {
            let channels = info.channel_count as usize;
            if channels > MAX_CHANNELS {
                return Err(format!(
                    "Input bus {} declares {} channels, but MAX_CHANNELS is {}",
                    i, channels, MAX_CHANNELS
                ));
            }
        }
    }

    // Validate channel counts for each output bus
    for i in 0..num_outputs {
        if let Some(info) = plugin.output_bus_info(i) {
            let channels = info.channel_count as usize;
            if channels > MAX_CHANNELS {
                return Err(format!(
                    "Output bus {} declares {} channels, but MAX_CHANNELS is {}",
                    i, channels, MAX_CHANNELS
                ));
            }
        }
    }

    Ok(())
}

/// Validate that a speaker arrangement doesn't exceed MAX_CHANNELS.
///
/// Returns `Ok(())` if valid, or `Err` with a descriptive message if exceeded.
fn validate_speaker_arrangement(arrangement: SpeakerArrangement) -> Result<(), String> {
    let channel_count = arrangement.count_ones() as usize;
    if channel_count > MAX_CHANNELS {
        return Err(format!(
            "Speaker arrangement has {} channels, but MAX_CHANNELS is {}",
            channel_count, MAX_CHANNELS
        ));
    }
    Ok(())
}

// =============================================================================
// ProcessBufferStorage - Pre-allocated channel pointer storage
// =============================================================================

use beamer_core::sample::Sample;

/// Pre-allocated storage for channel pointers during audio processing.
///
/// This struct holds Vec storage with pre-reserved capacity based on the
/// plugin's BusInfo declarations. During process(), we use clear()+push()
/// which is O(1) and never allocates since capacity is pre-reserved.
///
/// Generic over sample type S (f32 or f64) to avoid code duplication.
///
/// # Real-Time Safety
///
/// After `allocate()` is called in `setupProcessing()`:
/// - `clear()` is O(1), sets len=0 without deallocating
/// - `push()` never allocates because capacity is sufficient
/// - No heap operations occur in the audio processing path
struct ProcessBufferStorage<S: Sample> {
    /// Main bus input channel pointers
    main_inputs: Vec<*const S>,
    /// Main bus output channel pointers
    main_outputs: Vec<*mut S>,
    /// Auxiliary bus input channel pointers (per-bus Vec)
    aux_inputs: Vec<Vec<*const S>>,
    /// Auxiliary bus output channel pointers (per-bus Vec)
    aux_outputs: Vec<Vec<*mut S>>,
}

// Safety: ProcessBufferStorage is Send because:
// - Raw pointers point to host-owned audio buffers valid only during process()
// - The struct is only accessed from the audio thread
// - Pointers are never dereferenced outside the process() call scope
unsafe impl<S: Sample> Send for ProcessBufferStorage<S> {}

// Safety: ProcessBufferStorage is Sync because:
// - Raw pointers are only populated and cleared during process()
// - VST3 guarantees process() is called from one thread at a time
// - No shared mutable state is accessed across threads
unsafe impl<S: Sample> Sync for ProcessBufferStorage<S> {}

impl<S: Sample> ProcessBufferStorage<S> {
    /// Create empty storage (no capacity reserved).
    fn new() -> Self {
        Self {
            main_inputs: Vec::new(),
            main_outputs: Vec::new(),
            aux_inputs: Vec::new(),
            aux_outputs: Vec::new(),
        }
    }

    /// Pre-allocate storage based on plugin's bus declarations.
    ///
    /// Reserves Vec capacity for the exact channel counts declared by the plugin.
    /// This ensures that subsequent push() calls in process() never allocate.
    ///
    /// # Panics
    ///
    /// Does NOT panic - validation should be done separately via `validate_bus_limits()`.
    fn allocate<P: Plugin>(plugin: &P) -> Self {
        let num_input_buses = plugin.input_bus_count();
        let num_output_buses = plugin.output_bus_count();

        // Get main bus channel counts (bus 0)
        let main_in_channels = plugin
            .input_bus_info(0)
            .map(|b| b.channel_count as usize)
            .unwrap_or(0);
        let main_out_channels = plugin
            .output_bus_info(0)
            .map(|b| b.channel_count as usize)
            .unwrap_or(0);

        // Pre-allocate main bus storage
        let main_inputs = Vec::with_capacity(main_in_channels);
        let main_outputs = Vec::with_capacity(main_out_channels);

        // Pre-allocate auxiliary bus storage
        let aux_input_bus_count = num_input_buses.saturating_sub(1);
        let aux_output_bus_count = num_output_buses.saturating_sub(1);

        let mut aux_inputs = Vec::with_capacity(aux_input_bus_count);
        for bus_idx in 1..num_input_buses {
            if let Some(info) = plugin.input_bus_info(bus_idx) {
                aux_inputs.push(Vec::with_capacity(info.channel_count as usize));
            } else {
                aux_inputs.push(Vec::new());
            }
        }

        let mut aux_outputs = Vec::with_capacity(aux_output_bus_count);
        for bus_idx in 1..num_output_buses {
            if let Some(info) = plugin.output_bus_info(bus_idx) {
                aux_outputs.push(Vec::with_capacity(info.channel_count as usize));
            } else {
                aux_outputs.push(Vec::new());
            }
        }

        Self {
            main_inputs,
            main_outputs,
            aux_inputs,
            aux_outputs,
        }
    }

    /// Clear all pointer storage for reuse.
    ///
    /// O(1) operation - does not deallocate, just sets len to 0.
    #[inline]
    fn clear(&mut self) {
        self.main_inputs.clear();
        self.main_outputs.clear();
        for bus in &mut self.aux_inputs {
            bus.clear();
        }
        for bus in &mut self.aux_outputs {
            bus.clear();
        }
    }
}

/// Generic VST3 processor wrapping any [`Plugin`] implementation.
///
/// This struct implements the VST3 combined component pattern, providing
/// `IComponent`, `IAudioProcessor`, and `IEditController` interfaces that
/// delegate to the wrapped plugin.
///
/// # Usage
///
/// ```ignore
/// use beamer_vst3::{export_vst3, Vst3Processor, PluginConfig};
///
/// struct MyPlugin { /* ... */ }
/// impl Plugin for MyPlugin { /* ... */ }
///
/// static CONFIG: PluginConfig = PluginConfig::new("MyPlugin", MY_UID);
/// export_vst3!(CONFIG, Vst3Processor<MyPlugin>);
/// ```
///
/// # Thread Safety
///
/// VST3 guarantees that `process()` is called from a single thread at a time.
/// We use `UnsafeCell` for interior mutability in `process()` since the COM
/// interface only provides `&self`.
pub struct Vst3Processor<P: Plugin> {
    /// The wrapped plugin instance (uses UnsafeCell for interior mutability)
    plugin: UnsafeCell<P>,
    /// Plugin configuration reference
    config: &'static PluginConfig,
    /// Current sample rate
    sample_rate: UnsafeCell<f64>,
    /// Maximum block size
    max_block_size: UnsafeCell<usize>,
    /// Current symbolic sample size (kSample32 or kSample64)
    symbolic_sample_size: UnsafeCell<i32>,
    /// MIDI input buffer (reused each process call to avoid stack overflow)
    midi_input: UnsafeCell<MidiBuffer>,
    /// MIDI output buffer (reused each process call)
    midi_output: UnsafeCell<MidiBuffer>,
    /// SysEx output buffer pool (for VST3 DataEvent pointer stability)
    sysex_output_pool: UnsafeCell<SysExOutputPool>,
    /// Conversion buffers for f64→f32 processing
    conversion_buffers: UnsafeCell<ConversionBuffers>,
    /// Pre-allocated channel pointer storage for f32 processing
    buffer_storage_f32: UnsafeCell<ProcessBufferStorage<f32>>,
    /// Pre-allocated channel pointer storage for f64 processing
    buffer_storage_f64: UnsafeCell<ProcessBufferStorage<f64>>,
    /// Marker for the plugin type
    _marker: PhantomData<P>,
}

// Safety: Vst3Processor is Send because:
// - Plugin: Send is required by the Plugin trait
// - UnsafeCell contents are only accessed from VST3's guaranteed single-threaded contexts
unsafe impl<P: Plugin> Send for Vst3Processor<P> {}

// Safety: Vst3Processor is Sync because:
// - VST3 guarantees process() is called from one thread at a time
// - Parameter access through Parameters trait requires Sync
unsafe impl<P: Plugin> Sync for Vst3Processor<P> {}

impl<P: Plugin + 'static> Vst3Processor<P> {
    /// Create a new VST3 processor wrapping the given plugin configuration.
    pub fn new(config: &'static PluginConfig) -> Self {
        Self {
            plugin: UnsafeCell::new(P::create()),
            config,
            sample_rate: UnsafeCell::new(44100.0),
            max_block_size: UnsafeCell::new(1024),
            symbolic_sample_size: UnsafeCell::new(SymbolicSampleSizes_::kSample32 as i32),
            midi_input: UnsafeCell::new(MidiBuffer::new()),
            midi_output: UnsafeCell::new(MidiBuffer::new()),
            sysex_output_pool: UnsafeCell::new(SysExOutputPool::with_capacity(
                config.sysex_slots,
                config.sysex_buffer_size,
            )),
            conversion_buffers: UnsafeCell::new(ConversionBuffers::new()),
            buffer_storage_f32: UnsafeCell::new(ProcessBufferStorage::new()),
            buffer_storage_f64: UnsafeCell::new(ProcessBufferStorage::new()),
            _marker: PhantomData,
        }
    }

    /// Get a reference to the wrapped plugin.
    ///
    /// # Safety
    /// Must only be called when no mutable reference exists.
    #[inline]
    unsafe fn plugin(&self) -> &P {
        &*self.plugin.get()
    }

    /// Get a mutable reference to the wrapped plugin.
    ///
    /// # Safety
    /// Must only be called from contexts where VST3 guarantees single-threaded access
    /// (e.g., process(), setupProcessing()).
    #[inline]
    #[allow(clippy::mut_from_ref)]
    unsafe fn plugin_mut(&self) -> &mut P {
        &mut *self.plugin.get()
    }

    // =========================================================================
    // Audio Processing Helpers
    // =========================================================================
    //
    // NOTE: process_audio_f32() and process_audio_f64_native() have similar
    // structure but cannot be deduplicated because:
    //
    // 1. VST3's ProcessData uses a C union: channelBuffers32 and channelBuffers64
    //    are separate pointer fields, not generic over sample type
    //
    // 2. Rust's type system can't easily abstract over C FFI union field access
    //    (ProcessData.__field0.channelBuffers32 vs .channelBuffers64)
    //
    // 3. A macro or trait-based abstraction would add complexity for just two
    //    concrete implementations; explicit code is clearer for maintainability
    //
    // If adding a third sample type (e.g., i32 for fixed-point), consider
    // refactoring to a macro-based approach.
    // =========================================================================

    /// Process audio at 32-bit (f32) precision.
    ///
    /// This is the standard processing path used when the host uses kSample32.
    /// Uses pre-allocated ProcessBufferStorage - no heap allocations.
    #[inline]
    unsafe fn process_audio_f32(
        &self,
        process_data: &ProcessData,
        num_samples: usize,
        plugin: &mut P,
        context: &CoreProcessContext,
    ) {
        // Get pre-allocated storage and clear for reuse (O(1), no deallocation)
        let storage = &mut *self.buffer_storage_f32.get();
        storage.clear();

        // Collect main input channel pointers (bounded by pre-allocated capacity)
        if process_data.numInputs > 0 && !process_data.inputs.is_null() {
            let bus = &*process_data.inputs;
            let num_channels = bus.numChannels as usize;
            let max_channels = storage.main_inputs.capacity();
            if num_channels > 0 && !bus.__field0.channelBuffers32.is_null() {
                let channel_ptrs =
                    slice::from_raw_parts(bus.__field0.channelBuffers32, num_channels);
                for &ptr in channel_ptrs.iter().take(max_channels) {
                    if !ptr.is_null() {
                        storage.main_inputs.push(ptr);
                    }
                }
            }
        }

        // Collect main output channel pointers (bounded by pre-allocated capacity)
        if process_data.numOutputs > 0 && !process_data.outputs.is_null() {
            let bus = &*process_data.outputs;
            let num_channels = bus.numChannels as usize;
            let max_channels = storage.main_outputs.capacity();
            if num_channels > 0 && !bus.__field0.channelBuffers32.is_null() {
                let channel_ptrs =
                    slice::from_raw_parts(bus.__field0.channelBuffers32, num_channels);
                for &ptr in channel_ptrs.iter().take(max_channels) {
                    if !ptr.is_null() {
                        storage.main_outputs.push(ptr);
                    }
                }
            }
        }

        // Collect auxiliary input channel pointers (bounded by pre-allocated capacity)
        if process_data.numInputs > 1 && !process_data.inputs.is_null() {
            let input_buses =
                slice::from_raw_parts(process_data.inputs, process_data.numInputs as usize);
            for (aux_idx, bus) in input_buses[1..].iter().enumerate() {
                if aux_idx < storage.aux_inputs.len() {
                    let num_channels = bus.numChannels as usize;
                    let max_channels = storage.aux_inputs[aux_idx].capacity();
                    if num_channels > 0 && !bus.__field0.channelBuffers32.is_null() {
                        let channel_ptrs =
                            slice::from_raw_parts(bus.__field0.channelBuffers32, num_channels);
                        for &ptr in channel_ptrs.iter().take(max_channels) {
                            if !ptr.is_null() {
                                storage.aux_inputs[aux_idx].push(ptr);
                            }
                        }
                    }
                }
            }
        }

        // Collect auxiliary output channel pointers (bounded by pre-allocated capacity)
        if process_data.numOutputs > 1 && !process_data.outputs.is_null() {
            let output_buses =
                slice::from_raw_parts(process_data.outputs, process_data.numOutputs as usize);
            for (aux_idx, bus) in output_buses[1..].iter().enumerate() {
                if aux_idx < storage.aux_outputs.len() {
                    let num_channels = bus.numChannels as usize;
                    let max_channels = storage.aux_outputs[aux_idx].capacity();
                    if num_channels > 0 && !bus.__field0.channelBuffers32.is_null() {
                        let channel_ptrs =
                            slice::from_raw_parts(bus.__field0.channelBuffers32, num_channels);
                        for &ptr in channel_ptrs.iter().take(max_channels) {
                            if !ptr.is_null() {
                                storage.aux_outputs[aux_idx].push(ptr);
                            }
                        }
                    }
                }
            }
        }

        // Create slices from pointers (safe: ProcessData lifetime covers this scope)
        let main_in_iter = storage
            .main_inputs
            .iter()
            .map(|&ptr| slice::from_raw_parts(ptr, num_samples));
        let main_out_iter = storage
            .main_outputs
            .iter()
            .map(|&ptr| slice::from_raw_parts_mut(ptr, num_samples));

        let aux_in_iter = storage.aux_inputs.iter().map(|bus| {
            bus.iter()
                .map(|&ptr| slice::from_raw_parts(ptr, num_samples))
        });
        let aux_out_iter = storage.aux_outputs.iter().map(|bus| {
            bus.iter()
                .map(|&ptr| slice::from_raw_parts_mut(ptr, num_samples))
        });

        // Construct buffers and process
        let mut buffer = Buffer::new(main_in_iter, main_out_iter, num_samples);
        let mut aux = AuxiliaryBuffers::new(aux_in_iter, aux_out_iter, num_samples);

        plugin.process(&mut buffer, &mut aux, context);
    }

    /// Process audio at 64-bit (f64) precision with native plugin support.
    ///
    /// Used when host uses kSample64 and plugin.supports_double_precision() is true.
    /// Uses pre-allocated ProcessBufferStorage - no heap allocations.
    #[inline]
    unsafe fn process_audio_f64_native(
        &self,
        process_data: &ProcessData,
        num_samples: usize,
        plugin: &mut P,
        context: &CoreProcessContext,
    ) {
        // Get pre-allocated storage and clear for reuse (O(1), no deallocation)
        let storage = &mut *self.buffer_storage_f64.get();
        storage.clear();

        // Collect main input channel pointers (bounded by pre-allocated capacity)
        if process_data.numInputs > 0 && !process_data.inputs.is_null() {
            let bus = &*process_data.inputs;
            let num_channels = bus.numChannels as usize;
            let max_channels = storage.main_inputs.capacity();
            if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                let channel_ptrs =
                    slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                for &ptr in channel_ptrs.iter().take(max_channels) {
                    if !ptr.is_null() {
                        storage.main_inputs.push(ptr);
                    }
                }
            }
        }

        // Collect main output channel pointers (bounded by pre-allocated capacity)
        if process_data.numOutputs > 0 && !process_data.outputs.is_null() {
            let bus = &*process_data.outputs;
            let num_channels = bus.numChannels as usize;
            let max_channels = storage.main_outputs.capacity();
            if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                let channel_ptrs =
                    slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                for &ptr in channel_ptrs.iter().take(max_channels) {
                    if !ptr.is_null() {
                        storage.main_outputs.push(ptr);
                    }
                }
            }
        }

        // Collect auxiliary input channel pointers (bounded by pre-allocated capacity)
        if process_data.numInputs > 1 && !process_data.inputs.is_null() {
            let input_buses =
                slice::from_raw_parts(process_data.inputs, process_data.numInputs as usize);
            for (aux_idx, bus) in input_buses[1..].iter().enumerate() {
                if aux_idx < storage.aux_inputs.len() {
                    let num_channels = bus.numChannels as usize;
                    let max_channels = storage.aux_inputs[aux_idx].capacity();
                    if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                        let channel_ptrs =
                            slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                        for &ptr in channel_ptrs.iter().take(max_channels) {
                            if !ptr.is_null() {
                                storage.aux_inputs[aux_idx].push(ptr);
                            }
                        }
                    }
                }
            }
        }

        // Collect auxiliary output channel pointers (bounded by pre-allocated capacity)
        if process_data.numOutputs > 1 && !process_data.outputs.is_null() {
            let output_buses =
                slice::from_raw_parts(process_data.outputs, process_data.numOutputs as usize);
            for (aux_idx, bus) in output_buses[1..].iter().enumerate() {
                if aux_idx < storage.aux_outputs.len() {
                    let num_channels = bus.numChannels as usize;
                    let max_channels = storage.aux_outputs[aux_idx].capacity();
                    if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                        let channel_ptrs =
                            slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                        for &ptr in channel_ptrs.iter().take(max_channels) {
                            if !ptr.is_null() {
                                storage.aux_outputs[aux_idx].push(ptr);
                            }
                        }
                    }
                }
            }
        }

        // Create slices from pointers (safe: ProcessData lifetime covers this scope)
        let main_in_iter = storage
            .main_inputs
            .iter()
            .map(|&ptr| slice::from_raw_parts(ptr, num_samples));
        let main_out_iter = storage
            .main_outputs
            .iter()
            .map(|&ptr| slice::from_raw_parts_mut(ptr, num_samples));

        let aux_in_iter = storage.aux_inputs.iter().map(|bus| {
            bus.iter()
                .map(|&ptr| slice::from_raw_parts(ptr, num_samples))
        });
        let aux_out_iter = storage.aux_outputs.iter().map(|bus| {
            bus.iter()
                .map(|&ptr| slice::from_raw_parts_mut(ptr, num_samples))
        });

        // Construct buffers and process
        let mut buffer: Buffer<f64> = Buffer::new(main_in_iter, main_out_iter, num_samples);
        let mut aux: AuxiliaryBuffers<f64> =
            AuxiliaryBuffers::new(aux_in_iter, aux_out_iter, num_samples);

        plugin.process_f64(&mut buffer, &mut aux, context);
    }

    /// Process audio at 64-bit (f64) with conversion to/from f32.
    ///
    /// Used when host uses kSample64 but plugin.supports_double_precision() is false.
    /// Converts f64→f32, calls process(), converts f32→f64.
    #[inline]
    unsafe fn process_audio_f64_converted(
        &self,
        process_data: &ProcessData,
        num_samples: usize,
        plugin: &mut P,
        context: &CoreProcessContext,
    ) {
        let conv = &mut *self.conversion_buffers.get();

        // Convert main input f64 → f32
        if process_data.numInputs > 0 && !process_data.inputs.is_null() {
            let input_buses = slice::from_raw_parts(process_data.inputs, 1);
            let bus = &input_buses[0];
            let num_channels = (bus.numChannels as usize).min(conv.main_input_f32.len());
            if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                let channel_ptrs = slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                for (ch, &ptr) in channel_ptrs.iter().enumerate() {
                    if !ptr.is_null() && ch < conv.main_input_f32.len() {
                        let src = slice::from_raw_parts(ptr, num_samples);
                        for (i, &s) in src.iter().enumerate() {
                            conv.main_input_f32[ch][i] = s as f32;
                        }
                    }
                }
            }
        }

        // Convert aux input f64 → f32
        for (bus_idx, aux_bus) in conv.aux_input_f32.iter_mut().enumerate() {
            let vst_bus_idx = bus_idx + 1; // aux buses start at index 1
            if process_data.numInputs as usize > vst_bus_idx && !process_data.inputs.is_null() {
                let input_buses = slice::from_raw_parts(
                    process_data.inputs,
                    process_data.numInputs as usize,
                );
                let bus = &input_buses[vst_bus_idx];
                let num_channels = (bus.numChannels as usize).min(aux_bus.len());
                if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                    let channel_ptrs = slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                    for (ch, &ptr) in channel_ptrs.iter().enumerate() {
                        if !ptr.is_null() && ch < aux_bus.len() {
                            let src = slice::from_raw_parts(ptr, num_samples);
                            for (i, &s) in src.iter().enumerate() {
                                aux_bus[ch][i] = s as f32;
                            }
                        }
                    }
                }
            }
        }

        // Build f32 buffer slices using iterators (no allocation)
        let main_input_iter = conv.main_input_f32
            .iter()
            .map(|v| &v[..num_samples]);
        let main_output_iter = conv.main_output_f32
            .iter_mut()
            .map(|v| &mut v[..num_samples]);

        let aux_input_iter = conv.aux_input_f32
            .iter()
            .map(|bus| bus.iter().map(|v| &v[..num_samples]));
        let aux_output_iter = conv.aux_output_f32
            .iter_mut()
            .map(|bus| bus.iter_mut().map(|v| &mut v[..num_samples]));

        // Construct f32 buffers and process
        let mut buffer = Buffer::new(main_input_iter, main_output_iter, num_samples);
        let mut aux = AuxiliaryBuffers::new(aux_input_iter, aux_output_iter, num_samples);

        plugin.process(&mut buffer, &mut aux, context);

        // Convert main output f32 → f64
        if process_data.numOutputs > 0 && !process_data.outputs.is_null() {
            let output_buses = slice::from_raw_parts(process_data.outputs, 1);
            let bus = &output_buses[0];
            let num_channels = (bus.numChannels as usize).min(conv.main_output_f32.len());
            if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                let channel_ptrs = slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                for (ch, &ptr) in channel_ptrs.iter().enumerate() {
                    if !ptr.is_null() && ch < conv.main_output_f32.len() {
                        let dst = slice::from_raw_parts_mut(ptr, num_samples);
                        for (i, sample) in conv.main_output_f32[ch][..num_samples].iter().enumerate() {
                            dst[i] = *sample as f64;
                        }
                    }
                }
            }
        }

        // Convert aux output f32 → f64
        for (bus_idx, aux_bus) in conv.aux_output_f32.iter().enumerate() {
            let vst_bus_idx = bus_idx + 1;
            if process_data.numOutputs as usize > vst_bus_idx && !process_data.outputs.is_null() {
                let output_buses = slice::from_raw_parts(
                    process_data.outputs,
                    process_data.numOutputs as usize,
                );
                let bus = &output_buses[vst_bus_idx];
                let num_channels = (bus.numChannels as usize).min(aux_bus.len());
                if num_channels > 0 && !bus.__field0.channelBuffers64.is_null() {
                    let channel_ptrs = slice::from_raw_parts(bus.__field0.channelBuffers64, num_channels);
                    for (ch, &ptr) in channel_ptrs.iter().enumerate() {
                        if !ptr.is_null() && ch < aux_bus.len() {
                            let dst = slice::from_raw_parts_mut(ptr, num_samples);
                            for (i, sample) in aux_bus[ch][..num_samples].iter().enumerate() {
                                dst[i] = *sample as f64;
                            }
                        }
                    }
                }
            }
        }
    }
}

impl<P: Plugin + 'static> ComponentFactory for Vst3Processor<P> {
    fn create(config: &'static PluginConfig) -> Self {
        Self::new(config)
    }
}

impl<P: Plugin + 'static> Class for Vst3Processor<P> {
    type Interfaces = (
        IComponent,
        IAudioProcessor,
        IProcessContextRequirements,
        IEditController,
        IUnitInfo,
        IMidiMapping,
        IMidiLearn,
        IMidiMapping2,
        IMidiLearn2,
        INoteExpressionController,
        IKeyswitchController,
        INoteExpressionPhysicalUIMapping,
        IVst3WrapperMPESupport,
    );
}

// =============================================================================
// IPluginBase implementation
// =============================================================================

impl<P: Plugin + 'static> IPluginBaseTrait for Vst3Processor<P> {
    unsafe fn initialize(&self, _context: *mut FUnknown) -> tresult {
        kResultOk
    }

    unsafe fn terminate(&self) -> tresult {
        kResultOk
    }
}

// =============================================================================
// IComponent implementation
// =============================================================================

impl<P: Plugin + 'static> IComponentTrait for Vst3Processor<P> {
    unsafe fn getControllerClassId(&self, class_id: *mut TUID) -> tresult {
        if class_id.is_null() {
            return kInvalidArgument;
        }

        // For combined component, return the controller UID if set, otherwise kNotImplemented
        if let Some(controller) = self.config.controller_uid {
            *class_id = controller;
            kResultOk
        } else {
            kNotImplemented
        }
    }

    unsafe fn setIoMode(&self, _mode: IoMode) -> tresult {
        kResultOk
    }

    unsafe fn getBusCount(&self, media_type: MediaType, dir: BusDirection) -> i32 {
        let plugin = self.plugin();

        match media_type as MediaTypes {
            MediaTypes_::kAudio => match dir as BusDirections {
                BusDirections_::kInput => plugin.input_bus_count() as i32,
                BusDirections_::kOutput => plugin.output_bus_count() as i32,
                _ => 0,
            },
            MediaTypes_::kEvent => {
                // Return 1 event bus in each direction if plugin wants MIDI
                if plugin.wants_midi() {
                    1
                } else {
                    0
                }
            }
            _ => 0,
        }
    }

    unsafe fn getBusInfo(
        &self,
        media_type: MediaType,
        dir: BusDirection,
        index: i32,
        bus: *mut BusInfo,
    ) -> tresult {
        if bus.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();

        match media_type as MediaTypes {
            MediaTypes_::kAudio => {
                let info = match dir as BusDirections {
                    BusDirections_::kInput => plugin.input_bus_info(index as usize),
                    BusDirections_::kOutput => plugin.output_bus_info(index as usize),
                    _ => None,
                };

                if let Some(info) = info {
                    let bus = &mut *bus;
                    bus.mediaType = MediaTypes_::kAudio as MediaType;
                    bus.direction = dir;
                    bus.channelCount = info.channel_count as i32;
                    copy_wstring(info.name, &mut bus.name);
                    bus.busType = match info.bus_type {
                        CoreBusType::Main => BusTypes_::kMain,
                        CoreBusType::Aux => BusTypes_::kAux,
                    } as BusType;
                    bus.flags = if info.is_default_active {
                        BusInfo_::BusFlags_::kDefaultActive
                    } else {
                        0
                    };
                    kResultOk
                } else {
                    kInvalidArgument
                }
            }
            MediaTypes_::kEvent => {
                // Only index 0 for event bus, and only if plugin wants MIDI
                if index != 0 || !plugin.wants_midi() {
                    return kInvalidArgument;
                }

                let bus = &mut *bus;
                bus.mediaType = MediaTypes_::kEvent as MediaType;
                bus.direction = dir;
                bus.channelCount = 1; // Single event channel
                let name = match dir as BusDirections {
                    BusDirections_::kInput => "MIDI In",
                    BusDirections_::kOutput => "MIDI Out",
                    _ => "MIDI",
                };
                copy_wstring(name, &mut bus.name);
                bus.busType = BusTypes_::kMain as BusType;
                bus.flags = BusInfo_::BusFlags_::kDefaultActive;
                kResultOk
            }
            _ => kInvalidArgument,
        }
    }

    unsafe fn getRoutingInfo(
        &self,
        _in_info: *mut RoutingInfo,
        _out_info: *mut RoutingInfo,
    ) -> tresult {
        kNotImplemented
    }

    unsafe fn activateBus(
        &self,
        _media_type: MediaType,
        _dir: BusDirection,
        _index: i32,
        _state: TBool,
    ) -> tresult {
        kResultOk
    }

    unsafe fn setActive(&self, state: TBool) -> tresult {
        let plugin = self.plugin_mut();
        plugin.set_active(state != 0);
        kResultOk
    }

    unsafe fn setState(&self, state: *mut IBStream) -> tresult {
        if state.is_null() {
            return kInvalidArgument;
        }

        let stream = match ComRef::from_raw(state) {
            Some(s) => s,
            None => return kInvalidArgument,
        };

        // Read all bytes from stream
        let mut buffer = Vec::new();
        let mut chunk = [0u8; 4096];
        loop {
            let mut bytes_read: i32 = 0;
            let result = stream.read(
                chunk.as_mut_ptr() as *mut c_void,
                chunk.len() as i32,
                &mut bytes_read,
            );

            if result != kResultOk || bytes_read <= 0 {
                break;
            }

            buffer.extend_from_slice(&chunk[..bytes_read as usize]);
        }

        if buffer.is_empty() {
            return kResultOk;
        }

        // Load into plugin
        let plugin = self.plugin_mut();
        match plugin.load_state(&buffer) {
            Ok(()) => {
                // Apply current sample rate (if available) and reset smoothers
                use beamer_core::param_types::Params;
                let sample_rate = *self.sample_rate.get();
                if sample_rate > 0.0 {
                    plugin.params_mut().set_sample_rate(sample_rate);
                }
                plugin.params_mut().reset_smoothing();
                kResultOk
            }
            Err(_) => kResultFalse,
        }
    }

    unsafe fn getState(&self, state: *mut IBStream) -> tresult {
        if state.is_null() {
            return kInvalidArgument;
        }

        // Get plugin state as bytes
        let plugin = self.plugin();
        let data = match plugin.save_state() {
            Ok(d) => d,
            Err(_) => return kResultFalse,
        };

        if data.is_empty() {
            return kResultOk;
        }

        // Write to IBStream
        let stream = match ComRef::from_raw(state) {
            Some(s) => s,
            None => return kInvalidArgument,
        };
        let mut bytes_written: i32 = 0;
        let result = stream.write(
            data.as_ptr() as *mut c_void,
            data.len() as i32,
            &mut bytes_written,
        );

        if result == kResultOk && bytes_written == data.len() as i32 {
            kResultOk
        } else {
            kResultFalse
        }
    }
}

// =============================================================================
// IAudioProcessor implementation
// =============================================================================

impl<P: Plugin + 'static> IAudioProcessorTrait for Vst3Processor<P> {
    unsafe fn setBusArrangements(
        &self,
        inputs: *mut SpeakerArrangement,
        num_ins: i32,
        outputs: *mut SpeakerArrangement,
        num_outs: i32,
    ) -> tresult {
        // Early rejection: negative counts or bus count exceeds compile-time limits
        if num_ins < 0
            || num_outs < 0
            || num_ins as usize > MAX_BUSES
            || num_outs as usize > MAX_BUSES
        {
            return kResultFalse;
        }

        // Early rejection: null pointers with non-zero counts
        if (num_ins > 0 && inputs.is_null()) || (num_outs > 0 && outputs.is_null()) {
            return kInvalidArgument;
        }

        let plugin = self.plugin();

        // Check if the requested arrangement matches our bus configuration
        if num_ins as usize != plugin.input_bus_count()
            || num_outs as usize != plugin.output_bus_count()
        {
            return kResultFalse;
        }

        // Validate each input bus
        for i in 0..num_ins as usize {
            // Early rejection: channel count exceeds compile-time limits
            let requested = *inputs.add(i);
            if validate_speaker_arrangement(requested).is_err() {
                return kResultFalse;
            }

            if let Some(info) = plugin.input_bus_info(i) {
                let expected = channel_count_to_speaker_arrangement(info.channel_count);
                if requested != expected {
                    return kResultFalse;
                }
            }
        }

        // Validate each output bus
        for i in 0..num_outs as usize {
            // Early rejection: channel count exceeds compile-time limits
            let requested = *outputs.add(i);
            if validate_speaker_arrangement(requested).is_err() {
                return kResultFalse;
            }

            if let Some(info) = plugin.output_bus_info(i) {
                let expected = channel_count_to_speaker_arrangement(info.channel_count);
                if requested != expected {
                    return kResultFalse;
                }
            }
        }

        kResultTrue
    }

    unsafe fn getBusArrangement(
        &self,
        dir: BusDirection,
        index: i32,
        arr: *mut SpeakerArrangement,
    ) -> tresult {
        if arr.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        let info = match dir as BusDirections {
            BusDirections_::kInput => plugin.input_bus_info(index as usize),
            BusDirections_::kOutput => plugin.output_bus_info(index as usize),
            _ => None,
        };

        if let Some(info) = info {
            *arr = channel_count_to_speaker_arrangement(info.channel_count);
            kResultOk
        } else {
            kInvalidArgument
        }
    }

    unsafe fn canProcessSampleSize(&self, symbolic_sample_size: i32) -> tresult {
        match symbolic_sample_size as SymbolicSampleSizes {
            SymbolicSampleSizes_::kSample32 => kResultOk,
            SymbolicSampleSizes_::kSample64 => kResultOk, // Support 64-bit via native or conversion
            _ => kNotImplemented,
        }
    }

    unsafe fn getLatencySamples(&self) -> u32 {
        self.plugin().latency_samples()
    }

    unsafe fn setupProcessing(&self, setup: *mut ProcessSetup) -> tresult {
        if setup.is_null() {
            return kInvalidArgument;
        }

        let setup = &*setup;

        // Store setup parameters
        *self.sample_rate.get() = setup.sampleRate;
        *self.max_block_size.get() = setup.maxSamplesPerBlock as usize;
        *self.symbolic_sample_size.get() = setup.symbolicSampleSize;

        // Notify the plugin
        let plugin = self.plugin_mut();
        plugin.setup(setup.sampleRate, setup.maxSamplesPerBlock as usize);

        // Validate plugin's bus configuration against compile-time limits
        // This prevents silent truncation during processing
        if let Err(msg) = validate_bus_limits(plugin) {
            log::error!("Plugin bus configuration exceeds limits: {}", msg);
            return kResultFalse;
        }

        // Pre-allocate buffer storage based on plugin's bus declarations
        // This ensures no heap allocation during process()
        *self.buffer_storage_f32.get() = ProcessBufferStorage::allocate(plugin);
        *self.buffer_storage_f64.get() = ProcessBufferStorage::allocate(plugin);

        // Pre-allocate conversion buffers for f64→f32 processing
        // Only needed if host is using 64-bit and plugin doesn't support native f64
        if setup.symbolicSampleSize == SymbolicSampleSizes_::kSample64 as i32
            && !plugin.supports_double_precision()
        {
            *self.conversion_buffers.get() =
                ConversionBuffers::allocate(plugin, setup.maxSamplesPerBlock as usize);
        }

        kResultOk
    }

    unsafe fn setProcessing(&self, _state: TBool) -> tresult {
        kResultOk
    }

    unsafe fn process(&self, data: *mut ProcessData) -> tresult {
        if data.is_null() {
            return kInvalidArgument;
        }

        let process_data = &*data;
        let num_samples = process_data.numSamples as usize;

        if num_samples == 0 {
            return kResultOk;
        }

        // 1. Handle incoming parameter changes from host
        if let Some(param_changes) = ComRef::from_raw(process_data.inputParameterChanges) {
            let params = self.plugin().params();
            let param_count = param_changes.getParameterCount();

            for i in 0..param_count {
                if let Some(queue) = ComRef::from_raw(param_changes.getParameterData(i)) {
                    let param_id = queue.getParameterId();
                    let point_count = queue.getPointCount();

                    if point_count > 0 {
                        let mut sample_offset = 0;
                        let mut value = 0.0;
                        // Get the last value in the queue (simplest approach)
                        if queue.getPoint(point_count - 1, &mut sample_offset, &mut value)
                            == kResultTrue
                        {
                            params.set_normalized(param_id, value);
                        }
                    }
                }
            }
        }

        // 2. Handle MIDI events (reuse pre-allocated buffer to avoid stack overflow)
        let midi_input = &mut *self.midi_input.get();
        midi_input.clear();

        if let Some(event_list) = ComRef::from_raw(process_data.inputEvents) {
            let event_count = event_list.getEventCount();

            for i in 0..event_count {
                let mut event: Event = std::mem::zeroed();
                if event_list.getEvent(i, &mut event) == kResultOk {
                    if let Some(midi_event) = convert_vst3_to_midi(&event) {
                        midi_input.push(midi_event);
                    }
                }
            }
        }

        // Check for MIDI input buffer overflow (once per block)
        if midi_input.has_overflowed() {
            warn!(
                "MIDI input buffer overflow: {} events max, some events were dropped",
                beamer_core::midi::MAX_MIDI_EVENTS
            );
        }

        // Clear and prepare MIDI output buffer and SysEx pool
        let midi_output = &mut *self.midi_output.get();
        midi_output.clear();
        let sysex_pool = &mut *self.sysex_output_pool.get();

        // Clear pool FIRST so next_slot is reset to 0 before draining fallback
        sysex_pool.clear();

        // With heap fallback enabled, emit any overflow messages from previous block first.
        // These allocate slots starting from 0; new plugin output will append after them.
        #[cfg(feature = "sysex-heap-fallback")]
        if sysex_pool.has_fallback() {
            if let Some(event_list) = ComRef::from_raw(process_data.outputEvents) {
                for sysex_data in sysex_pool.take_fallback() {
                    // Allocate from pool (succeeds since we just cleared it)
                    if let Some((ptr, len)) = sysex_pool.allocate(&sysex_data) {
                        let mut event: Event = std::mem::zeroed();
                        event.busIndex = 0;
                        event.sampleOffset = 0; // Delayed message, emit at start of block
                        event.ppqPosition = 0.0;
                        event.flags = 0;
                        event.r#type = K_DATA_EVENT;
                        event.__field0.data.r#type = DATA_TYPE_MIDI_SYSEX;
                        event.__field0.data.size = len as u32;
                        event.__field0.data.bytes = ptr;
                        let _ = event_list.addEvent(&mut event);
                    }
                }
            }
            // Log that we recovered from overflow
            warn!(
                "SysEx fallback: emitted delayed messages from previous block overflow"
            );
        }
        // NOTE: Don't clear again - fallback events occupy slots 0..N, new events append after

        // Process MIDI events
        let plugin = self.plugin_mut();
        plugin.process_midi(midi_input.as_slice(), midi_output);

        // Write output MIDI events
        if let Some(event_list) = ComRef::from_raw(process_data.outputEvents) {
            for midi_event in midi_output.iter() {
                if let Some(mut vst3_event) = convert_midi_to_vst3(midi_event, sysex_pool) {
                    let _ = event_list.addEvent(&mut vst3_event);
                }
            }
        }

        // Check for MIDI buffer overflow (once per block)
        if midi_output.has_overflowed() {
            warn!(
                "MIDI output buffer overflow: {} events reached capacity, some events were dropped",
                midi_output.len()
            );
        }

        // Check for SysEx pool overflow (once per block)
        if sysex_pool.has_overflowed() {
            warn!(
                "SysEx output pool overflow: {} slots exhausted, some SysEx messages were dropped",
                sysex_pool.capacity()
            );
        }

        // 3. Extract transport info from VST3 ProcessContext
        let transport = extract_transport(process_data.processContext);
        let sample_rate = *self.sample_rate.get();
        let context = CoreProcessContext::new(sample_rate, num_samples, transport);

        // 4. Process audio based on sample size
        let symbolic_sample_size = *self.symbolic_sample_size.get();
        let plugin = self.plugin_mut();

        if symbolic_sample_size == SymbolicSampleSizes_::kSample64 as i32 {
            // 64-bit processing path
            if plugin.supports_double_precision() {
                // Native f64: extract f64 buffers and call process_f64()
                self.process_audio_f64_native(process_data, num_samples, plugin, &context);
            } else {
                // Conversion: f64→f32, process, f32→f64
                self.process_audio_f64_converted(process_data, num_samples, plugin, &context);
            }
        } else {
            // 32-bit processing path (default)
            self.process_audio_f32(process_data, num_samples, plugin, &context);
        }

        kResultOk
    }

    unsafe fn getTailSamples(&self) -> u32 {
        let plugin = self.plugin();
        plugin.tail_samples().saturating_add(plugin.bypass_ramp_samples())
    }
}

impl<P: Plugin + 'static> IProcessContextRequirementsTrait for Vst3Processor<P> {
    unsafe fn getProcessContextRequirements(&self) -> u32 {
        // Request all available transport information from host.
        // These flags tell the host which ProcessContext fields we need.
        // See VST3 SDK: IProcessContextRequirements interface
        const K_NEED_SYSTEM_TIME: u32 = 1 << 0;
        const K_NEED_CONTINUOUS_TIME_SAMPLES: u32 = 1 << 1;
        const K_NEED_PROJECT_TIME_MUSIC: u32 = 1 << 2;
        const K_NEED_BAR_POSITION_MUSIC: u32 = 1 << 3;
        const K_NEED_CYCLE_MUSIC: u32 = 1 << 4;
        const K_NEED_SAMPLES_TO_NEXT_CLOCK: u32 = 1 << 5;
        const K_NEED_TEMPO: u32 = 1 << 6;
        const K_NEED_TIME_SIGNATURE: u32 = 1 << 7;
        const K_NEED_FRAME_RATE: u32 = 1 << 9;
        const K_NEED_TRANSPORT_STATE: u32 = 1 << 10;

        K_NEED_SYSTEM_TIME
            | K_NEED_CONTINUOUS_TIME_SAMPLES
            | K_NEED_PROJECT_TIME_MUSIC
            | K_NEED_BAR_POSITION_MUSIC
            | K_NEED_CYCLE_MUSIC
            | K_NEED_SAMPLES_TO_NEXT_CLOCK
            | K_NEED_TEMPO
            | K_NEED_TIME_SIGNATURE
            | K_NEED_FRAME_RATE
            | K_NEED_TRANSPORT_STATE
    }
}

// =============================================================================
// IEditController implementation
// =============================================================================

impl<P: Plugin + 'static> IEditControllerTrait for Vst3Processor<P> {
    unsafe fn setComponentState(&self, _state: *mut IBStream) -> tresult {
        // For combined component, state is handled by IComponent::setState
        kResultOk
    }

    unsafe fn setState(&self, _state: *mut IBStream) -> tresult {
        kResultOk
    }

    unsafe fn getState(&self, _state: *mut IBStream) -> tresult {
        kResultOk
    }

    unsafe fn getParameterCount(&self) -> i32 {
        self.plugin().params().count() as i32
    }

    unsafe fn getParameterInfo(&self, param_index: i32, info: *mut ParameterInfo) -> tresult {
        if info.is_null() {
            return kInvalidArgument;
        }

        let params = self.plugin().params();

        if let Some(param_info) = params.info(param_index as usize) {
            let info = &mut *info;
            info.id = param_info.id;
            copy_wstring(param_info.name, &mut info.title);
            copy_wstring(param_info.short_name, &mut info.shortTitle);
            copy_wstring(param_info.units, &mut info.units);
            info.stepCount = param_info.step_count;
            info.defaultNormalizedValue = param_info.default_normalized;
            info.unitId = param_info.unit_id;
            info.flags = {
                let mut flags = 0;
                if param_info.flags.can_automate {
                    flags |= ParameterInfo_::ParameterFlags_::kCanAutomate;
                }
                if param_info.flags.is_bypass {
                    flags |= ParameterInfo_::ParameterFlags_::kIsBypass;
                }
                flags
            };
            kResultOk
        } else {
            kInvalidArgument
        }
    }

    unsafe fn getParamStringByValue(
        &self,
        id: u32,
        value_normalized: f64,
        string: *mut String128,
    ) -> tresult {
        if string.is_null() {
            return kInvalidArgument;
        }

        let params = self.plugin().params();
        let display = params.normalized_to_string(id, value_normalized);
        copy_wstring(&display, &mut *string);
        kResultOk
    }

    unsafe fn getParamValueByString(
        &self,
        id: u32,
        string: *mut TChar,
        value_normalized: *mut f64,
    ) -> tresult {
        if string.is_null() || value_normalized.is_null() {
            return kInvalidArgument;
        }

        let len = len_wstring(string as *const TChar);
        if let Ok(s) = String::from_utf16(slice::from_raw_parts(string as *const u16, len)) {
            let params = self.plugin().params();
            if let Some(value) = params.string_to_normalized(id, &s) {
                *value_normalized = value;
                return kResultOk;
            }
        }
        kInvalidArgument
    }

    unsafe fn normalizedParamToPlain(&self, id: u32, value_normalized: f64) -> f64 {
        self.plugin().params().normalized_to_plain(id, value_normalized)
    }

    unsafe fn plainParamToNormalized(&self, id: u32, plain_value: f64) -> f64 {
        self.plugin().params().plain_to_normalized(id, plain_value)
    }

    unsafe fn getParamNormalized(&self, id: u32) -> f64 {
        self.plugin().params().get_normalized(id)
    }

    unsafe fn setParamNormalized(&self, id: u32, value: f64) -> tresult {
        self.plugin().params().set_normalized(id, value);
        kResultOk
    }

    unsafe fn setComponentHandler(&self, _handler: *mut IComponentHandler) -> tresult {
        // TODO: Store handler for notifying host of parameter changes from GUI
        kResultOk
    }

    unsafe fn createView(&self, name: *const c_char) -> *mut IPlugView {
        if name.is_null() {
            return std::ptr::null_mut();
        }

        // Check if this is an "editor" view request
        let name_str = std::ffi::CStr::from_ptr(name).to_str().unwrap_or("");
        if name_str != "editor" {
            return std::ptr::null_mut();
        }

        // TODO: Integrate WebView editor via EditorDelegate in Phase 2
        // For now, return null (no editor)
        std::ptr::null_mut()
    }
}

// =============================================================================
// IUnitInfo implementation (VST3 Unit/Group hierarchy)
// =============================================================================

impl<P: Plugin + 'static> IUnitInfoTrait for Vst3Processor<P> {
    unsafe fn getUnitCount(&self) -> i32 {
        use beamer_core::params::Units;
        self.plugin().params().unit_count() as i32
    }

    unsafe fn getUnitInfo(&self, unit_index: i32, info: *mut UnitInfo) -> tresult {
        if info.is_null() || unit_index < 0 {
            return kInvalidArgument;
        }

        use beamer_core::params::Units;
        let params = self.plugin().params();

        if let Some(unit_info) = params.unit_info(unit_index as usize) {
            let info = &mut *info;
            info.id = unit_info.id;
            info.parentUnitId = unit_info.parent_id;
            info.programListId = kNoProgramListId;
            copy_wstring(unit_info.name, &mut info.name);
            kResultOk
        } else {
            kInvalidArgument
        }
    }

    unsafe fn getProgramListCount(&self) -> i32 {
        0 // No program lists (presets) for now
    }

    unsafe fn getProgramListInfo(
        &self,
        _list_index: i32,
        _info: *mut ProgramListInfo,
    ) -> tresult {
        kNotImplemented
    }

    unsafe fn getProgramName(&self, _list_id: i32, _program_index: i32, _name: *mut String128) -> tresult {
        kNotImplemented
    }

    unsafe fn getProgramInfo(
        &self,
        _list_id: i32,
        _program_index: i32,
        _attribute_id: *const c_char,
        _attribute_value: *mut String128,
    ) -> tresult {
        kNotImplemented
    }

    unsafe fn hasProgramPitchNames(&self, _list_id: i32, _program_index: i32) -> tresult {
        kResultFalse
    }

    unsafe fn getProgramPitchName(
        &self,
        _list_id: i32,
        _program_index: i32,
        _midi_pitch: i16,
        _name: *mut String128,
    ) -> tresult {
        kNotImplemented
    }

    unsafe fn getSelectedUnit(&self) -> i32 {
        0 // Return root unit
    }

    unsafe fn selectUnit(&self, _unit_id: i32) -> tresult {
        kResultOk // Accept but ignore unit selection
    }

    unsafe fn getUnitByBus(
        &self,
        _media_type: MediaType,
        _dir: BusDirection,
        _bus_index: i32,
        _channel: i32,
        _unit_id: *mut i32,
    ) -> tresult {
        kNotImplemented
    }

    unsafe fn setUnitProgramData(
        &self,
        _list_or_unit_id: i32,
        _program_index: i32,
        _data: *mut IBStream,
    ) -> tresult {
        kNotImplemented
    }
}

// =============================================================================
// IMidiMapping implementation (VST3 SDK 3.8.0)
// =============================================================================

impl<P: Plugin + 'static> IMidiMappingTrait for Vst3Processor<P> {
    unsafe fn getMidiControllerAssignment(
        &self,
        bus_index: i32,
        channel: i16,
        midi_controller_number: i16,
        id: *mut u32,
    ) -> tresult {
        if id.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        if let Some(param_id) =
            plugin.midi_cc_to_param(bus_index, channel, midi_controller_number as u8)
        {
            *id = param_id;
            kResultOk
        } else {
            kResultFalse
        }
    }
}

// =============================================================================
// IMidiLearn implementation (VST3 SDK 3.8.0)
// =============================================================================

impl<P: Plugin + 'static> IMidiLearnTrait for Vst3Processor<P> {
    unsafe fn onLiveMIDIControllerInput(
        &self,
        bus_index: i32,
        channel: i16,
        midi_cc: i16,
    ) -> tresult {
        let plugin = self.plugin_mut();
        if plugin.on_midi_learn(bus_index, channel, midi_cc as u8) {
            kResultOk
        } else {
            kResultFalse
        }
    }
}

// =============================================================================
// IMidiMapping2 implementation (VST3 SDK 3.8.0 - MIDI 2.0)
// =============================================================================

impl<P: Plugin + 'static> IMidiMapping2Trait for Vst3Processor<P> {
    unsafe fn getNumMidi1ControllerAssignments(&self, direction: BusDirections) -> u32 {
        // Only support input direction
        if direction != BusDirections_::kInput {
            return 0;
        }
        let plugin = self.plugin();
        plugin.midi1_assignments().len() as u32
    }

    unsafe fn getMidi1ControllerAssignments(
        &self,
        direction: BusDirections,
        list: *const Midi1ControllerParamIDAssignmentList,
    ) -> tresult {
        if list.is_null() || direction != BusDirections_::kInput {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        let assignments = plugin.midi1_assignments();
        let list_ref = &*list;

        if (list_ref.count as usize) < assignments.len() {
            return kResultFalse;
        }

        if assignments.is_empty() {
            return kResultOk;
        }

        let map = slice::from_raw_parts_mut(list_ref.map, assignments.len());
        for (i, a) in assignments.iter().enumerate() {
            map[i] = Midi1ControllerParamIDAssignment {
                pId: a.assignment.param_id,
                busIndex: a.assignment.bus_index,
                channel: a.assignment.channel,
                controller: a.controller as i16,
            };
        }

        kResultOk
    }

    unsafe fn getNumMidi2ControllerAssignments(&self, direction: BusDirections) -> u32 {
        // Only support input direction
        if direction != BusDirections_::kInput {
            return 0;
        }
        let plugin = self.plugin();
        plugin.midi2_assignments().len() as u32
    }

    unsafe fn getMidi2ControllerAssignments(
        &self,
        direction: BusDirections,
        list: *const Midi2ControllerParamIDAssignmentList,
    ) -> tresult {
        if list.is_null() || direction != BusDirections_::kInput {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        let assignments = plugin.midi2_assignments();
        let list_ref = &*list;

        if (list_ref.count as usize) < assignments.len() {
            return kResultFalse;
        }

        if assignments.is_empty() {
            return kResultOk;
        }

        let map = slice::from_raw_parts_mut(list_ref.map, assignments.len());
        for (i, a) in assignments.iter().enumerate() {
            map[i] = Midi2ControllerParamIDAssignment {
                pId: a.assignment.param_id,
                busIndex: a.assignment.bus_index,
                channel: a.assignment.channel,
                controller: Midi2Controller {
                    bank: a.controller.bank,
                    registered: if a.controller.registered { 1 } else { 0 },
                    index: a.controller.index,
                    reserved: 0,
                },
            };
        }

        kResultOk
    }
}

// =============================================================================
// IMidiLearn2 implementation (VST3 SDK 3.8.0 - MIDI 2.0)
// =============================================================================

impl<P: Plugin + 'static> IMidiLearn2Trait for Vst3Processor<P> {
    unsafe fn onLiveMidi1ControllerInput(
        &self,
        bus_index: i32,
        channel: u8,
        midi_cc: i16,
    ) -> tresult {
        let plugin = self.plugin_mut();
        if plugin.on_midi1_learn(bus_index, channel, midi_cc as u8) {
            kResultOk
        } else {
            kResultFalse
        }
    }

    unsafe fn onLiveMidi2ControllerInput(
        &self,
        bus_index: i32,
        channel: u8,
        midi_cc: Midi2Controller,
    ) -> tresult {
        let plugin = self.plugin_mut();
        let controller = beamer_core::Midi2Controller {
            bank: midi_cc.bank,
            registered: midi_cc.registered != 0,
            index: midi_cc.index,
        };
        if plugin.on_midi2_learn(bus_index, channel, controller) {
            kResultOk
        } else {
            kResultFalse
        }
    }
}

// =============================================================================
// INoteExpressionController implementation (VST3 SDK 3.5.0)
// =============================================================================

impl<P: Plugin + 'static> INoteExpressionControllerTrait for Vst3Processor<P> {
    unsafe fn getNoteExpressionCount(&self, bus_index: i32, channel: i16) -> i32 {
        let plugin = self.plugin();
        plugin.note_expression_count(bus_index, channel) as i32
    }

    unsafe fn getNoteExpressionInfo(
        &self,
        bus_index: i32,
        channel: i16,
        note_expression_index: i32,
        info: *mut NoteExpressionTypeInfo,
    ) -> tresult {
        if info.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        if let Some(expr_info) =
            plugin.note_expression_info(bus_index, channel, note_expression_index as usize)
        {
            let vst_info = &mut *info;
            vst_info.typeId = expr_info.type_id;
            copy_wstring(expr_info.title_str(), &mut vst_info.title);
            copy_wstring(expr_info.short_title_str(), &mut vst_info.shortTitle);
            copy_wstring(expr_info.units_str(), &mut vst_info.units);
            vst_info.unitId = expr_info.unit_id;
            vst_info.valueDesc.minimum = expr_info.value_desc.minimum;
            vst_info.valueDesc.maximum = expr_info.value_desc.maximum;
            vst_info.valueDesc.defaultValue = expr_info.value_desc.default_value;
            vst_info.valueDesc.stepCount = expr_info.value_desc.step_count;
            vst_info.associatedParameterId = expr_info.associated_parameter_id as u32;
            vst_info.flags = expr_info.flags.0;
            kResultOk
        } else {
            kInvalidArgument
        }
    }

    unsafe fn getNoteExpressionStringByValue(
        &self,
        bus_index: i32,
        channel: i16,
        id: NoteExpressionTypeID,
        value_normalized: NoteExpressionValue,
        string: *mut String128,
    ) -> tresult {
        if string.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        let display = plugin.note_expression_value_to_string(bus_index, channel, id, value_normalized);
        copy_wstring(&display, &mut *string);
        kResultOk
    }

    unsafe fn getNoteExpressionValueByString(
        &self,
        bus_index: i32,
        channel: i16,
        id: NoteExpressionTypeID,
        string: *const TChar,
        value_normalized: *mut NoteExpressionValue,
    ) -> tresult {
        if string.is_null() || value_normalized.is_null() {
            return kInvalidArgument;
        }

        let len = len_wstring(string);
        if let Ok(s) = String::from_utf16(slice::from_raw_parts(string, len)) {
            let plugin = self.plugin();
            if let Some(value) = plugin.note_expression_string_to_value(bus_index, channel, id, &s)
            {
                *value_normalized = value;
                return kResultOk;
            }
        }
        kResultFalse
    }
}

// =============================================================================
// IKeyswitchController implementation (VST3 SDK 3.5.0)
// =============================================================================

impl<P: Plugin + 'static> IKeyswitchControllerTrait for Vst3Processor<P> {
    unsafe fn getKeyswitchCount(&self, bus_index: i32, channel: i16) -> i32 {
        let plugin = self.plugin();
        plugin.keyswitch_count(bus_index, channel) as i32
    }

    unsafe fn getKeyswitchInfo(
        &self,
        bus_index: i32,
        channel: i16,
        keyswitch_index: i32,
        info: *mut KeyswitchInfo,
    ) -> tresult {
        if info.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        if let Some(ks_info) =
            plugin.keyswitch_info(bus_index, channel, keyswitch_index as usize)
        {
            let vst_info = &mut *info;
            vst_info.typeId = ks_info.type_id;
            copy_wstring(ks_info.title_str(), &mut vst_info.title);
            copy_wstring(ks_info.short_title_str(), &mut vst_info.shortTitle);
            vst_info.keyswitchMin = ks_info.keyswitch_min;
            vst_info.keyswitchMax = ks_info.keyswitch_max;
            vst_info.keyRemapped = ks_info.key_remapped;
            vst_info.unitId = ks_info.unit_id;
            vst_info.flags = ks_info.flags;
            kResultOk
        } else {
            kInvalidArgument
        }
    }
}

// =============================================================================
// INoteExpressionPhysicalUIMapping implementation (VST3 SDK 3.6.11)
// =============================================================================

impl<P: Plugin + 'static> INoteExpressionPhysicalUIMappingTrait for Vst3Processor<P> {
    unsafe fn getPhysicalUIMapping(
        &self,
        bus_index: i32,
        channel: i16,
        list: *mut PhysicalUIMapList,
    ) -> tresult {
        if list.is_null() {
            return kInvalidArgument;
        }

        let plugin = self.plugin();
        let mappings = plugin.physical_ui_mappings(bus_index, channel);
        let list_ref = &mut *list;

        // Fill in the mappings up to the provided count
        let fill_count = (list_ref.count as usize).min(mappings.len());
        if fill_count > 0 && !list_ref.map.is_null() {
            let map_slice = slice::from_raw_parts_mut(list_ref.map, fill_count);
            for (i, mapping) in mappings.iter().take(fill_count).enumerate() {
                map_slice[i].physicalUITypeID = mapping.physical_ui_type_id;
                map_slice[i].noteExpressionTypeID = mapping.note_expression_type_id;
            }
        }

        kResultOk
    }
}

// =============================================================================
// IVst3WrapperMPESupport implementation (VST3 SDK 3.6.12)
// =============================================================================

impl<P: Plugin + 'static> IVst3WrapperMPESupportTrait for Vst3Processor<P> {
    unsafe fn enableMPEInputProcessing(&self, state: TBool) -> tresult {
        let plugin = self.plugin_mut();
        if plugin.enable_mpe_input_processing(state != 0) {
            kResultOk
        } else {
            kResultFalse
        }
    }

    unsafe fn setMPEInputDeviceSettings(
        &self,
        master_channel: i32,
        member_begin_channel: i32,
        member_end_channel: i32,
    ) -> tresult {
        let plugin = self.plugin_mut();
        let settings = beamer_core::MpeInputDeviceSettings {
            master_channel,
            member_begin_channel,
            member_end_channel,
        };
        if plugin.set_mpe_input_device_settings(settings) {
            kResultOk
        } else {
            kResultFalse
        }
    }
}

// =============================================================================
// Helper functions
// =============================================================================

/// Convert UTF-16 slice to UTF-8, writing into a fixed-size buffer.
///
/// Returns the number of bytes written. Handles BMP characters (most common)
/// and replaces non-BMP surrogates with replacement character.
fn utf16_to_utf8(utf16: &[u16], utf8_buf: &mut [u8]) -> usize {
    let mut utf8_pos = 0;

    for &code_unit in utf16 {
        if code_unit == 0 {
            // Null terminator
            break;
        }

        // Calculate how many UTF-8 bytes this character needs
        let (bytes_needed, char_value) = if code_unit < 0x80 {
            // ASCII (1 byte)
            (1, code_unit as u32)
        } else if code_unit < 0x800 {
            // 2-byte UTF-8
            (2, code_unit as u32)
        } else if (0xD800..=0xDFFF).contains(&code_unit) {
            // Surrogate pair (non-BMP) - simplified: use replacement char
            // Full implementation would need to look ahead for the low surrogate
            (3, 0xFFFD) // Unicode replacement character
        } else {
            // 3-byte UTF-8 (most non-ASCII BMP characters)
            (3, code_unit as u32)
        };

        // Check if we have room
        if utf8_pos + bytes_needed > utf8_buf.len() {
            break;
        }

        // Encode to UTF-8
        match bytes_needed {
            1 => {
                utf8_buf[utf8_pos] = char_value as u8;
            }
            2 => {
                utf8_buf[utf8_pos] = (0xC0 | (char_value >> 6)) as u8;
                utf8_buf[utf8_pos + 1] = (0x80 | (char_value & 0x3F)) as u8;
            }
            3 => {
                utf8_buf[utf8_pos] = (0xE0 | (char_value >> 12)) as u8;
                utf8_buf[utf8_pos + 1] = (0x80 | ((char_value >> 6) & 0x3F)) as u8;
                utf8_buf[utf8_pos + 2] = (0x80 | (char_value & 0x3F)) as u8;
            }
            _ => unreachable!(),
        }

        utf8_pos += bytes_needed;
    }

    utf8_pos
}

/// Convert a channel count to the corresponding VST3 speaker arrangement.
fn channel_count_to_speaker_arrangement(channel_count: u32) -> SpeakerArrangement {
    match channel_count {
        1 => SpeakerArr::kMono,
        2 => SpeakerArr::kStereo,
        // For other channel counts, create a bitmask with that many speakers
        n => (1u64 << n) - 1,
    }
}

/// Convert a VST3 Event to a MIDI event.
///
/// Returns None for unsupported event types.
unsafe fn convert_vst3_to_midi(event: &Event) -> Option<MidiEvent> {
    let sample_offset = event.sampleOffset as u32;

    match event.r#type {
        K_NOTE_ON_EVENT => {
            let note_on = &event.__field0.noteOn;
            Some(MidiEvent::note_on(
                sample_offset,
                note_on.channel as u8,
                note_on.pitch as u8,
                note_on.velocity,
                note_on.noteId,
                note_on.tuning,
                note_on.length,
            ))
        }
        K_NOTE_OFF_EVENT => {
            let note_off = &event.__field0.noteOff;
            Some(MidiEvent::note_off(
                sample_offset,
                note_off.channel as u8,
                note_off.pitch as u8,
                note_off.velocity,
                note_off.noteId,
                note_off.tuning,
            ))
        }
        K_POLY_PRESSURE_EVENT => {
            let poly = &event.__field0.polyPressure;
            Some(MidiEvent::poly_pressure(
                sample_offset,
                poly.channel as u8,
                poly.pitch as u8,
                poly.pressure,
                poly.noteId,
            ))
        }
        K_DATA_EVENT => {
            let data_event = &event.__field0.data;
            // Only handle SysEx data type
            if data_event.r#type == DATA_TYPE_MIDI_SYSEX {
                let mut sysex = SysEx::new();
                let copy_len = (data_event.size as usize).min(MAX_SYSEX_SIZE);
                if copy_len > 0 && !data_event.bytes.is_null() {
                    let src = std::slice::from_raw_parts(data_event.bytes, copy_len);
                    sysex.data[..copy_len].copy_from_slice(src);
                    sysex.len = copy_len as u16;
                }
                Some(MidiEvent {
                    sample_offset,
                    event: MidiEventKind::SysEx(Box::new(sysex)),
                })
            } else {
                None
            }
        }
        K_NOTE_EXPRESSION_VALUE_EVENT => {
            let expr = &event.__field0.noteExpressionValue;
            Some(MidiEvent {
                sample_offset,
                event: MidiEventKind::NoteExpressionValue(CoreNoteExpressionValue {
                    note_id: expr.noteId,
                    expression_type: expr.typeId,
                    value: expr.value,
                }),
            })
        }
        K_NOTE_EXPRESSION_INT_VALUE_EVENT => {
            let expr = &event.__field0.noteExpressionIntValue;
            Some(MidiEvent {
                sample_offset,
                event: MidiEventKind::NoteExpressionInt(NoteExpressionInt {
                    note_id: expr.noteId,
                    expression_type: expr.typeId,
                    value: expr.value,
                }),
            })
        }
        K_NOTE_EXPRESSION_TEXT_EVENT => {
            let expr = &event.__field0.noteExpressionText;
            let mut text_event = NoteExpressionText {
                note_id: expr.noteId,
                expression_type: expr.typeId,
                text: [0u8; MAX_EXPRESSION_TEXT_SIZE],
                text_len: 0,
            };
            // Convert UTF-16 to UTF-8
            let text_len = expr.textLen as usize;
            if !expr.text.is_null() && text_len > 0 {
                let text_slice = std::slice::from_raw_parts(expr.text, text_len);
                let utf8_len = utf16_to_utf8(text_slice, &mut text_event.text);
                text_event.text_len = utf8_len as u8;
            }
            Some(MidiEvent {
                sample_offset,
                event: MidiEventKind::NoteExpressionText(text_event),
            })
        }
        K_CHORD_EVENT => {
            let chord = &event.__field0.chord;
            let mut info = ChordInfo {
                root: chord.root as i8,
                bass_note: chord.bassNote as i8,
                mask: chord.mask as u16,
                name: [0u8; MAX_CHORD_NAME_SIZE],
                name_len: 0,
            };
            // Convert UTF-16 to UTF-8
            let text_len = chord.textLen as usize;
            if !chord.text.is_null() && text_len > 0 {
                let text_slice = std::slice::from_raw_parts(chord.text, text_len);
                let utf8_len = utf16_to_utf8(text_slice, &mut info.name);
                info.name_len = utf8_len as u8;
            }
            Some(MidiEvent {
                sample_offset,
                event: MidiEventKind::ChordInfo(info),
            })
        }
        K_SCALE_EVENT => {
            let scale = &event.__field0.scale;
            let mut info = ScaleInfo {
                root: scale.root as i8,
                mask: scale.mask as u16,
                name: [0u8; MAX_SCALE_NAME_SIZE],
                name_len: 0,
            };
            // Convert UTF-16 to UTF-8
            let text_len = scale.textLen as usize;
            if !scale.text.is_null() && text_len > 0 {
                let text_slice = std::slice::from_raw_parts(scale.text, text_len);
                let utf8_len = utf16_to_utf8(text_slice, &mut info.name);
                info.name_len = utf8_len as u8;
            }
            Some(MidiEvent {
                sample_offset,
                event: MidiEventKind::ScaleInfo(info),
            })
        }
        K_LEGACY_MIDI_CC_OUT_EVENT => {
            let cc_event = &event.__field0.midiCCOut;
            let channel = cc_event.channel as u8;

            match cc_event.controlNumber {
                0..=127 => {
                    // Standard Control Change
                    Some(MidiEvent::control_change(
                        sample_offset,
                        channel,
                        cc_event.controlNumber,
                        cc_event.value as f32 / 127.0,
                    ))
                }
                LEGACY_CC_CHANNEL_PRESSURE => Some(MidiEvent::channel_pressure(
                    sample_offset,
                    channel,
                    cc_event.value as f32 / 127.0,
                )),
                LEGACY_CC_PITCH_BEND => {
                    // Pitch bend: 14-bit value split across value (LSB) and value2 (MSB)
                    // Cast to u8 first to avoid sign extension issues
                    let lsb = (cc_event.value as u8) as u16;
                    let msb = (cc_event.value2 as u8) as u16;
                    let raw = (msb << 7) | (lsb & 0x7F);
                    let normalized = (raw as f32 - 8192.0) / 8192.0;
                    Some(MidiEvent::pitch_bend(sample_offset, channel, normalized))
                }
                LEGACY_CC_PROGRAM_CHANGE => Some(MidiEvent::program_change(
                    sample_offset,
                    channel,
                    cc_event.value as u8,
                )),
                _ => None, // Unknown control number
            }
        }
        _ => None, // Unsupported event type
    }
}

/// Convert a MIDI event to a VST3 Event.
///
/// The `sysex_pool` parameter provides stable storage for SysEx data during the
/// process() call, ensuring the pointers remain valid until the host processes them.
///
/// Note: ChordInfo, ScaleInfo, and NoteExpressionText are primarily input events
/// (DAW → plugin) and are not output.
fn convert_midi_to_vst3(midi: &MidiEvent, sysex_pool: &mut SysExOutputPool) -> Option<Event> {
    let mut event: Event = unsafe { std::mem::zeroed() };
    event.busIndex = 0;
    event.sampleOffset = midi.sample_offset as i32;
    event.ppqPosition = 0.0;
    event.flags = 0;

    // Note: Writing to union fields is safe in Rust, only reading requires unsafe
    match &midi.event {
        MidiEventKind::NoteOn(note_on) => {
            event.r#type = K_NOTE_ON_EVENT;
            event.__field0.noteOn.channel = note_on.channel as i16;
            event.__field0.noteOn.pitch = note_on.pitch as i16;
            event.__field0.noteOn.velocity = note_on.velocity;
            event.__field0.noteOn.noteId = note_on.note_id;
            event.__field0.noteOn.tuning = note_on.tuning;
            event.__field0.noteOn.length = note_on.length;
        }
        MidiEventKind::NoteOff(note_off) => {
            event.r#type = K_NOTE_OFF_EVENT;
            event.__field0.noteOff.channel = note_off.channel as i16;
            event.__field0.noteOff.pitch = note_off.pitch as i16;
            event.__field0.noteOff.velocity = note_off.velocity;
            event.__field0.noteOff.noteId = note_off.note_id;
            event.__field0.noteOff.tuning = note_off.tuning;
        }
        MidiEventKind::PolyPressure(poly) => {
            event.r#type = K_POLY_PRESSURE_EVENT;
            event.__field0.polyPressure.channel = poly.channel as i16;
            event.__field0.polyPressure.pitch = poly.pitch as i16;
            event.__field0.polyPressure.pressure = poly.pressure;
            event.__field0.polyPressure.noteId = poly.note_id;
        }
        MidiEventKind::ControlChange(cc) => {
            event.r#type = K_LEGACY_MIDI_CC_OUT_EVENT;
            event.__field0.midiCCOut.controlNumber = cc.controller;
            event.__field0.midiCCOut.channel = cc.channel as i8;
            event.__field0.midiCCOut.value = (cc.value * 127.0) as i8;
            event.__field0.midiCCOut.value2 = 0;
        }
        MidiEventKind::PitchBend(pb) => {
            event.r#type = K_LEGACY_MIDI_CC_OUT_EVENT;
            event.__field0.midiCCOut.controlNumber = LEGACY_CC_PITCH_BEND;
            event.__field0.midiCCOut.channel = pb.channel as i8;
            // Convert -1.0..1.0 to 14-bit value (0-16383, center at 8192)
            let raw = ((pb.value * 8192.0) + 8192.0).clamp(0.0, 16383.0) as i16;
            event.__field0.midiCCOut.value = (raw & 0x7F) as i8;
            event.__field0.midiCCOut.value2 = ((raw >> 7) & 0x7F) as i8;
        }
        MidiEventKind::ChannelPressure(cp) => {
            event.r#type = K_LEGACY_MIDI_CC_OUT_EVENT;
            event.__field0.midiCCOut.controlNumber = LEGACY_CC_CHANNEL_PRESSURE;
            event.__field0.midiCCOut.channel = cp.channel as i8;
            event.__field0.midiCCOut.value = (cp.pressure * 127.0) as i8;
            event.__field0.midiCCOut.value2 = 0;
        }
        MidiEventKind::ProgramChange(pc) => {
            event.r#type = K_LEGACY_MIDI_CC_OUT_EVENT;
            event.__field0.midiCCOut.controlNumber = LEGACY_CC_PROGRAM_CHANGE;
            event.__field0.midiCCOut.channel = pc.channel as i8;
            event.__field0.midiCCOut.value = pc.program as i8;
            event.__field0.midiCCOut.value2 = 0;
        }
        MidiEventKind::NoteExpressionValue(expr) => {
            event.r#type = K_NOTE_EXPRESSION_VALUE_EVENT;
            event.__field0.noteExpressionValue.noteId = expr.note_id;
            event.__field0.noteExpressionValue.typeId = expr.expression_type;
            event.__field0.noteExpressionValue.value = expr.value;
        }
        MidiEventKind::NoteExpressionInt(expr) => {
            event.r#type = K_NOTE_EXPRESSION_INT_VALUE_EVENT;
            event.__field0.noteExpressionIntValue.noteId = expr.note_id;
            event.__field0.noteExpressionIntValue.typeId = expr.expression_type;
            event.__field0.noteExpressionIntValue.value = expr.value;
        }
        MidiEventKind::SysEx(sysex) => {
            // Allocate a slot in the pool for stable pointer storage
            if let Some((ptr, len)) = sysex_pool.allocate(sysex.as_slice()) {
                event.r#type = K_DATA_EVENT;
                event.__field0.data.r#type = DATA_TYPE_MIDI_SYSEX;
                event.__field0.data.size = len as u32;
                event.__field0.data.bytes = ptr;
            } else {
                // Pool is full, drop this SysEx
                return None;
            }
        }
        // ChordInfo/ScaleInfo are DAW → plugin only (chord track metadata).
        // Plugins receive these from the DAW but don't generate them.
        MidiEventKind::ChordInfo(_) => return None,
        MidiEventKind::ScaleInfo(_) => return None,

        // TODO: NoteExpressionText output not yet implemented.
        // Some vocal/granular synths emit phoneme or waveform text data.
        // Implementation would require a UTF-8→UTF-16 buffer pool (like SysEx)
        // to provide stable pointers for the host. Low priority but valid use case.
        MidiEventKind::NoteExpressionText(_) => return None,
    }

    Some(event)
}
