/*
 * fio xNVMe IO Engine
 *
 * IO engine using the xNVMe C API.
 *
 * See: http://xnvme.io/
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <stdlib.h>
#include <assert.h>
#include <libxnvme.h>
#include "fio.h"
#include "verify.h"
#include "zbd_types.h"
#include "fdp.h"
#include "optgroup.h"

static pthread_mutex_t g_serialize = PTHREAD_MUTEX_INITIALIZER;

struct xnvme_fioe_fwrap {
	/* fio file representation */
	struct fio_file *fio_file;

	/* xNVMe device handle */
	struct xnvme_dev *dev;
	/* xNVMe device geometry */
	const struct xnvme_geo *geo;

	struct xnvme_queue *queue;

	uint32_t ssw;
	uint32_t lba_nbytes;
	uint32_t md_nbytes;
	uint32_t lba_pow2;

	uint8_t _pad[16];
};
XNVME_STATIC_ASSERT(sizeof(struct xnvme_fioe_fwrap) == 64, "Incorrect size")

struct xnvme_fioe_data {
	/* I/O completion queue */
	struct io_u **iocq;

	/* # of iocq entries; incremented via getevents()/cb_pool() */
	uint64_t completed;

	/*
	 *  # of errors; incremented when observed on completion via
	 *  getevents()/cb_pool()
	 */
	uint64_t ecount;

	/* Controller which device/file to select */
	int32_t prev;
	int32_t cur;

	/* Number of devices/files for which open() has been called */
	int64_t nopen;
	/* Number of devices/files allocated in files[] */
	uint64_t nallocated;

	struct iovec *iovec;
	struct iovec *md_iovec;

	struct xnvme_fioe_fwrap files[];
};
XNVME_STATIC_ASSERT(sizeof(struct xnvme_fioe_data) == 64, "Incorrect size")

struct xnvme_fioe_request {
	/* Context for NVMe PI */
	struct xnvme_pi_ctx pi_ctx;

	/* Separate metadata buffer pointer */
	void *md_buf;
};

struct xnvme_fioe_options {
	void *padding;
	unsigned int hipri;
	unsigned int sqpoll_thread;
	unsigned int xnvme_dev_nsid;
	unsigned int xnvme_iovec;
	unsigned int md_per_io_size;
	unsigned int pi_act;
	unsigned int apptag;
	unsigned int apptag_mask;
	unsigned int prchk;
	char *xnvme_be;
	char *xnvme_mem;
	char *xnvme_async;
	char *xnvme_sync;
	char *xnvme_admin;
	char *xnvme_dev_subnqn;
};

static int str_pi_chk_cb(void *data, const char *str)
{
	struct xnvme_fioe_options *o = data;

	if (strstr(str, "GUARD") != NULL)
		o->prchk = XNVME_PI_FLAGS_GUARD_CHECK;
	if (strstr(str, "REFTAG") != NULL)
		o->prchk |= XNVME_PI_FLAGS_REFTAG_CHECK;
	if (strstr(str, "APPTAG") != NULL)
		o->prchk |= XNVME_PI_FLAGS_APPTAG_CHECK;

	return 0;
}

static struct fio_option options[] = {
	{
		.name = "hipri",
		.lname = "High Priority",
		.type = FIO_OPT_STR_SET,
		.off1 = offsetof(struct xnvme_fioe_options, hipri),
		.help = "Use polled IO completions",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "sqthread_poll",
		.lname = "Kernel SQ thread polling",
		.type = FIO_OPT_STR_SET,
		.off1 = offsetof(struct xnvme_fioe_options, sqpoll_thread),
		.help = "Offload submission/completion to kernel thread",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_be",
		.lname = "xNVMe Backend",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_be),
		.help = "Select xNVMe backend [spdk,linux,fbsd]",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_mem",
		.lname = "xNVMe Memory Backend",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_mem),
		.help = "Select xNVMe memory backend",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_async",
		.lname = "xNVMe Asynchronous command-interface",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_async),
		.help = "Select xNVMe async. interface: "
			"[emu,thrpool,io_uring,io_uring_cmd,libaio,posix,vfio,nil]",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_sync",
		.lname = "xNVMe Synchronous. command-interface",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_sync),
		.help = "Select xNVMe sync. interface: [nvme,psync,block]",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_admin",
		.lname = "xNVMe Admin command-interface",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_admin),
		.help = "Select xNVMe admin. cmd-interface: [nvme,block]",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_dev_nsid",
		.lname = "xNVMe Namespace-Identifier, for user-space NVMe driver",
		.type = FIO_OPT_INT,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_dev_nsid),
		.help = "xNVMe Namespace-Identifier, for user-space NVMe driver",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_dev_subnqn",
		.lname = "Subsystem nqn for Fabrics",
		.type = FIO_OPT_STR_STORE,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_dev_subnqn),
		.help = "Subsystem NQN for Fabrics",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name = "xnvme_iovec",
		.lname = "Vectored IOs",
		.type = FIO_OPT_STR_SET,
		.off1 = offsetof(struct xnvme_fioe_options, xnvme_iovec),
		.help = "Send vectored IOs",
		.category = FIO_OPT_C_ENGINE,
		.group = FIO_OPT_G_XNVME,
	},
	{
		.name	= "md_per_io_size",
		.lname	= "Separate Metadata Buffer Size per I/O",
		.type	= FIO_OPT_INT,
		.off1	= offsetof(struct xnvme_fioe_options, md_per_io_size),
		.def	= "0",
		.help	= "Size of separate metadata buffer per I/O (Default: 0)",
		.category = FIO_OPT_C_ENGINE,
		.group	= FIO_OPT_G_XNVME,
	},
	{
		.name	= "pi_act",
		.lname	= "Protection Information Action",
		.type	= FIO_OPT_BOOL,
		.off1	= offsetof(struct xnvme_fioe_options, pi_act),
		.def	= "1",
		.help	= "Protection Information Action bit (pi_act=1 or pi_act=0)",
		.category = FIO_OPT_C_ENGINE,
		.group	= FIO_OPT_G_XNVME,
	},
	{
		.name	= "pi_chk",
		.lname	= "Protection Information Check",
		.type	= FIO_OPT_STR_STORE,
		.def	= NULL,
		.help	= "Control of Protection Information Checking (pi_chk=GUARD,REFTAG,APPTAG)",
		.cb	= str_pi_chk_cb,
		.category = FIO_OPT_C_ENGINE,
		.group	= FIO_OPT_G_XNVME,
	},
	{
		.name	= "apptag",
		.lname	= "Application Tag used in Protection Information",
		.type	= FIO_OPT_INT,
		.off1	= offsetof(struct xnvme_fioe_options, apptag),
		.def	= "0x1234",
		.help	= "Application Tag used in Protection Information field (Default: 0x1234)",
		.category = FIO_OPT_C_ENGINE,
		.group	= FIO_OPT_G_XNVME,
	},
	{
		.name	= "apptag_mask",
		.lname	= "Application Tag Mask",
		.type	= FIO_OPT_INT,
		.off1	= offsetof(struct xnvme_fioe_options, apptag_mask),
		.def	= "0xffff",
		.help	= "Application Tag Mask used with Application Tag (Default: 0xffff)",
		.category = FIO_OPT_C_ENGINE,
		.group	= FIO_OPT_G_XNVME,
	},

	{
		.name = NULL,
	},
};

