use crate::midi::io::MidiEvent;
use crate::mutex::UnsafeMutex;
#[cfg(any(
    target_os = "macos",
    target_os = "linux",
    target_os = "freebsd",
    target_os = "openbsd"
))]
use crate::plugins::paths;
use libloading::Library;
use serde::{Deserialize, Serialize};
use std::ffi::{CStr, CString, c_char, c_void};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};

#[derive(Clone, Debug, PartialEq)]
pub struct ClapParameterInfo {
    pub id: u32,
    pub name: String,
    pub module: String,
    pub min_value: f64,
    pub max_value: f64,
    pub default_value: f64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginState {
    pub bytes: Vec<u8>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClapMidiOutputEvent {
    pub port: usize,
    pub event: MidiEvent,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct ClapTransportInfo {
    pub transport_sample: usize,
    pub playing: bool,
    pub loop_enabled: bool,
    pub loop_range_samples: Option<(usize, usize)>,
    pub bpm: f64,
    pub tsig_num: u16,
    pub tsig_denom: u16,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClapGuiInfo {
    pub api: String,
    pub supports_embedded: bool,
}

#[derive(Clone, Copy, Debug)]
pub struct ClapParamUpdate {
    pub param_id: u32,
    pub value: f64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginInfo {
    pub name: String,
    pub path: String,
    pub capabilities: Option<ClapPluginCapabilities>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClapPluginCapabilities {
    pub has_gui: bool,
    pub gui_apis: Vec<String>,
    pub supports_embedded: bool,
    pub supports_floating: bool,
    pub has_params: bool,
    pub has_state: bool,
    pub audio_inputs: usize,
    pub audio_outputs: usize,
    pub midi_inputs: usize,
    pub midi_outputs: usize,
}

#[repr(C)]
#[derive(Clone, Copy)]
struct ClapVersion {
    major: u32,
    minor: u32,
    revision: u32,
}

const CLAP_VERSION: ClapVersion = ClapVersion {
    major: 1,
    minor: 2,
    revision: 0,
};

#[repr(C)]
struct ClapHost {
    clap_version: ClapVersion,
    host_data: *mut c_void,
    name: *const c_char,
    vendor: *const c_char,
    url: *const c_char,
    version: *const c_char,
    get_extension: Option<unsafe extern "C" fn(*const ClapHost, *const c_char) -> *const c_void>,
    request_restart: Option<unsafe extern "C" fn(*const ClapHost)>,
    request_process: Option<unsafe extern "C" fn(*const ClapHost)>,
    request_callback: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapPluginEntry {
    clap_version: ClapVersion,
    init: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
    deinit: Option<unsafe extern "C" fn()>,
    get_factory: Option<unsafe extern "C" fn(*const c_char) -> *const c_void>,
}

#[repr(C)]
struct ClapPluginFactory {
    get_plugin_count: Option<unsafe extern "C" fn(*const ClapPluginFactory) -> u32>,
    get_plugin_descriptor:
        Option<unsafe extern "C" fn(*const ClapPluginFactory, u32) -> *const ClapPluginDescriptor>,
    create_plugin: Option<
        unsafe extern "C" fn(
            *const ClapPluginFactory,
            *const ClapHost,
            *const c_char,
        ) -> *const ClapPlugin,
    >,
}

#[repr(C)]
struct ClapPluginDescriptor {
    clap_version: ClapVersion,
    id: *const c_char,
    name: *const c_char,
    vendor: *const c_char,
    url: *const c_char,
    manual_url: *const c_char,
    support_url: *const c_char,
    version: *const c_char,
    description: *const c_char,
    features: *const *const c_char,
}

#[repr(C)]
struct ClapPlugin {
    desc: *const ClapPluginDescriptor,
    plugin_data: *mut c_void,
    init: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
    activate: Option<unsafe extern "C" fn(*const ClapPlugin, f64, u32, u32) -> bool>,
    deactivate: Option<unsafe extern "C" fn(*const ClapPlugin)>,
    start_processing: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
    stop_processing: Option<unsafe extern "C" fn(*const ClapPlugin)>,
    reset: Option<unsafe extern "C" fn(*const ClapPlugin)>,
    process: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapProcess) -> i32>,
    get_extension: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char) -> *const c_void>,
    on_main_thread: Option<unsafe extern "C" fn(*const ClapPlugin)>,
}

#[repr(C)]
struct ClapInputEvents {
    ctx: *const c_void,
    size: Option<unsafe extern "C" fn(*const ClapInputEvents) -> u32>,
    get: Option<unsafe extern "C" fn(*const ClapInputEvents, u32) -> *const ClapEventHeader>,
}

#[repr(C)]
struct ClapOutputEvents {
    ctx: *mut c_void,
    try_push: Option<unsafe extern "C" fn(*const ClapOutputEvents, *const ClapEventHeader) -> bool>,
}

#[repr(C)]
struct ClapEventHeader {
    size: u32,
    time: u32,
    space_id: u16,
    type_: u16,
    flags: u32,
}

#[repr(C)]
struct ClapAudioPortInfoRaw {
    id: u32,
    name: [c_char; 256],
    flags: u32,
    channel_count: u32,
    port_type: *const c_char,
    in_place_pair: u32,
}

#[repr(C)]
struct ClapPluginAudioPorts {
    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
    get: Option<
        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapAudioPortInfoRaw) -> bool,
    >,
}

#[repr(C)]
struct ClapNotePortInfoRaw {
    id: u16,
    supported_dialects: u32,
    preferred_dialect: u32,
    name: [c_char; 256],
}

#[repr(C)]
struct ClapPluginNotePorts {
    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
    get: Option<
        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapNotePortInfoRaw) -> bool,
    >,
}

#[repr(C)]
struct ClapPluginGui {
    is_api_supported: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
    get_preferred_api:
        Option<unsafe extern "C" fn(*const ClapPlugin, *mut *const c_char, *mut bool) -> bool>,
    create: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
    set_scale: Option<unsafe extern "C" fn(*const ClapPlugin, f64) -> bool>,
    get_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
    can_resize: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
    get_resize_hints: Option<unsafe extern "C" fn(*const ClapPlugin, *mut c_void) -> bool>,
    adjust_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
    set_size: Option<unsafe extern "C" fn(*const ClapPlugin, u32, u32) -> bool>,
    set_parent: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
    set_transient: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
    suggest_title: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char)>,
    show: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
    hide: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
}

#[repr(C)]
union ClapWindowHandle {
    x11: usize,
    native: *mut c_void,
    cocoa: *mut c_void,
}

#[repr(C)]
struct ClapWindow {
    api: *const c_char,
    handle: ClapWindowHandle,
}

#[repr(C)]
struct ClapHostThreadCheck {
    is_main_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
    is_audio_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
}

#[repr(C)]
struct ClapHostLatency {
    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapHostTail {
    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapHostTimerSupport {
    register_timer: Option<unsafe extern "C" fn(*const ClapHost, u32, *mut u32) -> bool>,
    unregister_timer: Option<unsafe extern "C" fn(*const ClapHost, u32) -> bool>,
}

#[repr(C)]
struct ClapHostGui {
    resize_hints_changed: Option<unsafe extern "C" fn(*const ClapHost)>,
    request_resize: Option<unsafe extern "C" fn(*const ClapHost, u32, u32) -> bool>,
    request_show: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
    request_hide: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
    closed: Option<unsafe extern "C" fn(*const ClapHost, bool)>,
}

#[repr(C)]
struct ClapHostParams {
    rescan: Option<unsafe extern "C" fn(*const ClapHost, u32)>,
    clear: Option<unsafe extern "C" fn(*const ClapHost, u32, u32)>,
    request_flush: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapHostState {
    mark_dirty: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapHostAudioPorts {
    is_rescan_flag_supported: Option<unsafe extern "C" fn(*const ClapHost, flag: u32) -> bool>,
    rescan: Option<unsafe extern "C" fn(*const ClapHost, flags: u32)>,
}

#[repr(C)]
struct ClapHostNotePorts {
    supported_dialects: Option<unsafe extern "C" fn(*const ClapHost) -> u32>,
    rescan: Option<unsafe extern "C" fn(*const ClapHost, flags: u32)>,
}

#[repr(C)]
struct ClapHostNoteName {
    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
}

#[repr(C)]
struct ClapAudioBuffer {
    data32: *mut *mut f32,
    data64: *mut *mut f64,
    channel_count: u32,
    latency: u32,
    constant_mask: u64,
}

#[repr(C)]
struct ClapProcess {
    steady_time: i64,
    frames_count: u32,
    transport: *const c_void,
    audio_inputs: *mut ClapAudioBuffer,
    audio_outputs: *mut ClapAudioBuffer,
    audio_inputs_count: u32,
    audio_outputs_count: u32,
    in_events: *const ClapInputEvents,
    out_events: *mut ClapOutputEvents,
}

#[derive(Default, Clone, Copy)]
struct HostCallbackFlags {
    restart: bool,
    process: bool,
    callback: bool,
}

#[derive(Clone, Copy)]
struct HostTimer {
    id: u32,
}

struct HostRuntimeState {
    callback_flags: UnsafeMutex<HostCallbackFlags>,
    timers: UnsafeMutex<Vec<HostTimer>>,
    ui_should_close: AtomicU32,
    ui_active: AtomicU32,
    param_flush_requested: AtomicU32,
    state_dirty_requested: AtomicU32,
    note_names_dirty: AtomicU32,
    audio_ports_rescan_requested: AtomicU32,
    note_ports_rescan_requested: AtomicU32,
}

static HOST_THREAD_CHECK_EXT: ClapHostThreadCheck = ClapHostThreadCheck {
    is_main_thread: Some(host_is_main_thread),
    is_audio_thread: Some(host_is_audio_thread),
};
static HOST_LATENCY_EXT: ClapHostLatency = ClapHostLatency {
    changed: Some(host_latency_changed),
};
static HOST_TAIL_EXT: ClapHostTail = ClapHostTail {
    changed: Some(host_tail_changed),
};
static HOST_TIMER_EXT: ClapHostTimerSupport = ClapHostTimerSupport {
    register_timer: Some(host_timer_register),
    unregister_timer: Some(host_timer_unregister),
};
static HOST_GUI_EXT: ClapHostGui = ClapHostGui {
    resize_hints_changed: Some(host_gui_resize_hints_changed),
    request_resize: Some(host_gui_request_resize),
    request_show: Some(host_gui_request_show),
    request_hide: Some(host_gui_request_hide),
    closed: Some(host_gui_closed),
};
static HOST_PARAMS_EXT: ClapHostParams = ClapHostParams {
    rescan: Some(host_params_rescan),
    clear: Some(host_params_clear),
    request_flush: Some(host_params_request_flush),
};
static HOST_STATE_EXT: ClapHostState = ClapHostState {
    mark_dirty: Some(host_state_mark_dirty),
};
static HOST_NOTE_NAME_EXT: ClapHostNoteName = ClapHostNoteName {
    changed: Some(host_note_name_changed),
};
static HOST_AUDIO_PORTS_EXT: ClapHostAudioPorts = ClapHostAudioPorts {
    is_rescan_flag_supported: Some(host_audio_ports_is_rescan_flag_supported),
    rescan: Some(host_audio_ports_rescan),
};
static HOST_NOTE_PORTS_EXT: ClapHostNotePorts = ClapHostNotePorts {
    supported_dialects: Some(host_note_ports_supported_dialects),
    rescan: Some(host_note_ports_rescan),
};
static NEXT_TIMER_ID: AtomicU32 = AtomicU32::new(1);

fn host_runtime_state(host: *const ClapHost) -> Option<&'static HostRuntimeState> {
    if host.is_null() {
        return None;
    }
    let state_ptr = unsafe { (*host).host_data as *const HostRuntimeState };
    if state_ptr.is_null() {
        return None;
    }
    Some(unsafe { &*state_ptr })
}

unsafe extern "C" fn host_get_extension(
    _host: *const ClapHost,
    _extension_id: *const c_char,
) -> *const c_void {
    if _extension_id.is_null() {
        return std::ptr::null();
    }

    let id = unsafe { CStr::from_ptr(_extension_id) }.to_string_lossy();
    match id.as_ref() {
        "clap.host.thread-check" => {
            (&HOST_THREAD_CHECK_EXT as *const ClapHostThreadCheck).cast::<c_void>()
        }
        "clap.host.latency" => (&HOST_LATENCY_EXT as *const ClapHostLatency).cast::<c_void>(),
        "clap.host.tail" => (&HOST_TAIL_EXT as *const ClapHostTail).cast::<c_void>(),
        "clap.host.timer-support" => {
            (&HOST_TIMER_EXT as *const ClapHostTimerSupport).cast::<c_void>()
        }
        "clap.host.gui" => host_runtime_state(_host)
            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
            .map(|_| (&HOST_GUI_EXT as *const ClapHostGui).cast::<c_void>())
            .unwrap_or(std::ptr::null()),
        "clap.host.params" => host_runtime_state(_host)
            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
            .map(|_| (&HOST_PARAMS_EXT as *const ClapHostParams).cast::<c_void>())
            .unwrap_or(std::ptr::null()),
        "clap.host.state" => host_runtime_state(_host)
            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
            .map(|_| (&HOST_STATE_EXT as *const ClapHostState).cast::<c_void>())
            .unwrap_or(std::ptr::null()),
        "clap.host.note-name" => (&HOST_NOTE_NAME_EXT as *const ClapHostNoteName).cast::<c_void>(),
        "clap.host.audio-ports" => {
            (&HOST_AUDIO_PORTS_EXT as *const ClapHostAudioPorts).cast::<c_void>()
        }
        "clap.host.note-ports" => {
            (&HOST_NOTE_PORTS_EXT as *const ClapHostNotePorts).cast::<c_void>()
        }
        _ => std::ptr::null(),
    }
}

unsafe extern "C" fn host_request_process(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.callback_flags.lock().process = true;
    }
}

unsafe extern "C" fn host_request_callback(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.callback_flags.lock().callback = true;
    }
}

unsafe extern "C" fn host_request_restart(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.callback_flags.lock().restart = true;
    }
}

unsafe extern "C" fn host_audio_ports_is_rescan_flag_supported(
    _host: *const ClapHost,
    _flag: u32,
) -> bool {
    true
}

unsafe extern "C" fn host_audio_ports_rescan(_host: *const ClapHost, _flags: u32) {
    if let Some(state) = host_runtime_state(_host) {
        state
            .audio_ports_rescan_requested
            .store(1, Ordering::Release);
    }
}

unsafe extern "C" fn host_note_ports_rescan(_host: *const ClapHost, _flags: u32) {
    if let Some(state) = host_runtime_state(_host) {
        state
            .note_ports_rescan_requested
            .store(1, Ordering::Release);
    }
}

unsafe extern "C" fn host_note_ports_supported_dialects(_host: *const ClapHost) -> u32 {
    let _ = _host;
    0x1F
}

unsafe extern "C" fn host_note_name_changed(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.note_names_dirty.store(1, Ordering::Release);
    }
}

unsafe extern "C" fn host_is_main_thread(_host: *const ClapHost) -> bool {
    true
}

unsafe extern "C" fn host_is_audio_thread(_host: *const ClapHost) -> bool {
    false
}

unsafe extern "C" fn host_latency_changed(_host: *const ClapHost) {}

unsafe extern "C" fn host_tail_changed(_host: *const ClapHost) {}

unsafe extern "C" fn host_timer_register(
    _host: *const ClapHost,
    _period_ms: u32,
    timer_id: *mut u32,
) -> bool {
    if timer_id.is_null() {
        return false;
    }
    let id = NEXT_TIMER_ID.fetch_add(1, Ordering::Relaxed);
    if let Some(state) = host_runtime_state(_host) {
        state.timers.lock().push(HostTimer { id });
    }

    unsafe {
        *timer_id = id;
    }
    true
}

unsafe extern "C" fn host_timer_unregister(_host: *const ClapHost, _timer_id: u32) -> bool {
    if let Some(state) = host_runtime_state(_host) {
        state.timers.lock().retain(|timer| timer.id != _timer_id);
    }
    true
}

unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const ClapHost) {}

