// =============================================================== //
//                                                                 //
//   File      : AW_inotify.cxx                                    //
//   Purpose   : watch file/directory changes                      //
//                                                                 //
//   Coded by Ralf Westram (coder@reallysoft.de) in October 2017   //
//   http://www.arb-home.de/                                       //
//                                                                 //
// =============================================================== //

#include "aw_inotify.hxx"

#include "aw_root.hxx"
#include <arbdbt.h>
#include <arb_file.h>

#include <unistd.h>

#include <string>
#include <list>
#include <set>
#include <tuple>

#include <sys/stat.h>

using namespace std;

#if defined(LINUX)
# define USE_INOTIFY
#endif

#if defined(USE_INOTIFY)
# define DETECT_NFS
# define IF_USE_INOTIFY(x) x
#else
# define USE_STATPOLL
# define IF_USE_INOTIFY(x)
#endif

#if defined(USE_INOTIFY)
# include <sys/inotify.h>
#endif

#if defined(DETECT_NFS)
# include <sys/vfs.h>
# include <linux/magic.h> // For NFS_SUPER_MAGIC

static bool is_nfs(const char *path) {
    struct statfs buf;

    int result = statfs(path, &buf);
    if (result != 0) {
#if defined(ASSERTION_USED)
        fprintf(stderr, "failed to statfs(%s) result=%i\n", path, result);
        aw_assert(0); // shall only be called for existing paths
#endif
        return false;
    }
    return buf.f_type == NFS_SUPER_MAGIC;
}
#endif


#if defined(DEBUG)
// #define TRACE_INOTIFY
// #define TRACE_INOTIFY_BASIC
#endif

#if defined(TRACE_INOTIFY)

# define TRACE(format,...) fprintf(stderr, format, ##__VA_ARGS__)
# define IF_TRACE_INOTIFY(x) x
class TraceBlock {
    string blockname;
    void log(const char *state) { fprintf(stderr, "%s [%s]\n", blockname.c_str(), state); }
public:
    TraceBlock(string bn) : blockname(bn) { log("start"); }
    ~TraceBlock() { log("end"); }
};

#else

# define TRACE(format,...)
# define IF_TRACE_INOTIFY(x)

#endif

#if defined(TRACE_INOTIFY_BASIC)
# define TRACE_BASIC(format,...) fprintf(stderr, format, ##__VA_ARGS__)
#else
# define TRACE_BASIC(format,...)
#endif

inline int timespec_cmp(const timespec& a, const timespec& b) {
    /*! Compare for timespec providing chronological order. */
    return compareAsLTC(a, b,
                        // manually define precedence of properties for comparison:
                        [](const timespec& t) { return std::tie(t.tv_sec, t.tv_nsec); });
}
inline bool operator <  (const timespec& a, const timespec& b) { return timespec_cmp(a, b) <  0; }
inline bool operator <= (const timespec& a, const timespec& b) { return timespec_cmp(a, b) <= 0; }
inline bool operator >  (const timespec& a, const timespec& b) { return timespec_cmp(a, b) >  0; }
inline bool operator >= (const timespec& a, const timespec& b) { return timespec_cmp(a, b) >= 0; }
inline bool operator == (const timespec& a, const timespec& b) { return timespec_cmp(a, b) == 0; }
inline bool operator != (const timespec& a, const timespec& b) { return timespec_cmp(a, b) != 0; }

typedef set<FileChangedCallback> CallbackList;
class TrackedFiles;

const timespec zeroTimeSpec = { 0, 0 };

class TrackedFile : public Noncopyable {
    string        file;
    const string& parent; // parent directory
    CallbackList  callbacks;

#if defined(USE_INOTIFY)
    int watch_descriptor;
#endif

    struct State {
        timespec nanoModtime;
        enum FileType {
            MISSING   = 0,
            EXISTS    = 1,
            DIRECTORY = 2,
            ON_NFS    = 4,
            UNKNOWN   = 8,

            DIRECTORY_ON_NFS = EXISTS | DIRECTORY | ON_NFS,
            FILE_ON_NFS      = EXISTS | ON_NFS,

        } type;

        State() : nanoModtime(zeroTimeSpec),
                  type(UNKNOWN) {}

        void update(const string& file) {
            const char *path = file.c_str();
            if (GB_nanotime_of_file(path, nanoModtime)) {
                type                            = EXISTS;
                if (GB_is_directory(path)) type = FileType(type|DIRECTORY);
#if defined(DETECT_NFS)
                if (is_nfs(path)) type          = FileType(type|ON_NFS);
#endif
            }
            else {
                type        = MISSING;
                nanoModtime = zeroTimeSpec;
            }
        }

        bool trackedExists() const { return type & EXISTS; }
    };

    void update(State& state) const { state.update(file); }

    mutable State previously;

