//! Loading a library at run time: the encoders a device has are never a build
//! dependency, so a binary built here runs on a host without them and says which is
//! missing.

use std::ffi::{CStr, CString, c_char, c_int, c_void};

#[cfg_attr(target_os = "linux", link(name = "dl"))]
unsafe extern "C" {
    fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
    fn dlerror() -> *const c_char;
}

#[cfg(target_os = "linux")]
const RTLD_NOW: c_int = 2;
#[cfg(not(target_os = "linux"))]
const RTLD_NOW: c_int = 2;
#[cfg(target_os = "linux")]
const RTLD_LOCAL: c_int = 0;
#[cfg(not(target_os = "linux"))]
const RTLD_LOCAL: c_int = 4;

/// A loaded library. It is never unloaded: an encoder's library may keep threads
/// that outlive a session, and unloading it under them would be unsound.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Lib {
    handle: *mut c_void,
}

// SAFETY: a dlopen handle is a process-wide token; dlsym on it is thread-safe.
unsafe impl Send for Lib {}
unsafe impl Sync for Lib {}

fn last_error(name: &str) -> String {
    // SAFETY: dlerror returns null or a C string valid until the next dl call.
    let e = unsafe { dlerror() };
    if e.is_null() { format!("{name}: cannot load") } else { unsafe { CStr::from_ptr(e) }.to_string_lossy().into_owned() }
}

impl Lib {
    /// Load `name` (a file name the loader searches for, or a path).
    pub(crate) fn open(name: &str) -> Result<Lib, String> {
        let c = CString::new(name).map_err(|_| format!("{name}: a NUL in the name"))?;
        // SAFETY: dlopen with a valid C string; a failure returns null.
        let handle = unsafe { dlopen(c.as_ptr(), RTLD_NOW | RTLD_LOCAL) };
        if handle.is_null() { Err(last_error(name)) } else { Ok(Lib { handle }) }
    }

    /// The first of `names` that loads, or every reason none did.
    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
    pub(crate) fn open_first(names: &[&str]) -> Result<Lib, String> {
        let mut why = Vec::new();
        for n in names {
            match Lib::open(n) {
                Ok(l) => return Ok(l),
                Err(e) => why.push(e),
            }
        }
        Err(why.join("; "))
    }

    /// The address of `symbol`, or why there is none.
    pub(crate) fn sym(&self, symbol: &str) -> Result<*mut c_void, String> {
        let c = CString::new(symbol).map_err(|_| format!("{symbol}: a NUL in the name"))?;
        // SAFETY: dlsym on a live handle with a valid C string.
        let p = unsafe { dlsym(self.handle, c.as_ptr()) };
        if p.is_null() { Err(format!("no symbol {symbol}")) } else { Ok(p) }
    }

    /// `symbol` as the function pointer type `F`.
    ///
    /// # Safety
    ///
    /// `F` must be exactly the C signature the library declares for `symbol`.
    pub(crate) unsafe fn func<F: Copy>(&self, symbol: &str) -> Result<F, String> {
        let p = self.sym(symbol)?;
        assert_eq!(std::mem::size_of::<F>(), std::mem::size_of::<*mut c_void>(), "a function pointer");
        // SAFETY: the sizes agree, and the caller names the symbol's signature.
        Ok(unsafe { std::mem::transmute_copy(&p) })
    }
}

/// The SHA-256 of the file at `path`, lowercase hex, or why it cannot be read.
pub(crate) fn file_sha256(path: &std::path::Path) -> Result<String, String> {
    std::fs::read(path).map(|b| super::sha256_hex(&b)).map_err(|e| format!("{}: {e}", path.display()))
}