unsafe extern "C" fn host_gui_request_resize(
    _host: *const ClapHost,
    _width: u32,
    _height: u32,
) -> bool {
    true
}

unsafe extern "C" fn host_gui_request_show(_host: *const ClapHost) -> bool {
    true
}

unsafe extern "C" fn host_gui_request_hide(_host: *const ClapHost) -> bool {
    if let Some(state) = host_runtime_state(_host) {
        if state.ui_active.load(Ordering::Acquire) != 0 {
            state.ui_should_close.store(1, Ordering::Release);
        }
        true
    } else {
        false
    }
}

unsafe extern "C" fn host_gui_closed(_host: *const ClapHost, _was_destroyed: bool) {
    if let Some(state) = host_runtime_state(_host)
        && state.ui_active.load(Ordering::Acquire) != 0
    {
        state.ui_should_close.store(1, Ordering::Release);
    }
}

unsafe extern "C" fn host_params_rescan(_host: *const ClapHost, _flags: u32) {}

unsafe extern "C" fn host_params_clear(_host: *const ClapHost, _param_id: u32, _flags: u32) {}

unsafe extern "C" fn host_params_request_flush(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.param_flush_requested.store(1, Ordering::Release);
        state.callback_flags.lock().callback = true;
    }
}

unsafe extern "C" fn host_state_mark_dirty(_host: *const ClapHost) {
    if let Some(state) = host_runtime_state(_host) {
        state.state_dirty_requested.store(1, Ordering::Release);
        state.callback_flags.lock().callback = true;
    }
}