    ChangeReason was_changed() const {
        ChangeReason changeType = CR_NONE;
        State        currently;
        update(currently);

        if (currently.trackedExists()) {
            if (previously.nanoModtime < currently.nanoModtime) { // if target has changed ..
                changeType = previously.trackedExists() ? CR_MODIFIED : CR_CREATED;
            }

            TRACE("was_changed? file='%s' changed=%i, previously.nanoModtime=%li,%li currently.nanoModtime=%li,%li\n",
                  file.c_str(), int(changeType),
                  previously.nanoModtime.tv_sec, previously.nanoModtime.tv_nsec,
                  currently.nanoModtime.tv_sec, currently.nanoModtime.tv_nsec);
        }
        else { // file does not exist (or vanished)
            if (previously.trackedExists()) changeType = CR_DELETED;
        }
        if (changeType != CR_NONE) previously = currently;
        return changeType;
    }

public:
    TrackedFile(const string& filename, const string& parentPath) :
        file(filename),
        parent(parentPath)
    {
#if defined(USE_INOTIFY)
        watch_descriptor = -1;
#endif
        update(previously);

        TRACE("new TrackedFile: file='%s' previously.nanoModtime=%li,%li\n",
              file.c_str(), previously.nanoModtime.tv_sec, previously.nanoModtime.tv_nsec);
    }
#if defined(USE_INOTIFY)
    ~TrackedFile() {
        aw_assert(watch_descriptor == -1); // watch has to be removed before destruction
    }
#endif

    const string& get_name() const { return file; }
    const string& get_parent_dir() const { return parent; }

    bool has_parent(string p) const { return p == get_parent_dir(); }

    void add_callback   (const FileChangedCallback& cb) { callbacks.insert(cb); }
    void remove_callback(const FileChangedCallback& cb) { callbacks.erase(cb); }

    bool empty() const { return callbacks.empty(); }

    void callAll(ChangeReason reason) const {
#if defined(ASSERTION_USED)
        bool deleted_still_exists = reason == CR_DELETED && GB_is_regularfile(file.c_str()); // this condition been introduced with [16536].
#endif
        aw_assert(reason != CR_NONE);     // require a reason
        aw_assert(!deleted_still_exists); // require file has vanished

        for (const auto& cb : CallbackList(callbacks)) { // operate on copy of 'callbacks' list, to allow that callbacks remove themselves.
            if (callbacks.find(cb) != callbacks.end()) { // avoid calling already deleted callbacks
                cb(file.c_str(), reason);
            }
        }
    }

    bool was_directory() const { return previously.type & State::FileType::DIRECTORY; }

#if defined(DETECT_NFS)

    bool was_nfs_dir() const { return previously.type == State::FileType::DIRECTORY_ON_NFS; }
    bool was_nfs_file() const { return previously.type == State::FileType::FILE_ON_NFS; }

#endif

#if defined(USE_INOTIFY)
    int get_watch_descriptor() const {
        return watch_descriptor;
    }
    GB_ERROR add_watch(int inotifier) {
        aw_assert(watch_descriptor == -1);
        watch_descriptor = inotify_add_watch(inotifier, file.c_str(), IN_DELETE_SELF|IN_MOVE_SELF|IN_CLOSE_WRITE|IN_DELETE);
        return watch_descriptor<0 ? GB_IO_error("watching", file.c_str()) : NULp;
    }
    GB_ERROR remove_watch(int inotifier) {
        GB_ERROR err = NULp;
        if (watch_descriptor != -1) {
            if (inotify_rm_watch(inotifier, watch_descriptor)<0) {
                err = GB_IO_error("un-watching", file.c_str());
            }
            watch_descriptor = -1;
        }
        return err;
    }
    void mark_as_disabled() {
        watch_descriptor = -1;
    }
    void track_creation(TrackedFiles *tracked);
    void track_nfs_change(TrackedFiles *tracked);
#endif

    bool callback_if_changed() const {
        ChangeReason typeOfChange = was_changed();
        if (typeOfChange != CR_NONE) {
            callAll(typeOfChange);
            return true;
        }
        return false;
    }

    bool operator<(const TrackedFile& other) const {
        return file < other.file; // compare file names
    }
};

typedef SmartPtr<TrackedFile> TrackedFilePtr;

class TrackedFiles : virtual Noncopyable {
    typedef list<TrackedFilePtr> FileList;
    typedef set<TrackedFilePtr>  FileSet;

    FileList files;
    FileSet  filesToCheck; // TrackedFiles that need to get checked for changes

    set<string> parents;

#if defined(USE_INOTIFY)
    int inotifier;

    RefPtr<const char> error;

    SmartCharPtr ievent_buffer;
    int          oversize;

    bool reactivate_stale;

    size_t get_ievent_buffer_size() {
        return sizeof(inotify_event)+oversize;
    }
    void realloc_buffer(int new_oversize) {
        aw_assert(new_oversize>oversize);
        oversize      = new_oversize;
        ievent_buffer = ARB_alloc<char>(get_ievent_buffer_size());
    }

    inotify_event *get_ievent_buffer() {
        if (oversize<0) realloc_buffer(9*sizeof(inotify_event)); // reserve memory for 10 events
        return reinterpret_cast<inotify_event*>(&*ievent_buffer);
    }
    void increase_buffer_oversize() {
        aw_assert(oversize>0);
        realloc_buffer(oversize*3/2);
    }

