#include "fsearch_folder_monitor_inotify.h"

#include "fsearch_database_entry.h"
#include "fsearch_folder_monitor_event.h"
#include "fsearch_main_context_utils.h"

#include <config.h>
#include <errno.h>
#include <glib.h>
#include <glib-unix.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <sys/types.h>
#include <unistd.h>


#define INOTIFY_FOLDER_MASK                                                                                            \
    (IN_ATTRIB | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | IN_UNMOUNT | IN_MOVE_SELF      \
     | IN_CLOSE_WRITE)

struct FsearchFolderMonitorInotify {
    GHashTable *watch_descriptors_to_folders;
    GHashTable *watched_folders_to_descriptors;

    GSource *monitor_source;
    GMainContext *monitor_context;

    GAsyncQueue *event_queue;

    int32_t fd;

    GMutex mutex;
};

static FsearchFolderMonitorEventKind
get_index_event_kind_for_inotify_mask(uint32_t mask) {
    if (mask & IN_ATTRIB) {
        return FSEARCH_FOLDER_MONITOR_EVENT_ATTRIB;
    }
    else if (mask & IN_MOVED_FROM) {
        return FSEARCH_FOLDER_MONITOR_EVENT_MOVED_FROM;
    }
    else if (mask & IN_MOVED_TO) {
        return FSEARCH_FOLDER_MONITOR_EVENT_MOVED_TO;
    }
    else if (mask & IN_DELETE) {
        return FSEARCH_FOLDER_MONITOR_EVENT_DELETE;
    }
    else if (mask & IN_CREATE) {
        return FSEARCH_FOLDER_MONITOR_EVENT_CREATE;
    }
    else if (mask & IN_DELETE_SELF) {
        return FSEARCH_FOLDER_MONITOR_EVENT_DELETE_SELF;
    }
    else if (mask & IN_UNMOUNT) {
        return FSEARCH_FOLDER_MONITOR_EVENT_UNMOUNT;
    }
    else if (mask & IN_MOVE_SELF) {
        return FSEARCH_FOLDER_MONITOR_EVENT_MOVE_SELF;
    }
    else if (mask & IN_CLOSE_WRITE) {
        return FSEARCH_FOLDER_MONITOR_EVENT_CLOSE_WRITE;
    }
    return 0;
}

static gboolean
inotify_listener_cb(int fd, GIOCondition condition, gpointer user_data) {
    FsearchFolderMonitorInotify *self = user_data;

    // Assert that this function is run in the right monitor thread
    g_assert(g_main_context_is_owner(self->monitor_context));

    char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
    const struct inotify_event *event;

    /* Loop while events can be read from inotify file descriptor. */
    while (true) {
        /* Read some events. */
        ssize_t len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            g_debug("failed to read from fd!");
            return G_SOURCE_REMOVE;
        }

        /* If the nonblocking read() found no events to read, then
           it returns -1 with errno set to EAGAIN. In that case,
           we exit the loop. */
        if (len <= 0) {
            return G_SOURCE_CONTINUE;
        }

        /* Loop over all events in the buffer. */
        for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) {
            event = (const struct inotify_event *)ptr;

            // Membership check only -- never dereference an entry off this thread.
            g_mutex_lock(&self->mutex);
            const bool is_watched = g_hash_table_contains(self->watch_descriptors_to_folders,
                                                           GINT_TO_POINTER(event->wd));
            g_mutex_unlock(&self->mutex);

            if (!is_watched) {
                if (event->mask & IN_IGNORED) {
                    // The only expected situation when a watched entry is no longer present for a given event,
                    // is when the IN_IGNORED bit is set. This happens after a watched folder was removed or
                    // moved to a different filesystem and we already removed the watch descriptor while handling
                    // this earlier event.
                    g_debug("[inotify_listener] no watched entry for watch descriptor found: %s (%d) -> %s",
                            fsearch_folder_monitor_event_kind_to_string(
                                get_index_event_kind_for_inotify_mask(event->mask)),
                            event->mask,
                            event->len ? event->name : "UNKNOWN");
                }
                else {
                    // The IN_IGNORED bit is not set and we don't have an associate watched entry. This is probably
                    // a bug.
                    g_assert_not_reached();
                }
                continue;
            }
            g_async_queue_push(self->event_queue,
                               fsearch_folder_monitor_event_new(event->len ? event->name : NULL,
                                                                GINT_TO_POINTER(event->wd),
                                                                get_index_event_kind_for_inotify_mask(event->mask),
                                                                FSEARCH_FOLDER_MONITOR_INOTIFY,
                                                                event->mask & IN_ISDIR ? true : false));
        }
    }
    return G_SOURCE_CONTINUE;
}

static void
unwatch_folder_cb(gpointer key, gpointer value, gpointer user_data);

