/*
 * Copyright (C) 2019 Western Digital Corporation or its affiliates.
 *
 * This file is released under the GPL.
 *
 * libzbc engine
 * IO engine using libzbc library to talk to SMR disks.
 */
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <libzbc/zbc.h>

#include "fio.h"
#include "err.h"
#include "zbd_types.h"

struct libzbc_data {
	struct zbc_device	*zdev;
	enum zbc_dev_model	model;
	uint64_t		nr_sectors;
};

static int libzbc_get_dev_info(struct libzbc_data *ld, struct fio_file *f)
{
	struct zbc_device_info *zinfo;

	zinfo = calloc(1, sizeof(*zinfo));
	if (!zinfo)
		return -ENOMEM;

	zbc_get_device_info(ld->zdev, zinfo);
	ld->model = zinfo->zbd_model;
	ld->nr_sectors = zinfo->zbd_sectors;

	dprint(FD_ZBD, "%s: vendor_id:%s, type: %s, model: %s\n",
	       f->file_name, zinfo->zbd_vendor_id,
	       zbc_device_type_str(zinfo->zbd_type),
	       zbc_device_model_str(zinfo->zbd_model));

	free(zinfo);

	return 0;
}

static int libzbc_open_dev(struct thread_data *td, struct fio_file *f,
			   struct libzbc_data **p_ld)
{
	struct libzbc_data *ld = td->io_ops_data;
	int ret, flags = OS_O_DIRECT;

	if (ld) {
		/* Already open */
		assert(ld->zdev);
		goto out;
	}

	if (f->filetype != FIO_TYPE_BLOCK && f->filetype != FIO_TYPE_CHAR) {
		td_verror(td, EINVAL, "wrong file type");
		log_err("ioengine libzbc only works on block or character devices\n");
		return -EINVAL;
	}

	if (td_write(td)) {
		if (!read_only)
			flags |= O_RDWR;
	} else if (td_read(td)) {
		if (f->filetype == FIO_TYPE_CHAR && !read_only)
			flags |= O_RDWR;
		else
			flags |= O_RDONLY;
	} else if (td_trim(td)) {
		td_verror(td, EINVAL, "libzbc does not support trim");
		log_err("%s: libzbc does not support trim\n", f->file_name);
		return -EINVAL;
	}

	if (td->o.oatomic) {
		td_verror(td, EINVAL, "libzbc does not support O_ATOMIC");
		log_err("%s: libzbc does not support O_ATOMIC\n", f->file_name);
		return -EINVAL;
	}

	ld = calloc(1, sizeof(*ld));
	if (!ld)
		return -ENOMEM;

	ret = zbc_open(f->file_name,
		       flags | ZBC_O_DRV_SCSI | ZBC_O_DRV_ATA, &ld->zdev);
	if (ret) {
		log_err("%s: zbc_open() failed, err=%d\n",
			f->file_name, ret);
		goto err;
	}

	ret = libzbc_get_dev_info(ld, f);
	if (ret)
		goto err_close;

	td->io_ops_data = ld;
out:
	if (p_ld)
		*p_ld = ld;

	return 0;

err_close:
	zbc_close(ld->zdev);
err:
	free(ld);
	return ret;
}

static int libzbc_close_dev(struct thread_data *td)
{
	struct libzbc_data *ld = td->io_ops_data;
	int ret = 0;

	td->io_ops_data = NULL;
	if (ld) {
		if (ld->zdev)
			ret = zbc_close(ld->zdev);
		free(ld);
	}

	return ret;
}
static int libzbc_open_file(struct thread_data *td, struct fio_file *f)
{
	return libzbc_open_dev(td, f, NULL);
}

static int libzbc_close_file(struct thread_data *td, struct fio_file *f)
{
	int ret;

	ret = libzbc_close_dev(td);
	if (ret)
		log_err("%s: close device failed err %d\n",
			f->file_name, ret);

	return ret;
}

static void libzbc_cleanup(struct thread_data *td)
{
	libzbc_close_dev(td);
}

static int libzbc_invalidate(struct thread_data *td, struct fio_file *f)
{
	/* Passthrough IO do not cache data. Nothing to do */
	return 0;
}