    FileList::iterator find_watch_descriptor(int wd) {
        for (FileList::iterator f = files.begin(); f != files.end(); ++f) {
            if (wd == (*f)->get_watch_descriptor()) {
                return f;
            }
        }
        return files.end();
    }

    void mark_for_check(TrackedFilePtr trackedFile, const char *IF_TRACE_INOTIFY(reason)) {
#if defined(TRACE_INOTIFY)
        auto result =
#endif
            filesToCheck.insert(trackedFile);
#if defined(TRACE_INOTIFY)
        bool inserted = result.second;
        if (inserted) {
            TRACE("-> add '%s' to filesToCheck (reason: %s)\n",
                  trackedFile->get_name().c_str(),
                  reason);
        }
#endif
    }

    void check_for_created_files();
#endif

    void mark_files_to_check();

    FileList::iterator find(const string& file) {
        for (FileList::iterator f = files.begin(); f != files.end(); ++f) {
            if (file == (*f)->get_name()) return f;
        }
        return files.end();
    }

    const string& getParent(const string& file) {
        char *parent_dir;
        GB_split_full_path(file.c_str(), &parent_dir, NULp, NULp, NULp);
        std::pair<set<string>::const_iterator, bool> stored = parents.insert(parent_dir);
        free(parent_dir);
        return *stored.first;
    }

public:

#if defined(USE_INOTIFY)
    TrackedFiles() :
        error(NULp),
        oversize(-1),
        reactivate_stale(false)
    {
        inotifier = inotify_init();
        if (inotifier<0) {
            error = GB_IO_error("creating", "<inotify-instance>");
        }
    }
    ~TrackedFiles() {
        for (FileList::iterator fi = files.begin(); fi != files.end(); ++fi) {
            TrackedFilePtr f = *fi;
            if (f->get_watch_descriptor() >= 0) {
                TRACE("~TrackedFiles: remove_watch for '%s': wd=%i\n", f->get_name().c_str(), f->get_watch_descriptor());
                GB_ERROR  ferr  = f->remove_watch(inotifier);
                if (ferr) fprintf(stderr, "Error in ~TrackedFiles: %s\n", ferr);
            }
            aw_assert(f->get_watch_descriptor() == -1);
        }
        close(inotifier);
    }
#endif

    GB_ERROR get_error() {
#if defined(USE_INOTIFY)
        GB_ERROR err = error;
        error        = NULp;
        return err;
#else // USE_STATPOLL
        return NULp;
#endif
    }

    bool empty() const {
        return files.empty();
    }

    void insert(string file, const FileChangedCallback& fccb) {
        file = GB_canonical_path(file.c_str());

        TRACE_BASIC("[inotifier] + %s\n", file.c_str());

#if defined(USE_INOTIFY)
        aw_assert(!error);
#endif
        FileList::iterator foundIter = find(file);
        TrackedFilePtr     found;

        if (foundIter != files.end()) {
            found = *foundIter;
        }
        else {
            const string& parent = getParent(file); // string persists until TrackedFiles get destroyed
            found = new TrackedFile(file, parent);
            files.push_back(found);

#if defined(USE_INOTIFY)
            GB_ERROR werr = found->add_watch(inotifier);
            if (werr) found->track_creation(this);
            TRACE("insert: add_watch for '%s': %s wd=%i\n", found->get_name().c_str(), werr, found->get_watch_descriptor());
            if (found->was_nfs_file()) found->track_nfs_change(this);
#endif
        }
        found->add_callback(fccb);
    }

    void erase(string file, const FileChangedCallback& fccb) {
        file = GB_canonical_path(file.c_str());

        TRACE_BASIC("[inotifier] - %s\n", file.c_str());

#if defined(USE_INOTIFY)
        aw_assert(!error);
#endif

        FileList::iterator foundIter = find(file);
        if (foundIter != files.end()) {
            TrackedFilePtr found = *foundIter;
            found->remove_callback(fccb);
            if (found->empty()) {
#if defined(USE_INOTIFY)
                if (found->get_watch_descriptor() != -1) {
                    TRACE("erase: remove_watch for '%s': wd=%i\n", found->get_name().c_str(), found->get_watch_descriptor());
                    error = found->remove_watch(inotifier);
                }
#endif
                files.erase(foundIter);
            }
        }
    }

    void check_changes(bool intensive);

#if defined(USE_INOTIFY)
    void request_reactivate_stale() { reactivate_stale = true; }
#endif
};

#if defined(USE_INOTIFY)

static void parent_modified_cb(const char *IF_TRACE_INOTIFY(parent_dir), ChangeReason IF_TRACE_INOTIFY(reason), TrackedFiles *tracked) {
    TRACE("parent_modified_cb(dir='%s', reason=%i) called\n", parent_dir, int(reason));
    tracked->request_reactivate_stale();
}
static void nfs_parent_modified_cb(const char *IF_TRACE_INOTIFY(parent_dir), ChangeReason IF_TRACE_INOTIFY(reason), TrackedFiles *tracked) {
    TRACE("nfs_parent_modified_cb(dir='%s', reason=%i) called\n", parent_dir, int(reason));
    tracked->request_reactivate_stale();
}