static void cb_pool(struct xnvme_cmd_ctx *ctx, void *cb_arg)
{
	struct io_u *io_u = cb_arg;
	struct xnvme_fioe_data *xd = io_u->mmap_data;
	struct xnvme_fioe_request *fio_req = io_u->engine_data;
	struct xnvme_fioe_fwrap *fwrap = &xd->files[io_u->file->fileno];
	bool pi_act = (fio_req->pi_ctx.pi_flags >> 3);
	int err;

	if (xnvme_cmd_ctx_cpl_status(ctx)) {
		xnvme_cmd_ctx_pr(ctx, XNVME_PR_DEF);
		xd->ecount += 1;
		io_u->error = EIO;
	}

	if (!io_u->error && fwrap->geo->pi_type && (io_u->ddir == DDIR_READ) && !pi_act) {
		err = xnvme_pi_verify(&fio_req->pi_ctx, io_u->xfer_buf,
				      fio_req->md_buf, io_u->xfer_buflen / fwrap->lba_nbytes);
		if (err) {
			xd->ecount += 1;
			io_u->error = EIO;
		}
	}

	xd->iocq[xd->completed++] = io_u;
	xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);
}

static struct xnvme_opts xnvme_opts_from_fioe(struct thread_data *td)
{
	struct xnvme_fioe_options *o = td->eo;
	struct xnvme_opts opts = xnvme_opts_default();

	opts.nsid = o->xnvme_dev_nsid;
	opts.subnqn = o->xnvme_dev_subnqn;
	opts.be = o->xnvme_be;
	opts.mem = o->xnvme_mem;
	opts.async = o->xnvme_async;
	opts.sync = o->xnvme_sync;
	opts.admin = o->xnvme_admin;

	opts.poll_io = o->hipri;
	opts.poll_sq = o->sqpoll_thread;

	opts.direct = td->o.odirect;

	return opts;
}

static void _dev_close(struct thread_data *td, struct xnvme_fioe_fwrap *fwrap)
{
	if (fwrap->dev)
		xnvme_queue_term(fwrap->queue);

	xnvme_dev_close(fwrap->dev);

	memset(fwrap, 0, sizeof(*fwrap));
}

static void xnvme_fioe_cleanup(struct thread_data *td)
{
	struct xnvme_fioe_data *xd = NULL;
	int err;

	if (!td->io_ops_data)
		return;

	xd = td->io_ops_data;

	err = pthread_mutex_lock(&g_serialize);
	if (err)
		log_err("ioeng->cleanup(): pthread_mutex_lock(), err(%d)\n", err);
		/* NOTE: not returning here */

	for (uint64_t i = 0; i < xd->nallocated; ++i)
		_dev_close(td, &xd->files[i]);

	if (!err) {
		err = pthread_mutex_unlock(&g_serialize);
		if (err)
			log_err("ioeng->cleanup(): pthread_mutex_unlock(), err(%d)\n", err);
	}

	free(xd->iocq);
	free(xd->iovec);
	free(xd->md_iovec);
	free(xd);
	td->io_ops_data = NULL;
}

static int _verify_options(struct thread_data *td, struct fio_file *f,
			   struct xnvme_fioe_fwrap *fwrap)
{
	struct xnvme_fioe_options *o = td->eo;
	unsigned int correct_md_size;

	for_each_rw_ddir(ddir) {
		if (td->o.min_bs[ddir] % fwrap->lba_nbytes || td->o.max_bs[ddir] % fwrap->lba_nbytes) {
			if (!fwrap->lba_pow2) {
				log_err("ioeng->_verify_options(%s): block size must be a multiple of %u "
					"(LBA data size + Metadata size)\n", f->file_name, fwrap->lba_nbytes);
			} else {
				log_err("ioeng->_verify_options(%s): block size must be a multiple of LBA data size\n",
					f->file_name);
			}
			return 1;
		}
		if (ddir == DDIR_TRIM)
			continue;

		correct_md_size = (td->o.max_bs[ddir] / fwrap->lba_nbytes) * fwrap->md_nbytes;
		if (fwrap->md_nbytes && fwrap->lba_pow2 && (o->md_per_io_size < correct_md_size)) {
			log_err("ioeng->_verify_options(%s): md_per_io_size should be at least %u bytes\n",
				f->file_name, correct_md_size);
			return 1;
		}
	}

