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

#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <unistd.h>

#include "conffile.h"
#include "xmalloc.h"
#include "xlog.h"
#include "xstat.h"
#include "nfslib.h"
#include "nfsd_path.h"
#include "workqueue.h"

static struct xthread_workqueue *nfsd_wq;

static int
nfsd_path_isslash(const char *path)
{
	return path[0] == '/' && path[1] == '/';
}

static int
nfsd_path_isdot(const char *path)
{
	return path[0] == '.' && path[1] == '/';
}

static const char *
nfsd_path_strip(const char *path)
{
	if (!path || *path == '\0')
		goto out;
	for (;;) {
		if (nfsd_path_isslash(path)) {
			path++;
			continue;
		}
		if (nfsd_path_isdot(path)) {
			path += 2;
			continue;
		}
		break;
	}
out:
	return path;
}

const char *
nfsd_path_nfsd_rootdir(void)
{
	const char *rootdir;

	rootdir = nfsd_path_strip(conf_get_str("exports", "rootdir"));
	if (!rootdir || rootdir[0] ==  '\0')
		return NULL;
	if (rootdir[0] == '/' && rootdir[1] == '\0')
		return NULL;
	return rootdir;
}

char *
nfsd_path_strip_root(char *pathname)
{
	char buffer[PATH_MAX];
	const char *dir = nfsd_path_nfsd_rootdir();

	if (!dir)
		goto out;

	if (realpath(dir, buffer))
		return strstr(pathname, buffer) == pathname ?
			pathname + strlen(buffer) : NULL;

	xlog(D_GENERAL, "%s: failed to resolve path %s: %m", __func__, dir);
out:
	return pathname;
}

char *
nfsd_path_prepend_dir(const char *dir, const char *pathname)
{
	size_t len, dirlen;
	char *ret;

	dirlen = strlen(dir);
	while (dirlen > 0 && dir[dirlen - 1] == '/')
		dirlen--;
	if (!dirlen)
		return NULL;
	while (pathname[0] == '/')
		pathname++;
	len = dirlen + strlen(pathname) + 1;
	ret = xmalloc(len + 1);
	snprintf(ret, len+1, "%.*s/%s", (int)dirlen, dir, pathname);
	return ret;
}

static void
nfsd_setup_workqueue(void)
{
	const char *rootdir = nfsd_path_nfsd_rootdir();

	if (!rootdir)
		return;

	nfsd_wq = xthread_workqueue_alloc();
	if (!nfsd_wq)
		return;
	xthread_workqueue_chroot(nfsd_wq, rootdir);
}

void
nfsd_path_init(void)
{
	nfsd_setup_workqueue();
}

struct nfsd_stat_data {
	const char *pathname;
	struct stat *statbuf;
	int ret;
	int err;
};

static void
nfsd_statfunc(void *data)
{
	struct nfsd_stat_data *d = data;

	d->ret = xstat(d->pathname, d->statbuf);
	if (d->ret < 0)
		d->err = errno;
}

static void
nfsd_lstatfunc(void *data)
{
	struct nfsd_stat_data *d = data;

	d->ret = xlstat(d->pathname, d->statbuf);
	if (d->ret < 0)
		d->err = errno;
}

static int
nfsd_run_stat(struct xthread_workqueue *wq,
		void (*func)(void *),
		const char *pathname,
		struct stat *statbuf)
{
	struct nfsd_stat_data data = {
		pathname,
		statbuf,
		0,
		0
	};
	xthread_work_run_sync(wq, func, &data);
	if (data.ret < 0)
		errno = data.err;
	return data.ret;
}

int
nfsd_path_stat(const char *pathname, struct stat *statbuf)
{
	if (!nfsd_wq)
		return xstat(pathname, statbuf);
	return nfsd_run_stat(nfsd_wq, nfsd_statfunc, pathname, statbuf);
}

int
nfsd_path_lstat(const char *pathname, struct stat *statbuf)
{
	if (!nfsd_wq)
		return xlstat(pathname, statbuf);
	return nfsd_run_stat(nfsd_wq, nfsd_lstatfunc, pathname, statbuf);
}

struct nfsd_statfs_data {
	const char *pathname;
	struct statfs *statbuf;
	int ret;
	int err;
};

static void
nfsd_statfsfunc(void *data)
{
	struct nfsd_statfs_data *d = data;

	d->ret = statfs(d->pathname, d->statbuf);
	if (d->ret < 0)
		d->err = errno;
}

