//! Filesystem notifications.
//!
//! See [`Watcher`].
//!
//! # Implementation Notes
//!
//! The underlying implementation on which the `Watcher` is build work
//! differently. A10 tries to provide a usable cross-platform interface, but
//! it's worth reading the implementation notes below to be aware of the
//! differences.
//!
//! ## Notes for inotify Implementation
//!
//! The implementation based on [`inotify(7)`] 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
//!
//! ## Notes for kqueue Implementation
//!
//! The implementation based on [`kqueue(2)`] also has a number of caveats.
//!
//! Unlike the `inotify` based implementation a new file descriptor has to be
//! opened for each watched file and directory, including each file within a
//! watched directory. Meaning that if a directory with a lot of files is
//! watched it's possible to run out of file descriptors. The maximum number of
//! opened file descriptor can be increased by using [`setrlimit(2)`] with
//! `RLIMIT_NOFILE`.
//!
//! Not all interests are supported. Events for opening or closing a file, and
//! reading from a file are only supported on FreeBSD & NetBSD, not on e.g.
//! OpenBSD or macOS. Events for moving a file/directory into a watched a
//! directory don't work ([`Event::file_moved_into`]), [`Event::file_created`]
//! will be triggered.
//!
//! For events that set [`Event::file_created`] the following methods return
//! incorrect results:
//!  * [`Event::is_dir`] -- always true.
//!  * [`Events::path_for`] -- only contains the parent directory.
//!
//! [`kqueue(2)`]: https://man.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
//! [`setrlimit(2)`]: https://man.freebsd.org/cgi/man.cgi?query=setrlimit&sektion=2

use std::borrow::Cow;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{self, Poll};
use std::{fmt, io};

use crate::sys::fs_notify::{self as sys, EventsState, Watching, watch, watch_recursive};
use crate::{AsyncFd, SubmissionQueue, new_flag};

/// 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)` or `kqueue(2)` file descriptor.
    pub(crate) fd: AsyncFd,
    /// Implementation specific state of the files/directories we're watching.
    pub(crate) watching: Watching,
}

impl Watcher {
    /// Create a new file system watcher.
    pub fn new(sq: SubmissionQueue) -> io::Result<Watcher> {
        Watcher::new_sys(sq)
    }

    /// 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<()> {
        watch_recursive(&self.fd, &mut self.watching, dir, interest, recursive, true)
    }

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

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

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

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 = sys::INTEREST_ALL,
        /// File was accessed, e.g. read.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        ACCESS = sys::INTEREST_ACCESS,
        /// File was modified, e.g. written.
        MODIFY = sys::INTEREST_MODIFY,
        /// Metadata or attribute changed, e.g. permissions where changed.
        METADATA = sys::INTEREST_METADATA,
        /// File opened for writing was closed.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        CLOSE_WRITE = sys::INTEREST_CLOSE_WRITE,
        /// File or directory not opened for writing was closed.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        CLOSE_NOWRITE = sys::INTEREST_CLOSE_NOWRITE,
        /// Combination of [`Interest::CLOSE_WRITE`] and
        /// [`Interest::CLOSE_NOWRITE`] to get all closing events.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        CLOSE = sys::INTEREST_CLOSE,
        /// File or directory was opened.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        OPEN = sys::INTEREST_OPEN,
        /// A file was moved out of the watched directory.
        MOVE_FROM = sys::INTEREST_MOVE_FROM,
        /// A file was moved into the watched directory.
        ///
        /// # Notes
        ///
        /// This is not supported on kqueue as it can't differentiate between
        /// file moved into the watched directory and a new file created.
        /// Instead this will trigger events with [`Event::file_created`]
        /// instead of [`Event::file_moved_into`]. Implementations should check
        /// both methods.
        MOVE_INTO = sys::INTEREST_MOVE_INTO,
        /// A file was moved into or out of the watched directory.
        MOVE = sys::INTEREST_MOVE,
        /// File or directory was created in a watched directory.
        CREATE = sys::INTEREST_CREATE,
        /// File or directory was deleted from a watched directory.
        DELETE = sys::INTEREST_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 = sys::INTEREST_DELETE_SELF,
        /// File or directory itself was moved.
        MOVE_SELF = sys::INTEREST_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> {
    // NOTE: need to split the fields as we can't mutably borrow the fd in this
    pub(crate) fd: &'w AsyncFd,
    // type and in the call to read.
    /// See [`Watcher::watching`].
    pub(crate) watching: &'w mut Watching,
    pub(crate) state: EventsState<'w>,
}

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> {
        self.path_for_sys(&event.0)
    }

    /// See [`Watcher::watch_directory`].
    pub fn watch_directory(
        &mut self,
        dir: PathBuf,
        interest: Interest,
        recursive: Recursive,
    ) -> io::Result<()> {
        watch_recursive(self.fd, self.watching, dir, interest, recursive, true)
    }

    /// See [`Watcher::watch_file`].
    pub fn watch_file(&mut self, file: PathBuf, interest: Interest) -> io::Result<()> {
        watch(self.fd, self.watching, file, interest)
    }

    /// See [`Watcher::watch`].
    pub fn watch(
        &mut self,
        path: PathBuf,
        interest: Interest,
        recursive: Recursive,
    ) -> io::Result<()> {
        watch_recursive(self.fd, self.watching, path, interest, recursive, false)
    }

    /// 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(
        self: Pin<&mut Self>,
        ctx: &mut task::Context<'_>,
    ) -> Poll<Option<io::Result<&'w Event>>> {
        self.poll_sys(ctx)
    }
}