static int libzbc_get_file_size(struct thread_data *td, struct fio_file *f)
{
	struct libzbc_data *ld;
	int ret;

	if (fio_file_size_known(f))
		return 0;

	ret = libzbc_open_dev(td, f, &ld);
	if (ret)
		return ret;

	f->real_file_size = ld->nr_sectors << 9;
	fio_file_set_size_known(f);

	return 0;
}

static int libzbc_get_zoned_model(struct thread_data *td, struct fio_file *f,
				  enum zbd_zoned_model *model)
{
	struct libzbc_data *ld;
	int ret;

	if (f->filetype != FIO_TYPE_BLOCK && f->filetype != FIO_TYPE_CHAR) {
		*model = ZBD_IGNORE;
		return 0;
	}

	ret = libzbc_open_dev(td, f, &ld);
	if (ret)
		return ret;

	switch (ld->model) {
	case ZBC_DM_HOST_AWARE:
		*model = ZBD_HOST_AWARE;
		break;
	case ZBC_DM_HOST_MANAGED:
		*model = ZBD_HOST_MANAGED;
		break;
	default:
		*model = ZBD_NONE;
		break;
	}

	return 0;
}

static int libzbc_report_zones(struct thread_data *td, struct fio_file *f,
			       uint64_t offset, struct zbd_zone *zbdz,
			       unsigned int nr_zones)
{
	struct libzbc_data *ld;
	uint64_t sector = offset >> 9;
	struct zbc_zone *zones;
	unsigned int i;
	int ret;

	ret = libzbc_open_dev(td, f, &ld);
	if (ret)
		return ret;

	if (sector >= ld->nr_sectors)
		return 0;

	zones = calloc(nr_zones, sizeof(struct zbc_zone));
	if (!zones) {
		ret = -ENOMEM;
		goto out;
	}

	ret = zbc_report_zones(ld->zdev, sector, ZBC_RO_ALL, zones, &nr_zones);
	if (ret < 0) {
		log_err("%s: zbc_report_zones failed, err=%d\n",
			f->file_name, ret);
		goto out;
	}

	for (i = 0; i < nr_zones; i++, zbdz++) {
		zbdz->start = zones[i].zbz_start << 9;
		zbdz->len = zones[i].zbz_length << 9;
		zbdz->wp = zones[i].zbz_write_pointer << 9;
		/*
		 * ZBC/ZAC do not define zone capacity, so use the zone size as
		 * the zone capacity.
		 */
		zbdz->capacity = zbdz->len;

		switch (zones[i].zbz_type) {
		case ZBC_ZT_CONVENTIONAL:
			zbdz->type = ZBD_ZONE_TYPE_CNV;
			break;
		case ZBC_ZT_SEQUENTIAL_REQ:
			zbdz->type = ZBD_ZONE_TYPE_SWR;
			break;
		case ZBC_ZT_SEQUENTIAL_PREF:
			zbdz->type = ZBD_ZONE_TYPE_SWP;
			break;
		default:
			td_verror(td, errno, "invalid zone type");
			log_err("%s: invalid type for zone at sector %llu.\n",
				f->file_name, (unsigned long long)zbdz->start);
			ret = -EIO;
			goto out;
		}

		switch (zones[i].zbz_condition) {
		case ZBC_ZC_NOT_WP:
			zbdz->cond = ZBD_ZONE_COND_NOT_WP;
			break;
		case ZBC_ZC_EMPTY:
			zbdz->cond = ZBD_ZONE_COND_EMPTY;
			break;
		case ZBC_ZC_IMP_OPEN:
			zbdz->cond = ZBD_ZONE_COND_IMP_OPEN;
			break;
		case ZBC_ZC_EXP_OPEN:
			zbdz->cond = ZBD_ZONE_COND_EXP_OPEN;
			break;
		case ZBC_ZC_CLOSED:
			zbdz->cond = ZBD_ZONE_COND_CLOSED;
			break;
		case ZBC_ZC_FULL:
			zbdz->cond = ZBD_ZONE_COND_FULL;
			break;
		case ZBC_ZC_RDONLY:
		case ZBC_ZC_OFFLINE:
		default:
			/* Treat all these conditions as offline (don't use!) */
			zbdz->cond = ZBD_ZONE_COND_OFFLINE;
			break;
		}
	}

	ret = nr_zones;
out:
	free(zones);
	return ret;
}