static void
unwatch_all(FsearchFolderMonitorInotify *self) {
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);

    g_hash_table_foreach(self->watch_descriptors_to_folders, (GHFunc)unwatch_folder_cb, self);
    g_hash_table_remove_all(self->watch_descriptors_to_folders);
    g_hash_table_remove_all(self->watched_folders_to_descriptors);
}

FsearchFolderMonitorInotify *
fsearch_folder_monitor_inotify_new(GMainContext *monitor_context, GAsyncQueue *event_queue) {
    g_return_val_if_fail(monitor_context, NULL);
    g_return_val_if_fail(event_queue, NULL);

    const int32_t fd = inotify_init1(IN_NONBLOCK);
    if (fd < 0) {
        return NULL;
    }

    FsearchFolderMonitorInotify *self = calloc(1, sizeof(FsearchFolderMonitorInotify));

    self->fd = fd;

    self->event_queue = g_async_queue_ref(event_queue);

    self->watch_descriptors_to_folders = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL);
    self->watched_folders_to_descriptors = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL);

    self->monitor_context = monitor_context;

    self->monitor_source = g_unix_fd_source_new(self->fd, G_IO_IN | G_IO_ERR | G_IO_HUP);
    g_source_set_callback(self->monitor_source, (GSourceFunc)inotify_listener_cb, self, NULL);
    g_source_attach(self->monitor_source, self->monitor_context);

    return self;
}

void
fsearch_folder_monitor_inotify_free(FsearchFolderMonitorInotify *self) {
    g_return_if_fail(self);

    if (self->monitor_source) {
        fsearch_main_context_blocking_call(self->monitor_context,
                                           (FsearchMainContextFunc)g_source_destroy,
                                           self->monitor_source);
    }

    g_clear_pointer(&self->monitor_source, g_source_unref);

    unwatch_all(self);
    g_clear_pointer(&self->watched_folders_to_descriptors, g_hash_table_unref);
    g_clear_pointer(&self->watch_descriptors_to_folders, g_hash_table_unref);

    g_clear_pointer(&self->event_queue, g_async_queue_unref);

    if (self->fd >= 0) {
        close(self->fd);
    }
    g_mutex_clear(&self->mutex);

    g_clear_pointer(&self, free);
}

bool
fsearch_folder_monitor_inotify_watch(FsearchFolderMonitorInotify *self,
                                     FsearchDatabaseEntry *folder,
                                     const char *path) {
    const int32_t wd = inotify_add_watch(self->fd, path, INOTIFY_FOLDER_MASK);
    if (wd < 0) {
        g_debug("failed to add inotify watch");
        return false;
    }
    db_entry_set_monitored_inotify(folder);

    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);

    g_hash_table_insert(self->watch_descriptors_to_folders, GINT_TO_POINTER(wd), folder);
    g_hash_table_insert(self->watched_folders_to_descriptors, folder, GINT_TO_POINTER(wd));
    return true;
}

static void
unwatch_folder(FsearchDatabaseEntry *folder, int fd, const int32_t wd) {
    if (inotify_rm_watch(fd, wd)) {
        g_debug("[unwatch_folder] failed to remove inotify watch descriptor: %d", wd);
    }
    db_entry_set_unmonitored_inotify(folder);
}

static void
unwatch_folder_cb(gpointer key, gpointer value, gpointer user_data) {
    FsearchFolderMonitorInotify *self = user_data;
    FsearchDatabaseEntry *folder = value;
    const int32_t wd = GPOINTER_TO_INT(g_hash_table_lookup(self->watched_folders_to_descriptors, folder));
    unwatch_folder(folder, self->fd, wd);
}

void
fsearch_folder_monitor_inotify_unwatch(FsearchFolderMonitorInotify *self, FsearchDatabaseEntry *folder) {
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);

    const int32_t wd = GPOINTER_TO_INT(g_hash_table_lookup(self->watched_folders_to_descriptors, folder));
    FsearchDatabaseEntry *watched_folder = g_hash_table_lookup(self->watch_descriptors_to_folders, GINT_TO_POINTER(wd));
    if (watched_folder == folder) {
        unwatch_folder(folder, self->fd, wd);
        g_hash_table_remove(self->watch_descriptors_to_folders, GINT_TO_POINTER(wd));
        g_hash_table_remove(self->watched_folders_to_descriptors, folder);
    }
    else {
        g_autoptr(GString) path_full = db_entry_get_path_full(folder);
        g_debug("[unwatch_folder] no inotify fd found for folder: %s", path_full->str);
    }
}

FsearchDatabaseEntry *
fsearch_folder_monitor_inotify_resolve(FsearchFolderMonitorInotify *self, gpointer handle) {
    g_return_val_if_fail(self, NULL);
    g_autoptr(GMutexLocker) locker = g_mutex_locker_new(&self->mutex);
    g_assert_nonnull(locker);
    return g_hash_table_lookup(self->watch_descriptors_to_folders, handle);
}
