/*
 * Copyright (c) International Business Machines  Corp., 2001
 * Copyright (c) 2013 Oracle and/or its affiliates. All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it would be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write the Free Software Foundation,
 * Inc.,  51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * This module will test block io layer.
 *
 * module: tbio
 *
 *  FILE        : tbio.c
 *  USAGE       : kernel_space:./load_tbio.sh
 *                user_space  :./test_bio
 *
 *  DESCRIPTION : The module will test block i/o layer for kernel 2.5
 *  REQUIREMENTS:
 *                1) glibc 2.1.91 or above.
 *
 *  HISTORY     :
 *      11/19/2003 Kai Zhao (ltcd3@cn.ibm.com)
 *
 *  CODE COVERAGE: 74.9% - fs/bio.c (Total Coverage)
 *
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/buffer_head.h>

#include "tbio.h"

MODULE_AUTHOR("Kai Zhao <ltcd3@cn.ibm.com>");
MODULE_AUTHOR("Alexey Kodanev <alexey.kodanev@oracle.com>");
MODULE_DESCRIPTION(TMOD_DRIVER_NAME);
MODULE_LICENSE("GPL");

#define prk_err(fmt, ...) \
	pr_err(TBIO_DEVICE_NAME ": " fmt "\n", ##__VA_ARGS__)
#define prk_info(fmt, ...) \
	pr_info(TBIO_DEVICE_NAME ": " fmt "\n", ##__VA_ARGS__)

static int nsectors = 4096;
module_param(nsectors, int, 0444);
MODULE_PARM_DESC(nsectors, "The number of sectors");

static struct bio *tbiop, *tbiop_dup;

static struct tbio_device {
	unsigned long size;
	spinlock_t lock;
	u8 *data;
	struct gendisk *gd;
	struct block_device *bdev;
	struct request_queue *q;
} tbio_dev;

static int send_request(struct request_queue *q, struct bio *bio,
	struct block_device *bdev, struct tbio_interface *inter,
	int writing)
{
	struct request *rq;
	rq = blk_make_request(q, bio, GFP_KERNEL);
	if (!rq) {
		prk_err("failed to make request");
		return -EFAULT;
	}

	rq->cmd_len = inter->cmd_len;

	if (copy_from_user(rq->cmd, inter->cmd, inter->cmd_len))
		goto out_request;

	if (sizeof(rq->cmd) != inter->cmd_len) {
		memset(rq->cmd + inter->cmd_len, 0,
			sizeof(rq->cmd) - inter->cmd_len);
	}

	rq->__sector = bio->bi_sector;

	if (blk_execute_rq(q, bdev->bd_disk, rq, 0))
		goto out_request;

	blk_put_request(rq);

	return 0;

out_request:

	blk_put_request(rq);
	return -EFAULT;
}

static int tbio_io(struct block_device *bdev, struct tbio_interface *uptr)
{
	int ret;
	tbio_interface_t inter;
	struct bio *bio = NULL;
	int reading = 0, writing = 0;
	void *buf = NULL;
	struct request_queue *q = bdev_get_queue(bdev);

	if (copy_from_user(&inter, uptr, sizeof(tbio_interface_t))) {
		prk_err("copy_from_user");
		return -EFAULT;
	}

	if (inter.data_len > (q->limits.max_sectors << 9)) {
		prk_err("inter.in_len > q->max_sectors << 9");
		return -EIO;
	}

	if (inter.data_len) {

		switch (inter.direction) {
		default:
			return -EINVAL;
		case TBIO_TO_DEV:
			writing = 1;
			break;
		case TBIO_FROM_DEV:
			reading = 1;
			break;
		}

		bio = bio_map_user(q, bdev, (unsigned long)inter.data,
			inter.data_len, reading, GFP_KERNEL);

		if (!bio) {
			prk_err("bio_map_user failed");
			buf = kmalloc(inter.data_len, q->bounce_gfp | GFP_USER);
			if (!buf) {
				prk_err("buffer no memory");
				return -1;
			}
			ret = copy_from_user(buf, inter.data, inter.data_len);
			if (ret)
				prk_err("copy_from_user() failed");

			prk_info("buffer %s\n, copy_from_user returns '%d'",
				(char *)buf, ret);
		}

	}

	send_request(q, bio, bdev, &inter, writing);

	if (bio)
		bio_unmap_user(bio);
	return 0;
}

static int test_bio_put(struct bio *biop)
{
	if (biop)
		bio_put(biop);

	return 0;
}

static int test_bio_clone(void)
{
	tbiop_dup = bio_clone(tbiop, GFP_NOIO);
	if (tbiop_dup == NULL) {
		prk_err("bio_clone failed");
		return -1;
	}

	test_bio_put(tbiop_dup);

	return 0;
}

static int test_bio_add_page(void)
{
	int ret = 0, i = 0, offset = 0;
	unsigned long addr = 0;
	struct page *ppage = NULL;

	for (i = 0; i < 128; i++) {

		addr = get_zeroed_page(GFP_KERNEL);

		if (addr == 0) {
			prk_err("get free page failed %ld", addr);
			ret = -1;
			break;
		}

		ppage = virt_to_page(addr);
		if (ppage == NULL) {
			prk_err("covert virture page to page struct failed");
			ret = -1;
			break;
		}

		ret = bio_add_page(tbiop, ppage, PAGE_SIZE, offset);
		if (ret < 0) {
			prk_err("bio_add_page failed");
			break;
		}
		offset += ret;
	}

	return ret;
}

static int test_do_bio_alloc(int num)
{
	tbiop = bio_alloc(GFP_KERNEL, num);
	if (tbiop == NULL) {
		prk_err("bio_alloc failed");
		return -1;
	}
	bio_put(tbiop);

	return 0;
}

static int test_bio_alloc(void)
{
	if (test_do_bio_alloc(2) < 0) {
		prk_err("can not alloc bio for %d", 2);
		return -1;
	}

	if (test_do_bio_alloc(8) < 0) {
		prk_err("can not alloc bio for %d", 8);
		return -1;
	}

	if (test_do_bio_alloc(32) < 0) {
		prk_err("can not alloc bio for %d", 32);
		return -1;
	}

	if (test_do_bio_alloc(96) < 0) {
		prk_err("can not alloc bio for %d", 96);
		return -1;
	}

	if (test_do_bio_alloc(BIO_MAX_PAGES) < 0) {
		prk_err("can not alloc bio for %d", BIO_MAX_PAGES);
		return -1;
	}

	tbiop = bio_alloc(GFP_KERNEL, BIO_MAX_PAGES);
	if (tbiop == NULL) {
		prk_err("bio_alloc failed");
		return -1;
	}

	tbiop->bi_bdev = tbio_dev.bdev;
	tbiop->bi_sector = 0;

	return 0;
}

static int test_bio_split(struct block_device *bdev,
			  struct tbio_interface *uptr)
{
	int ret;
	tbio_interface_t inter;
	struct bio *bio = NULL;
	struct bio_pair *bio_pairp = NULL;
	int reading = 0, writing = 0;
	void *buf = NULL;
	struct request_queue *q = bdev_get_queue(bdev);
	if (!q) {
		prk_err("bdev_get_queue() failed");
		return -EFAULT;
	}

	prk_info("test_bio_split");

	if (copy_from_user(&inter, uptr, sizeof(tbio_interface_t))) {
		prk_err("copy_from_user");
		return -EFAULT;
	}

	if (inter.data_len > (q->limits.max_sectors << 9)) {
		prk_err("inter.in_len > q->limits.max_sectors << 9");
		return -EIO;
	}

	prk_info("inter.data_len is %d", inter.data_len);
	if (inter.data_len) {

		switch (inter.direction) {
		default:
			return -EINVAL;
		case TBIO_TO_DEV:
			writing = 1;
			break;
		case TBIO_FROM_DEV:
			reading = 1;
			break;
		}

		bio = bio_map_user(q, bdev, (unsigned long)inter.data,
			inter.data_len, reading, GFP_KERNEL);

		if (!bio) {
			prk_err("bio_map_user failed");
			buf = kmalloc(inter.data_len, q->bounce_gfp | GFP_USER);
			if (!buf) {
				prk_err("buffer no memory");
				return -1;
			}
			ret = copy_from_user(buf, inter.data, inter.data_len);
			if (ret)
				prk_err("copy_from_user() failed");

			prk_info("buffer %s", (char *)buf);
		} else {
			bio_pairp = bio_split(bio, 2);

			if (bio_pairp == NULL) {
				prk_err("bio_split failed");
				bio_unmap_user(bio);
				return -1;
			}
		}

	}

	send_request(q, &(bio_pairp->bio1), bdev, &inter, writing);
	q = bdev_get_queue(bdev);
	send_request(q, &(bio_pairp->bio2), bdev, &inter, writing);

	if (bio_pairp)
		bio_pair_release(bio_pairp);

	if (bio)
		bio_unmap_user(bio);

	return 0;
}

static int test_bio_get_nr_vecs(void)
{
	int number = 0;

	number = bio_get_nr_vecs(tbio_dev.bdev);

	if (number < 0) {
		prk_err("bio_get_nr_vec failed");
		return -1;
	}

	prk_info("bio_get_nr_vecs: %d", number);
	return 0;
}

static int tbio_ioctl(struct block_device *blk, fmode_t mode,
	unsigned cmd, unsigned long arg)
{
	int err = 0;

	tbio_dev.bdev = blkdev_get_by_path(
		DEVICE_NAME, FMODE_READ | FMODE_WRITE, NULL);

	switch (cmd) {
	case LTP_TBIO_DO_IO:
		prk_info("TEST-CASE: LTP_TBIO_DO_IO:");
		err = tbio_io(tbio_dev.bdev, (struct tbio_interface *)arg);
	break;
	case LTP_TBIO_CLONE:
		prk_info("TEST-CASE: LTP_TBIO_CLONE:");
		err = test_bio_clone();
		break;
	case LTP_TBIO_ADD_PAGE:
		prk_info("TEST-CASE: LTP_TBIO_ADD_PAGE:");
		err = test_bio_add_page();
		break;
	case LTP_TBIO_ALLOC:
		prk_info("TEST-CASE: LTP_TBIO_ALLOC:");
		err = test_bio_alloc();
		break;
	case LTP_TBIO_GET_NR_VECS:
		prk_info("TEST-CASE: LTP_TBIO_GET_NR_VECS:");
		err = test_bio_get_nr_vecs();
		break;
	case LTP_TBIO_PUT:
		prk_info("TEST-CASE: LTP_TBIO_PUT:");
		err = test_bio_put(tbiop);
		break;
	case LTP_TBIO_SPLIT:
		prk_info("TEST-CASE: LTP_TBIO_SPLIT:");
		err = test_bio_split(tbio_dev.bdev,
			(struct tbio_interface *)arg);
	break;
	}

	prk_info("TEST-CASE DONE");
	blkdev_put(tbio_dev.bdev, FMODE_READ | FMODE_WRITE);
	return err;
}

static int tbio_transfer(struct request *req, struct tbio_device *dev)
{
	unsigned int i = 0, offset = 0;
	char *buf;
	unsigned long flags;
	size_t size;

	struct bio_vec *bv;
	struct req_iterator iter;

	size = blk_rq_cur_bytes(req);
	prk_info("bio req of size %zu:", size);
	offset = blk_rq_pos(req) * 512;

	rq_for_each_segment(bv, req, iter) {
		size = bv->bv_len;
		prk_info("%s bio(%u), segs(%u) sect(%u) pos(%lu) off(%u)",
			(bio_data_dir(iter.bio) == READ) ? "READ" : "WRITE",
			i, bio_segments(iter.bio), bio_sectors(iter.bio),
			iter.bio->bi_sector, offset);

		if (get_capacity(req->rq_disk) * 512 < offset) {
			prk_info("Error, small capacity %zu, offset %u",
				get_capacity(req->rq_disk) * 512,
				offset);
			continue;
		}

		buf = bvec_kmap_irq(bv, &flags);
		if (bio_data_dir(iter.bio) == WRITE)
			memcpy(dev->data + offset, buf, size);
		else
			memcpy(buf, dev->data + offset, size);
		offset += size;
		flush_kernel_dcache_page(bv->bv_page);
		bvec_kunmap_irq(buf, &flags);
		++i;
	}

	return 0;
}

static void tbio_request(struct request_queue *q)
{
	int ret = 0;
	struct request *req;

	while ((req = blk_fetch_request(q)) != NULL) {

		ret = tbio_transfer(req, &tbio_dev);

		spin_unlock_irq(q->queue_lock);
		blk_end_request_all(req, ret);
		spin_lock_irq(q->queue_lock);
	}
}

static int tbio_open(struct block_device *blk, fmode_t mode)
{
	return 0;
}

#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
static int tbio_release(struct gendisk *gd, fmode_t mode)
#else
static void tbio_release(struct gendisk *gd, fmode_t mode)
#endif
{

#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
	return 0;
#endif
}

int tbio_media_changed(struct gendisk *gd)
{
	return 0;
}

int tbio_revalidate(struct gendisk *gd)
{
	return 0;
}

static const struct block_device_operations tbio_ops = {
	.owner = THIS_MODULE,
	.open = tbio_open,
	.ioctl = tbio_ioctl,
	.release = tbio_release,
	.media_changed = tbio_media_changed,
	.revalidate_disk = tbio_revalidate
};

static int __init tbio_init(void)
{
	tbio_dev.size = nsectors * 512;

	tbio_dev.data = vmalloc(tbio_dev.size);
	if (tbio_dev.data == NULL)
		return -ENOMEM;
	strcpy(tbio_dev.data, "tbio data");
	tbio_dev.bdev = NULL;

	TBIO_MAJOR = register_blkdev(0, DEVICE_NAME);
	if (TBIO_MAJOR <= 0) {
		prk_err("unable to get major number");
		goto out;
	}
	prk_info("register_blkdev major %d", TBIO_MAJOR);

	spin_lock_init(&tbio_dev.lock);
	tbio_dev.q = blk_init_queue(tbio_request, &tbio_dev.lock);
	if (!tbio_dev.q) {
		prk_err("failed to init queue");
		goto out_unregister;
	}

	tbio_dev.gd = alloc_disk(1);
	if (!tbio_dev.gd)
		goto out_unregister;
	tbio_dev.gd->major	= TBIO_MAJOR;
	tbio_dev.gd->first_minor	= 0;
	tbio_dev.gd->fops		= &tbio_ops;
	tbio_dev.gd->private_data	= &tbio_dev;
	tbio_dev.gd->queue	= tbio_dev.q;
	strcpy(tbio_dev.gd->disk_name, "tbio");
	set_capacity(tbio_dev.gd, nsectors);
	tbio_dev.gd->queue->queuedata = tbio_dev.gd;

	add_disk(tbio_dev.gd);

	return 0;

out_unregister:
	unregister_blkdev(TBIO_MAJOR, DEVICE_NAME);
out:
	vfree(tbio_dev.data);
	return -ENOMEM;
}
module_init(tbio_init);

static void tbio_exit(void)
{
	blk_cleanup_queue(tbio_dev.q);
	del_gendisk(tbio_dev.gd);
	put_disk(tbio_dev.gd);
	unregister_blkdev(TBIO_MAJOR, DEVICE_NAME);
	vfree(tbio_dev.data);
	prk_info("exit");
}
module_exit(tbio_exit);