pub fn list_plugins() -> Vec<ClapPluginInfo> {
    list_plugins_with_capabilities(false)
}

pub fn is_supported_clap_binary(path: &Path) -> bool {
    path.extension()
        .is_some_and(|ext| ext.eq_ignore_ascii_case("clap"))
}

pub fn list_plugins_with_capabilities(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
    let mut roots = default_clap_search_roots();

    if let Ok(extra) = std::env::var("CLAP_PATH") {
        for p in std::env::split_paths(&extra) {
            if !p.as_os_str().is_empty() {
                roots.push(p);
            }
        }
    }

    let mut out = Vec::new();
    for root in roots {
        collect_clap_plugins(&root, &mut out, scan_capabilities);
    }

    out.sort_by_key(|a| a.name.to_lowercase());
    out.dedup_by(|a, b| a.name.eq_ignore_ascii_case(&b.name) && a.path.eq_ignore_ascii_case(&b.path));
    out
}

fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
    let Ok(entries) = std::fs::read_dir(root) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        let Ok(ft) = entry.file_type() else {
            continue;
        };
        if ft.is_dir() {
            if path
                .file_name()
                .and_then(|name| name.to_str())
                .is_some_and(|name| {
                    matches!(
                        name,
                        "deps" | "build" | "incremental" | ".fingerprint" | "examples"
                    )
                })
            {
                continue;
            }
            collect_clap_plugins(&path, out, scan_capabilities);
            continue;
        }

        if is_supported_clap_binary(&path) {
            let infos = scan_bundle_descriptors(&path, scan_capabilities);
            if infos.is_empty() {
                let name = path
                    .file_stem()
                    .map(|s| s.to_string_lossy().to_string())
                    .unwrap_or_else(|| path.to_string_lossy().to_string());
                out.push(ClapPluginInfo {
                    name,
                    path: path.to_string_lossy().to_string(),
                    capabilities: None,
                });
            } else {
                out.extend(infos);
            }
        }
    }
}

