//! Raw FFI bindings to a statically-linked SuperCollider `libscsynth`.
//!
//! The bindings are generated at build time by `bindgen` over `SC_WorldOptions.h` and included
//! below. See `build.rs` for the CMake build of `libscsynth`, the `WorldOptions` default shim,
//! and the link configuration.
//!
//! This crate is the only place `unsafe` FFI lives; safe abstractions belong in the `scsynth`
//! crate.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]

use core::ffi::{c_int, c_uint, c_void};

// On wasm32 the C/C++ engine is linked against a from-source musl libc (see `build.rs`). Pulling the
// crate in with `extern crate` forces its malloc/free/etc. to be linked even though Rust references
// none of its items directly.
#[cfg(target_arch = "wasm32")]
extern crate scsynth_wasm32_libc;

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

unsafe extern "C" {
    /// Fill `out` with a default-constructed `WorldOptions`.
    ///
    /// `WorldOptions` has a C++ constructor and default member initializers that `bindgen`
    /// cannot reproduce, so this shim (see `csrc/shim.cpp`) constructs one C++-side. Compiled on
    /// both targets.
    pub fn scsynth_default_world_options(out: *mut WorldOptions);

    /// Read back the reply context (`mReplyData`) stored on a [`ReplyAddress`].
    ///
    /// `ReplyAddress` is opaque to Rust (it embeds a `boost::asio` address on native), so reply
    /// functions cannot name its fields. This shim (see `csrc/shim.cpp`) returns the `void*` context
    /// the safe wrapper handed to `World_SendPacketWithContext` (native) / `scsynth_wasm_perform`
    /// (wasm); node replies like `/n_end` carry the same context because the packet's reply address
    /// is copied onto the nodes it creates. Declared by hand on both targets because it is a shim
    /// symbol, not part of `SC_WorldOptions.h`'s include graph. Returns null for a null `addr`.
    pub fn scsynth_reply_context(addr: *mut ReplyAddress) -> *mut c_void;

    /// Copy soundbuffer `index` out of the engine, handing its samples to `fn` (with context `ctx`).
    ///
    /// Wraps `World_CopySndBuf` (see `csrc/shim.cpp`): it snapshots the buffer under the engine's NRT
    /// lock into engine-owned, scsynth-aligned memory, which must not cross into Rust to be freed
    /// (Rust's allocator differs - and on wasm the C and Rust heaps are wholly separate). So the
    /// callback copies the samples out while that memory is still alive, and the shim frees it before
    /// returning. `fn` is invoked at most once, only on success, with the interleaved sample data,
    /// the total sample count (`num_channels * num_frames`), the channel count and the frame count.
    /// Returns the engine's `SCErr` (`0` = success; non-zero, e.g. index-out-of-range, leaves `fn`
    /// uncalled). Declared by hand on both targets because it is a shim symbol, not part of
    /// `SC_WorldOptions.h`'s include graph.
    pub fn scsynth_copy_buffer(
        world: *mut World,
        index: c_uint,
        func: ScBufferFunc,
        ctx: *mut c_void,
    ) -> c_int;
}

/// Callback receiving a copied soundbuffer's interleaved samples (see [`scsynth_copy_buffer`]).
pub type ScBufferFunc = Option<
    unsafe extern "C" fn(
        ctx: *mut c_void,
        data: *const f32,
        num_samples: c_int,
        num_channels: c_int,
        num_frames: c_int,
    ),
>;

unsafe extern "C" {
    /// Install (`func = Some`) or clear (`func = None`) a process-global sink for scsynth's
    /// diagnostic log output.
    ///
    /// scsynth prints via `scprintf`, which dispatches to a single process-global function pointer
    /// (`gPrint`) carrying no per-call context, so - unlike the reply path - log capture cannot be
    /// per-`World`. This shim (see `csrc/shim.cpp`) installs a trampoline that formats each message
    /// and forwards the bytes to `func` (with context `ctx`) under a mutex (the message arrives from
    /// both the RT and NRT threads). Passing `None` restores the engine's default (stdout). Declared
    /// by hand on both targets because it is a shim symbol, not part of `SC_WorldOptions.h`.
    pub fn scsynth_set_log_func(func: ScLogFunc, ctx: *mut c_void);
}

/// Callback receiving one formatted scsynth log message (see [`scsynth_set_log_func`]). `text` is
/// `len` bytes, valid only for the duration of the call.
pub type ScLogFunc =
    Option<unsafe extern "C" fn(ctx: *mut c_void, text: *const core::ffi::c_char, len: c_int)>;

// Native uses SuperCollider's host-pumped external driver (`csrc/SC_ExternalDriver.cpp`), which
// requires a realtime `World` and a running scheduler.
#[cfg(not(target_arch = "wasm32"))]
unsafe extern "C" {
    /// Run `num_frames` of synthesis through the world's host-pumped external driver, reading
    /// interleaved `input` and writing interleaved `output`. Provided by `csrc/SC_ExternalDriver.cpp`
    /// (compiled into `libscsynth` via the `AUDIOAPI=external` build patch). Requires a realtime
    /// `World`. `num_frames` should be a whole multiple of the world's block size.
    pub fn scsynth_pump(
        world: *mut World,
        input: *const f32,
        num_input_channels: c_int,
        output: *mut f32,
        num_output_channels: c_int,
        num_frames: c_int,
    );
}

// On wasm there is no audio device or thread, so the engine runs driverless (`mRealTime=false`) and
// is driven synchronously by the pump shim (`csrc/SC_WasmPump.cpp`). `scsynth_wasm_new` builds the
// world from a `WorldOptions` (forcing `mRealTime=false`/`mLoadGraphDefs=false`); `scsynth_wasm_perform`
// feeds one OSC packet (it copies the buffer internally); `scsynth_wasm_pump` renders audio.
#[cfg(target_arch = "wasm32")]
unsafe extern "C" {
    /// Tear down a `World` created by [`scsynth_wasm_new`]. Declared by hand here because libclang
    /// does not surface the `extern "C"` `World_*` prototypes to bindgen under the wasm32 target
    /// (see `build.rs`); the engine still exports this symbol from the compiled SC sources. Mirrors
    /// the bindgen-generated native signature (`unload_plugins` has no C++ default across FFI).
    pub fn World_Cleanup(world: *mut World, unload_plugins: bool);

    /// Create a driverless, synchronous `World` from `options`. The shim forces `mRealTime=false`
    /// and `mLoadGraphDefs=false`, then starts the world. Returns null on failure.
    pub fn scsynth_wasm_new(options: *mut WorldOptions) -> *mut World;

    /// Feed one OSC packet (message or bundle) synchronously; `/d_recv` and `/s_new` complete in
    /// full before returning. The shim copies `data` internally, so the buffer need not outlive the
    /// call. `reply_func` (with `reply_ctx` as its context) receives any `/done`/`/fail`/`/n_end`
    /// replies emitted inline during processing; pass `None`/null to drop them.
    pub fn scsynth_wasm_perform(
        world: *mut World,
        data: *const u8,
        size: c_int,
        reply_func: ReplyFunc,
        reply_ctx: *mut c_void,
    );

    /// Render `nframes` of synthesis, reading interleaved `input` and writing interleaved `output`
    /// (same signature/semantics as `scsynth_pump`, but driverless).
    pub fn scsynth_wasm_pump(
        world: *mut World,
        input: *const f32,
        num_input_channels: c_int,
        output: *mut f32,
        num_output_channels: c_int,
        num_frames: c_int,
    );
}
