// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license.  See the
// accompanying file LICENSE for details.

#![allow(clippy::missing_safety_doc)]

use crate::errors::*;
use crate::PlatformHandle;
use std::{convert::TryInto, ffi::c_void, slice};

#[cfg(unix)]
pub use unix::SharedMem;
#[cfg(windows)]
pub use windows::SharedMem;

#[derive(Copy, Clone)]
struct SharedMemView {
    ptr: *mut c_void,
    size: usize,
}

unsafe impl Send for SharedMemView {}

impl SharedMemView {
    pub unsafe fn get_slice(&self, size: usize) -> Result<&[u8]> {
        let map = slice::from_raw_parts(self.ptr as _, self.size);
        if size <= self.size {
            Ok(&map[..size])
        } else {
            bail!("mmap size");
        }
    }

    pub unsafe fn get_mut_slice(&mut self, size: usize) -> Result<&mut [u8]> {
        let map = slice::from_raw_parts_mut(self.ptr as _, self.size);
        if size <= self.size {
            Ok(&mut map[..size])
        } else {
            bail!("mmap size")
        }
    }
}

#[cfg(unix)]
mod unix {
    use super::*;
    use memmap2::{MmapMut, MmapOptions};
    use std::fs::File;
    use std::os::unix::io::{AsRawFd, FromRawFd};

    #[cfg(target_os = "android")]
    fn open_shm_file(_id: &str, size: usize) -> Result<File> {
        unsafe {
            let fd = ashmem::ASharedMemory_create(std::ptr::null(), size);
            if fd >= 0 {
                // Drop PROT_EXEC
                let r = ashmem::ASharedMemory_setProt(fd, libc::PROT_READ | libc::PROT_WRITE);
                assert_eq!(r, 0);
                return Ok(File::from_raw_fd(fd.try_into().unwrap()));
            }
            Err(std::io::Error::last_os_error().into())
        }
    }

    #[cfg(not(target_os = "android"))]
    fn open_shm_file(id: &str, size: usize) -> Result<File> {
        let file = open_shm_file_impl(id)?;
        allocate_file(&file, size)?;
        Ok(file)
    }

    #[cfg(not(target_os = "android"))]
    fn open_shm_file_impl(id: &str) -> Result<File> {
        use std::env::temp_dir;
        use std::fs::{remove_file, OpenOptions};

        let id_cstring = std::ffi::CString::new(id).unwrap();

        #[cfg(target_os = "linux")]
        {
            unsafe {
                let r = libc::syscall(libc::SYS_memfd_create, id_cstring.as_ptr(), 0);
                if r >= 0 {
                    return Ok(File::from_raw_fd(r.try_into().unwrap()));
                }
            }

            let mut path = std::path::PathBuf::from("/dev/shm");
            path.push(id);

            if let Ok(file) = OpenOptions::new()
                .read(true)
                .write(true)
                .create_new(true)
                .open(&path)
            {
                let _ = remove_file(&path);
                return Ok(file);
            }
        }

        unsafe {
            let fd = libc::shm_open(
                id_cstring.as_ptr(),
                libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
                0o600,
            );
            if fd >= 0 {
                libc::shm_unlink(id_cstring.as_ptr());
                return Ok(File::from_raw_fd(fd));
            }
        }

        let mut path = temp_dir();
        path.push(id);

        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create_new(true)
            .open(&path)?;

        let _ = remove_file(&path);
        Ok(file)
    }

    #[cfg(not(target_os = "android"))]
    fn handle_enospc(s: &str) -> Result<()> {
        let err = std::io::Error::last_os_error();
        let errno = err.raw_os_error().unwrap_or(0);
        assert_ne!(errno, 0);
        debug!("allocate_file: {} failed errno={}", s, errno);
        if errno == libc::ENOSPC {
            return Err(err.into());
        }
        Ok(())
    }

    #[cfg(not(target_os = "android"))]
    fn allocate_file(file: &File, size: usize) -> Result<()> {
        // First, set the file size.  This may create a sparse file on
        // many systems, which can fail with SIGBUS when accessed via a
        // mapping and the lazy backing allocation fails due to low disk
        // space.  To avoid this, try to force the entire file to be
        // preallocated before mapping using OS-specific approaches below.

        file.set_len(size.try_into().unwrap())?;

        let fd = file.as_raw_fd();
        let size: libc::off_t = size.try_into().unwrap();

        // Try Linux-specific fallocate.
        #[cfg(target_os = "linux")]
        {
            if unsafe { libc::fallocate(fd, 0, 0, size) } == 0 {
                return Ok(());
            }
            handle_enospc("fallocate()")?;
        }

        // Try macOS-specific fcntl.
        #[cfg(target_os = "macos")]
        {
            let params = libc::fstore_t {
                fst_flags: libc::F_ALLOCATEALL,
                fst_posmode: libc::F_PEOFPOSMODE,
                fst_offset: 0,
                fst_length: size,
                fst_bytesalloc: 0,
            };
            if unsafe { libc::fcntl(fd, libc::F_PREALLOCATE, &params) } == 0 {
                return Ok(());
            }
            handle_enospc("fcntl(F_PREALLOCATE)")?;
        }

        // Fall back to portable version, where available.
        #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly"))]
        {
            if unsafe { libc::posix_fallocate(fd, 0, size) } == 0 {
                return Ok(());
            }
            handle_enospc("posix_fallocate()")?;
        }

        Ok(())
    }