fn scan_bundle_descriptors(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
    let path_str = path.to_string_lossy().to_string();
    let factory_id = c"clap.plugin-factory";
    let mut host_runtime_state = Box::new(HostRuntimeState {
        callback_flags: UnsafeMutex::new(HostCallbackFlags::default()),
        timers: UnsafeMutex::new(Vec::new()),
        ui_should_close: AtomicU32::new(0),
        ui_active: AtomicU32::new(0),
        param_flush_requested: AtomicU32::new(0),
        state_dirty_requested: AtomicU32::new(0),
        note_names_dirty: AtomicU32::new(0),
        audio_ports_rescan_requested: AtomicU32::new(0),
        note_ports_rescan_requested: AtomicU32::new(0),
    });
    let host_runtime = ClapHost {
        clap_version: CLAP_VERSION,
        host_data: (&mut *host_runtime_state as *mut HostRuntimeState).cast::<c_void>(),
        name: c"Maolan".as_ptr(),
        vendor: c"Maolan".as_ptr(),
        url: c"https://example.invalid".as_ptr(),
        version: c"0.0.1".as_ptr(),
        get_extension: Some(host_get_extension),
        request_restart: Some(host_request_restart),
        request_process: Some(host_request_process),
        request_callback: Some(host_request_callback),
    };

    let library = match unsafe { Library::new(path) } {
        Ok(lib) => lib,
        Err(_) => return Vec::new(),
    };

    let entry_ptr = unsafe {
        match library.get::<*const ClapPluginEntry>(b"clap_entry\0") {
            Ok(sym) => *sym,
            Err(_) => return Vec::new(),
        }
    };
    if entry_ptr.is_null() {
        return Vec::new();
    }

    let entry = unsafe { &*entry_ptr };
    let Some(init) = entry.init else {
        return Vec::new();
    };
    let host_ptr = &host_runtime;

    if unsafe { !init(host_ptr) } {
        return Vec::new();
    }
    let mut out = Vec::new();
    if let Some(get_factory) = entry.get_factory {
        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
        if !factory.is_null() {
            let factory_ref = unsafe { &*factory };
            if let (Some(get_count), Some(get_desc)) = (
                factory_ref.get_plugin_count,
                factory_ref.get_plugin_descriptor,
            ) {
                let count = unsafe { get_count(factory) };
                for i in 0..count {
                    let desc = unsafe { get_desc(factory, i) };
                    if desc.is_null() {
                        continue;
                    }

                    let desc = unsafe { &*desc };
                    if desc.id.is_null() || desc.name.is_null() {
                        continue;
                    }

                    let id = unsafe { CStr::from_ptr(desc.id) }
                        .to_string_lossy()
                        .to_string();

                    let name = unsafe { CStr::from_ptr(desc.name) }
                        .to_string_lossy()
                        .to_string();

                    let capabilities = if scan_capabilities {
                        scan_plugin_capabilities(factory_ref, factory, &host_runtime, &id)
                    } else {
                        None
                    };

                    out.push(ClapPluginInfo {
                        name,
                        path: format!("{path_str}::{id}"),
                        capabilities,
                    });
                }
            }
        }
    }

    if let Some(deinit) = entry.deinit {
        unsafe { deinit() };
    }
    out
}

