#![unstable(reason = "not public", issue = "none", feature = "fd")]

#[cfg(test)]
mod tests;

#[cfg(not(any(
    target_os = "linux",
    target_os = "l4re",
    target_os = "android",
    target_os = "hurd",
)))]
use libc::off_t as off64_t;
#[cfg(any(
    target_os = "android",
    target_os = "linux",
    target_os = "l4re",
    target_os = "hurd",
))]
use libc::off64_t;

cfg_select! {
    any(
        all(target_os = "linux", not(target_env = "musl")),
        target_os = "android",
        target_os = "hurd",
    ) => {
        // Prefer explicit pread64 for 64-bit offset independently of libc
        // #[cfg(gnu_file_offset_bits64)].
        use libc::pread64;
    }
    _ => {
        use libc::pread as pread64;
    }
}

use crate::cmp;
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, Read};
use crate::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
#[cfg(all(target_os = "android", target_pointer_width = "64"))]
use crate::sys::pal::weak::syscall;
#[cfg(any(all(target_os = "android", target_pointer_width = "32"), target_vendor = "apple"))]
use crate::sys::pal::weak::weak;
use crate::sys::{AsInner, FromInner, IntoInner, cvt};

#[derive(Debug)]
pub struct FileDesc(OwnedFd);

// The maximum read limit on most POSIX-like systems is `SSIZE_MAX`,
// with the man page quoting that if the count of bytes to read is
// greater than `SSIZE_MAX` the result is "unspecified".
//
// On Apple targets however, apparently the 64-bit libc is either buggy or
// intentionally showing odd behavior by rejecting any read with a size
// larger than INT_MAX. To handle both of these the read size is capped on
// both platforms.
const READ_LIMIT: usize = if cfg!(target_vendor = "apple") {
    libc::c_int::MAX as usize
} else {
    libc::ssize_t::MAX as usize
};

#[cfg(any(
    target_os = "dragonfly",
    target_os = "freebsd",
    target_os = "netbsd",
    target_os = "openbsd",
    target_vendor = "apple",
    target_os = "cygwin",
))]
const fn max_iov() -> usize {
    libc::IOV_MAX as usize
}

#[cfg(any(
    target_os = "android",
    target_os = "emscripten",
    target_os = "linux",
    target_os = "nto",
))]
const fn max_iov() -> usize {
    libc::UIO_MAXIOV as usize
}

#[cfg(not(any(
    target_os = "android",
    target_os = "dragonfly",
    target_os = "emscripten",
    target_os = "espidf",
    target_os = "freebsd",
    target_os = "linux",
    target_os = "netbsd",
    target_os = "nuttx",
    target_os = "nto",
    target_os = "openbsd",
    target_os = "horizon",
    target_os = "vita",
    target_vendor = "apple",
    target_os = "cygwin",
)))]
const fn max_iov() -> usize {
    16 // The minimum value required by POSIX.
}

