//! <div class="warning">
//!
//! This module is not intended to be used directly. It is only public for
//! documentation purposes.
//!
//! </div>

use std::io;
use std::num::NonZeroUsize;
use std::os::fd::OwnedFd;

use rustix::pipe::{PipeFlags, fcntl_setpipe_size, pipe_with};

/// `MAXIMUM_PIPE_SIZE` is the maximum amount of data we ask the kernel to move
/// in a single call to `splice(2)`.
///
/// We use 1MB as `splice(2)` writes data through a pipe, and 1MB is the default
/// maximum pipe buffer size, which is determined by
/// `/proc/sys/fs/pipe-max-size`.
///
/// Applications running as an unprivileged user may have their page usage
/// limited. See [`pipe(7)`] for details.
///
/// [`pipe(7)`]: https://man7.org/linux/man-pages/man7/pipe.7.html
pub(crate) const MAXIMUM_PIPE_SIZE: NonZeroUsize = NonZeroUsize::new(1 << 20).unwrap();

/// Since Linux 2.6.11, the pipe capacity is 16 pages (i.e., 65,536 bytes in a
/// system with a page size of 4096 bytes).
pub(crate) const DEFAULT_PIPE_SIZE: NonZeroUsize = NonZeroUsize::new(1 << 16).unwrap();

#[derive(Debug)]
/// A pipe for `splice(2)`.
pub(crate) struct Pipe {
    /// File descriptor for reading from the pipe.
    rfd: OwnedFd,

    /// File descriptor for writing to the pipe.
    wfd: OwnedFd,

    /// The capacity of the pipe, in bytes.
    cap: NonZeroUsize,
}

impl Pipe {
    /// Creates a [`Pipe`] with flags `O_CLOEXEC` and `O_NONBLOCK`.
    ///
    /// This will also attempt to reserve [`MAXIMUM_PIPE_SIZE`] bytes of
    /// capacity to minimize the number of syscalls required to move large
    /// amounts of data. When this fails, we assume the capacity of 16 pages
    /// ([`DEFAULT_PIPE_SIZE`]).
    ///
    /// # Errors
    ///
    /// See [`pipe(2)`].
    ///
    /// [`pipe(2)`]: https://man7.org/linux/man-pages/man2/pipe.2.html
    pub(crate) fn new() -> io::Result<Self> {
        pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK)
            .map(|(rfd, wfd)| Self {
                rfd,
                wfd,
                cap: DEFAULT_PIPE_SIZE,
            })
            .map(|mut this| {
                // Ignore errors here, as a smaller buffer size will work,
                // although it will require more system calls.
                let _ = this.reserve(MAXIMUM_PIPE_SIZE);

                this
            })
            .map_err(Into::into)
    }

    /// Reserves **at least** `cap` bytes of capacity with `fcntl(fd,
    /// F_SETPIPE_SZ)`.
    ///
    /// ## Errors
    ///
    /// - The file descriptor is invalid (unlikely).
    /// - `F_SETPIPE_SZ` returns zero (unlikely).
    /// - `F_SETPIPE_SZ` fails with an error, see [`fcntl(2)`] for details.
    ///
    ///   Note that `EBUSY` will be returned if the specified size is smaller
    ///   than the amount of data currently held in the pipe.
    ///
    /// [`fcntl(2)`]: https://man7.org/linux/man-pages/man2/fcntl.2.html.
    pub(crate) fn reserve(&mut self, cap: NonZeroUsize) -> io::Result<NonZeroUsize> {
        match fcntl_setpipe_size(&self.rfd, cap.get()) {
            Ok(cap) => {
                self.cap = NonZeroUsize::new(cap)
                    .ok_or_else(|| io::Error::other("`F_SETPIPE_SZ` returns zero"))?;
            }
            Err(e) => return Err(io::Error::from(e)),
        }

        Ok(self.cap)
    }

    #[inline]
    /// Returns the capacity of the pipe, in bytes.
    pub(crate) const fn capacity(&self) -> NonZeroUsize {
        self.cap
    }

    #[inline]
    pub(crate) const fn rfd(&self) -> &OwnedFd {
        &self.rfd
    }

    #[inline]
    pub(crate) const fn wfd(&self) -> &OwnedFd {
        &self.wfd
    }
}
