#define _POSIX_C_SOURCE 200809L

#include <sys/stat.h>
#include <sys/types.h>

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

struct concurrent_ctx {
	const unsigned char *expected;
	size_t length;
	off_t offset;
	long loops;
	int actual_fd;
	int expected_errno;
	pthread_barrier_t barrier;
	pthread_mutex_t lock;
	int failure;
	ssize_t mismatch_count;
	int mismatch_errno;
};

struct vnode_ctx {
	const char *actual_path;
	const unsigned char *expected;
	size_t expected_size;
	long loops;
	pthread_barrier_t barrier;
	pthread_mutex_t lock;
	int failure;
};

static int
fail(const char *message)
{
	fprintf(stderr, "B32 probe failure: %s\n", message);
	return (1);
}

static long
parse_long(const char *text, long minimum, long maximum)
{
	char *end;
	long value;

	errno = 0;
	value = strtol(text, &end, 10);
	if (errno != 0 || *text == '\0' || *end != '\0' || value < minimum ||
	    value > maximum)
		return (-1);
	return (value);
}

static unsigned char *
read_expected(const char *path, off_t offset, size_t length)
{
	unsigned char *buffer;
	ssize_t count;
	int descriptor;

	descriptor = open(path, O_RDONLY);
	if (descriptor < 0)
		return (NULL);
	buffer = malloc(length);
	if (buffer == NULL) {
		close(descriptor);
		return (NULL);
	}
	count = pread(descriptor, buffer, length, offset);
	close(descriptor);
	if (count != (ssize_t)length) {
		free(buffer);
		return (NULL);
	}
	return (buffer);
}

static void
record_failure(pthread_mutex_t *lock, int *failure, int error)
{
	pthread_mutex_lock(lock);
	if (*failure == 0)
		*failure = error != 0 ? error : EIO;
	pthread_mutex_unlock(lock);
}

static void
record_concurrent_failure(struct concurrent_ctx *ctx, ssize_t count,
    int saved_errno)
{

	pthread_mutex_lock(&ctx->lock);
	if (ctx->failure == 0) {
		ctx->failure = saved_errno != 0 ? saved_errno : EIO;
		ctx->mismatch_count = count;
		ctx->mismatch_errno = saved_errno;
	}
	pthread_mutex_unlock(&ctx->lock);
}

static void *
concurrent_worker(void *opaque)
{
	struct concurrent_ctx *ctx;
	unsigned char *buffer;
	ssize_t count;
	long loop;
	int barrier_result, saved_errno;

	ctx = opaque;
	buffer = malloc(ctx->length);
	if (buffer == NULL) {
		record_failure(&ctx->lock, &ctx->failure, ENOMEM);
		return (NULL);
	}
	barrier_result = pthread_barrier_wait(&ctx->barrier);
	if (barrier_result != 0 && barrier_result != PTHREAD_BARRIER_SERIAL_THREAD) {
		record_failure(&ctx->lock, &ctx->failure, barrier_result);
		free(buffer);
		return (NULL);
	}
	for (loop = 0; loop < ctx->loops; ++loop) {
		errno = 0;
		count = pread(ctx->actual_fd, buffer, ctx->length, ctx->offset);
		saved_errno = errno;
		if (ctx->expected_errno != 0) {
			if (count != -1 || saved_errno != ctx->expected_errno) {
				record_concurrent_failure(ctx, count, saved_errno);
				break;
			}
		} else if (count != (ssize_t)ctx->length ||
		    memcmp(buffer, ctx->expected, ctx->length) != 0) {
			record_concurrent_failure(ctx, count, saved_errno);
			break;
		}
	}
	free(buffer);
	return (NULL);
}

