//! Filesystem notifications.
//!
//! See [`Watcher`].
//!
//! # Implementation Notes
//!
//! This implementation is based on [`inotify(7)`], which has a number of
//! caveats. Most of them are straight from the manual.
//!
//! Events are not generated for files inside a watched directory that are
//! performed via a symbolic link that lies outside the watched directory.
//!
//! Events are only reported that are generated by user-space programs. Thus
//! watching remote filesystems and pseudo-filesystems (e.g. `/proc` or `/sys`)
//! is not supported.
//!
//! No events are generated for modifications made through `mmap(2)`,
//! `msync(2)`, and `munmap(2)`.
//!
//! This API (and `inotify(7)`) work with paths, however this means there is
//! always a race condition between the time the event is generated and the time
//! the event is processed, in which the file at the path can be deleted or
//! renamed, etc.
//!
//! The event queue can overflow. In this case, events are lost. See
//! `/proc/sys/fs/inotify/max_queued_events` for the maximum queue length.
//!
//! If a filesystem is mounted on top of a monitored directory, no event is
//! generated, and no events are generated for objects immediately under the new
//! mount point. If the filesystem is subsequently unmounted, events will
//! subsequently be generated for the directory and the objects it contains.
//!
//! `inotify` events only carry path information relative to the watched
//! file/directory, meaning that for events on watched files and directories
//! themselves the path information ([`Event::file_path`]) is empty. To get the
//! full path to files/directories you can use [`Events::path_for`], but note
//! that this has a number of gotchas, see its documentation for more.
//!
//! [`inotify(7)`]: https://man7.org/linux/man-pages/man7/inotify.7.html

// NOTE: currently `Watcher` always uses a regular file descriptor as
// `inotify_add_watch` (in `Watcher::watch_path`) only works with regular file
// descriptors, not direct ones.

use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::{CString, OsStr, OsString};
use std::mem::replace;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{self, Poll};
use std::{fmt, io, ptr, slice};

use crate::io::Read;
use crate::{AsyncFd, SubmissionQueue, new_flag, syscall};

/// Filesystem watcher.
///
/// This can be used to watch directories and files for changes and certain
/// operations such as files being opened and closed. See the [module
/// documentation] for caveats.
///
/// [module documentation]: crate::fs::notify
#[derive(Debug)]
pub struct Watcher {
    /// `inotify(7)` file descriptor.
    fd: AsyncFd,
    /// The watch descriptors (wds) and the path to the file or directory they
    /// are watching.
    watching: HashMap<WatchFd, PathBufWithNull>,
}

/// A valid [`PathBuf`] null terminated string, encoding is OS specific.
type PathBufWithNull = CString;

/// Watch descriptor for the `Watcher` instance.
type WatchFd = std::os::fd::RawFd;

const BUF_SIZE: usize = size_of::<libc::inotify_event>() + libc::NAME_MAX as usize + 1 /* NULL. */;

impl Watcher {
    /// Create a new file system watcher.
    pub fn new(sq: SubmissionQueue) -> io::Result<Watcher> {
        let ifd = syscall!(inotify_init1(libc::IN_CLOEXEC))?;
        // SAFETY: we've just created the `ifd` above, so it's valid.
        let fd = unsafe { AsyncFd::from_raw_fd(ifd, sq) };
        Ok(Watcher {
            fd,
            watching: HashMap::new(),
        })
    }

    /// Watch a directory.
    ///
    /// If `dir` is a not a directory this returns an error. See
    /// [`Watcher::watch`] if you want to watch a path and don't care if it's a
    /// file or directory.
    ///
    /// If `recursive` is `Recursive::All` it recursively watches all
    /// directories in `dir`.
    ///
    /// # Notes
    ///
    /// This will walk the entire directory tree (using synchronous I/O) to
    /// create watches for each sub-directory. This may take some time for large
    /// directories.
    pub fn watch_directory(
        &mut self,
        dir: PathBuf,
        interest: Interest,
        recursive: Recursive,
    ) -> io::Result<()> {
        self.watch_path_recursive(dir, interest, recursive, true)
    }

    /// Watch a file.
    pub fn watch_file(&mut self, file: PathBuf, interest: Interest) -> io::Result<()> {
        self.watch_path(file, interest.0)
    }