	/*
	 * For extended logical block sizes we cannot use verify when
	 * end to end data protection checks are enabled, as the PI
	 * section of data buffer conflicts with verify.
	 */
	if (fwrap->md_nbytes && fwrap->geo->pi_type && !fwrap->lba_pow2 &&
	    td->o.verify != VERIFY_NONE) {
		log_err("ioeng->_verify_options(%s): for extended LBA, verify cannot be used when E2E data protection is enabled\n",
			f->file_name);
		return 1;
	}

	return 0;
}

/**
 * Helper function setting up device handles as addressed by the naming
 * convention of the given `fio_file` filename.
 *
 * Checks thread-options for explicit control of asynchronous implementation via
 * the ``--xnvme_async={thrpool,emu,posix,io_uring,libaio,nil}``.
 */
static int _dev_open(struct thread_data *td, struct fio_file *f)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_fioe_options *o = td->eo;
	struct xnvme_fioe_data *xd = td->io_ops_data;
	struct xnvme_fioe_fwrap *fwrap;
	int flags = 0;
	int err;

	if (f->fileno > (int)xd->nallocated) {
		log_err("ioeng->_dev_open(%s): invalid assumption\n", f->file_name);
		return 1;
	}

	fwrap = &xd->files[f->fileno];

	err = pthread_mutex_lock(&g_serialize);
	if (err) {
		log_err("ioeng->_dev_open(%s): pthread_mutex_lock(), err(%d)\n", f->file_name,
			err);
		return -err;
	}

	fwrap->dev = xnvme_dev_open(f->file_name, &opts);
	if (!fwrap->dev) {
		log_err("ioeng->_dev_open(%s): xnvme_dev_open(), err(%d)\n", f->file_name, errno);
		goto failure;
	}
	fwrap->geo = xnvme_dev_get_geo(fwrap->dev);

	if (xnvme_queue_init(fwrap->dev, td->o.iodepth, flags, &(fwrap->queue))) {
		log_err("ioeng->_dev_open(%s): xnvme_queue_init(), err(?)\n", f->file_name);
		goto failure;
	}
	xnvme_queue_set_cb(fwrap->queue, cb_pool, NULL);

	fwrap->ssw = xnvme_dev_get_ssw(fwrap->dev);
	fwrap->lba_nbytes = fwrap->geo->lba_nbytes;
	fwrap->md_nbytes = fwrap->geo->nbytes_oob;

	if (fwrap->geo->lba_extended)
		fwrap->lba_pow2 = 0;
	else
		fwrap->lba_pow2 = 1;

	/*
	 * When PI action is set and PI size is equal to metadata size, the
	 * controller inserts/removes PI. So update the LBA data and metadata
	 * sizes accordingly.
	 */
	if (o->pi_act && fwrap->geo->pi_type &&
	    fwrap->geo->nbytes_oob == xnvme_pi_size(fwrap->geo->pi_format)) {
		if (fwrap->geo->lba_extended) {
			fwrap->lba_nbytes -= fwrap->geo->nbytes_oob;
			fwrap->lba_pow2 = 1;
		}
		fwrap->md_nbytes = 0;
	}

	if (_verify_options(td, f, fwrap)) {
		td_verror(td, EINVAL, "_dev_open");
		goto failure;
	}

	fwrap->fio_file = f;
	fwrap->fio_file->filetype = FIO_TYPE_BLOCK;
	fwrap->fio_file->real_file_size = fwrap->geo->tbytes;
	fio_file_set_size_known(fwrap->fio_file);

	err = pthread_mutex_unlock(&g_serialize);
	if (err)
		log_err("ioeng->_dev_open(%s): pthread_mutex_unlock(), err(%d)\n", f->file_name,
			err);

	return 0;

failure:
	xnvme_queue_term(fwrap->queue);
	xnvme_dev_close(fwrap->dev);

	err = pthread_mutex_unlock(&g_serialize);
	if (err)
		log_err("ioeng->_dev_open(%s): pthread_mutex_unlock(), err(%d)\n", f->file_name,
			err);

	return 1;
}

static int xnvme_fioe_init(struct thread_data *td)
{
	struct xnvme_fioe_data *xd = NULL;
	struct xnvme_fioe_options *o = td->eo;
	struct fio_file *f;
	unsigned int i;

	if (!td->o.use_thread) {
		log_err("ioeng->init(): --thread=1 is required\n");
		return 1;
	}

	/* Allocate xd and iocq */
	xd = calloc(1, sizeof(*xd) + sizeof(*xd->files) * td->o.nr_files);
	if (!xd) {
		log_err("ioeng->init(): !calloc(), err(%d)\n", errno);
		return 1;
	}

	xd->iocq = calloc(td->o.iodepth, sizeof(struct io_u *));
	if (!xd->iocq) {
		free(xd);
		log_err("ioeng->init(): !calloc(xd->iocq), err(%d)\n", errno);
		return 1;
	}

	if (o->xnvme_iovec) {
		xd->iovec = calloc(td->o.iodepth, sizeof(*xd->iovec));
		if (!xd->iovec) {
			free(xd->iocq);
			free(xd);
			log_err("ioeng->init(): !calloc(xd->iovec), err(%d)\n", errno);
			return 1;
		}
	}

	if (o->xnvme_iovec && o->md_per_io_size) {
		xd->md_iovec = calloc(td->o.iodepth, sizeof(*xd->md_iovec));
		if (!xd->md_iovec) {
			free(xd->iocq);
			free(xd->iovec);
			free(xd);
			log_err("ioeng->init(): !calloc(xd->md_iovec), err(%d)\n", errno);
			return 1;
		}
	}

	xd->prev = -1;
	td->io_ops_data = xd;

	for_each_file(td, f, i)
	{
		if (_dev_open(td, f)) {
			/*
			 * Note: We are not freeing xd, iocq, iovec and md_iovec.
			 * This will be done as part of cleanup routine.
			 */
			log_err("ioeng->init(): failed; _dev_open(%s)\n", f->file_name);
			return 1;
		}

		++(xd->nallocated);
	}

	if (xd->nallocated != td->o.nr_files) {
		log_err("ioeng->init(): failed; nallocated != td->o.nr_files\n");
		return 1;
	}

	return 0;
}