static int
run_concurrent(const char *actual_path, const char *expected_path,
    long expected_errno, long workers, long loops, off_t offset, size_t length)
{
	struct concurrent_ctx ctx;
	pthread_t *threads;
	unsigned char *expected;
	long index;

	memset(&ctx, 0, sizeof(ctx));
	expected = expected_errno == 0 ?
	    read_expected(expected_path, offset, length) : calloc(1, length);
	if (expected == NULL)
		return (fail("could not prepare expected bytes"));
	ctx.expected = expected;
	ctx.length = length;
	ctx.offset = offset;
	ctx.loops = loops;
	ctx.expected_errno = (int)expected_errno;
	ctx.actual_fd = open(actual_path, O_RDONLY);
	if (ctx.actual_fd < 0) {
		free(expected);
		return (fail("could not open actual file"));
	}
	if (pthread_barrier_init(&ctx.barrier, NULL, (unsigned int)workers) != 0 ||
	    pthread_mutex_init(&ctx.lock, NULL) != 0) {
		close(ctx.actual_fd);
		free(expected);
		return (fail("could not initialize concurrent controls"));
	}
	threads = calloc((size_t)workers, sizeof(*threads));
	if (threads == NULL)
		return (fail("could not allocate worker handles"));
	for (index = 0; index < workers; ++index) {
		if (pthread_create(&threads[index], NULL, concurrent_worker, &ctx) != 0)
			return (fail("could not create concurrent worker"));
	}
	for (index = 0; index < workers; ++index) {
		if (pthread_join(threads[index], NULL) != 0)
			return (fail("could not join concurrent worker"));
	}
	free(threads);
	pthread_barrier_destroy(&ctx.barrier);
	pthread_mutex_destroy(&ctx.lock);
	close(ctx.actual_fd);
	free(expected);
	if (ctx.failure != 0) {
		fprintf(stderr,
		    "concurrent mismatch count=%zd errno=%d expected_errno=%d\n",
		    ctx.mismatch_count, ctx.mismatch_errno, ctx.expected_errno);
		errno = ctx.failure;
		perror("concurrent read");
		return (1);
	}
	printf("concurrent PASS workers=%ld loops=%ld assertions=%ld errno=%ld "
	    "offset=%ld length=%ld\n", workers, loops, workers * loops,
	    expected_errno, (long)offset, (long)length);
	return (0);
}

static void *
vnode_worker(void *opaque)
{
	struct vnode_ctx *ctx;
	unsigned char buffer[4096];
	struct stat status;
	ssize_t count;
	long loop;
	int barrier_result, descriptor, saved_errno;

	ctx = opaque;
	barrier_result = pthread_barrier_wait(&ctx->barrier);
	if (barrier_result != 0 && barrier_result != PTHREAD_BARRIER_SERIAL_THREAD) {
		record_failure(&ctx->lock, &ctx->failure, barrier_result);
		return (NULL);
	}
	for (loop = 0; loop < ctx->loops; ++loop) {
		descriptor = open(ctx->actual_path, O_RDONLY);
		if (descriptor < 0) {
			record_failure(&ctx->lock, &ctx->failure, errno);
			break;
		}
		if (fstat(descriptor, &status) != 0 ||
		    (size_t)status.st_size != ctx->expected_size) {
			saved_errno = errno;
			close(descriptor);
			record_failure(&ctx->lock, &ctx->failure,
			    saved_errno != 0 ? saved_errno : EIO);
			break;
		}
		count = pread(descriptor, buffer, sizeof(buffer), 0);
		saved_errno = errno;
		close(descriptor);
		if (count != (ssize_t)sizeof(buffer) ||
		    memcmp(buffer, ctx->expected, sizeof(buffer)) != 0) {
			record_failure(&ctx->lock, &ctx->failure,
			    saved_errno != 0 ? saved_errno : EIO);
			break;
		}
	}
	return (NULL);
}

static int
run_vnode_race(const char *actual_path, const char *expected_path, long workers,
    long loops)
{
	struct vnode_ctx ctx;
	struct stat status;
	pthread_t *threads;
	long index;

	memset(&ctx, 0, sizeof(ctx));
	if (stat(expected_path, &status) != 0 || status.st_size < 4096)
		return (fail("could not stat vnode-race reference"));
	ctx.actual_path = actual_path;
	ctx.expected_size = (size_t)status.st_size;
	ctx.expected = read_expected(expected_path, 0, 4096);
	ctx.loops = loops;
	if (ctx.expected == NULL)
		return (fail("could not read vnode-race reference"));
	if (pthread_barrier_init(&ctx.barrier, NULL, (unsigned int)workers) != 0 ||
	    pthread_mutex_init(&ctx.lock, NULL) != 0)
		return (fail("could not initialize vnode-race controls"));
	threads = calloc((size_t)workers, sizeof(*threads));
	if (threads == NULL)
		return (fail("could not allocate vnode-race handles"));
	for (index = 0; index < workers; ++index) {
		if (pthread_create(&threads[index], NULL, vnode_worker, &ctx) != 0)
			return (fail("could not create vnode-race worker"));
	}
	for (index = 0; index < workers; ++index) {
		if (pthread_join(threads[index], NULL) != 0)
			return (fail("could not join vnode-race worker"));
	}
	free(threads);
	pthread_barrier_destroy(&ctx.barrier);
	pthread_mutex_destroy(&ctx.lock);
	free((void *)ctx.expected);
	if (ctx.failure != 0) {
		errno = ctx.failure;
		perror("vnode race");
		return (1);
	}
	printf("vnode-race PASS workers=%ld loops=%ld assertions=%ld\n", workers,
	    loops, workers * loops);
	return (0);
}