    /// Watch a directory or file.
    ///
    /// See [`Watcher::watch_directory`] for the usage of `recursive`, for files
    /// it's ignored.
    pub fn watch(
        &mut self,
        dir: PathBuf,
        interest: Interest,
        recursive: Recursive,
    ) -> io::Result<()> {
        self.watch_path_recursive(dir, interest, recursive, false)
    }

    fn watch_path_recursive(
        &mut self,
        dir: PathBuf,
        interest: Interest,
        recursive: Recursive,
        dir_only: bool,
    ) -> io::Result<()> {
        if let Recursive::All = recursive {
            match std::fs::read_dir(&dir) {
                Ok(read_dir) => {
                    for result in read_dir {
                        let entry = result?;
                        if entry.file_type()?.is_dir() {
                            let path = entry.path();
                            self.watch_directory(path, interest, Recursive::All)?;
                        }
                    }
                }
                Err(ref err) if !dir_only && err.kind() == io::ErrorKind::NotADirectory => {
                    // Ignore the error.
                }
                Err(err) => return Err(err),
            }
        }

        let mask = interest.0 | if dir_only { libc::IN_ONLYDIR } else { 0 };
        self.watch_path(dir, mask)
    }

    fn watch_path(&mut self, path: PathBuf, mask: u32) -> io::Result<()> {
        let path = unsafe {
            PathBufWithNull::from_vec_unchecked(OsString::from(path).into_encoded_bytes())
        };
        let mask = mask
            // Don't follow symbolic links.
            | libc::IN_DONT_FOLLOW
            // When files are moved out of a watched directory don't generate
            // events for them.
            | libc::IN_EXCL_UNLINK
            // Instead of replacing a watch combine the watched events.
            | libc::IN_MASK_ADD;
        let fd = self.fd.fd();
        let wd = syscall!(inotify_add_watch(fd, path.as_ptr(), mask))?;
        // NOTE: it's possible the `wd` is already watched, we'll overwrite the
        // path, the watched interested is combined (within the kernel).
        _ = self.watching.insert(wd, path);
        Ok(())
    }

    /// Wait for filesystem events.
    pub fn events<'w>(&'w mut self) -> Events<'w> {
        Events {
            watching: &mut self.watching,
            // NOTE: would be nice to use multishot read here, but as of Linux
            // 6.17 that doesn't work.
            state: EventsState::Reading(self.fd.read(Vec::with_capacity(BUF_SIZE))),
        }
    }
}

new_flag!(
    /// What kind of filesystem changes we're interested in monitering.
    ///
    /// See [`Watcher::watch_directory`] and [`Watcher::watch_file`].
    pub struct Interest(u32) impl BitOr {
        /// Watch everything.
        ALL = libc::IN_ALL_EVENTS,
        /// File was accessed, e.g. read.
        ACCESS = libc::IN_ACCESS,
        /// File was modified, e.g. written.
        MODIFY = libc::IN_MODIFY,
        /// Metadata or attribute changed, e.g. permissions where changed.
        METADATA = libc::IN_ATTRIB,
        /// File opened for writing was closed.
        CLOSE_WRITE = libc::IN_CLOSE_WRITE,
        /// File or directory not opened for writing was closed.
        CLOSE_NOWRITE = libc::IN_CLOSE_NOWRITE,
        /// Combination of [`Interest::CLOSE_WRITE`] and
        /// [`Interest::CLOSE_NOWRITE`] to get all closing events.
        CLOSE = libc::IN_CLOSE,
        /// File or directory was opened.
        OPEN = libc::IN_OPEN,
        /// A file was moved out of the watched directory was renamed.
        MOVE_FROM = libc::IN_MOVED_FROM,
        /// A file was moved into the watched directory.
        MOVE_INTO = libc::IN_MOVED_TO,
        /// Combination of [`Interest::MOVE_FROM`] and [`Interest::MOVE_INTO`]
        /// to get all moving events.
        MOVE = libc::IN_MOVE,
        /// File or directory was created in a watched directory.
        CREATE = libc::IN_CREATE,
        /// File or directory was deleted from a watched directory.
        DELETE = libc::IN_DELETE,
        /// File or directory itself was deleted.
        ///
        /// # Notes
        ///
        /// This event also occurs if an object is moved to another filesystem,
        /// since a move in effect copies the file to the other filesystem and
        /// then deletes it from the original filesystem.
        DELETE_SELF = libc::IN_DELETE_SELF,
        /// File or directory itself was moved.
        MOVE_SELF = libc::IN_MOVE_SELF,
    }
);

