use core::ptr::null_mut;

pub type Result<T> = core::result::Result<T, Error>;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Error(pub i32);

impl Error {
    #[inline(always)]
    pub const fn errno(self) -> i32 {
        self.0
    }
}

pub const PROT_NONE: u32 = libc::PROT_NONE as u32;
pub const PROT_READ: u32 = libc::PROT_READ as u32;
pub const PROT_WRITE: u32 = libc::PROT_WRITE as u32;

pub const MAP_SHARED: u32 = libc::MAP_SHARED as u32;
pub const MAP_PRIVATE: u32 = libc::MAP_PRIVATE as u32;
pub const MAP_FIXED: u32 = libc::MAP_FIXED as u32;
pub const MAP_ANONYMOUS: u32 = libc::MAP_ANONYMOUS as u32;

pub const MFD_CLOEXEC: u32 = libc::MFD_CLOEXEC;

pub const FALLOC_FL_KEEP_SIZE: u32 = libc::FALLOC_FL_KEEP_SIZE as u32;
pub const FALLOC_FL_PUNCH_HOLE: u32 = libc::FALLOC_FL_PUNCH_HOLE as u32;

pub const FUTEX_WAIT: u32 = libc::FUTEX_WAIT as u32;
pub const FUTEX_WAKE: u32 = libc::FUTEX_WAKE as u32;
pub const FUTEX_PRIVATE_FLAG: u32 = libc::FUTEX_PRIVATE_FLAG as u32;
pub const FUTEX_WAIT_PRIVATE: u32 = FUTEX_WAIT | FUTEX_PRIVATE_FLAG;
pub const FUTEX_WAKE_PRIVATE: u32 = FUTEX_WAKE | FUTEX_PRIVATE_FLAG;

pub const EINTR: i32 = libc::EINTR;
pub const EAGAIN: i32 = libc::EAGAIN;
pub const EFAULT: i32 = libc::EFAULT;

#[inline(always)]
fn last_error() -> Error {
    Error(
        std::io::Error::last_os_error()
            .raw_os_error()
            .unwrap_or(libc::EINVAL),
    )
}

#[inline(always)]
fn map_c_int(result: libc::c_int) -> Result<libc::c_int> {
    if result == -1 {
        Err(last_error())
    } else {
        Ok(result)
    }
}

#[inline(always)]
fn map_c_long(result: libc::c_long) -> Result<libc::c_long> {
    if result == -1 {
        Err(last_error())
    } else {
        Ok(result)
    }
}

#[inline(always)]
fn to_off_t(value: u64) -> Result<libc::off_t> {
    value.try_into().map_err(|_| Error(libc::EINVAL))
}

#[inline(always)]
pub fn getpid() -> Result<u32> {
    let pid = unsafe { libc::getpid() };
    if pid == -1 {
        Err(last_error())
    } else {
        Ok(pid as u32)
    }
}

#[inline(always)]
pub fn gettid() -> Result<u32> {
    unsafe { map_c_long(libc::syscall(libc::SYS_gettid)).map(|value| value as u32) }
}

#[inline(always)]
pub fn close(fd: i32) -> Result<()> {
    unsafe { map_c_int(libc::close(fd)).map(|_| ()) }
}

#[inline(always)]
pub fn memfd_create(name: *const u8, flags: u32) -> Result<i32> {
    unsafe {
        map_c_long(libc::syscall(
            libc::SYS_memfd_create,
            name.cast::<libc::c_char>(),
            flags as libc::c_uint,
        ))
        .map(|fd| fd as i32)
    }
}

#[inline(always)]
pub fn ftruncate(fd: i32, len: u64) -> Result<()> {
    let len = to_off_t(len)?;
    unsafe { map_c_int(libc::ftruncate(fd, len)).map(|_| ()) }
}

#[inline(always)]
pub fn fallocate(fd: i32, mode: u32, offset: u64, len: u64) -> Result<()> {
    let offset = to_off_t(offset)?;
    let len = to_off_t(len)?;
    unsafe { map_c_int(libc::fallocate(fd, mode as libc::c_int, offset, len)).map(|_| ()) }
}

#[inline(always)]
pub fn getrandom(buf: &mut [u8], flags: u32) -> Result<usize> {
    let result =
        unsafe { libc::getrandom(buf.as_mut_ptr().cast(), buf.len(), flags as libc::c_uint) };
    if result == -1 {
        Err(last_error())
    } else {
        Ok(result as usize)
    }
}

#[inline(always)]
/// # Safety
///
/// `uaddr` must be valid for the kernel to read as a futex word for the duration of the call.
pub unsafe fn futex_wait(uaddr: *const u32, op: u32, expected: u32) -> Result<()> {
    unsafe {
        map_c_long(libc::syscall(
            libc::SYS_futex,
            uaddr,
            op as libc::c_int,
            expected,
            null_mut::<libc::timespec>(),
            0,
            0,
        ))
        .map(|_| ())
    }
}

#[inline(always)]
/// # Safety
///
/// `uaddr` must be valid for the kernel to access as a futex word for the duration of the call.
pub unsafe fn futex_wake(uaddr: *const u32, op: u32, count: u32) -> Result<u32> {
    unsafe {
        map_c_long(libc::syscall(
            libc::SYS_futex,
            uaddr,
            op as libc::c_int,
            count,
            null_mut::<libc::timespec>(),
            0,
            0,
        ))
        .map(|woken| woken as u32)
    }
}

#[inline(always)]
/// # Safety
///
/// The caller must uphold the platform `mmap(2)` contract for the provided arguments and manage
/// any returned mapping according to Rust aliasing and lifetime rules.
pub unsafe fn mmap(
    addr: *mut u8,
    len: usize,
    prot: u32,
    flags: u32,
    fd: i32,
    offset: u64,
) -> Result<*mut u8> {
    let offset = to_off_t(offset)?;
    let result = unsafe {
        libc::mmap(
            addr.cast(),
            len,
            prot as libc::c_int,
            flags as libc::c_int,
            fd,
            offset,
        )
    };
    if result == libc::MAP_FAILED {
        Err(last_error())
    } else {
        Ok(result.cast())
    }
}

#[inline(always)]
/// # Safety
///
/// The caller must later unmap the returned memory exactly once.
pub unsafe fn map_anonymous(len: usize, prot: u32) -> Result<*mut u8> {
    unsafe { mmap(null_mut(), len, prot, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) }
}

#[inline(always)]
/// # Safety
///
/// `addr..addr+len` must refer to a valid mapped region.
pub unsafe fn mprotect(addr: *mut u8, len: usize, prot: u32) -> Result<()> {
    unsafe { map_c_int(libc::mprotect(addr.cast(), len, prot as libc::c_int)).map(|_| ()) }
}

#[inline(always)]
/// # Safety
///
/// `addr..addr+len` must refer to a valid mapping that can be unmapped exactly once.
pub unsafe fn munmap(addr: *mut u8, len: usize) -> Result<()> {
    unsafe { map_c_int(libc::munmap(addr.cast(), len)).map(|_| ()) }
}
