#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/gmon.h>

extern char *optarg;
extern int optind;

#ifndef O_DIRECT
#define O_DIRECT 0x00080000
#endif

int
main(int argc, char **argv)
{
	int c, i, fd, ifd, ofd;
	int mode, count, nrep, reps, seek, dofsync, gprof, rv;
	char *file, *ifile, *ofile, *buf, *cp;
	size_t size;
	struct timeval tv1, tv2;
	int mib[3];
	int gprof_state, sz;
	int value;
	int direct, trunc, touch, openflags, pgsz;
	size_t actual;

	pgsz = getpagesize();
	sz = sizeof(gprof_state);
	file = NULL;
	ifile = NULL;
	ifd = 0;
	ofile = NULL;
	ofd = 0;
	mode = 0;
	count = 300;
	size = 1024 * 1024;
	seek = 0;
	nrep = 1;
	dofsync = 0;
	direct = 0;
	gprof = 0;
	value = 'x';
	trunc = 0;
	touch = 0;
	actual = 0;
	while ((c = getopt(argc, argv, "rwdc:s:n:k:flpv:tTo:i:")) != -1) {
		switch (c) {
		case 'r':
			mode = 1;
			break;

		case 'w':
			mode = 2;
			break;

		case 'd':
			direct = 1;
			break;

		case 'c':
			count = strtol(optarg, NULL, 0);
			break;

		case 'n':
			nrep = strtol(optarg, NULL, 0);
			break;

		case 's':
			size = strtol(optarg, NULL, 0);
			break;

		case 'k':
			seek = strtol(optarg, NULL, 0);
			break;

		case 'f':
			dofsync = 1;
			break;

		case 'l':
			if (mlockall(MCL_CURRENT|MCL_FUTURE) < 0) {
				err(1, "mlockall");
			}
			break;

		case 'p':
			gprof = 1;
			break;

		case 'v':
			value = *optarg;
			break;

		case 't':
			trunc = 1;
			break;

		case 'T':
			touch = 1;
			break;

		case 'i':
			ifile = optarg;
			break;

		case 'o':
			ofile = optarg;
			break;

		default:
			errx(1, "bogus option `%c'", c);
		}
	}
	if (optind == argc) {
		errx(1, "must specify a file");
	}
	file = argv[optind];
	if (mode == 0) {
		errx(1, "must use one of `-r' or `-w'");
	}

	openflags = (mode == 1) ? O_RDONLY : (O_WRONLY|O_CREAT);
	if (trunc) {
		openflags |= O_TRUNC;
	}
	if (direct) {
		openflags |= O_DIRECT;
	}
	fd = open(file, openflags, 0666);
	if (fd < 0) {
		err(1, "open");
	}

	if (ifile) {
		ifd = open(ifile, O_RDONLY);
		if (ifd < 0) {
			err(1, "open ifile");
		}
	}
	if (ofile) {
		ofd = open(ofile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
		if (ofd < 0) {
			err(1, "open ofile");
		}
	}

	buf = malloc(size);
	if (buf == NULL) {
		err(1, "malloc");
	}
	memset(buf, value, size);

	if (gprof) {
		mib[0] = CTL_KERN;
		mib[1] = KERN_PROF;
		mib[2] = GPROF_STATE;
		gprof_state = GMON_PROF_ON;
		if (sysctl(mib, 3, NULL, NULL, &gprof_state, sz) < 0) {
			err(1, "sysctl gprof on");
		}
	}
	if (gettimeofday(&tv1, NULL) < 0) {
		err(1, "gettimeofday 1");
	}
	reps = nrep;
	while (reps--) {
		if (mode == 1) {
			for (i = 0; i < count; i++) {
				rv = read(fd, buf, size);
				if (rv < 0) {
					err(1, "read");
				}
				if (rv == 0) {
					fprintf(stderr, "read hit EOF\n");
					break;
				}
				actual += rv;
				if (ofile) {
					(void) write(ofd, buf, rv);
				}
				if (touch) {
					for (cp = buf; cp < buf + size;
					     cp += pgsz) {
						*cp = 1;
					}
				}
				if (seek && lseek(fd, seek, SEEK_CUR) < 0) {
					err(1, "lseek");
				}
			}
		} else {
			for (i = 0; i < count; i++) {
				if (ifile) {
					rv = read(ifd, buf, size);
					if (rv < 0) {
						err(1, "read ifile");
					}
				}
				rv = write(fd, buf, size);
				if (rv < 0) {
					err(1, "write");
				}
				if (touch) {
					for (cp = buf; cp < buf + size;
					     cp += pgsz) {
						*cp = 1;
					}
				}
				actual += rv;
				if (seek && lseek(fd, seek, SEEK_CUR) < 0) {
					err(1, "lseek");
				}
			}
		}
		if (dofsync) {
			rv = fsync(fd);
			if (rv < 0) {
				err(1, "fsync");
			}
		}
		if (lseek(fd, 0, SEEK_SET) < 0) {
			err(1, "lseek");
		}
	}
	if (gettimeofday(&tv2, NULL) < 0) {
		err(1, "gettimeofday 1");
	}
	if (gprof) {
		gprof_state = GMON_PROF_OFF;
		if (sysctl(mib, 3, NULL, NULL, &gprof_state, sz) < 0) {
			err(1, "sysctl gprof off");
		}
	}

	timersub(&tv2, &tv1, &tv2);
	printf("%lld bytes transferred in %ld.%03ld secs (%lld bytes/sec)\n",
	       ((uint64_t)actual), tv2.tv_sec, tv2.tv_usec / 1000,
	       (((uint64_t)actual * 1000000) /
		((uint64_t)tv2.tv_sec * 1000000 + tv2.tv_usec)));
	exit(0);
}