/// How to recursively watch a directory.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum Recursive {
    /// Don't watch recursively.
    ///
    /// Only get events for the files and directories directly in the watched
    /// directory.
    ///
    /// # Examples
    ///
    /// The following illustraties which files and directories and watches and
    /// which aren't.
    ///
    /// ```text
    /// # While watching `src/`.
    /// src/main.rs   # Watched
    /// src/fs        # Watched.
    /// src/fs/mod.rs # Not watched.
    /// ```
    No,
    /// Watch recursively.
    ///
    /// # Notes
    ///
    /// When a new directory is created within a recursively watched directory
    /// this new directory **NOT** watched automatically. This is because adding
    /// new watches is a synchronous operation that would have to take place
    /// while using the [`Events`] type, which is an `AsyncIterator` and thus
    /// shouldn't block.
    All,
}

/// [`AsyncIterator`] behind [`Watcher::events`].
///
/// [`AsyncIterator`]: std::async_iter::AsyncIterator
#[must_use = "`AsyncIterator`s do nothing unless polled"]
#[derive(Debug)]
pub struct Events<'w> {
    /// See [`Watcher::watching`].
    watching: &'w mut HashMap<WatchFd, PathBufWithNull>,
    state: EventsState<'w>,
}

/// State of [`Events`].
#[derive(Debug)]
enum EventsState<'w> {
    /// Currently reading.
    Reading(Read<'w, Vec<u8>>),
    /// Processing read events.
    Processing {
        buf: Vec<u8>,
        processed: usize,
        fd: &'w AsyncFd,
    },
    /// No more events.
    Done,
}

impl<'w> Events<'w> {
    /// Returns the path for `event`.
    ///
    /// # Notes
    ///
    /// The return value here depends on the path passed to the `watch*`
    /// functions on `Watcher`, e.g. [`Watcher::watch_directory`]. When a
    /// relative path is passed to the `watch` function this will return a
    /// relative path, if an absolute path is passed this will return an
    /// absolute path.
    ///
    /// Some examples:
    ///
    /// | Watched path   | File triggering event | Returned value         |
    /// |----------------|-----------------------|------------------------|
    /// | `src`          | `file.rs`             | `src/file.rs`          |
    /// | `./src`        | `some/file.rs`        | `./src/some/file.rs`   |
    /// | `/path/to/src` | `file.rs`             | `/path/to/src/file.rs` |
    ///
    /// Internally we keep track of which paths are watched, for which we use
    /// the path as it's passed to `watch` function. To ensure consistent paths
    /// you can canonicalize the paths, using [`Path::canonicalize`], before
    /// calling the watch function.
    ///
    /// If a file is deleted from the file system it is removed from our
    /// internal bookkeeping, meaning that this will return the same value as
    /// [`Event::file_path`] (which is empty for files). To ensure that you
    /// always get the full path call this method **before** calling
    /// [`Events::poll_next`] when processing events.
    pub fn path_for<'a>(&'a self, event: &'a Event) -> Cow<'a, Path> {
        let file_path = event.file_path();
        match self.watched_path(&event.event.wd) {
            Some(path) if file_path.as_os_str().is_empty() => Cow::Borrowed(path),
            Some(path) => Cow::Owned(path.join(file_path)),
            None => Cow::Borrowed(file_path),
        }
    }