void TrackedFile::track_creation(TrackedFiles *tracked) {
    aw_assert(get_watch_descriptor() == -1); // otherwise it exists
    TRACE("TrackedFile::track_creation: file='%s' parent='%s'\n", file.c_str(), get_parent_dir().c_str());
    AW_add_inotification(get_parent_dir(), makeFileChangedCallback(parent_modified_cb, tracked));
}
void TrackedFile::track_nfs_change(TrackedFiles *tracked) {
    TRACE("TrackedFile::track_nfs_change: file='%s' parent='%s'\n", file.c_str(), get_parent_dir().c_str());
    AW_add_inotification(get_parent_dir(), makeFileChangedCallback(nfs_parent_modified_cb, tracked));
}

void TrackedFiles::check_for_created_files() {
    aw_assert(reactivate_stale);

    bool seen_stale_watch_descriptor = false;
    for (TrackedFilePtr f : files) {
        if (f->get_watch_descriptor() == -1) {
            GB_ERROR err = f->add_watch(inotifier);
            if (!err) { // successfully reactivated
                mark_for_check(f, "had stale watch descriptor");
            }
            else {
                seen_stale_watch_descriptor = true;
            }
            TRACE("check_for_created_files: add_watch for '%s': %s wd=%i\n", f->get_name().c_str(), err, f->get_watch_descriptor());
        }
    }
    if (!seen_stale_watch_descriptor) {
        // no "stale" watch descriptor left = > uninstall all watches of parent_modified_cb

        FileChangedCallback pmc = makeFileChangedCallback(parent_modified_cb, this);

        FileList filesCopy = files; // clone filelist to avoid deletion from list, while iterating it
        for (TrackedFilePtr f : filesCopy) {
            if (f->was_directory()) {
                AW_remove_inotification(f->get_name().c_str(), pmc);
            }
        }
    }
}

#endif // USE_INOTIFY


#define TRACE_REACTION_BLOCK(name) IF_TRACE_INOTIFY(TraceBlock tb("reaction: " name));