/* NOTE: using the first device for buffer-allocators) */
static int xnvme_fioe_iomem_alloc(struct thread_data *td, size_t total_mem)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;
	struct xnvme_fioe_fwrap *fwrap = &xd->files[0];

	if (!fwrap->dev) {
		log_err("ioeng->iomem_alloc(): failed; no dev-handle\n");
		return 1;
	}

	td->orig_buffer = xnvme_buf_alloc(fwrap->dev, total_mem);

	return td->orig_buffer == NULL;
}

/* NOTE: using the first device for buffer-allocators) */
static void xnvme_fioe_iomem_free(struct thread_data *td)
{
	struct xnvme_fioe_data *xd = NULL;
	struct xnvme_fioe_fwrap *fwrap = NULL;

	if (!td->io_ops_data)
		return;

	xd = td->io_ops_data;
	fwrap = &xd->files[0];

	if (!fwrap->dev) {
		log_err("ioeng->iomem_free(): failed no dev-handle\n");
		return;
	}

	xnvme_buf_free(fwrap->dev, td->orig_buffer);
}

static int xnvme_fioe_io_u_init(struct thread_data *td, struct io_u *io_u)
{
	struct xnvme_fioe_request *fio_req;
	struct xnvme_fioe_options *o = td->eo;
	struct xnvme_fioe_data *xd = td->io_ops_data;
	struct xnvme_fioe_fwrap *fwrap = &xd->files[0];

	if (!fwrap->dev) {
		log_err("ioeng->io_u_init(): failed; no dev-handle\n");
		return 1;
	}

	io_u->mmap_data = td->io_ops_data;
	io_u->engine_data = NULL;

	fio_req = calloc(1, sizeof(*fio_req));
	if (!fio_req) {
		log_err("ioeng->io_u_init(): !calloc(fio_req), err(%d)\n", errno);
		return 1;
	}

	if (o->md_per_io_size) {
		fio_req->md_buf = xnvme_buf_alloc(fwrap->dev, o->md_per_io_size);
		if (!fio_req->md_buf) {
			free(fio_req);
			return 1;
		}
	}

	io_u->engine_data = fio_req;

	return 0;
}

static void xnvme_fioe_io_u_free(struct thread_data *td, struct io_u *io_u)
{
	struct xnvme_fioe_data *xd = NULL;
	struct xnvme_fioe_fwrap *fwrap = NULL;
	struct xnvme_fioe_request *fio_req = NULL;

	if (!td->io_ops_data)
		return;

	xd = td->io_ops_data;
	fwrap = &xd->files[0];

	if (!fwrap->dev) {
		log_err("ioeng->io_u_free(): failed no dev-handle\n");
		return;
	}

	fio_req = io_u->engine_data;
	if (fio_req->md_buf)
		xnvme_buf_free(fwrap->dev, fio_req->md_buf);

	free(fio_req);

	io_u->mmap_data = NULL;
}

static struct io_u *xnvme_fioe_event(struct thread_data *td, int event)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;

	assert(event >= 0);
	assert((unsigned)event < xd->completed);

	return xd->iocq[event];
}

static int xnvme_fioe_getevents(struct thread_data *td, unsigned int min, unsigned int max,
				const struct timespec *t)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;
	struct xnvme_fioe_fwrap *fwrap = NULL;
	int nfiles = xd->nallocated;
	int err = 0;

	if (xd->prev != -1 && ++xd->prev < nfiles) {
		fwrap = &xd->files[xd->prev];
		xd->cur = xd->prev;
	}

	xd->completed = 0;
	for (;;) {
		if (fwrap == NULL || xd->cur == nfiles) {
			fwrap = &xd->files[0];
			xd->cur = 0;
		}

		while (fwrap != NULL && xd->cur < nfiles && err >= 0) {
			err = xnvme_queue_poke(fwrap->queue, max - xd->completed);
			if (err < 0) {
				switch (err) {
				case -EBUSY:
				case -EAGAIN:
					usleep(1);
					break;

				default:
					log_err("ioeng->getevents(): unhandled IO error\n");
					assert(false);
					return 0;
				}
			}
			if (xd->completed >= min) {
				xd->prev = xd->cur;
				return xd->completed;
			}
			xd->cur++;
			fwrap = &xd->files[xd->cur];

			if (err < 0) {
				switch (err) {
				case -EBUSY:
				case -EAGAIN:
					usleep(1);
					break;
				}
			}
		}
	}

	xd->cur = 0;

	return xd->completed;
}