    #[allow(clippy::trivially_copy_pass_by_ref)]
    fn watched_path<'a>(&'a self, wd: &WatchFd) -> Option<&'a Path> {
        self.watching.get(wd).map(move |path| {
            // SAFETY: the path was passed to us as a valid `PathBuf`, so it
            // must be a valid `Path`.
            let path = unsafe { OsStr::from_encoded_bytes_unchecked(path.as_bytes()) };
            Path::new(path)
        })
    }

    /// This is the same as the [`AsyncIterator::poll_next`] function, but then
    /// available on stable Rust.
    ///
    /// [`AsyncIterator::poll_next`]: std::async_iter::AsyncIterator::poll_next
    pub fn poll_next(
        mut self: Pin<&mut Self>,
        ctx: &mut task::Context<'_>,
    ) -> Poll<Option<io::Result<&'w Event>>> {
        let this = &mut *self;
        loop {
            match &mut this.state {
                EventsState::Processing { buf, processed, .. } => {
                    if buf.len() > *processed {
                        // SAFETY: the kernel writes zero or more `inotify_event` to
                        // `buf` so we should always be get an inotify_event we reach
                        // this code.
                        debug_assert!(buf.len() >= *processed + size_of::<libc::inotify_event>());
                        #[allow(clippy::cast_ptr_alignment)]
                        let event_ptr = unsafe {
                            buf.as_ptr()
                                .byte_add(*processed)
                                .cast::<libc::inotify_event>()
                        };

                        // Length of the events' path is dynamic.
                        let len = unsafe { (&*event_ptr).len as usize };
                        *processed += size_of::<libc::inotify_event>() + len;
                        debug_assert!(buf.len() >= *processed);

                        // `IN_IGNORED` means the file is no longer watched. An
                        // event before this should contain the information why
                        // (e.g. the file was deleted).
                        let mask = unsafe { (&*event_ptr).mask };
                        if mask & libc::IN_IGNORED != 0 {
                            let wd = unsafe { (&*event_ptr).wd };
                            _ = this.watching.remove(&wd);
                            continue; // Continue to the next event.
                        }

                        if mask & libc::IN_Q_OVERFLOW != 0 {
                            log::warn!("inotify event queue overflowed");
                            continue;
                        }

                        // The path can contain null bytes as padding for alignment,
                        // remove those.
                        let path = unsafe {
                            slice::from_raw_parts(
                                event_ptr
                                    .byte_add(size_of::<libc::inotify_event>())
                                    .cast::<u8>(),
                                len,
                            )
                        };
                        let path_len = path.iter().rposition(|b| *b != 0).map_or(len, |n| n + 1);

                        // SAFETY: this is not really safe. This should use
                        // `ptr::from_raw_parts`, but that's unstable (has been
                        // since 2021)
                        // <https://github.com/rust-lang/rust/issues/81513>.
                        #[allow(clippy::cast_ptr_alignment)]
                        let event: &'w Event = unsafe {
                            &*(ptr::slice_from_raw_parts(event_ptr.cast::<u8>(), path_len)
                                as *const Event)
                        };
                        return Poll::Ready(Some(Ok(event)));
                    }

                    // Processed all events in the buffer, switch to reading
                    // again.
                    let (mut buf, fd) = match replace(&mut this.state, EventsState::Done) {
                        EventsState::Processing {
                            buf,
                            processed: _,
                            fd,
                        } => (buf, fd),
                        EventsState::Reading(_) | EventsState::Done => unreachable!(),
                    };
                    buf.clear();
                    this.state = EventsState::Reading(fd.read(buf));
                    // Going to start the read in the next loop iteration.
                }
                EventsState::Reading(read) => {
                    match Pin::new(&mut *read).poll(ctx) {
                        Poll::Ready(Ok(buf)) => {
                            if buf.is_empty() {
                                this.state = EventsState::Done;
                                return Poll::Ready(None);
                            }

                            this.state = EventsState::Processing {
                                buf,
                                processed: 0,
                                fd: read.fd(),
                            };
                            // Processing the events in the next loop iteration.
                        }
                        Poll::Ready(Err(err)) => {
                            // Ensure that we don't poll the read future again.
                            this.state = EventsState::Done;
                            return Poll::Ready(Some(Err(err)));
                        }
                        Poll::Pending => return Poll::Pending,
                    }
                }
                EventsState::Done => return Poll::Ready(None),
            }
        }
    }
}

#[cfg(feature = "nightly")]
impl<'w> std::async_iter::AsyncIterator for Events<'w> {
    type Item = io::Result<&'w Event>;