#[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.
#[repr(transparent)]
pub struct Event(sys::Event);

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.
    #[deprecated(
        note = "this doesn't work for the kqueue implementation, use `Events::path_for` instead"
    )]
    pub fn file_path(&self) -> &Path {
        self.0.file_path()
    }

    // Getters for the events.
    bit_checks!(
        /// Return true if the subject of this event is a directory.
        is_dir;
        /// Returns true if:
        ///  * the watched file was accessed, or
        ///  * a file within a watched directory was accessed.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        accessed, sys::EVENT_ACCESSED;
        /// Returns true if:
        ///  * the watched file was modified, or
        ///  * a file within a watched directory was modified.
        modified;
        /// 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, sys::EVENT_METADATA_CHANGED;
        /// 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
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        closed_write, sys::EVENT_CLOSED_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
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        closed_no_write, sys::EVENT_CLOSED_NO_WRITE;
        /// Returns true if:
        ///  * the watched file was closed, or
        ///  * a file within a watched directory was closed.
        ///  * the watched directory was closed.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        closed, sys::EVENT_CLOSED;
        /// Returns true if:
        ///  * the watched file was opened.
        ///  * a file within a watched directory was opened, or
        ///  * the watched directory was opened.
        #[cfg(any(target_os = "android", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))]
        opened, sys::EVENT_OPENED;
        /// Returns true if:
        ///  * the watched file was deleted.
        ///  * the watched directory was deleted.
        deleted;
        /// 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, sys::EVENT_MOVED;
        /// Returns true if the filesystem containing the watched file or
        /// directory was unmounted.
        unmounted, sys::EVENT_UNMOUNTED;

        // Directory only.

        /// Returns true if:
        ///  * a file within a watched directory was moved out of the watched directory.
        file_moved_from;
        /// Returns true if:
        ///  * a file within a watched directory was moved into the watched directory.
        ///
        /// # Notes
        ///
        /// This is not supported on kqueue (and will always return false) as it
        /// can't differentiate between file moved into the watched directory
        /// and a new file created. When using this method also consider using
        /// [`Event::file_created`] to ensure events are created for newly
        /// created files.
        file_moved_into;
        /// Returns true if:
        ///  * a file within a watched directory was moved (into or of out of the watched directory).
        ///
        /// The limitation documented in [`Event::file_moved_into`] also applies
        /// here.
        file_moved;
        /// Returns true if:
        ///  * a file within a watched directory was created.
        ///
        /// # Notes
        ///
        /// For the kqueue implementation the path returned is the directory,
        /// where as inotify returns the path of the file/directory that is
        /// created.
        ///
        /// Furthermore, for the kqueue implementation if this returns true the
        /// [`Event::is_dir`] method will also always return true, even if a
        /// file was created.
        file_created;
        /// Returns true if:
        ///  * a file within a watched directory was deleted.
        file_deleted;
    );

    const fn mask(&self) -> u32 {
        self.0.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 $(, $libc: ident :: $bit: ident)? ; )* ) => {
        $(
        $crate::fs::notify::bit_checks!(__fn $( #[$meta] )* $fn_name $(, $libc :: $bit )?);
        )*

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

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

            Events(self)
        }
    };
    (__fn $(#[$meta: meta])* $fn_name: ident) => {
        $( #[$meta] )*
        pub fn $fn_name(&self) -> bool {
            self.0.$fn_name()
        }
    };
    (__fn $(#[$meta: meta])* $fn_name: ident, $libc: ident :: $bit: ident ) => {
        $( #[$meta] )*
        pub fn $fn_name(&self) -> bool {
            self.mask() & $libc::$bit != 0
        }
    };
}

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");
        self.0.fmt(&mut f).field("events", &self.events()).finish()
    }
}
