use libc::c_void;
use libc;

use std::ffi::{CStr, CString};
use std::io;
use std::marker::PhantomData;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

/// A dynamic library handle.
#[derive(Debug)]
pub struct Dl {
    handle: *mut c_void,
}

impl Dl {
    /// Open a dynamic library. This function is unsafe because it may call
    /// constructors. Likewise, the [`Drop`] implementation may call
    /// destructors.
    pub unsafe fn open<P>(path: P) -> io::Result<Self>
        where P: AsRef<Path> {
        let c_path = cstr(path.as_ref())?;
        let handle = libc::dlopen(c_path.as_ptr(), libc::RTLD_LAZY);
        if handle.is_null() {
            let error_kind = io::ErrorKind::Other;
            let error = CStr::from_ptr(libc::dlerror()).to_string_lossy().into_owned();
            return Err(io::Error::new(error_kind, error));
        }
        Ok(Dl{handle})
    }

    /// Resolve a symbol, if it exists.
    pub fn resolve<N>(&self, symbol: N) -> Option<Value>
        where N: AsRef<CStr> {
        unsafe {
            libc::dlerror();
            let value = libc::dlsym(self.handle, symbol.as_ref().as_ptr());
            let error = libc::dlerror();
            if !error.is_null() {
                return None;
            }
            Some(Value{value, phantom: PhantomData})
        }
    }
}

impl Drop for Dl {
    fn drop(&mut self) {
        unsafe {
            libc::dlclose(self.handle);
        }
    }
}

unsafe impl Send for Dl {
}

unsafe impl Sync for Dl {
}

/// A symbol from a [`Dl`].
pub struct Value<'a> {
    value: *mut c_void,
    phantom: PhantomData<&'a ()>,
}

impl<'a> Value<'a> {
    /// Extract the value of the symbol. It is up to the caller to ensure that
    /// the value is not dereferenced after the [`Dl`] it came from has been
    /// dropped.
    pub fn get(&self) -> *mut c_void {
        self.value
    }
}

fn cstr(path: &Path) -> io::Result<CString> {
    Ok(CString::new(path.as_os_str().as_bytes())?)
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::mem;

    #[test]
    fn test_resolve() {
        let dl = unsafe { Dl::open("/lib/libm-2.25.so").unwrap() };
        let symbol = CStr::from_bytes_with_nul(b"sin\0").unwrap();
        let value = dl.resolve(symbol).unwrap();
        let function = unsafe {
            let ptr = value.get();
            mem::transmute::<*mut c_void, extern "C" fn(f64) -> f64>(ptr)
        };
        assert!(function(10.0) >= -0.6 && function(10.0) <= -0.4);
    }
}