    fn poll_next(self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> Poll<Option<Self::Item>> {
        self.poll_next(ctx)
    }
}

/// Event that represent a file system change.
pub struct Event {
    event: libc::inotify_event,
    path: [u8],
}

impl Event {
    /// Path to the file within the watched directory.
    ///
    /// This will only be non-empty for events triggered by files/directories in
    /// watched directories. It be empty for events on watched files and
    /// directories themselves. See [`Events::path_for`] to the get the full
    /// path for watched files and directories.
    pub fn file_path(&self) -> &Path {
        // SAFETY: the path comes from the OS, so it should be a valid OS
        // string.
        Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(&self.path) })
    }

    // Getters for the events.
    bit_checks!(
        /// Return true if the subject of this event is a directory.
        is_dir, IN_ISDIR;
        /// Returns true if:
        ///  * the watched file was accessed, or
        ///  * a file within a watched directory was accessed.
        accessed, IN_ACCESS;
        /// Returns true if:
        ///  * the watched file was modified, or
        ///  * a file within a watched directory was modified.
        modified, IN_MODIFY;
        /// Returns true if:
        ///  * the watched file had its metadata (attributes) changed,
        ///  * a file within a watched directory had its metadata changed, or
        ///  * the watched directory had its metadata changed.
        metadata_changed, IN_ATTRIB;
        /// Returns true if:
        ///  * the watched file, opened for writing, was closed, or
        ///  * a file, opened for writing, within a watched directory was closed.
        ///
        ///  # Notes
        ///
        ///  See [`closed`] for a check that ignores whether or not the file
        ///  was opened for writing or not.
        ///
        ///  [`closed`]: Self::closed
        closed_write, IN_CLOSE_WRITE;
        /// Returns true if:
        ///  * the watched file, not opened for writing, was closed, or
        ///  * a file, not opened for writing, within a watched directory was closed.
        ///  * the watched directory was closed.
        ///
        ///  # Notes
        ///
        ///  See [`closed`] for a check that ignores whether or not the file
        ///  was opened for writing or not.
        ///
        ///  [`closed`]: Self::closed
        closed_no_write, IN_CLOSE_NOWRITE;
        /// Returns true if:
        ///  * the watched file was closed, or
        ///  * a file within a watched directory was closed.
        ///  * the watched directory was closed.
        closed, IN_CLOSE;
        /// Returns true if:
        ///  * the watched file was opened.
        ///  * a file within a watched directory was opened, or
        ///  * the watched directory was opened.
        opened, IN_OPEN;
        /// Returns true if:
        ///  * the watched file was deleted.
        ///  * the watched directory was deleted.
        deleted, IN_DELETE_SELF;
        /// Returns true if:
        ///  * the watched file was moved.
        ///  * the watched directory was moved.
        ///
        /// # Notes
        ///
        /// If a file is moved to another file system this will not trigger
        /// this, but instead trigger [`deleted`].
        ///
        /// [`deleted`]: Self::deleted
        moved, IN_MOVE_SELF;
        /// Returns true if the filesystem containing the watched file or
        /// directory was unmounted.
        unmounted, IN_UNMOUNT;

        // Directory only.

        /// Returns true if:
        ///  * a file within a watched directory was moved out of the watched directory.
        file_moved_from, IN_MOVED_FROM;
        /// Returns true if:
        ///  * a file within a watched directory was moved into the watched directory.
        file_moved_into, IN_MOVED_TO;
        /// Returns true if:
        ///  * a file within a watched directory was moved (into or of out of the watched directory).
        file_moved, IN_MOVE;
        /// Returns true if:
        ///  * a file within a watched directory was created.
        file_created, IN_CREATE;
        /// Returns true if:
        ///  * a file within a watched directory was deleted.
        file_deleted, IN_DELETE;
    );

    const fn mask(&self) -> u32 {
        self.event.mask
    }
}

/// Macro to create functions to check bits set and include them in the
/// fmt::Debug impl.
macro_rules! bit_checks {
    ( $( $(#[$meta: meta])* $fn_name: ident, $bit: ident ; )+ ) => {
        $(
        $( #[$meta] )*
        pub fn $fn_name(&self) -> bool {
            self.mask() & libc::$bit != 0
        }
        )+

        fn events(&self) -> impl fmt::Debug {
            struct Events<'a>(&'a Event);

            impl<'a> fmt::Debug for Events<'a> {
                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                    let mut f = f.debug_list();
                    $(
                    if self.0.$fn_name() {
                        _ = f.entry(&stringify!($fn_name));
                    }
                    )+
                    f.finish()
                }
            }

            Events(self)
        }
    };
}

use bit_checks;

#[allow(clippy::missing_fields_in_debug)]
impl fmt::Debug for Event {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut f = f.debug_struct("Event");
        f.field("wd", &self.event.wd)
            .field("mask", &self.event.mask)
            .field("cookie", &self.event.cookie)
            .field("file_path", &self.file_path())
            .field("events", &self.events())
            .finish()
    }
}