static bool
marker_exists(const char *directory, const char *name)
{
	char path[1024];

	if (snprintf(path, sizeof(path), "%s/%s", directory, name) >=
	    (int)sizeof(path))
		return (false);
	return (access(path, F_OK) == 0);
}

static int
write_marker(const char *directory, const char *name, const char *content)
{
	char path[1024];
	ssize_t length;
	int descriptor;

	if (snprintf(path, sizeof(path), "%s/%s", directory, name) >=
	    (int)sizeof(path))
		return (-1);
	descriptor = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
	if (descriptor < 0)
		return (-1);
	length = (ssize_t)strlen(content);
	if (write(descriptor, content, (size_t)length) != length) {
		close(descriptor);
		return (-1);
	}
	return (close(descriptor));
}

static int
run_lifecycle(const char *path, const char *directory)
{
	struct timespec delay = { .tv_sec = 0, .tv_nsec = 10000000 };
	char result[128];
	ssize_t count;
	long attempt;
	int descriptor, saved_errno;
	bool closed;

	descriptor = open(path, O_RDONLY);
	if (descriptor < 0)
		return (fail("lifecycle open failed"));
	if (write_marker(directory, "ready", "ready\n") != 0)
		return (fail("lifecycle ready marker failed"));
	closed = false;
	for (attempt = 0; attempt < 18000; ++attempt) {
		if (!closed && marker_exists(directory, "close")) {
			if (close(descriptor) != 0)
				return (fail("lifecycle close failed"));
			closed = true;
			if (write_marker(directory, "closed", "closed\n") != 0)
				return (fail("lifecycle closed marker failed"));
		}
		if (marker_exists(directory, "read")) {
			errno = 0;
			count = pread(descriptor, result, sizeof(result), 0);
			saved_errno = errno;
			if (snprintf(result, sizeof(result), "%zd %d\n", count,
			    saved_errno) >= (int)sizeof(result) ||
			    write_marker(directory, "result", result) != 0)
				return (fail("lifecycle result marker failed"));
			if (!closed)
				close(descriptor);
			return (0);
		}
		if (marker_exists(directory, "exit")) {
			if (!closed)
				close(descriptor);
			return (0);
		}
		nanosleep(&delay, NULL);
	}
	if (!closed)
		close(descriptor);
	return (fail("lifecycle control timed out"));
}

int
main(int argc, char **argv)
{
	long expected_errno, workers, loops, offset, length;

	if (argc == 9 && strcmp(argv[1], "concurrent") == 0) {
		expected_errno = parse_long(argv[4], 0, 255);
		workers = parse_long(argv[5], 1, 256);
		loops = parse_long(argv[6], 1, 10000);
		offset = parse_long(argv[7], 0, INT64_MAX);
		length = parse_long(argv[8], 1, 1048576);
		if (expected_errno < 0 || workers < 0 || loops < 0 || offset < 0 ||
		    length < 0)
			return (fail("invalid concurrent arguments"));
		return (run_concurrent(argv[2], argv[3], expected_errno, workers,
		    loops, (off_t)offset, (size_t)length));
	}
	if (argc == 6 && strcmp(argv[1], "vnode-race") == 0) {
		workers = parse_long(argv[4], 1, 256);
		loops = parse_long(argv[5], 1, 10000);
		if (workers < 0 || loops < 0)
			return (fail("invalid vnode-race arguments"));
		return (run_vnode_race(argv[2], argv[3], workers, loops));
	}
	if (argc == 4 && strcmp(argv[1], "lifecycle") == 0)
		return (run_lifecycle(argv[2], argv[3]));
	return (fail("usage: concurrent ACTUAL EXPECTED ERRNO WORKERS LOOPS OFFSET LENGTH | vnode-race ACTUAL EXPECTED WORKERS LOOPS | lifecycle PATH CONTROL_DIR"));
}