    pub struct SharedMem {
        file: File,
        _mmap: MmapMut,
        view: SharedMemView,
    }

    impl SharedMem {
        pub fn new(id: &str, size: usize) -> Result<SharedMem> {
            let file = open_shm_file(id, size)?;
            let mut mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
            assert_eq!(mmap.len(), size);
            let view = SharedMemView {
                ptr: mmap.as_mut_ptr() as _,
                size,
            };
            Ok(SharedMem {
                file,
                _mmap: mmap,
                view,
            })
        }

        pub unsafe fn make_handle(&self) -> Result<PlatformHandle> {
            PlatformHandle::duplicate(self.file.as_raw_fd()).map_err(|e| e.into())
        }

        pub unsafe fn from(handle: PlatformHandle, size: usize) -> Result<SharedMem> {
            let file = File::from_raw_fd(handle.into_raw());
            let mut mmap = MmapOptions::new().len(size).map_mut(&file)?;
            assert_eq!(mmap.len(), size);
            let view = SharedMemView {
                ptr: mmap.as_mut_ptr() as _,
                size,
            };
            Ok(SharedMem {
                file,
                _mmap: mmap,
                view,
            })
        }

        pub unsafe fn get_slice(&self, size: usize) -> Result<&[u8]> {
            self.view.get_slice(size)
        }

        pub unsafe fn get_mut_slice(&mut self, size: usize) -> Result<&mut [u8]> {
            self.view.get_mut_slice(size)
        }

        pub fn get_size(&self) -> usize {
            self.view.size
        }
    }
}

#[cfg(windows)]
mod windows {
    use super::*;
    use std::ptr;
    use winapi::{
        shared::{minwindef::DWORD, ntdef::HANDLE},
        um::{
            handleapi::CloseHandle,
            memoryapi::{MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS},
            winbase::CreateFileMappingA,
            winnt::PAGE_READWRITE,
        },
    };

    use crate::INVALID_HANDLE_VALUE;

    pub struct SharedMem {
        handle: HANDLE,
        view: SharedMemView,
    }

    unsafe impl Send for SharedMem {}

    impl Drop for SharedMem {
        fn drop(&mut self) {
            unsafe {
                let ok = UnmapViewOfFile(self.view.ptr);
                assert_ne!(ok, 0);
                let ok = CloseHandle(self.handle);
                assert_ne!(ok, 0);
            }
        }
    }

    impl SharedMem {
        pub fn new(_id: &str, size: usize) -> Result<SharedMem> {
            unsafe {
                let handle = CreateFileMappingA(
                    INVALID_HANDLE_VALUE,
                    ptr::null_mut(),
                    PAGE_READWRITE,
                    (size as u64 >> 32).try_into().unwrap(),
                    (size as u64 & (DWORD::MAX as u64)).try_into().unwrap(),
                    ptr::null(),
                );
                if handle.is_null() {
                    return Err(std::io::Error::last_os_error().into());
                }

                let ptr = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, size);
                if ptr.is_null() {
                    return Err(std::io::Error::last_os_error().into());
                }

                Ok(SharedMem {
                    handle,
                    view: SharedMemView { ptr, size },
                })
            }
        }

        pub unsafe fn make_handle(&self) -> Result<PlatformHandle> {
            PlatformHandle::duplicate(self.handle).map_err(|e| e.into())
        }

        pub unsafe fn from(handle: PlatformHandle, size: usize) -> Result<SharedMem> {
            let handle = handle.into_raw();
            let ptr = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, size);
            if ptr.is_null() {
                return Err(std::io::Error::last_os_error().into());
            }
            Ok(SharedMem {
                handle,
                view: SharedMemView { ptr, size },
            })
        }

        pub unsafe fn get_slice(&self, size: usize) -> Result<&[u8]> {
            self.view.get_slice(size)
        }

        pub unsafe fn get_mut_slice(&mut self, size: usize) -> Result<&mut [u8]> {
            self.view.get_mut_slice(size)
        }

        pub fn get_size(&self) -> usize {
            self.view.size
        }
    }
}