static enum fio_q_status xnvme_fioe_queue(struct thread_data *td, struct io_u *io_u)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;
	struct xnvme_fioe_options *o = td->eo;
	struct xnvme_fioe_fwrap *fwrap;
	struct xnvme_cmd_ctx *ctx;
	struct xnvme_fioe_request *fio_req = io_u->engine_data;
	uint32_t nsid;
	uint64_t slba;
	uint16_t nlb;
	int err;
	bool vectored_io = ((struct xnvme_fioe_options *)td->eo)->xnvme_iovec;
	uint32_t dir = io_u->dtype;

	fio_ro_check(td, io_u);

	fwrap = &xd->files[io_u->file->fileno];
	nsid = xnvme_dev_get_nsid(fwrap->dev);

	if (fwrap->lba_pow2) {
		slba = io_u->offset >> fwrap->ssw;
		nlb = (io_u->xfer_buflen >> fwrap->ssw) - 1;
	} else {
		slba = io_u->offset / fwrap->lba_nbytes;
		nlb = (io_u->xfer_buflen / fwrap->lba_nbytes) - 1;
	}

	ctx = xnvme_queue_get_cmd_ctx(fwrap->queue);
	ctx->async.cb_arg = io_u;

	ctx->cmd.common.nsid = nsid;
	ctx->cmd.nvm.slba = slba;
	ctx->cmd.nvm.nlb = nlb;
	if (dir) {
		ctx->cmd.nvm.dtype = io_u->dtype;
		ctx->cmd.nvm.cdw13.dspec = io_u->dspec;
	}

	switch (io_u->ddir) {
	case DDIR_READ:
		ctx->cmd.common.opcode = XNVME_SPEC_NVM_OPC_READ;
		break;

	case DDIR_WRITE:
		ctx->cmd.common.opcode = XNVME_SPEC_NVM_OPC_WRITE;
		break;

	default:
		log_err("ioeng->queue(): ENOSYS: %u\n", io_u->ddir);
		xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);

		io_u->error = ENOSYS;
		assert(false);
		return FIO_Q_COMPLETED;
	}

	if (fwrap->geo->pi_type && !o->pi_act) {
		err = xnvme_pi_ctx_init(&fio_req->pi_ctx, fwrap->lba_nbytes,
					fwrap->geo->nbytes_oob, fwrap->geo->lba_extended,
					fwrap->geo->pi_loc, fwrap->geo->pi_type,
					(o->pi_act << 3 | o->prchk), slba, o->apptag_mask,
					o->apptag, fwrap->geo->pi_format);
		if (err) {
			log_err("ioeng->queue(): err: '%d'\n", err);

			xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);

			io_u->error = abs(err);
			return FIO_Q_COMPLETED;
		}

		if (io_u->ddir == DDIR_WRITE)
			xnvme_pi_generate(&fio_req->pi_ctx, io_u->xfer_buf, fio_req->md_buf,
					  nlb + 1);
	}

	if (fwrap->geo->pi_type)
		ctx->cmd.nvm.prinfo = (o->pi_act << 3 | o->prchk);

	switch (fwrap->geo->pi_type) {
	case XNVME_PI_TYPE1:
	case XNVME_PI_TYPE2:
		switch (fwrap->geo->pi_format) {
		case XNVME_SPEC_NVM_NS_16B_GUARD:
			if (o->prchk & XNVME_PI_FLAGS_REFTAG_CHECK)
				ctx->cmd.nvm.ilbrt = (uint32_t)slba;
			break;
		case XNVME_SPEC_NVM_NS_64B_GUARD:
			if (o->prchk & XNVME_PI_FLAGS_REFTAG_CHECK) {
				ctx->cmd.nvm.ilbrt = (uint32_t)slba;
				ctx->cmd.common.cdw03 = ((slba >> 32) & 0xffff);
			}
			break;
		default:
			break;
		}
		if (o->prchk & XNVME_PI_FLAGS_APPTAG_CHECK) {
			ctx->cmd.nvm.lbat = o->apptag;
			ctx->cmd.nvm.lbatm = o->apptag_mask;
		}
		break;
	case XNVME_PI_TYPE3:
		if (o->prchk & XNVME_PI_FLAGS_APPTAG_CHECK) {
			ctx->cmd.nvm.lbat = o->apptag;
			ctx->cmd.nvm.lbatm = o->apptag_mask;
		}
		break;
	case XNVME_PI_DISABLE:
		break;
	}

	if (vectored_io) {
		xd->iovec[io_u->index].iov_base = io_u->xfer_buf;
		xd->iovec[io_u->index].iov_len = io_u->xfer_buflen;
		if (fwrap->md_nbytes && fwrap->lba_pow2) {
			xd->md_iovec[io_u->index].iov_base = fio_req->md_buf;
			xd->md_iovec[io_u->index].iov_len = fwrap->md_nbytes * (nlb + 1);
			err = xnvme_cmd_passv(ctx, &xd->iovec[io_u->index], 1, io_u->xfer_buflen,
					      &xd->md_iovec[io_u->index], 1,
					      fwrap->md_nbytes * (nlb + 1));
		} else {
			err = xnvme_cmd_passv(ctx, &xd->iovec[io_u->index], 1, io_u->xfer_buflen,
					      NULL, 0, 0);
		}
	} else {
		if (fwrap->md_nbytes && fwrap->lba_pow2)
			err = xnvme_cmd_pass(ctx, io_u->xfer_buf, io_u->xfer_buflen,
					     fio_req->md_buf, fwrap->md_nbytes * (nlb + 1));
		else
			err = xnvme_cmd_pass(ctx, io_u->xfer_buf, io_u->xfer_buflen, NULL, 0);
	}
	switch (err) {
	case 0:
		return FIO_Q_QUEUED;

	case -EBUSY:
	case -EAGAIN:
		xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);
		return FIO_Q_BUSY;

	default:
		log_err("ioeng->queue(): err: '%d'\n", err);

		xnvme_queue_put_cmd_ctx(ctx->async.queue, ctx);

		io_u->error = abs(err);
		assert(false);
		return FIO_Q_COMPLETED;
	}
}

static int xnvme_fioe_close(struct thread_data *td, struct fio_file *f)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;

	dprint(FD_FILE, "xnvme close %s -- nopen: %ld\n", f->file_name, xd->nopen);

	--(xd->nopen);

	return 0;
}

static int xnvme_fioe_open(struct thread_data *td, struct fio_file *f)
{
	struct xnvme_fioe_data *xd = td->io_ops_data;

	dprint(FD_FILE, "xnvme open %s -- nopen: %ld\n", f->file_name, xd->nopen);

	if (f->fileno > (int)xd->nallocated) {
		log_err("ioeng->open(): f->fileno > xd->nallocated; invalid assumption\n");
		return 1;
	}
	if (xd->files[f->fileno].fio_file != f) {
		log_err("ioeng->open(): fio_file != f; invalid assumption\n");
		return 1;
	}

	++(xd->nopen);

	return 0;
}

static int xnvme_fioe_invalidate(struct thread_data *td, struct fio_file *f)
{
	/* Consider only doing this with be:spdk */
	return 0;
}