impl FileDesc {
    #[inline]
    pub fn try_clone(&self) -> io::Result<Self> {
        self.duplicate()
    }

    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::read(
                self.as_raw_fd(),
                buf.as_mut_ptr() as *mut libc::c_void,
                cmp::min(buf.len(), READ_LIMIT),
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(not(any(
        target_os = "espidf",
        target_os = "horizon",
        target_os = "vita",
        target_os = "nuttx"
    )))]
    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::readv(
                self.as_raw_fd(),
                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(any(
        target_os = "espidf",
        target_os = "horizon",
        target_os = "vita",
        target_os = "nuttx"
    ))]
    pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
        io::default_read_vectored(|b| self.read(b), bufs)
    }

    #[inline]
    pub fn is_read_vectored(&self) -> bool {
        cfg!(not(any(
            target_os = "espidf",
            target_os = "horizon",
            target_os = "vita",
            target_os = "nuttx",
            target_os = "wasi",
        )))
    }

    pub fn read_to_end(&self, buf: &mut Vec<u8>) -> io::Result<usize> {
        let mut me = self;
        (&mut me).read_to_end(buf)
    }

    pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
        cvt(unsafe {
            pread64(
                self.as_raw_fd(),
                buf.as_mut_ptr() as *mut libc::c_void,
                cmp::min(buf.len(), READ_LIMIT),
                offset as off64_t, // EINVAL if offset + count overflows
            )
        })
        .map(|n| n as usize)
    }

    pub fn read_buf(&self, mut cursor: BorrowedCursor<'_>) -> io::Result<()> {
        // SAFETY: `cursor.as_mut()` starts with `cursor.capacity()` writable bytes
        let ret = cvt(unsafe {
            libc::read(
                self.as_raw_fd(),
                cursor.as_mut().as_mut_ptr().cast::<libc::c_void>(),
                cmp::min(cursor.capacity(), READ_LIMIT),
            )
        })?;

        // SAFETY: `ret` bytes were written to the initialized portion of the buffer
        unsafe {
            cursor.advance_unchecked(ret as usize);
        }
        Ok(())
    }

    pub fn read_buf_at(&self, mut cursor: BorrowedCursor<'_>, offset: u64) -> io::Result<()> {
        // SAFETY: `cursor.as_mut()` starts with `cursor.capacity()` writable bytes
        let ret = cvt(unsafe {
            pread64(
                self.as_raw_fd(),
                cursor.as_mut().as_mut_ptr().cast::<libc::c_void>(),
                cmp::min(cursor.capacity(), READ_LIMIT),
                offset as off64_t, // EINVAL if offset + count overflows
            )
        })?;

        // SAFETY: `ret` bytes were written to the initialized portion of the buffer
        unsafe {
            cursor.advance_unchecked(ret as usize);
        }
        Ok(())
    }

    #[cfg(any(
        target_os = "aix",
        target_os = "dragonfly", // DragonFly 1.5
        target_os = "emscripten",
        target_os = "freebsd",
        target_os = "fuchsia",
        target_os = "hurd",
        target_os = "illumos",
        target_os = "linux",
        target_os = "netbsd",
        target_os = "openbsd", // OpenBSD 2.7
    ))]
    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::preadv(
                self.as_raw_fd(),
                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
                offset as _,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(not(any(
        target_os = "aix",
        target_os = "android",
        target_os = "dragonfly",
        target_os = "emscripten",
        target_os = "freebsd",
        target_os = "fuchsia",
        target_os = "hurd",
        target_os = "illumos",
        target_os = "linux",
        target_os = "netbsd",
        target_os = "openbsd",
        target_vendor = "apple",
    )))]
    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
        io::default_read_vectored(|b| self.read_at(b, offset), bufs)
    }

    // We support some old Android versions that do not have `preadv` in libc,
    // so we use weak linkage and fallback to a direct syscall if not available.
    //
    // On 32-bit targets, we don't want to deal with weird ABI issues around
    // passing 64-bits parameters to syscalls, so we fallback to the default
    // implementation if `preadv` is not available.
    #[cfg(all(target_os = "android", target_pointer_width = "64"))]
    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
        syscall!(
            fn preadv(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        let ret = cvt(unsafe {
            preadv(
                self.as_raw_fd(),
                bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
                offset as _,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(all(target_os = "android", target_pointer_width = "32"))]
    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
        weak!(
            fn preadv64(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        match preadv64.get() {
            Some(preadv) => {
                let ret = cvt(unsafe {
                    preadv(
                        self.as_raw_fd(),
                        bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
                        offset as _,
                    )
                })?;
                Ok(ret as usize)
            }
            None => io::default_read_vectored(|b| self.read_at(b, offset), bufs),
        }
    }

    // We support old MacOS, iOS, watchOS, tvOS and visionOS. `preadv` was added in the following
    // Apple OS versions:
    // ios 14.0
    // tvos 14.0
    // macos 11.0
    // watchos 7.0
    //
    // These versions may be newer than the minimum supported versions of OS's we support so we must
    // use "weak" linking.
    #[cfg(target_vendor = "apple")]
    pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result<usize> {
        weak!(
            fn preadv(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        match preadv.get() {
            Some(preadv) => {
                let ret = cvt(unsafe {
                    preadv(
                        self.as_raw_fd(),
                        bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
                        offset as _,
                    )
                })?;
                Ok(ret as usize)
            }
            None => io::default_read_vectored(|b| self.read_at(b, offset), bufs),
        }
    }

    pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::write(
                self.as_raw_fd(),
                buf.as_ptr() as *const libc::c_void,
                cmp::min(buf.len(), READ_LIMIT),
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(not(any(
        target_os = "espidf",
        target_os = "horizon",
        target_os = "vita",
        target_os = "nuttx"
    )))]
    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::writev(
                self.as_raw_fd(),
                bufs.as_ptr() as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(any(
        target_os = "espidf",
        target_os = "horizon",
        target_os = "vita",
        target_os = "nuttx"
    ))]
    pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
        io::default_write_vectored(|b| self.write(b), bufs)
    }

    #[inline]
    pub fn is_write_vectored(&self) -> bool {
        cfg!(not(any(
            target_os = "espidf",
            target_os = "horizon",
            target_os = "vita",
            target_os = "nuttx",
            target_os = "wasi",
        )))
    }

    pub fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
        #[cfg(not(any(
            all(target_os = "linux", not(target_env = "musl")),
            target_os = "android",
            target_os = "hurd"
        )))]
        use libc::pwrite as pwrite64;
        #[cfg(any(
            all(target_os = "linux", not(target_env = "musl")),
            target_os = "android",
            target_os = "hurd"
        ))]
        use libc::pwrite64;

        unsafe {
            cvt(pwrite64(
                self.as_raw_fd(),
                buf.as_ptr() as *const libc::c_void,
                cmp::min(buf.len(), READ_LIMIT),
                offset as off64_t,
            ))
            .map(|n| n as usize)
        }
    }

    #[cfg(any(
        target_os = "aix",
        target_os = "dragonfly", // DragonFly 1.5
        target_os = "emscripten",
        target_os = "freebsd",
        target_os = "fuchsia",
        target_os = "hurd",
        target_os = "illumos",
        target_os = "linux",
        target_os = "netbsd",
        target_os = "openbsd", // OpenBSD 2.7
    ))]
    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
        let ret = cvt(unsafe {
            libc::pwritev(
                self.as_raw_fd(),
                bufs.as_ptr() as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
                offset as _,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(not(any(
        target_os = "aix",
        target_os = "android",
        target_os = "dragonfly",
        target_os = "emscripten",
        target_os = "freebsd",
        target_os = "fuchsia",
        target_os = "hurd",
        target_os = "illumos",
        target_os = "linux",
        target_os = "netbsd",
        target_os = "openbsd",
        target_vendor = "apple",
    )))]
    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
        io::default_write_vectored(|b| self.write_at(b, offset), bufs)
    }

    // We support some old Android versions that do not have `pwritev` in libc,
    // so we use weak linkage and fallback to a direct syscall if not available.
    //
    // On 32-bit targets, we don't want to deal with weird ABI issues around
    // passing 64-bits parameters to syscalls, so we fallback to the default
    // implementation if `pwritev` is not available.
    #[cfg(all(target_os = "android", target_pointer_width = "64"))]
    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
        syscall!(
            fn pwritev(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        let ret = cvt(unsafe {
            pwritev(
                self.as_raw_fd(),
                bufs.as_ptr() as *const libc::iovec,
                cmp::min(bufs.len(), max_iov()) as libc::c_int,
                offset as _,
            )
        })?;
        Ok(ret as usize)
    }

    #[cfg(all(target_os = "android", target_pointer_width = "32"))]
    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
        weak!(
            fn pwritev64(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        match pwritev64.get() {
            Some(pwritev) => {
                let ret = cvt(unsafe {
                    pwritev(
                        self.as_raw_fd(),
                        bufs.as_ptr() as *const libc::iovec,
                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
                        offset as _,
                    )
                })?;
                Ok(ret as usize)
            }
            None => io::default_write_vectored(|b| self.write_at(b, offset), bufs),
        }
    }

    // We support old MacOS, iOS, watchOS, tvOS and visionOS. `pwritev` was added in the following
    // Apple OS versions:
    // ios 14.0
    // tvos 14.0
    // macos 11.0
    // watchos 7.0
    //
    // These versions may be newer than the minimum supported versions of OS's we support so we must
    // use "weak" linking.
    #[cfg(target_vendor = "apple")]
    pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result<usize> {
        weak!(
            fn pwritev(
                fd: libc::c_int,
                iovec: *const libc::iovec,
                n_iovec: libc::c_int,
                offset: off64_t,
            ) -> isize;
        );

        match pwritev.get() {
            Some(pwritev) => {
                let ret = cvt(unsafe {
                    pwritev(
                        self.as_raw_fd(),
                        bufs.as_ptr() as *const libc::iovec,
                        cmp::min(bufs.len(), max_iov()) as libc::c_int,
                        offset as _,
                    )
                })?;
                Ok(ret as usize)
            }
            None => io::default_write_vectored(|b| self.write_at(b, offset), bufs),
        }
    }

    #[cfg(not(any(
        target_env = "newlib",
        target_os = "solaris",
        target_os = "illumos",
        target_os = "emscripten",
        target_os = "fuchsia",
        target_os = "l4re",
        target_os = "linux",
        target_os = "cygwin",
        target_os = "haiku",
        target_os = "redox",
        target_os = "vxworks",
        target_os = "nto",
        target_os = "wasi",
    )))]
    pub fn set_cloexec(&self) -> io::Result<()> {
        unsafe {
            cvt(libc::ioctl(self.as_raw_fd(), libc::FIOCLEX))?;
            Ok(())
        }
    }
    #[cfg(any(
        all(
            target_env = "newlib",
            not(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))
        ),
        target_os = "solaris",
        target_os = "illumos",
        target_os = "emscripten",
        target_os = "fuchsia",
        target_os = "l4re",
        target_os = "linux",
        target_os = "cygwin",
        target_os = "haiku",
        target_os = "redox",
        target_os = "vxworks",
        target_os = "nto",
        target_os = "wasi",
    ))]
    pub fn set_cloexec(&self) -> io::Result<()> {
        unsafe {
            let previous = cvt(libc::fcntl(self.as_raw_fd(), libc::F_GETFD))?;
            let new = previous | libc::FD_CLOEXEC;
            if new != previous {
                cvt(libc::fcntl(self.as_raw_fd(), libc::F_SETFD, new))?;
            }
            Ok(())
        }
    }
    #[cfg(any(target_os = "espidf", target_os = "horizon", target_os = "vita"))]
    pub fn set_cloexec(&self) -> io::Result<()> {
        // FD_CLOEXEC is not supported in ESP-IDF, Horizon OS and Vita but there's no need to,
        // because none of them supports spawning processes.
        Ok(())
    }

    #[cfg(target_os = "linux")]
    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
        unsafe {
            let v = nonblocking as libc::c_int;
            cvt(libc::ioctl(self.as_raw_fd(), libc::FIONBIO, &v))?;
            Ok(())
        }
    }

    #[cfg(not(target_os = "linux"))]
    pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
        unsafe {
            let previous = cvt(libc::fcntl(self.as_raw_fd(), libc::F_GETFL))?;
            let new = if nonblocking {
                previous | libc::O_NONBLOCK
            } else {
                previous & !libc::O_NONBLOCK
            };
            if new != previous {
                cvt(libc::fcntl(self.as_raw_fd(), libc::F_SETFL, new))?;
            }
            Ok(())
        }
    }

    #[inline]
    pub fn duplicate(&self) -> io::Result<FileDesc> {
        Ok(Self(self.0.try_clone()?))
    }
}