fn scan_plugin_capabilities(
    factory: &ClapPluginFactory,
    factory_ptr: *const ClapPluginFactory,
    host: &ClapHost,
    plugin_id: &str,
) -> Option<ClapPluginCapabilities> {
    let create = factory.create_plugin?;

    let id_cstring = CString::new(plugin_id).ok()?;

    let plugin = unsafe { create(factory_ptr, host, id_cstring.as_ptr()) };
    if plugin.is_null() {
        return None;
    }

    let plugin_ref = unsafe { &*plugin };
    let plugin_init = plugin_ref.init?;

    if unsafe { !plugin_init(plugin) } {
        return None;
    }

    let mut capabilities = ClapPluginCapabilities {
        has_gui: false,
        gui_apis: Vec::new(),
        supports_embedded: false,
        supports_floating: false,
        has_params: false,
        has_state: false,
        audio_inputs: 0,
        audio_outputs: 0,
        midi_inputs: 0,
        midi_outputs: 0,
    };

    if let Some(get_extension) = plugin_ref.get_extension {
        let gui_ext_id = c"clap.gui";

        let gui_ptr = unsafe { get_extension(plugin, gui_ext_id.as_ptr()) };
        if !gui_ptr.is_null() {
            capabilities.has_gui = true;

            let gui = unsafe { &*(gui_ptr as *const ClapPluginGui) };

            if let Some(is_api_supported) = gui.is_api_supported {
                for api in ["x11", "cocoa"] {
                    if let Ok(api_cstr) = CString::new(api) {
                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), false) } {
                            capabilities.gui_apis.push(format!("{} (embedded)", api));
                            capabilities.supports_embedded = true;
                        }

                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), true) } {
                            if !capabilities.supports_embedded {
                                capabilities.gui_apis.push(format!("{} (floating)", api));
                            }
                            capabilities.supports_floating = true;
                        }
                    }
                }
            }
        }

        let params_ext_id = c"clap.params";

        let params_ptr = unsafe { get_extension(plugin, params_ext_id.as_ptr()) };
        capabilities.has_params = !params_ptr.is_null();

        let state_ext_id = c"clap.state";

        let state_ptr = unsafe { get_extension(plugin, state_ext_id.as_ptr()) };
        capabilities.has_state = !state_ptr.is_null();

        let audio_ports_ext_id = c"clap.audio-ports";

        let audio_ports_ptr = unsafe { get_extension(plugin, audio_ports_ext_id.as_ptr()) };
        if !audio_ports_ptr.is_null() {
            let audio_ports = unsafe { &*(audio_ports_ptr as *const ClapPluginAudioPorts) };
            if let Some(count_fn) = audio_ports.count {
                let in_count = unsafe { count_fn(plugin, true) };
                let out_count = unsafe { count_fn(plugin, false) };
                for pi in 0..in_count {
                    let mut info = ClapAudioPortInfoRaw {
                        id: 0,
                        name: [0; 256],
                        flags: 0,
                        channel_count: 0,
                        port_type: ptr::null(),
                        in_place_pair: 0,
                    };
                    if unsafe {
                        audio_ports
                            .get
                            .map(|f| f(plugin, pi, true, &mut info))
                            .unwrap_or(false)
                    } {
                        capabilities.audio_inputs += info.channel_count as usize;
                    }
                }
                for pi in 0..out_count {
                    let mut info = ClapAudioPortInfoRaw {
                        id: 0,
                        name: [0; 256],
                        flags: 0,
                        channel_count: 0,
                        port_type: ptr::null(),
                        in_place_pair: 0,
                    };
                    if unsafe {
                        audio_ports
                            .get
                            .map(|f| f(plugin, pi, false, &mut info))
                            .unwrap_or(false)
                    } {
                        capabilities.audio_outputs += info.channel_count as usize;
                    }
                }
            }
        }

        let note_ports_ext_id = c"clap.note-ports";

        let note_ports_ptr = unsafe { get_extension(plugin, note_ports_ext_id.as_ptr()) };
        if !note_ports_ptr.is_null() {
            let note_ports = unsafe { &*(note_ports_ptr as *const ClapPluginNotePorts) };
            if let Some(count_fn) = note_ports.count {
                capabilities.midi_inputs = unsafe { count_fn(plugin, true) } as usize;

                capabilities.midi_outputs = unsafe { count_fn(plugin, false) } as usize;
            }
        }
    }

    if let Some(destroy) = plugin_ref.destroy {
        unsafe { destroy(plugin) };
    }

    Some(capabilities)
}