static int libzbc_reset_wp(struct thread_data *td, struct fio_file *f,
			   uint64_t offset, uint64_t length)
{
	struct libzbc_data *ld = td->io_ops_data;
	uint64_t sector = offset >> 9;
	uint64_t end_sector = (offset + length) >> 9;
	unsigned int nr_zones;
	struct zbc_errno err;
	int i, ret;

	assert(ld);
	assert(ld->zdev);

	nr_zones = (length + td->o.zone_size - 1) / td->o.zone_size;
	if (!sector && end_sector >= ld->nr_sectors) {
		/* Reset all zones */
		ret = zbc_reset_zone(ld->zdev, 0, ZBC_OP_ALL_ZONES);
		if (ret)
			goto err;

		return 0;
	}

	for (i = 0; i < nr_zones; i++, sector += td->o.zone_size >> 9) {
		ret = zbc_reset_zone(ld->zdev, sector, 0);
		if (ret)
			goto err;
	}

	return 0;

err:
	zbc_errno(ld->zdev, &err);
	td_verror(td, errno, "zbc_reset_zone failed");
	if (err.sk)
		log_err("%s: reset wp failed %s:%s\n",
			f->file_name,
			zbc_sk_str(err.sk), zbc_asc_ascq_str(err.asc_ascq));
	return -ret;
}

ssize_t libzbc_rw(struct thread_data *td, struct io_u *io_u)
{
	struct libzbc_data *ld = td->io_ops_data;
	struct fio_file *f = io_u->file;
	uint64_t sector = io_u->offset >> 9;
	size_t count = io_u->xfer_buflen >> 9;
	struct zbc_errno err;
	ssize_t ret;

	if (io_u->ddir == DDIR_WRITE)
		ret = zbc_pwrite(ld->zdev, io_u->xfer_buf, count, sector);
	else
		ret = zbc_pread(ld->zdev, io_u->xfer_buf, count, sector);
	if (ret == count)
		return ret;

	if (ret > 0) {
		log_err("Short %s, len=%zu, ret=%zd\n",
			io_u->ddir == DDIR_READ ? "read" : "write",
			count << 9, ret << 9);
		return -EIO;
	}

	/* I/O error */
	zbc_errno(ld->zdev, &err);
	td_verror(td, errno, "libzbc i/o failed");
	if (err.sk) {
		log_err("%s: op %u offset %llu+%llu failed (%s:%s), err %zd\n",
			f->file_name, io_u->ddir,
			io_u->offset, io_u->xfer_buflen,
			zbc_sk_str(err.sk),
			zbc_asc_ascq_str(err.asc_ascq), ret);
	} else {
		log_err("%s: op %u offset %llu+%llu failed, err %zd\n",
			f->file_name, io_u->ddir,
			io_u->offset, io_u->xfer_buflen, ret);
	}

	return -EIO;
}

static enum fio_q_status libzbc_queue(struct thread_data *td, struct io_u *io_u)
{
	struct libzbc_data *ld = td->io_ops_data;
	struct fio_file *f = io_u->file;
	ssize_t ret = 0;

	fio_ro_check(td, io_u);

	dprint(FD_ZBD, "%p:%s: libzbc queue %llu\n",
	       td, f->file_name, io_u->offset);

	if (io_u->ddir == DDIR_READ || io_u->ddir == DDIR_WRITE) {
		ret = libzbc_rw(td, io_u);
	} else if (ddir_sync(io_u->ddir)) {
		ret = zbc_flush(ld->zdev);
		if (ret)
			log_err("zbc_flush error %zd\n", ret);
	} else if (io_u->ddir != DDIR_TRIM) {
		log_err("Unsupported operation %u\n", io_u->ddir);
		ret = -EINVAL;
	}
	if (ret < 0)
		io_u->error = -ret;

	return FIO_Q_COMPLETED;
}

FIO_STATIC struct ioengine_ops ioengine = {
	.name			= "libzbc",
	.version		= FIO_IOOPS_VERSION,
	.open_file		= libzbc_open_file,
	.close_file		= libzbc_close_file,
	.cleanup		= libzbc_cleanup,
	.invalidate		= libzbc_invalidate,
	.get_file_size		= libzbc_get_file_size,
	.get_zoned_model	= libzbc_get_zoned_model,
	.report_zones		= libzbc_report_zones,
	.reset_wp		= libzbc_reset_wp,
	.queue			= libzbc_queue,
	.flags			= FIO_SYNCIO | FIO_NOEXTEND | FIO_RAWIO,
};

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

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