use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};

use super::raw_sys;

#[derive(Clone, Copy, Debug)]
pub struct PlatformHooks {
    pub page_size: fn() -> usize,
    pub gettid: fn() -> raw_sys::Result<u32>,
    pub getrandom: fn(&mut [u8], u32) -> raw_sys::Result<usize>,
    pub memfd_create: fn(*const u8, u32) -> raw_sys::Result<i32>,
    pub ftruncate: fn(i32, u64) -> raw_sys::Result<()>,
    pub fallocate: fn(i32, u32, u64, u64) -> raw_sys::Result<()>,
    pub close: fn(i32) -> raw_sys::Result<()>,
    pub futex_wait: unsafe fn(*const u32, u32, u32) -> raw_sys::Result<()>,
    pub futex_wake: unsafe fn(*const u32, u32, u32) -> raw_sys::Result<u32>,
    pub mmap: unsafe fn(*mut u8, usize, u32, u32, i32, u64) -> raw_sys::Result<*mut u8>,
    pub map_anonymous: unsafe fn(usize, u32) -> raw_sys::Result<*mut u8>,
    pub mprotect: unsafe fn(*mut u8, usize, u32) -> raw_sys::Result<()>,
    pub munmap: unsafe fn(*mut u8, usize) -> raw_sys::Result<()>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlatformInstallError {
    AlreadyConfigured,
    AlreadyInUse,
}

static DEFAULT_PLATFORM_HOOKS: PlatformHooks = PlatformHooks {
    page_size: default_page_size,
    gettid: raw_sys::gettid,
    getrandom: raw_sys::getrandom,
    memfd_create: raw_sys::memfd_create,
    ftruncate: raw_sys::ftruncate,
    fallocate: raw_sys::fallocate,
    close: raw_sys::close,
    futex_wait: raw_sys::futex_wait,
    futex_wake: raw_sys::futex_wake,
    mmap: raw_sys::mmap,
    map_anonymous: raw_sys::map_anonymous,
    mprotect: raw_sys::mprotect,
    munmap: raw_sys::munmap,
};

static PLATFORM_HOOKS: AtomicPtr<PlatformHooks> =
    AtomicPtr::new((&DEFAULT_PLATFORM_HOOKS as *const PlatformHooks).cast_mut());
static PLATFORM_FROZEN: AtomicBool = AtomicBool::new(false);

pub fn install_platform_hooks(hooks: &'static PlatformHooks) -> Result<(), PlatformInstallError> {
    if PLATFORM_FROZEN.load(Ordering::Acquire) {
        return Err(PlatformInstallError::AlreadyInUse);
    }

    let default_ptr = (&DEFAULT_PLATFORM_HOOKS as *const PlatformHooks).cast_mut();
    let hooks_ptr = (hooks as *const PlatformHooks).cast_mut();
    match PLATFORM_HOOKS.compare_exchange(
        default_ptr,
        hooks_ptr,
        Ordering::AcqRel,
        Ordering::Acquire,
    ) {
        Ok(_) => Ok(()),
        Err(existing) if existing == hooks_ptr => Ok(()),
        Err(_) => Err(PlatformInstallError::AlreadyConfigured),
    }
}

#[inline(always)]
pub fn page_size() -> usize {
    (platform_hooks().page_size)()
}

#[inline(always)]
pub fn gettid() -> raw_sys::Result<u32> {
    (platform_hooks().gettid)()
}

#[inline(always)]
pub fn getrandom(buf: &mut [u8], flags: u32) -> raw_sys::Result<usize> {
    (platform_hooks().getrandom)(buf, flags)
}

#[inline(always)]
pub fn memfd_create(name: *const u8, flags: u32) -> raw_sys::Result<i32> {
    (platform_hooks().memfd_create)(name, flags)
}

#[inline(always)]
pub fn ftruncate(fd: i32, len: u64) -> raw_sys::Result<()> {
    (platform_hooks().ftruncate)(fd, len)
}

#[inline(always)]
pub fn fallocate(fd: i32, mode: u32, offset: u64, len: u64) -> raw_sys::Result<()> {
    (platform_hooks().fallocate)(fd, mode, offset, len)
}

#[inline(always)]
pub fn close(fd: i32) -> raw_sys::Result<()> {
    (platform_hooks().close)(fd)
}

#[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_wait(uaddr: *const u32, op: u32, expected: u32) -> raw_sys::Result<()> {
    unsafe { (platform_hooks().futex_wait)(uaddr, op, expected) }
}

#[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) -> raw_sys::Result<u32> {
    unsafe { (platform_hooks().futex_wake)(uaddr, op, count) }
}

#[inline(always)]
/// # Safety
///
/// The caller must ensure the mapping arguments satisfy the platform `mmap(2)` contract.
pub unsafe fn mmap(
    addr: *mut u8,
    len: usize,
    prot: u32,
    flags: u32,
    fd: i32,
    offset: u64,
) -> raw_sys::Result<*mut u8> {
    unsafe { (platform_hooks().mmap)(addr, len, prot, flags, fd, offset) }
}

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

#[inline(always)]
/// # Safety
///
/// `addr..addr+len` must refer to a valid mapped region.
pub unsafe fn mprotect(addr: *mut u8, len: usize, prot: u32) -> raw_sys::Result<()> {
    unsafe { (platform_hooks().mprotect)(addr, len, prot) }
}

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

#[inline(always)]
fn platform_hooks() -> &'static PlatformHooks {
    PLATFORM_FROZEN.store(true, Ordering::Release);
    let hooks = PLATFORM_HOOKS.load(Ordering::Acquire);
    unsafe { &*hooks }
}

fn default_page_size() -> usize {
    let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
    assert!(size > 0, "sysconf(_SC_PAGESIZE) failed");
    size as usize
}