static int xnvme_fioe_get_max_open_zones(struct thread_data *td, struct fio_file *f,
					 unsigned int *max_open_zones)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_dev *dev;
	const struct xnvme_spec_znd_idfy_ns *zns;
	int err = 0, err_lock;

	if (f->filetype != FIO_TYPE_FILE && f->filetype != FIO_TYPE_BLOCK &&
	    f->filetype != FIO_TYPE_CHAR) {
		log_info("ioeng->get_max_open_zoned(): ignoring filetype: %d\n", f->filetype);
		return 0;
	}
	err_lock = pthread_mutex_lock(&g_serialize);
	if (err_lock) {
		log_err("ioeng->get_max_open_zones(): pthread_mutex_lock(), err(%d)\n", err_lock);
		return -err_lock;
	}

	dev = xnvme_dev_open(f->file_name, &opts);
	if (!dev) {
		log_err("ioeng->get_max_open_zones(): xnvme_dev_open(), err(%d)\n", err_lock);
		err = -errno;
		goto exit;
	}
	if (xnvme_dev_get_geo(dev)->type != XNVME_GEO_ZONED) {
		errno = EINVAL;
		err = -errno;
		goto exit;
	}

	zns = (void *)xnvme_dev_get_ns_css(dev);
	if (!zns) {
		log_err("ioeng->get_max_open_zones(): xnvme_dev_get_ns_css(), err(%d)\n", errno);
		err = -errno;
		goto exit;
	}

	/*
	 * intentional overflow as the value is zero-based and NVMe
	 * defines 0xFFFFFFFF as unlimited thus overflowing to 0 which
	 * is how fio indicates unlimited and otherwise just converting
	 * to one-based.
	 */
	*max_open_zones = zns->mor + 1;

exit:
	xnvme_dev_close(dev);
	err_lock = pthread_mutex_unlock(&g_serialize);
	if (err_lock)
		log_err("ioeng->get_max_open_zones(): pthread_mutex_unlock(), err(%d)\n",
			err_lock);

	return err;
}

/**
 * Currently, this function is called before of I/O engine initialization, so,
 * we cannot consult the file-wrapping done when 'fioe' initializes.
 * Instead we just open based on the given filename.
 *
 * TODO: unify the different setup methods, consider keeping the handle around,
 * and consider how to support the --be option in this usecase
 */
static int xnvme_fioe_get_zoned_model(struct thread_data *td, struct fio_file *f,
				      enum zbd_zoned_model *model)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_dev *dev;
	int err = 0, err_lock;

	if (f->filetype != FIO_TYPE_FILE && f->filetype != FIO_TYPE_BLOCK &&
	    f->filetype != FIO_TYPE_CHAR) {
		log_info("ioeng->get_zoned_model(): ignoring filetype: %d\n", f->filetype);
		return -EINVAL;
	}

	err = pthread_mutex_lock(&g_serialize);
	if (err) {
		log_err("ioeng->get_zoned_model(): pthread_mutex_lock(), err(%d)\n", err);
		return -err;
	}

	dev = xnvme_dev_open(f->file_name, &opts);
	if (!dev) {
		log_err("ioeng->get_zoned_model(): xnvme_dev_open(%s) failed, errno: %d\n",
			f->file_name, errno);
		err = -errno;
		goto exit;
	}

	switch (xnvme_dev_get_geo(dev)->type) {
	case XNVME_GEO_UNKNOWN:
		dprint(FD_ZBD, "%s: got 'unknown', assigning ZBD_NONE\n", f->file_name);
		*model = ZBD_NONE;
		break;

	case XNVME_GEO_CONVENTIONAL:
		dprint(FD_ZBD, "%s: got 'conventional', assigning ZBD_NONE\n", f->file_name);
		*model = ZBD_NONE;
		break;

	case XNVME_GEO_ZONED:
		dprint(FD_ZBD, "%s: got 'zoned', assigning ZBD_HOST_MANAGED\n", f->file_name);
		*model = ZBD_HOST_MANAGED;
		break;

	default:
		dprint(FD_ZBD, "%s: hit-default, assigning ZBD_NONE\n", f->file_name);
		*model = ZBD_NONE;
		errno = EINVAL;
		err = -errno;
		break;
	}

exit:
	xnvme_dev_close(dev);

	err_lock = pthread_mutex_unlock(&g_serialize);
	if (err_lock)
		log_err("ioeng->get_zoned_model(): pthread_mutex_unlock(), err(%d)\n", err_lock);

	return err;
}

/**
 * Fills the given ``zbdz`` with at most ``nr_zones`` zone-descriptors.
 *
 * The implementation converts the NVMe Zoned Command Set log-pages for Zone
 * descriptors into the Linux Kernel Zoned Block Report format.
 *
 * NOTE: This function is called before I/O engine initialization, that is,
 * before ``_dev_open`` has been called and file-wrapping is setup. Thus is has
 * to do the ``_dev_open`` itself, and shut it down again once it is done
 * retrieving the log-pages and converting them to the report format.
 *
 * TODO: unify the different setup methods, consider keeping the handle around,
 * and consider how to support the --async option in this usecase
 */