fn default_clap_search_roots() -> Vec<PathBuf> {
    #[cfg(target_os = "macos")]
    {
        let mut roots = Vec::new();
        paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
        roots
    }
    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
    {
        let mut roots = Vec::new();
        paths::push_unix_plugin_roots(&mut roots, "clap");
        roots
    }
    #[cfg(not(any(
        target_os = "macos",
        target_os = "linux",
        target_os = "freebsd",
        target_os = "openbsd"
    )))]
    {
        Vec::new()
    }
}

#[cfg(test)]
mod tests {
    #[cfg(unix)]
    use super::collect_clap_plugins;
    #[cfg(unix)]
    use std::fs;
    #[cfg(unix)]
    use std::path::PathBuf;
    #[cfg(unix)]
    use std::time::{SystemTime, UNIX_EPOCH};

    #[cfg(unix)]
    fn make_symlink(src: &PathBuf, dst: &PathBuf) {
        std::os::unix::fs::symlink(src, dst).expect("should create symlink");
    }

    #[cfg(unix)]
    #[test]
    fn collect_clap_plugins_includes_symlinked_clap_files() {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be valid")
            .as_nanos();
        let root = std::env::temp_dir().join(format!(
            "maolan-clap-symlink-test-{}-{nanos}",
            std::process::id()
        ));
        fs::create_dir_all(&root).expect("should create temp dir");

        let target_file = root.join("librural_modeler.so");
        fs::write(&target_file, b"not a real clap binary").expect("should create target file");
        let clap_link = root.join("RuralModeler.clap");
        make_symlink(&PathBuf::from("librural_modeler.so"), &clap_link);

        let mut out = Vec::new();
        collect_clap_plugins(&root, &mut out, false);

        assert!(
            out.iter()
                .any(|info| info.path == clap_link.to_string_lossy()),
            "scanner should include symlinked .clap files"
        );

        fs::remove_dir_all(&root).expect("should remove temp dir");
    }
}