void TrackedFiles::mark_files_to_check() {
#if defined(USE_INOTIFY)
    aw_assert(!error);

    timespec timeout = zeroTimeSpec; // don't wait
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(inotifier, &readfds);

    int sel;
    while ((sel = pselect(inotifier+1, &readfds, NULp, NULp, &timeout, NULp))) { // only poll if events are waiting
        aw_assert(sel>0);

        inotify_event *ievent = get_ievent_buffer();
        ssize_t        got    = read(inotifier, ievent, get_ievent_buffer_size());

        if (got >= ssize_t(sizeof(inotify_event))) {
            while (got>0) {
                FileList::iterator foundIter = find_watch_descriptor(ievent->wd);
                if (foundIter != files.end()) {
                    TrackedFilePtr  found         = *foundIter;
                    bool            watch_removed = (ievent->mask & IN_IGNORED);
                    const char     *name          = (ievent->len>1) ? ievent->name : "";

                    TRACE("got inotify event for '%s' (wd=%i, mask=%X, watch_removed=%i) '%s'\n",
                          found->get_name().c_str(), ievent->wd, ievent->mask, int(watch_removed), name);

                    bool needToCheck = false;
                    if (watch_removed) {
                        TRACE_REACTION_BLOCK("watch_removed");
                        found->mark_as_disabled();
                        found->track_creation(this);
                    }
                    else if (ievent->mask & IN_DELETE_SELF) {
                        TRACE_REACTION_BLOCK("IN_DELETE_SELF");
                        needToCheck = true;
                    }
                    else if (ievent->mask & IN_MOVE_SELF) {
                        TRACE_REACTION_BLOCK("IN_MOVE_SELF");
                        needToCheck      = true;
                        TRACE("mark_files_to_check: remove_watch for '%s': wd=%i\n", found->get_name().c_str(), found->get_watch_descriptor());
                        found->remove_watch(inotifier); // invalidate watch descriptor of found
                        found->track_creation(this);    // ensure re-creation gets detected
                        reactivate_stale = true;        // causes check whether new target has a "stale" watch
                    }
                    else if (ievent->mask & (IN_DELETE|IN_CLOSE_WRITE)) { // item deleted from directory or file/dir modified
                        TRACE_REACTION_BLOCK("IN_DELETE|IN_CLOSE_WRITE");
                        needToCheck = true;
                    }
                    else {
                        TRACE_REACTION_BLOCK("unhandled");
                        aw_assert(0); // unhandled event from inotify
                    }

                    if (needToCheck) {
                        mark_for_check(found, "seen inotify event");
                        if (name[0]) {
                            // check mentioned file if tracked
                            GB_CSTR mentioned = GB_concat_path(found->get_name().c_str(), name);
                            FileList::iterator mentionedIter = find(mentioned);
                            if (mentionedIter != files.end()) {
                                mark_for_check(*mentionedIter, "mentioned by name in inotify event of parent");
                            }
                        }
                    }
                }
                else {
                    TRACE("ignoring event for unknown watch_descriptor=%i\n", ievent->wd);
                }

                int event_len  = sizeof(inotify_event)+ievent->len;
                got           -= event_len;
                ievent         = reinterpret_cast<inotify_event*>(reinterpret_cast<char*>(ievent)+event_len);
            }
        }
        else {
            bool     buffer_too_small = false;
            GB_ERROR read_err         = NULp;

            if (got == -1) {
                if (errno == EINVAL) buffer_too_small = true;
                else read_err = GB_IO_error("reading", "<inotify-descriptor>");
            }
            else if (got == 0) buffer_too_small = true;
            else {
                aw_assert(0);
            }

            if (read_err) {
                fprintf(stderr, "inotifier broken: %s\n", read_err);
                aw_assert(0);
                break; // while loop
            }

            if (!buffer_too_small) {
                aw_assert(buffer_too_small);
                GBK_terminate("inotify event queue broken? (neither buffer_too_small nor read_err)");
            }
            increase_buffer_oversize();
        }

        aw_assert(FD_ISSET(inotifier, &readfds));
    }

    if (reactivate_stale) {
        check_for_created_files();
        reactivate_stale = false; // do once if any dir-content changes
    }

#else // USE_STATPOLL

    // in poll mode we always check all files:
    for (TrackedFilePtr file : files) filesToCheck.insert(file);

#endif

}
void TrackedFiles::check_changes(bool IF_USE_INOTIFY(intensive)) {
    // called manually from unittest below
    // called via timer callback otherwise
    //
    // If param 'intensive' is false this method may miss several detectable changes,
    // e.g. file renames or deletes do not trigger the directory in inotify mode.
    //
    // To workaround this behavior we call this method with 'intensive' == true,
    // to force a modification check of all watched directories.

    mark_files_to_check();

#if defined(USE_INOTIFY)
    for (TrackedFilePtr file : files) {
        if (file->was_directory()) {
            if (intensive) {
                TRACE("force check of '%s' in intensive mode\n", file->get_name().c_str());
                filesToCheck.insert(file); // intensive mode always checks directories for changes
            }
#if defined(DETECT_NFS)
            else if (file->was_nfs_dir()) {
                TRACE("found directory on NFS -> force check of '%s'\n", file->get_name().c_str());
                filesToCheck.insert(file); // always checks NFS directories for changes
            }
#endif
        }

    }
    set<string> modified_dirs;
#endif

    FileList calledBack;
    for (TrackedFilePtr file : filesToCheck) {
        if (file->callback_if_changed()) { // this updates the State of 'file'
            calledBack.push_back(file);
#if defined(USE_INOTIFY)
            if (file->was_directory()) modified_dirs.insert(file->get_name());
#endif
        }
    }

#if defined(USE_INOTIFY)
    // For all unchecked files: if parent dir did change, then force check for file.
    for (TrackedFilePtr file : files) {
        if (filesToCheck.find(file) == filesToCheck.end()) { // with all unchecked files
            if (modified_dirs.find(file->get_parent_dir()) != modified_dirs.end()) {
                TRACE("unchecked file in modified directory -> force check '%s'\n", file->get_name().c_str());
                if (file->callback_if_changed()) {
                    calledBack.push_back(file);
                }
            }
        }
    }
#endif
    filesToCheck.clear();

#if defined(TRACE_INOTIFY)
    if (!calledBack.empty()) TRACE("changes were detected in %s mode\n", intensive ? "intensive" : "normal");
#endif

    while (!calledBack.empty()) {
        set<string> dirsToTrigger;
        for (TrackedFilePtr file : calledBack) { // mark all parent dirs
            dirsToTrigger.insert(file->get_parent_dir());
        }
        for (TrackedFilePtr file : calledBack) { // unmark all already triggered parent dirs
            dirsToTrigger.erase(file->get_name());
        }

        calledBack.clear();
        for (TrackedFilePtr dir : files) {
            if (dirsToTrigger.erase(dir->get_name())) {
                dir->callAll(CR_MODIFIED);
                calledBack.push_back(dir); // recurse upwards
            }
        }
    }

    aw_assert(filesToCheck.empty());
}

// --------------------------------------------------------------------------------

int64_t timespec_diff_ms(const struct timespec& start, const struct timespec& end) {
    return (int64_t)(end.tv_sec - start.tv_sec) * 1000LL +
        (int64_t)(end.tv_nsec - start.tv_nsec) / 1000000LL;
}

// --------------------------------------------------------------------------------

static SmartPtr<TrackedFiles> allTrackers(new TrackedFiles);

static bool maintain_timer_callback = true;

#if defined(USE_INOTIFY)
static timespec last_intensive_check    = zeroTimeSpec;
const unsigned AW_INOTIFY_TIMER     = 700;    // ms
const unsigned AW_INOTIFY_INTENSIVE = 2000;   // ms (a bit less than 3*AW_INOTIFY_TIMER)
#else // USE_STATPOLL
const unsigned AW_INOTIFY_TIMER     = 1250;   // ms
#endif

