/*
 * Copyright 2010-2016, Tarantool AUTHORS, please see AUTHORS file.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * 1. Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include "coio_file.h"
#include "coio_task.h"
#include "fiber.h"
#include "say.h"
#include "fio.h"
#include "errinj.h"
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>

/**
 * A context of libeio request for any
 * coio task.
 */
struct coio_file_task {
	ssize_t result;
	int errorno;
	struct fiber *fiber;
	bool done;

	union {
		struct {
			int fd;
			struct stat *buf;
		} fstat;

		struct {
			struct stat *buf;
			const char *pathname;
		} lstat;

		struct {
			const char *pattern;
			int flags;
			int (*errfunc) (const char *epath, int eerrno);
			glob_t *pglob;
		} glob;

		struct {
			int fd;
			off_t offset;
			int whence;
		} lseek;

		struct {
			int fd;
			const void *buf;
			size_t count;
		} write;

		struct {
			int fd;
			void *buf;
			size_t count;
		} read;

		struct {
			const char *pathname;
			char *buf;
			size_t bufsize;
		} readlink;

		struct {
			char *tpl;
		} tempdir;

		struct {
			char **bufp;
			const char *pathname;
		} readdir;

		struct {
			const char *source;
			const char *dest;
		} copyfile;
	};
};

#define INIT_COEIO_FILE(name)			\
	struct coio_file_task name;		\
	memset(&name, 0, sizeof(name));		\
	name.fiber = fiber();			\

/** A callback invoked by eio when a task is complete. */
static int
coio_complete(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;

	eio->errorno = req->errorno;
	eio->done = true;
	eio->result = req->result;

	fiber_wakeup(eio->fiber);
	return 0;
}

/**
 * Synchronously (from cooperative multitasking point of view)
 * wait for task completion.
 */
static ssize_t
coio_wait_done(eio_req *req, struct coio_file_task *eio)
{
	if (!req) {
		errno = ENOMEM;
		return -1;
	}

	while (!eio->done)
		fiber_yield();

	errno = eio->errorno;
	return eio->result;
}

