/* * Copyright (c) 2013 Eric Radman <ericshane@eradman.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/event.h>
#include <sys/inotify.h>
#include <sys/types.h>

#include <err.h>
#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "compat.h"

#include "../data.h"

/* globals */

extern WatchFile **files;
int read_stdin;

/* forwards */

static WatchFile *file_by_descriptor(int fd);

/* utility functions */

static WatchFile *
file_by_descriptor(int wd) {
	int i;

	for (i = 0; files[i] != NULL; i++) {
		if (files[i]->fd == wd)
			return files[i];
	}
	return NULL; /* lookup failed */
}

int
fs_sysctl(const int name) {
	FILE *file;
	char line[8];
	int value = 0;

	switch (name) {
	case INOTIFY_MAX_USER_WATCHES:
		file = fopen("/proc/sys/fs/inotify/max_user_watches", "r");

		if (file == NULL || fgets(line, sizeof(line), file) == NULL) {
			/* failed to read max_user_watches; sometimes inaccessible on Android */
			value = 0;
		} else
			value = atoi(line);

		if (file)
			fclose(file);
		break;
	}
	return value;
}

/* interface */

#define EVENT_SIZE (sizeof(struct inotify_event))
#define EVENT_BUF_LEN (32 * (EVENT_SIZE + 16))
#define IN_ALL                                                                                     \
	IN_CLOSE_WRITE | IN_DELETE_SELF | IN_MOVE_SELF | IN_MOVE | IN_ATTRIB | IN_CREATE | IN_DELETE

/*
 * inotify and kqueue ids both have the type `int`
 */
int
kqueue(void) {
	static int inotify_queue;

	if (inotify_queue == 0)
		inotify_queue = inotify_init1(IN_CLOEXEC);
	if (getenv("ENTR_INOTIFY_WORKAROUND"))
		warnx("broken inotify workaround enabled");
	else if (getenv("ENTR_INOTIFY_SYMLINK"))
		warnx("monitoring symlinks");
	return inotify_queue;
}

/*
 * Emulate kqueue(2). Only monitors STDIN for EVFILT_READ and only the
 * EVFILT_VNODE flags used in entr.c are considered. Returns the number of
 * eventlist structs filled by this call
 */
int
kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents,
    const struct timespec *timeout) {
	int n;
	int wd;
	WatchFile *file;
	char buf[EVENT_BUF_LEN];
	ssize_t len;
	int pos;
	struct inotify_event *iev;
	u_int fflags;
	const struct kevent *kev;
	int nfds;

	int timeout_ms = -1;
	int ignored = 0;
	struct pollfd pfd[2];

	pfd[0].fd = kq;
	pfd[0].events = POLLIN;
	pfd[0].revents = 0;
	pfd[1].fd = STDIN_FILENO;
	pfd[1].events = POLLIN;
	pfd[1].revents = 0;

	if (nchanges > 0) {
		for (n = 0; n < nchanges; n++) {
			kev = changelist + (sizeof(struct kevent) * n);
			file = (WatchFile *) kev->udata;

			if (kev->filter == EVFILT_READ) {
				if (kev->flags & EV_ADD)
					read_stdin = 1;
				if (kev->flags & EV_DELETE)
					read_stdin = 0;
			}

			if (kev->filter != EVFILT_VNODE)
				continue;

			if (kev->flags & EV_DELETE) {
				inotify_rm_watch(kq /* ifd */, kev->ident);
				file->fd = -1; /* invalidate */
			} else if (kev->flags & EV_ADD) {
				if (getenv("ENTR_INOTIFY_WORKAROUND"))
					wd = inotify_add_watch(kq, file->fn, IN_ALL | IN_MODIFY);
				else if (file->is_symlink)
					wd = inotify_add_watch(kq, file->fn, IN_ALL | IN_DONT_FOLLOW);
				else
					wd = inotify_add_watch(kq, file->fn, IN_ALL);
				if (wd < 0)
					return -1;
				close(file->fd);
				file->fd = wd; /* replace with watch descriptor */
			} else
				ignored++;
		}
		return nchanges - ignored;
	}

	if (read_stdin == 1)
		nfds = 2; /* inotify and stdin */
	else
		nfds = 1; /* inotify */

	if (timeout)
		timeout_ms = timeout->tv_nsec / 1000000;
	if (poll(pfd, nfds, timeout_ms) == -1)
		return -1;

	n = 0;
	do {
		if (pfd[0].revents & (POLLERR | POLLNVAL))
			errx(1, "bad fd %d", pfd[0].fd);
		if (pfd[0].revents & POLLIN) {
			pos = 0;
			len = read(kq /* ifd */, &buf, EVENT_BUF_LEN);
			if (len < 0) {
				/* SA_RESTART doesn't work for inotify fds */
				if (errno == EINTR)
					continue;
				else
					errx(1, "read of fd %d failed", pfd[0].fd);
			}
			while ((pos < len) && (n < nevents)) {
				iev = (struct inotify_event *) &buf[pos];
				pos += EVENT_SIZE + iev->len;

				/* convert iev->mask; to comparable kqueue flags */
				fflags = 0;
				if (iev->mask & IN_DELETE_SELF)
					fflags |= NOTE_DELETE;
				if (iev->mask & IN_CLOSE_WRITE)
					fflags |= NOTE_WRITE;
				if (iev->mask & IN_CREATE)
					fflags |= NOTE_WRITE;
				if (iev->mask & IN_DELETE)
					fflags |= NOTE_WRITE;
				if (iev->mask & IN_MOVE_SELF)
					fflags |= NOTE_RENAME;
				if (iev->mask & IN_MOVED_TO)
					fflags |= NOTE_RENAME;
				if (iev->mask & IN_MOVED_FROM)
					fflags |= NOTE_RENAME;
				if (iev->mask & IN_ATTRIB)
					fflags |= NOTE_ATTRIB;
				if (getenv("ENTR_INOTIFY_WORKAROUND"))
					if (iev->mask & IN_MODIFY)
						fflags |= NOTE_WRITE;
				if (fflags == 0)
					continue;

				/* merge events if we're not acting on a new file descriptor */
				if ((n > 0) && (eventlist[n - 1].ident == iev->wd))
					fflags |= eventlist[--n].fflags;

				eventlist[n].ident = iev->wd;
				eventlist[n].filter = EVFILT_VNODE;
				eventlist[n].flags = 0;
				eventlist[n].fflags = fflags;
				eventlist[n].data = 0;
				eventlist[n].udata = file_by_descriptor(iev->wd);
				if (eventlist[n].udata)
					n++;
			}
		}
		if (read_stdin == 1) {
			if (pfd[1].revents & (POLLERR | POLLNVAL))
				errx(1, "bad fd %d", pfd[1].fd);
			else if (pfd[1].revents & (POLLHUP | POLLIN)) {
				fflags = 0;
				eventlist[n].ident = pfd[1].fd;
				eventlist[n].filter = EVFILT_READ;
				eventlist[n].flags = 0;
				eventlist[n].fflags = fflags;
				eventlist[n].data = 0;
				eventlist[n].udata = NULL;
				n++;
				break;
			}
		}
	} while ((poll(pfd, nfds, 50) > 0));

	return n;
}