static unsigned timed_inotifications_check_cb() {
    // this callback triggers two types of checks:
    // - frequent checks (check for inotify events every 700ms)
    // - forced checks (poll directories every 2100ms)

#if defined(USE_INOTIFY)

    timespec now;
    clock_gettime(CLOCK_REALTIME, &now);

    int64_t since_last_intensive_ms = timespec_diff_ms(last_intensive_check, now);
    bool    intensive               = since_last_intensive_ms >= AW_INOTIFY_INTENSIVE;

#else // !USE_INOTIFY

    bool intensive = true; // should not matter (use same as in tests)

#endif

    allTrackers->check_changes(intensive);
    if (allTrackers->empty()) {
        TRACE_BASIC("[inotifier] - timer callback\n");
        return 0;
    }
#if defined(USE_INOTIFY)
    if (intensive) last_intensive_check = now;
#endif
    return AW_INOTIFY_TIMER;
}

static void show_tracked_error() {
    GB_ERROR err = allTrackers->get_error();
    if (err) {
        fprintf(stderr, "Error in TrackedFiles: %s\n", err);
    }
}

void AW_add_inotification(const string& file, const FileChangedCallback& fccb) {
    /*! callback after a file changes
     * @param file the file to watch
     * @param fccb callback to call
     */
    show_tracked_error();
    if (allTrackers->empty() && maintain_timer_callback) {
        TRACE_BASIC("[inotifier] + timer callback\n");
        AW_root::SINGLETON->add_timed_callback(AW_INOTIFY_TIMER, makeTimedCallback(timed_inotifications_check_cb));
    }
    allTrackers->insert(file, fccb);
    show_tracked_error();
}

void AW_remove_inotification(const string& file, const FileChangedCallback& fccb) {
    allTrackers->erase(file, fccb);
    show_tracked_error();
}

// --------------------------------------------------------------------------------

#ifdef UNIT_TESTS
#ifndef TEST_UNIT_H
#include <test_unit.h>
#endif
#include <arb_cs.h>
#include <arb_str.h>
#include <arb_sleep.h>

const int FLAGS = 4;
static int change_flag[FLAGS][CHANGE_REASONS];

static void trace_file_changed_cb(const char *IF_TRACE_INOTIFY(file), ChangeReason reason, int flag) {
    aw_assert(flag<FLAGS);
    change_flag[flag][reason]++;

    TRACE("trace_file_changed_cb: flag=%i reason=%i file='%s'\n", flag, int(reason), file);
}

// buf layout is "0123-0123-0123" (digits here are flag indices)
#define BUFIDX(f,r) ((r)*(FLAGS+1)+(f))

static const char *update_change_counts() {
    for (int f = 0; f<FLAGS; ++f) {
        for (int r = 0; r<CHANGE_REASONS; ++r) {
            change_flag[f][r] = 0;
        }
    }

    allTrackers->check_changes(true); // force intensive mode for tests

    static char buf[BUFIDX(0,CHANGE_REASONS)];
    for (int r = 0; r<CHANGE_REASONS; ++r) {
        for (int f = 0; f<FLAGS; ++f) {
            int calls = change_flag[f][r];
            if (f == 3 && calls>1) calls = 1; // do not care about number of callbacks for directory (== flag 3)
            buf[BUFIDX(f,r)] = '0'+calls;
        }
        buf[BUFIDX(FLAGS,r)] = '-';
    }
    buf[BUFIDX(0,CHANGE_REASONS)-1] = 0;
    TRACE("update_change_counts: buf='%s'\n", buf);
    return buf;
}

static arb_test::match_expectation change_counts_equal(const char *detected_counts, const char *expected_counts) {
    using namespace   arb_test;
    return that(detected_counts).is_equal_to(expected_counts);
}

#define WITH_DETECTED_DO(code)                          WITH_VALUE_DO(detected, update_change_counts(), code)
#define TEST_EXPECT_CHANGE_COUNTS(expected)             WITH_DETECTED_DO(TEST_EXPECTATION(change_counts_equal(detected,expected)))
#define TEST_EXPECT_CHANGE_COUNTS__BROKEN(expected,got) WITH_DETECTED_DO(TEST_EXPECTATION__BROKEN(change_counts_equal(detected,expected), change_counts_equal(detected,got)))

#define TEST_EXPECT_DEPENDENT_CHANGE_COUNTS(expected,other) do { TEST_EXPECT_CHANGE_COUNTS(expected); TEST_EXPECT_DIFFERENT(expected, other); } while(0)
#if defined(USE_STATPOLL)
#define TEST_EXPECT_CHANGE_COUNTS_MODEDEP(xpcted_inot,xpcted_poll) TEST_EXPECT_DEPENDENT_CHANGE_COUNTS(xpcted_poll,xpcted_inot)
#else // !USE_STATPOLL
#define TEST_EXPECT_CHANGE_COUNTS_MODEDEP(xpcted_inot,xpcted_poll) TEST_EXPECT_DEPENDENT_CHANGE_COUNTS(xpcted_inot,xpcted_poll)
#endif