impl<'a> Read for &'a FileDesc {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        (**self).read(buf)
    }

    fn read_buf(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
        (**self).read_buf(cursor)
    }

    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
        (**self).read_vectored(bufs)
    }

    #[inline]
    fn is_read_vectored(&self) -> bool {
        (**self).is_read_vectored()
    }
}

impl AsInner<OwnedFd> for FileDesc {
    #[inline]
    fn as_inner(&self) -> &OwnedFd {
        &self.0
    }
}

impl IntoInner<OwnedFd> for FileDesc {
    fn into_inner(self) -> OwnedFd {
        self.0
    }
}

impl FromInner<OwnedFd> for FileDesc {
    fn from_inner(owned_fd: OwnedFd) -> Self {
        Self(owned_fd)
    }
}

impl AsFd for FileDesc {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0.as_fd()
    }
}

impl AsRawFd for FileDesc {
    #[inline]
    fn as_raw_fd(&self) -> RawFd {
        self.0.as_raw_fd()
    }
}

impl IntoRawFd for FileDesc {
    fn into_raw_fd(self) -> RawFd {
        self.0.into_raw_fd()
    }
}

impl FromRawFd for FileDesc {
    unsafe fn from_raw_fd(raw_fd: RawFd) -> Self {
        Self(unsafe { FromRawFd::from_raw_fd(raw_fd) })
    }
}
