/*
 * Embedded Linux library
 * Copyright (C) 2017  Intel Corporation
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define _GNU_SOURCE
#include <unistd.h>
#include <dirent.h>
#include <limits.h>
#include <sys/inotify.h>
#include <errno.h>
#include <sys/stat.h>

#include "private.h"
#include "useful.h"
#include "queue.h"
#include "io.h"
#include "dir.h"

struct l_dir_watch {
	struct watch_desc *desc;
	l_dir_watch_event_func_t function;
	void *user_data;
	l_dir_watch_destroy_func_t destroy;
};

struct watch_desc {
	int wd;
	char *pathname;
	struct l_queue *events;
	struct l_queue *callbacks;
};

struct watch_event {
	char *pathname;
	uint32_t mask;
};

static struct l_io *inotify_io = NULL;
static struct l_queue *watch_list = NULL;

static void free_event(void *user_data)
{
	struct watch_event *event = user_data;

	l_free(event->pathname);
	l_free(event);
}

static bool desc_match_wd(const void *a, const void *b)
{
	const struct watch_desc *desc = a;
	int wd = L_PTR_TO_INT(b);

	return (desc->wd == wd);
}

static bool desc_match_pathname(const void *a, const void *b)
{
	const struct watch_desc *desc = a;
	const char *pathname = b;

	return !strcmp(desc->pathname, pathname);
}

static bool event_match_pathname(const void *a, const void *b)
{
	const struct watch_event *event = a;
	const char *pathname = b;

	return !strcmp(event->pathname, pathname);
}

static void handle_callback(struct watch_desc *desc, const char *pathname,
						enum l_dir_watch_event event)
{
	const struct l_queue_entry *entry;

	for (entry = l_queue_get_entries(desc->callbacks); entry;
							entry = entry->next) {
		struct l_dir_watch *watch = entry->data;

		if (watch->function)
			watch->function(pathname, event, watch->user_data);
	}
}

static void process_event(struct watch_desc *desc, const char *pathname,
								uint32_t mask)
{
	struct watch_event *event;

	if (!pathname)
		return;

	if (mask & (IN_ACCESS | IN_MODIFY | IN_OPEN | IN_CREATE)) {
		event = l_queue_find(desc->events, event_match_pathname,
								pathname);
		if (!event) {
			/*
			 * When the event for a given pathname is not yet
			 * created and it is from type IN_MODIFY, then it
			 * might have been caused by a truncate() system
			 * call that does not open() the file. However
			 * treat this as modified as well.
			 */
			if (mask & IN_MODIFY) {
				handle_callback(desc, pathname,
						L_DIR_WATCH_EVENT_MODIFIED);
			} else {
				event = l_new(struct watch_event, 1);
				event->pathname = l_strdup(pathname);
				event->mask = mask;

				l_queue_push_tail(desc->events, event);
			}
		} else {
			event->mask |= mask;
		}
	} else if (mask & (IN_CLOSE_WRITE)) {
		event = l_queue_remove_if(desc->events, event_match_pathname,
								pathname);
		if (event) {
			/*
			 * Creation of a new file is treated differently,
			 * then modification, but for that the original
			 * system call needs to be looked at.
			 */
			if (event->mask & IN_CREATE)
				handle_callback(desc, pathname,
						L_DIR_WATCH_EVENT_CREATED);
			else
				handle_callback(desc, pathname,
						L_DIR_WATCH_EVENT_MODIFIED);

			free_event(event);
		} else {
			handle_callback(desc, pathname,
					L_DIR_WATCH_EVENT_MODIFIED);
		}
	} else if (mask & (IN_CLOSE_NOWRITE)) {
		event = l_queue_remove_if(desc->events, event_match_pathname,
								pathname);
		if (event) {
			if (event->mask & IN_ACCESS)
				handle_callback(desc, pathname,
						L_DIR_WATCH_EVENT_ACCESSED);
			free_event(event);
		}
	} else if (mask & (IN_MOVED_FROM | IN_DELETE)) {
		handle_callback(desc, pathname, L_DIR_WATCH_EVENT_REMOVED);
	} else if (mask & (IN_MOVED_TO)) {
		handle_callback(desc, pathname, L_DIR_WATCH_EVENT_CREATED);
	} else if (mask & (IN_ATTRIB)) {
		handle_callback(desc, pathname, L_DIR_WATCH_EVENT_ATTRIB);
	}
}

static bool inotify_read_cb(struct l_io *io, void *user_data)
{
	int fd = l_io_get_fd(io);
	uint8_t buf[sizeof(struct inotify_event) + NAME_MAX + 1]
		__attribute__ ((aligned(__alignof__(struct inotify_event))));
	const void *ptr = buf;
	ssize_t len;

	len = L_TFR(read(fd, buf, sizeof(buf)));
	if (len <= 0)
		return true;

	while (len > 0) {
		const struct inotify_event *event = ptr;
		const char *name = event->len ? event->name : NULL;
		struct watch_desc *desc;

		desc = l_queue_find(watch_list, desc_match_wd,
						L_INT_TO_PTR(event->wd));
		if (desc)
			process_event(desc, name, event->mask);

		ptr += sizeof(struct inotify_event) + event->len;
		len -= sizeof(struct inotify_event) + event->len;
	}

	return true;
}