#if defined(TRACE_INOTIFY)
# define TRACE_FILE_ACTIONS
#endif
#if defined(TRACE_FILE_ACTIONS)
# define TRACE_FILE_ACTION(format,...) fprintf(stderr, format, ##__VA_ARGS__)
#else
# define TRACE_FILE_ACTION(format,...)
#endif

static void update_file(const char *filename) {
    TRACE_FILE_ACTION(">>> update_file %s\n", filename);
    FILE *out = fopen(filename, "wt");
    aw_assert(out);
    fputs("hi", out);
    fclose(out);
}

inline void touch_file(const char *fn) {
    timespec to;
    bool     exists = GB_nanotime_of_file(fn, to);
    while (1) {
        ARB_sleep(5, MS); // sleep first -> avoid that touch often overwrites the file twice
        update_file(fn);
        if (!exists) break; // created -> done
        timespec tn;
        TEST_EXPECT(GB_nanotime_of_file(fn, tn));
        if (to<tn) break; // newer -> done
    }
}
inline void touch_files(const char *f1, const char *f2 = NULp) {
    if (f1) touch_file(f1);
    if (f2) touch_file(f2);
}

inline void erase_file(const char *filename) {
    TRACE_FILE_ACTION(">>> erase_file %s\n", filename);
    TEST_EXPECT_MORE_EQUAL(GB_unlink(filename), 0);
}
inline void erase_files(const char *f1, const char *f2 = NULp) {
    ARB_sleep(5, MS);
    if (f1) erase_file(f1);
    if (f2) erase_file(f2);
}

inline void move_file(const char *src, const char *dst) {
    ARB_sleep(5, MS);
    TRACE_FILE_ACTION(">>> move_file %s -> %s\n", src, dst);
    TEST_EXPECT_NO_ERROR(GB_move_file(src, dst));
}
inline void double_move_file(const char *src, const char *tmp, const char *dst) {
    move_file(src, tmp);
    move_file(tmp, dst);
}

#define INOTIFY_TESTDIR "inotify"