int
coio_file_open(const char *path, int flags, mode_t mode)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_open(path, flags, mode, 0,
				coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_file_close(int fd)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_close(fd, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

ssize_t
coio_pwrite(int fd, const void *buf, size_t count, off_t offset)
{
	ssize_t left = count, pos = 0, res, chunk;
	eio_req *req;

	while (left > 0) {
		INIT_COEIO_FILE(eio);
		chunk = left;

		ERROR_INJECT(ERRINJ_COIO_WRITE_CHUNK, {
			chunk = 1;
		});

		req = eio_write(fd, (char *)buf + pos, chunk,
				offset + pos, EIO_PRI_DEFAULT,
				coio_complete, &eio);
		res = coio_wait_done(req, &eio);
		if (res < 0) {
			pos = -1;
			break;
		} else {
			left -= res;
			pos += res;
		}
	}
	return pos;
}

ssize_t
coio_pread(int fd, void *buf, size_t count, off_t offset)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_read(fd, buf, count,
				offset, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

ssize_t
coio_preadn(int fd, void *buf, size_t count, off_t offset)
{
	size_t n = 0;
	do {
		ssize_t r;
		do {
			r = coio_pread(fd, buf + n, count - n, offset + n);
		} while (r == -1 && errno == EINTR);
		if (r <= 0)
			return -1;
		n += r;
	} while (n < count);

	assert(n == count);
	return n;
}

static void
coio_do_write(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;

	ERROR_INJECT(ERRINJ_COIO_WRITE_CHUNK, {
		eio->write.count = 1;
	});

	req->result = write(eio->write.fd, eio->write.buf, eio->write.count);
	eio->errorno = errno;
}

ssize_t
coio_write(int fd, const void *buf, size_t count)
{
	ssize_t left = count, pos = 0, res;
	eio_req *req;

	while (left > 0) {
		INIT_COEIO_FILE(eio);

		eio.write.buf	= (char *)buf + pos;
		eio.write.count	= left;
		eio.write.fd	= fd;

		req = eio_custom(coio_do_write, EIO_PRI_DEFAULT,
				 coio_complete, &eio);
		res = coio_wait_done(req, &eio);
		if (res < 0) {
			pos = -1;
			break;
		} else {
			left -= res;
			pos += res;
		}
	}
	return pos;
}

static void
coio_do_read(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = read(eio->read.fd, eio->read.buf, eio->read.count);
	req->errorno = errno;
}

ssize_t
coio_read(int fd, void *buf, size_t count)
{
	INIT_COEIO_FILE(eio);
	eio.read.buf = buf;
	eio.read.count = count;
	eio.read.fd = fd;
	eio_req *req = eio_custom(coio_do_read, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}


static void
coio_do_lseek(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result =
		lseek(eio->lseek.fd, eio->lseek.offset, eio->lseek.whence);
	req->errorno = errno;
}

off_t
coio_lseek(int fd, off_t offset, int whence)
{
	INIT_COEIO_FILE(eio);

	eio.lseek.whence = whence;
	eio.lseek.offset = offset;
	eio.lseek.fd = fd;

	eio_req *req = eio_custom(coio_do_lseek, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_lstat(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = lstat(eio->lstat.pathname, eio->lstat.buf);
	req->errorno = errno;
}

int
coio_lstat(const char *pathname, struct stat *buf)
{
	INIT_COEIO_FILE(eio);
	eio.lstat.pathname = pathname;
	eio.lstat.buf = buf;
	eio_req *req = eio_custom(coio_do_lstat, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_stat(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = stat(eio->lstat.pathname, eio->lstat.buf);
	req->errorno = errno;
}

int
coio_stat(const char *pathname, struct stat *buf)
{
	INIT_COEIO_FILE(eio);
	eio.lstat.pathname = pathname;
	eio.lstat.buf = buf;
	eio_req *req = eio_custom(coio_do_stat, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_fstat(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = fstat(eio->fstat.fd, eio->fstat.buf);
	req->errorno = errno;
}

int
coio_fstat(int fd, struct stat *stat)
{
	INIT_COEIO_FILE(eio);
	eio.fstat.fd = fd;
	eio.fstat.buf = stat;

	eio_req *req = eio_custom(coio_do_fstat, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_rename(const char *oldpath, const char *newpath)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_rename(oldpath, newpath, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);

}

int
coio_unlink(const char *pathname)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_unlink(pathname, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_ftruncate(int fd, off_t length)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_ftruncate(fd, length, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_truncate(const char *path, off_t length)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_truncate(path, length, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_glob(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = glob(eio->glob.pattern,
			   eio->glob.flags, eio->glob.errfunc, eio->glob.pglob);
	req->errorno = errno;
}

int
coio_glob(const char *pattern, int flags,
		int (*errfunc) (const char *epath, int eerrno),
		glob_t *pglob)
{
	INIT_COEIO_FILE(eio);
	eio.glob.pattern = pattern;
	eio.glob.flags = flags;
	eio.glob.errfunc = errfunc;
	eio.glob.pglob = pglob;
	eio_req *req =
		eio_custom(coio_do_glob, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_chown(const char *path, uid_t owner, gid_t group)
{
	INIT_COEIO_FILE(eio);
	eio_req *req =
		eio_chown(path, owner, group, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_chmod(const char *path, mode_t mode)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_chmod(path, mode, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_mkdir(const char *pathname, mode_t mode)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_mkdir(pathname, mode, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_rmdir(const char *pathname)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_rmdir(pathname, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_link(const char *oldpath, const char *newpath)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_link(oldpath, newpath, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_symlink(const char *target, const char *linkpath)
{
	INIT_COEIO_FILE(eio);
	eio_req *req =
		eio_symlink(target, linkpath, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_readlink(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	req->result = readlink(eio->readlink.pathname,
			       eio->readlink.buf, eio->readlink.bufsize);
	req->errorno = errno;
}

int
coio_readlink(const char *pathname, char *buf, size_t bufsize)
{
	INIT_COEIO_FILE(eio);
	eio.readlink.pathname = pathname;
	eio.readlink.buf = buf;
	eio.readlink.bufsize = bufsize;
	eio_req *req = eio_custom(coio_do_readlink, 0,
				  coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_tempdir(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	char *res = mkdtemp(eio->tempdir.tpl);
	req->errorno = errno;
	if (res == NULL) {
		req->result = -1;
	} else {
		req->result = 0;
	}
}

int
coio_tempdir(char *path, size_t path_len)
{
	INIT_COEIO_FILE(eio);

	const char *tmpdir = getenv("TMPDIR");
	if (tmpdir == NULL)
		tmpdir = "/tmp";
	int rc = snprintf(path, path_len, "%s/XXXXXX", tmpdir);
	if (rc < 0)
		return -1;
	if ((size_t) rc >= path_len) {
		errno = ENOMEM;
		return -1;
	}
	eio.tempdir.tpl = path;
	eio_req *req =
		eio_custom(coio_do_tempdir, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_sync()
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_sync(0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_fsync(int fd)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_fsync(fd, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_fdatasync(int fd)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_fdatasync(fd, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_readdir(eio_req *req)
{
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	DIR *dirp = opendir(eio->readdir.pathname);
	if (dirp == NULL)
		goto error;
	size_t capacity = 128;
	size_t len = 0;
	struct dirent *entry;
	char *buf = (char *) malloc(capacity);
	if (buf == NULL)
		goto mem_error;
	req->result = 0;
	do {
		entry = readdir(dirp);
		if (entry == NULL ||
		    strcmp(entry->d_name, ".") == 0 ||
		    strcmp(entry->d_name, "..") == 0)
			continue;
		size_t namlen = strlen(entry->d_name);
		size_t needed = len + namlen + 1;
		if (needed > capacity) {
			if (needed <= capacity * 2)
				capacity *= 2;
			else
				capacity = needed * 2;
			char *new_buf = (char *) realloc(buf, capacity);
			if (new_buf == NULL)
				goto mem_error;
			buf = new_buf;
		}
		memcpy(&buf[len], entry->d_name, namlen);
		len += namlen;
		buf[len++] = '\n';
		req->result++;
	} while(entry != NULL);

	if (len > 0)
		buf[len - 1] = 0;
	else
		buf[0] = 0;

	*eio->readdir.bufp = buf;
	closedir(dirp);
	return;

mem_error:
	free(buf);
	closedir(dirp);
error:
	req->result = -1;
	req->errorno = errno;
}

int
coio_readdir(const char *dir_path, char **buf)
{
	INIT_COEIO_FILE(eio)
	eio.readdir.bufp = buf;
	eio.readdir.pathname = dir_path;
	eio_req *req = eio_custom(coio_do_readdir, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

static void
coio_do_copyfile(eio_req *req)
{
	struct errinj *inj = errinj(ERRINJ_COIO_SENDFILE_CHUNK, ERRINJ_INT);
	struct coio_file_task *eio = (struct coio_file_task *)req->data;
	off_t pos, ret, left, chunk;
	struct stat st;
	if (stat(eio->copyfile.source, &st) < 0) {
		goto error;
	}

	int source_fd = open(eio->copyfile.source, O_RDONLY);
	if (source_fd < 0) {
		goto error;
	}

	int dest_fd = open(eio->copyfile.dest, O_WRONLY|O_CREAT|O_TRUNC,
			   st.st_mode & 0777);
	if (dest_fd < 0) {
		goto error_dest;
	}

	if (inj != NULL && inj->iparam > 0)
		chunk = (off_t)inj->iparam;
	else
		chunk = st.st_size;

	for (left = st.st_size, pos = 0; left > 0;) {
		ret = eio_sendfile_sync(dest_fd, source_fd, pos, chunk);
		if (ret < 0) {
			say_syserror("sendfile, [%s -> %s]",
				     fio_filename(source_fd),
				     fio_filename(dest_fd));
			goto error_copy;
		}
		pos += ret;
		left -= ret;
	}

	req->result = 0;
	close(source_fd);
	close(dest_fd);
	return;

error_copy:
	close(dest_fd);
error_dest:
	close(source_fd);
error:
	req->errorno = errno;
	req->result = -1;
	return;
}

int
coio_copyfile(const char *source, const char *dest)
{
	INIT_COEIO_FILE(eio)
	eio.copyfile.source = source;
	eio.copyfile.dest = dest;
	eio_req *req = eio_custom(coio_do_copyfile, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}

int
coio_utime(const char *pathname, double atime, double mtime)
{
	INIT_COEIO_FILE(eio);
	eio_req *req = eio_utime(pathname, atime, mtime, 0, coio_complete, &eio);
	return coio_wait_done(req, &eio);
}