static int xnvme_fioe_report_zones(struct thread_data *td, struct fio_file *f, uint64_t offset,
				   struct zbd_zone *zbdz, unsigned int nr_zones)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	const struct xnvme_spec_znd_idfy_lbafe *lbafe = NULL;
	struct xnvme_dev *dev = NULL;
	const struct xnvme_geo *geo = NULL;
	struct xnvme_znd_report *rprt = NULL;
	uint32_t ssw;
	uint64_t slba;
	unsigned int limit = 0;
	int err = 0, err_lock;

	dprint(FD_ZBD, "%s: report_zones() offset: %zu, nr_zones: %u\n", f->file_name, offset,
	       nr_zones);

	err = pthread_mutex_lock(&g_serialize);
	if (err) {
		log_err("ioeng->report_zones(%s): pthread_mutex_lock(), err(%d)\n", f->file_name,
			err);
		return -err;
	}

	dev = xnvme_dev_open(f->file_name, &opts);
	if (!dev) {
		log_err("ioeng->report_zones(%s): xnvme_dev_open(), err(%d)\n", f->file_name,
			errno);
		goto exit;
	}

	geo = xnvme_dev_get_geo(dev);
	ssw = xnvme_dev_get_ssw(dev);
	lbafe = xnvme_znd_dev_get_lbafe(dev);

	limit = nr_zones > geo->nzone ? geo->nzone : nr_zones;

	dprint(FD_ZBD, "%s: limit: %u\n", f->file_name, limit);

	slba = ((offset >> ssw) / geo->nsect) * geo->nsect;

	rprt = xnvme_znd_report_from_dev(dev, slba, limit, 0);
	if (!rprt) {
		log_err("ioeng->report_zones(%s): xnvme_znd_report_from_dev(), err(%d)\n",
			f->file_name, errno);
		err = -errno;
		goto exit;
	}
	if (rprt->nentries != limit) {
		log_err("ioeng->report_zones(%s): nentries != nr_zones\n", f->file_name);
		err = 1;
		goto exit;
	}
	if (offset > geo->tbytes) {
		log_err("ioeng->report_zones(%s): out-of-bounds\n", f->file_name);
		goto exit;
	}

	/* Transform the zone-report */
	for (uint32_t idx = 0; idx < rprt->nentries; ++idx) {
		struct xnvme_spec_znd_descr *descr = XNVME_ZND_REPORT_DESCR(rprt, idx);

		zbdz[idx].start = descr->zslba << ssw;
		zbdz[idx].len = lbafe->zsze << ssw;
		zbdz[idx].capacity = descr->zcap << ssw;
		zbdz[idx].wp = descr->wp << ssw;

		switch (descr->zt) {
		case XNVME_SPEC_ZND_TYPE_SEQWR:
			zbdz[idx].type = ZBD_ZONE_TYPE_SWR;
			break;

		default:
			log_err("ioeng->report_zones(%s): invalid type for zone at offset(%zu)\n",
				f->file_name, zbdz[idx].start);
			err = -EIO;
			goto exit;
		}

		switch (descr->zs) {
		case XNVME_SPEC_ZND_STATE_EMPTY:
			zbdz[idx].cond = ZBD_ZONE_COND_EMPTY;
			break;
		case XNVME_SPEC_ZND_STATE_IOPEN:
			zbdz[idx].cond = ZBD_ZONE_COND_IMP_OPEN;
			break;
		case XNVME_SPEC_ZND_STATE_EOPEN:
			zbdz[idx].cond = ZBD_ZONE_COND_EXP_OPEN;
			break;
		case XNVME_SPEC_ZND_STATE_CLOSED:
			zbdz[idx].cond = ZBD_ZONE_COND_CLOSED;
			break;
		case XNVME_SPEC_ZND_STATE_FULL:
			zbdz[idx].cond = ZBD_ZONE_COND_FULL;
			break;

		case XNVME_SPEC_ZND_STATE_RONLY:
		case XNVME_SPEC_ZND_STATE_OFFLINE:
		default:
			zbdz[idx].cond = ZBD_ZONE_COND_OFFLINE;
			break;
		}
	}

exit:
	xnvme_buf_virt_free(rprt);

	xnvme_dev_close(dev);

	err_lock = pthread_mutex_unlock(&g_serialize);
	if (err_lock)
		log_err("ioeng->report_zones(): pthread_mutex_unlock(), err: %d\n", err_lock);

	dprint(FD_ZBD, "err: %d, nr_zones: %d\n", err, (int)nr_zones);

	return err ? err : (int)limit;
}

/**
 * NOTE: This function may get called before I/O engine initialization, that is,
 * before ``_dev_open`` has been called and file-wrapping is setup. In such
 * case it has to do ``_dev_open`` itself, and shut it down again once it is
 * done resetting write pointer of zones.
 */
static int xnvme_fioe_reset_wp(struct thread_data *td, struct fio_file *f, uint64_t offset,
			       uint64_t length)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_fioe_data *xd = NULL;
	struct xnvme_fioe_fwrap *fwrap = NULL;
	struct xnvme_dev *dev = NULL;
	const struct xnvme_geo *geo = NULL;
	uint64_t first, last;
	uint32_t ssw;
	uint32_t nsid;
	int err = 0, err_lock;

	if (td->io_ops_data) {
		xd = td->io_ops_data;
		fwrap = &xd->files[f->fileno];

		assert(fwrap->dev);
		assert(fwrap->geo);

		dev = fwrap->dev;
		geo = fwrap->geo;
		ssw = fwrap->ssw;
	} else {
		err = pthread_mutex_lock(&g_serialize);
		if (err) {
			log_err("ioeng->reset_wp(): pthread_mutex_lock(), err(%d)\n", err);
			return -err;
		}

		dev = xnvme_dev_open(f->file_name, &opts);
		if (!dev) {
			log_err("ioeng->reset_wp(): xnvme_dev_open(%s) failed, errno(%d)\n",
				f->file_name, errno);
			goto exit;
		}
		geo = xnvme_dev_get_geo(dev);
		ssw = xnvme_dev_get_ssw(dev);
	}

	nsid = xnvme_dev_get_nsid(dev);

	first = ((offset >> ssw) / geo->nsect) * geo->nsect;
	last = (((offset + length) >> ssw) / geo->nsect) * geo->nsect;
	dprint(FD_ZBD, "first: 0x%lx, last: 0x%lx\n", first, last);

	for (uint64_t zslba = first; zslba < last; zslba += geo->nsect) {
		struct xnvme_cmd_ctx ctx = xnvme_cmd_ctx_from_dev(dev);

		if (zslba >= (geo->nsect * geo->nzone)) {
			log_err("ioeng->reset_wp(): out-of-bounds\n");
			err = 0;
			break;
		}

		err = xnvme_znd_mgmt_send(&ctx, nsid, zslba, false,
					  XNVME_SPEC_ZND_CMD_MGMT_SEND_RESET, 0x0, NULL);
		if (err || xnvme_cmd_ctx_cpl_status(&ctx)) {
			err = err ? err : -EIO;
			log_err("ioeng->reset_wp(): err(%d), sc(%d)", err, ctx.cpl.status.sc);
			goto exit;
		}
	}