static int
nfsd_run_statfs(struct xthread_workqueue *wq,
		  const char *pathname,
		  struct statfs *statbuf)
{
	struct nfsd_statfs_data data = {
		pathname,
		statbuf,
		0,
		0
	};
	xthread_work_run_sync(wq, nfsd_statfsfunc, &data);
	if (data.ret < 0)
		errno = data.err;
	return data.ret;
}

int
nfsd_path_statfs(const char *pathname, struct statfs *statbuf)
{
	if (!nfsd_wq)
		return statfs(pathname, statbuf);
	return nfsd_run_statfs(nfsd_wq, pathname, statbuf);
}

struct nfsd_realpath_data {
	const char *pathname;
	char *resolved;
	int err;
};

static void
nfsd_realpathfunc(void *data)
{
	struct nfsd_realpath_data *d = data;

	d->resolved = realpath(d->pathname, d->resolved);
	if (!d->resolved)
		d->err = errno;
}

char *
nfsd_realpath(const char *path, char *resolved_path)
{
	struct nfsd_realpath_data data = {
		path,
		resolved_path,
		0
	};

	if (!nfsd_wq)
		return realpath(path, resolved_path);

	xthread_work_run_sync(nfsd_wq, nfsd_realpathfunc, &data);
	if (!data.resolved)
		errno = data.err;
	return data.resolved;
}

struct nfsd_read_data {
	int fd;
	char *buf;
	size_t len;
	ssize_t ret;
	int err;
};

static void
nfsd_readfunc(void *data)
{
	struct nfsd_read_data *d = data;

	d->ret = read(d->fd, d->buf, d->len);
	if (d->ret < 0)
		d->err = errno;
}

static ssize_t
nfsd_run_read(struct xthread_workqueue *wq, int fd, char *buf, size_t len)
{
	struct nfsd_read_data data = {
		fd,
		buf,
		len,
		0,
		0
	};
	xthread_work_run_sync(wq, nfsd_readfunc, &data);
	if (data.ret < 0)
		errno = data.err;
	return data.ret;
}

ssize_t
nfsd_path_read(int fd, char *buf, size_t len)
{
	if (!nfsd_wq)
		return read(fd, buf, len);
	return nfsd_run_read(nfsd_wq, fd, buf, len);
}

struct nfsd_write_data {
	int fd;
	const char *buf;
	size_t len;
	ssize_t ret;
	int err;
};

static void
nfsd_writefunc(void *data)
{
	struct nfsd_write_data *d = data;

	d->ret = write(d->fd, d->buf, d->len);
	if (d->ret < 0)
		d->err = errno;
}

static ssize_t
nfsd_run_write(struct xthread_workqueue *wq, int fd, const char *buf, size_t len)
{
	struct nfsd_write_data data = {
		fd,
		buf,
		len,
		0,
		0
	};
	xthread_work_run_sync(wq, nfsd_writefunc, &data);
	if (data.ret < 0)
		errno = data.err;
	return data.ret;
}

ssize_t
nfsd_path_write(int fd, const char *buf, size_t len)
{
	if (!nfsd_wq)
		return write(fd, buf, len);
	return nfsd_run_write(nfsd_wq, fd, buf, len);
}

#if defined(HAVE_NAME_TO_HANDLE_AT)
struct nfsd_handle_data {
	int fd;
	const char *path;
	struct file_handle *fh;
	int *mount_id;
	int flags;
	int ret;
	int err;
};

static void
nfsd_name_to_handle_func(void *data)
{
	struct nfsd_handle_data *d = data;

	d->ret = name_to_handle_at(d->fd, d->path,
			d->fh, d->mount_id, d->flags);
	if (d->ret < 0)
		d->err = errno;
}

static int
nfsd_run_name_to_handle_at(struct xthread_workqueue *wq,
		int fd, const char *path, struct file_handle *fh,
		int *mount_id, int flags)
{
	struct nfsd_handle_data data = {
		fd,
		path,
		fh,
		mount_id,
		flags,
		0,
		0
	};

	xthread_work_run_sync(wq, nfsd_name_to_handle_func, &data);
	if (data.ret < 0)
		errno = data.err;
	return data.ret;
}

int
nfsd_name_to_handle_at(int fd, const char *path, struct file_handle *fh,
		int *mount_id, int flags)
{
	if (!nfsd_wq)
		return name_to_handle_at(fd, path, fh, mount_id, flags);

	return nfsd_run_name_to_handle_at(nfsd_wq, fd, path, fh,
			mount_id, flags);
}
#else
int
nfsd_name_to_handle_at(int UNUSED(fd), const char *UNUSED(path),
		struct file_handle *UNUSED(fh),
		int *UNUSED(mount_id), int UNUSED(flags))
{
	errno = ENOSYS;
	return -1;
}
#endif