static int setup_inotify(void)
{
	struct l_io *io;
	int fd;

	if (inotify_io)
		goto done;

	fd = inotify_init1(IN_CLOEXEC);
	if (fd < 0)
		return -1;

	io = l_io_new(fd);
	if (!io) {
		close(fd);
		return -1;
	}

	l_io_set_close_on_destroy(io, true);

	if (!l_io_set_read_handler(io, inotify_read_cb, NULL, NULL)) {
		l_io_destroy(io);
		return -1;
	}

	watch_list = l_queue_new();
	inotify_io = io;

done:
	return l_io_get_fd(inotify_io);
}

static void shutdown_inotify(void)
{
	if (!inotify_io)
		return;

	if (l_queue_isempty(watch_list)) {
		l_io_destroy(inotify_io);
		inotify_io = NULL;

		l_queue_destroy(watch_list, NULL);
		watch_list = NULL;
	}
}

LIB_EXPORT struct l_dir_watch *l_dir_watch_new(const char *pathname,
					l_dir_watch_event_func_t function,
					void *user_data,
					l_dir_watch_destroy_func_t destroy)
{
	struct l_dir_watch *watch;
	struct watch_desc *desc;
	int fd;

	if (!pathname)
		return NULL;

	watch = l_new(struct l_dir_watch, 1);
	watch->function = function;
	watch->user_data = user_data;
	watch->destroy = destroy;

	desc = l_queue_find(watch_list, desc_match_pathname, pathname);
	if (desc)
		goto done;

	/*
	 * Returns the inotify file descriptor. It will create a new one
	 * if it doesn't exist yet or return the already opened one.
	 */
	fd = setup_inotify();
	if (fd < 0) {
		l_free(watch);
		return NULL;
	}

	desc = l_new(struct watch_desc, 1);

	desc->wd = inotify_add_watch(fd, pathname, IN_ALL_EVENTS |
							IN_ONLYDIR |
							IN_DONT_FOLLOW |
							IN_EXCL_UNLINK);
	if (desc->wd < 0) {
		/*
		 * If the setup_inotify() created the inotify file descriptor,
		 * then this will close it. Otherwise it will do nothing.
		 */
		shutdown_inotify();
		l_free(desc);
		l_free(watch);
		return NULL;
	}

	desc->pathname = l_strdup(pathname);
	desc->events = l_queue_new();
	desc->callbacks = l_queue_new();

	l_queue_push_tail(watch_list, desc);

done:
	l_queue_push_tail(desc->callbacks, watch);
	watch->desc = desc;
	return watch;
}

LIB_EXPORT void l_dir_watch_destroy(struct l_dir_watch *watch)
{
	struct watch_desc *desc;
	int fd;

	if (!watch)
		return;

	desc = watch->desc;
	l_queue_remove(desc->callbacks, watch);

	/*
	 * As long as the watch descriptor has callbacks registered, it is
	 * still needed to be active.
	 */
	if (!l_queue_isempty(desc->callbacks))
		goto done;

	if (!l_queue_remove(watch_list, desc))
		goto done;

	fd = l_io_get_fd(inotify_io);
	inotify_rm_watch(fd, desc->wd);

	l_queue_destroy(desc->callbacks, NULL);
	l_queue_destroy(desc->events, free_event);
	l_free(desc->pathname);
	l_free(desc);

	/*
	 * When the number of watches goes to zero, then this will close
	 * the inotify file descriptor, otherwise it will do nothing.
	 */
	shutdown_inotify();

done:
	if (watch->destroy)
		watch->destroy(watch->user_data);

	l_free(watch);
}

/**
 * l_dir_create:
 * @abspath: Absolute path of the directory to create
 *
 * Attempts to create a directory tree given by @abspath.  @abspath must be
 * an absolute path.
 *
 * Returns: 0 if successful, a negative errno otherwise
 **/
LIB_EXPORT int l_dir_create(const char *abspath)
{
	static const mode_t create_mode = S_IRUSR | S_IWUSR | S_IXUSR;
	struct stat st;
	_auto_(l_free) char *dir = NULL;
	const char *prev, *next;
	int err;

	if (!abspath || abspath[0] != '/')
		return -EINVAL;

	err = stat(abspath, &st);
	if (!err) {
		/* File exists */
		if (S_ISDIR(st.st_mode))
			return 0;

		return -ENOTDIR;
	}

	if (errno != ENOENT)
		return -errno;

	dir = l_malloc(strlen(abspath) + 1);
	dir[0] = '\0';

	for (prev = abspath; prev[0] && (next = strchrnul(prev + 1, '/'));
								prev = next) {
		/* Skip consecutive '/' characters */
		if (next - prev == 1)
			continue;

		strncat(dir, prev, next - prev);

		if (mkdir(dir, create_mode) == -1 && errno != EEXIST)
			return -errno;
	}

	return 0;
}