exit:
	if (!td->io_ops_data) {
		xnvme_dev_close(dev);

		err_lock = pthread_mutex_unlock(&g_serialize);
		if (err_lock)
			log_err("ioeng->reset_wp(): pthread_mutex_unlock(), err(%d)\n", err_lock);
	}

	return err;
}

static int xnvme_fioe_fetch_ruhs(struct thread_data *td, struct fio_file *f,
				 struct fio_ruhs_info *fruhs_info)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_dev *dev;
	struct xnvme_spec_ruhs *ruhs;
	struct xnvme_cmd_ctx ctx;
	uint32_t ruhs_nbytes;
	uint32_t nsid;
	int err = 0, err_lock;

	if (f->filetype != FIO_TYPE_CHAR && f->filetype != FIO_TYPE_FILE) {
		log_err("ioeng->fdp_ruhs(): ignoring filetype: %d\n", f->filetype);
		return -EINVAL;
	}

	err = pthread_mutex_lock(&g_serialize);
	if (err) {
		log_err("ioeng->fdp_ruhs(): pthread_mutex_lock(), err(%d)\n", err);
		return -err;
	}

	dev = xnvme_dev_open(f->file_name, &opts);
	if (!dev) {
		log_err("ioeng->fdp_ruhs(): xnvme_dev_open(%s) failed, errno: %d\n",
			f->file_name, errno);
		err = -errno;
		goto exit;
	}

	ruhs_nbytes = sizeof(*ruhs) + (FDP_MAX_RUHS * sizeof(struct xnvme_spec_ruhs_desc));
	ruhs = xnvme_buf_alloc(dev, ruhs_nbytes);
	if (!ruhs) {
		err = -errno;
		goto exit;
	}
	memset(ruhs, 0, ruhs_nbytes);

	ctx = xnvme_cmd_ctx_from_dev(dev);
	nsid = xnvme_dev_get_nsid(dev);

	err = xnvme_nvm_mgmt_recv(&ctx, nsid, XNVME_SPEC_IO_MGMT_RECV_RUHS, 0, ruhs, ruhs_nbytes);

	if (err || xnvme_cmd_ctx_cpl_status(&ctx)) {
		err = err ? err : -EIO;
		log_err("ioeng->fdp_ruhs(): err(%d), sc(%d)", err, ctx.cpl.status.sc);
		goto free_buffer;
	}

	fruhs_info->nr_ruhs = ruhs->nruhsd;
	for (uint32_t idx = 0; idx < fruhs_info->nr_ruhs; ++idx) {
		fruhs_info->plis[idx] = le16_to_cpu(ruhs->desc[idx].pi);
	}

free_buffer:
	xnvme_buf_free(dev, ruhs);
exit:
	xnvme_dev_close(dev);

	err_lock = pthread_mutex_unlock(&g_serialize);
	if (err_lock)
		log_err("ioeng->fdp_ruhs(): pthread_mutex_unlock(), err(%d)\n", err_lock);

	return err;
}

static int xnvme_fioe_get_file_size(struct thread_data *td, struct fio_file *f)
{
	struct xnvme_opts opts = xnvme_opts_from_fioe(td);
	struct xnvme_dev *dev;
	int ret = 0, err;

	if (fio_file_size_known(f))
		return 0;

	ret = pthread_mutex_lock(&g_serialize);
	if (ret) {
		log_err("ioeng->reset_wp(): pthread_mutex_lock(), err(%d)\n", ret);
		return -ret;
	}

	dev = xnvme_dev_open(f->file_name, &opts);
	if (!dev) {
		log_err("%s: failed retrieving device handle, errno: %d\n", f->file_name, errno);
		ret = -errno;
		goto exit;
	}

	f->real_file_size = xnvme_dev_get_geo(dev)->tbytes;
	fio_file_set_size_known(f);

	if (td->o.zone_mode == ZONE_MODE_ZBD)
		f->filetype = FIO_TYPE_BLOCK;

exit:
	xnvme_dev_close(dev);
	err = pthread_mutex_unlock(&g_serialize);
	if (err)
		log_err("ioeng->reset_wp(): pthread_mutex_unlock(), err(%d)\n", err);

	return ret;
}

FIO_STATIC struct ioengine_ops ioengine = {
	.name = "xnvme",
	.version = FIO_IOOPS_VERSION,
	.options = options,
	.option_struct_size = sizeof(struct xnvme_fioe_options),
	.flags = FIO_DISKLESSIO | FIO_NODISKUTIL | FIO_NOEXTEND | FIO_MEMALIGN | FIO_RAWIO,

	.cleanup = xnvme_fioe_cleanup,
	.init = xnvme_fioe_init,

	.iomem_free = xnvme_fioe_iomem_free,
	.iomem_alloc = xnvme_fioe_iomem_alloc,

	.io_u_free = xnvme_fioe_io_u_free,
	.io_u_init = xnvme_fioe_io_u_init,

	.event = xnvme_fioe_event,
	.getevents = xnvme_fioe_getevents,
	.queue = xnvme_fioe_queue,

	.close_file = xnvme_fioe_close,
	.open_file = xnvme_fioe_open,
	.get_file_size = xnvme_fioe_get_file_size,

	.invalidate = xnvme_fioe_invalidate,
	.get_max_open_zones = xnvme_fioe_get_max_open_zones,
	.get_zoned_model = xnvme_fioe_get_zoned_model,
	.report_zones = xnvme_fioe_report_zones,
	.reset_wp = xnvme_fioe_reset_wp,

	.fdp_fetch_ruhs = xnvme_fioe_fetch_ruhs,
};

static void fio_init fio_xnvme_register(void)
{
	register_ioengine(&ioengine);
}

static void fio_exit fio_xnvme_unregister(void)
{
	unregister_ioengine(&ioengine);
}