void TEST_inotifications() {
    const char *inotify_testdir = INOTIFY_TESTDIR;

    const char *testfile1 = INOTIFY_TESTDIR "/inotify1.testfile";
    const char *testfile2 = INOTIFY_TESTDIR "/inotify2.testfile";
    const char *testfile3 = INOTIFY_TESTDIR "/inotify3.testfile"; // not tracked
    const char *nofile    = INOTIFY_TESTDIR "/no.testfile";

    if (GB_is_directory(INOTIFY_TESTDIR)) {
        // remove any leftovers from last try
        erase_files(testfile1,testfile2);
        erase_files(testfile3, nofile);
        TEST_EXPECT_ZERO(rmdir(inotify_testdir));
    }

    TEST_EXPECT_NO_ERROR(GB_create_directory(INOTIFY_TESTDIR));

    FileChangedCallback fccb1 = makeFileChangedCallback(trace_file_changed_cb, 0);
    FileChangedCallback fccb2 = makeFileChangedCallback(trace_file_changed_cb, 1);
    FileChangedCallback fccb3 = makeFileChangedCallback(trace_file_changed_cb, 2);
    FileChangedCallback dccb4 = makeFileChangedCallback(trace_file_changed_cb, 3); // tracks directory

    maintain_timer_callback = false;
    erase_files(nofile); // make sure file does not exist

    // event order  (= order of ChangeReason) is "MODIFIED-DELETED-UNKNOWN" (position in each block is suffix of callback)
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000");

    AW_add_inotification(inotify_testdir, dccb4);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0001-0000-0000"); // detects something has changed in directory (files created)
    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // fails to detect dirchanges (if files are only modified). acceptable.

    AW_add_inotification(testfile1, fccb1);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("1001-0000-0000"); // detects: modify 'testfile1' + dirchange

    AW_add_inotification(testfile2, fccb2);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("1101-0000-0000"); // detects: modify for both testfiles + dirchange

    erase_files(testfile1); TEST_EXPECT_CHANGE_COUNTS("0001-1000-0000");  // dir changed + testfile1 deleted
    touch_files(testfile1); TEST_EXPECT_CHANGE_COUNTS("0001-0000-1000"); // dir changed + testfile1 (re-)created

    touch_files(testfile1); TEST_EXPECT_CHANGE_COUNTS("1001-0000-0000"); // dir changed + testfile1 modified

    erase_files(testfile2); TEST_EXPECT_CHANGE_COUNTS("0001-0100-0000"); // dir changed + testfile2 deleted

    AW_add_inotification(nofile, fccb3); // add a tracker on non-existing file
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    // touch non-yet-existing 'nofile' -> should detect creation
    touch_files(nofile);                      TEST_EXPECT_CHANGE_COUNTS("0001-0000-0010");  // detects creation of previously non-existing, but watched file
    erase_files(nofile); touch_files(nofile); TEST_EXPECT_CHANGE_COUNTS("0011-0000-0000");  // fine
    erase_files(nofile);                      TEST_EXPECT_CHANGE_COUNTS("0001-0010-0000");  // detects erase + dirchange
    touch_files(nofile); erase_files(nofile); TEST_EXPECT_CHANGE_COUNTS("0001-0000-0000");  // correctly detects only one dirchange

    // test moving files
    move_file(testfile1, nofile);                   TEST_EXPECT_CHANGE_COUNTS("0001-1000-0010");  // detects delete 'testfile1' and create 'nofile' + dirchange
    touch_files(nofile);                            TEST_EXPECT_CHANGE_COUNTS("0011-0000-0000");  // detects modify 'nofile'
    touch_files(testfile1);                         TEST_EXPECT_CHANGE_COUNTS("0001-0000-1000");  // detects create 'testfile1'
    move_file(nofile, testfile1);                   TEST_EXPECT_CHANGE_COUNTS("1001-0010-0000");  // detects delete 'nofile' + modify 'testfile1' + dirchange
    double_move_file(testfile1, nofile, testfile1); TEST_EXPECT_CHANGE_COUNTS("1001-0000-0000");  // detects modification of 'testfile1' + dirchange
    double_move_file(testfile1, nofile, testfile2); TEST_EXPECT_CHANGE_COUNTS("0001-1000-0100");  // detects delete 'testfile1' + create 'testfile2' + dirchange
    touch_files(testfile1, nofile);                 TEST_EXPECT_CHANGE_COUNTS("0001-0000-1010");  // detects create 'testfile1'+'nofile' + dirchange
    double_move_file(testfile2, nofile, testfile1); TEST_EXPECT_CHANGE_COUNTS("1001-0110-0000");  // detects delete 'testfile2' and 'nofile' + modify 'testfile1' + dirchange

    AW_remove_inotification(inotify_testdir, dccb4);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    // test moving files (w/o custom parent tracker; works nevertheless, probably because tracked but missing files always install parent trackers)
    touch_files(testfile2, nofile);                 TEST_EXPECT_CHANGE_COUNTS("0000-0000-0110");  // detects creation 'testfile2' and 'nofile'. no dirchange tracked.
    touch_files(testfile3);                         TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000");  // does not detect anything (expected!)

    touch_files(testfile3); // need repeated touch for NFS behavior (otherwise no dir change detected)

    move_file(testfile3, testfile1);                TEST_EXPECT_CHANGE_COUNTS("1000-0000-0000");  // detects modify 'testfile1'
    double_move_file(testfile2, nofile, testfile1); TEST_EXPECT_CHANGE_COUNTS("1000-0110-0000");  // detects delete 'testfile2' and 'nofile' + modify 'testfile1'

    AW_add_inotification(inotify_testdir, dccb4);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    AW_add_inotification(testfile1, fccb3);
    AW_add_inotification(testfile2, fccb3);
    // now flag #3 should trigger for testfile1, testfile2 and nofile
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("1011-0000-0110"); // dirchange + 'testfile1' modified (2cbs) + 'testfile2' created (2cbs)

    touch_files(testfile1);            TEST_EXPECT_CHANGE_COUNTS("1011-0000-0000"); // dirchange + 'testfile1' modified (2cbs)
    touch_files(testfile2);            TEST_EXPECT_CHANGE_COUNTS("0111-0000-0000");        // dirchange + 'testfile2' modified (2cbs)

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("1121-0000-0000"); // dirchange + 'testfile1' modified (2cbs) + 'testfile2' modified (2cbs)

    AW_add_inotification(testfile2, fccb3);      // (try to) add a tracker twice
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // adding a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("1121-0000-0000"); // same result as before effectless attempt to add existing callback

    AW_remove_inotification(testfile1, fccb1);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0121-0000-0000"); // no longer calls removed callback (when compared to previous test)

    // test if changing cd has an effect
    touch_files(testfile1, testfile2);
    {
        TEST_EXPECT_ZERO(chdir(inotify_testdir));
        TEST_EXPECT_CHANGE_COUNTS("0121-0000-0000"); // same as last
        TEST_EXPECT_ZERO(chdir(".."));
    }
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // no action shall cause no callbacks

    AW_remove_inotification(testfile2, fccb2);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0021-0000-0000"); // no longer calls next removed callback (when compared to previous test)

    AW_remove_inotification(testfile1, fccb3);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0011-0000-0000"); // no longer calls next removed callback (when compared to previous test)

    AW_remove_inotification(testfile2, fccb3);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    erase_files(testfile1); // force directory change
    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0001-0000-0000"); // no longer calls next removed callback (when compared to previous test)

    AW_remove_inotification(inotify_testdir, dccb4);
    TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // removing a notification shall never trigger

    touch_files(testfile1, testfile2); TEST_EXPECT_CHANGE_COUNTS("0000-0000-0000"); // all callbacks removed => nothing called

    TEST_EXPECT_ZERO(GB_unlink(testfile1));
    TEST_EXPECT_ZERO(GB_unlink(testfile2));
    TEST_EXPECT_ZERO(rmdir(inotify_testdir));

    allTrackers = new TrackedFiles; // test cleanup (normally occurs at program exit)
}

#endif // UNIT_TESTS

// --------------------------------------------------------------------------------
