/*
 * Copyright (c) 2014 Christoph Hellwig.
 */

#include <linux/vmalloc.h>

#include "blocklayout.h"

#define NFSDBG_FACILITY		NFSDBG_PNFS_LD

static inline struct pnfs_block_extent *
ext_node(struct rb_node *node)
{
	return rb_entry(node, struct pnfs_block_extent, be_node);
}

static struct pnfs_block_extent *
ext_tree_first(struct rb_root *root)
{
	struct rb_node *node = rb_first(root);
	return node ? ext_node(node) : NULL;
}

static struct pnfs_block_extent *
ext_tree_prev(struct pnfs_block_extent *be)
{
	struct rb_node *node = rb_prev(&be->be_node);
	return node ? ext_node(node) : NULL;
}

static struct pnfs_block_extent *
ext_tree_next(struct pnfs_block_extent *be)
{
	struct rb_node *node = rb_next(&be->be_node);
	return node ? ext_node(node) : NULL;
}

static inline sector_t
ext_f_end(struct pnfs_block_extent *be)
{
	return be->be_f_offset + be->be_length;
}

static struct pnfs_block_extent *
__ext_tree_search(struct rb_root *root, sector_t start)
{
	struct rb_node *node = root->rb_node;
	struct pnfs_block_extent *be = NULL;

	while (node) {
		be = ext_node(node);
		if (start < be->be_f_offset)
			node = node->rb_left;
		else if (start >= ext_f_end(be))
			node = node->rb_right;
		else
			return be;
	}

	if (be) {
		if (start < be->be_f_offset)
			return be;

		if (start >= ext_f_end(be))
			return ext_tree_next(be);
	}

	return NULL;
}

static bool
ext_can_merge(struct pnfs_block_extent *be1, struct pnfs_block_extent *be2)
{
	if (be1->be_state != be2->be_state)
		return false;
	if (be1->be_device != be2->be_device)
		return false;

	if (be1->be_f_offset + be1->be_length != be2->be_f_offset)
		return false;

	if (be1->be_state != PNFS_BLOCK_NONE_DATA &&
	    (be1->be_v_offset + be1->be_length != be2->be_v_offset))
		return false;

	if (be1->be_state == PNFS_BLOCK_INVALID_DATA &&
	    be1->be_tag != be2->be_tag)
		return false;

	return true;
}

static struct pnfs_block_extent *
ext_try_to_merge_left(struct rb_root *root, struct pnfs_block_extent *be)
{
	struct pnfs_block_extent *left = ext_tree_prev(be);

	if (left && ext_can_merge(left, be)) {
		left->be_length += be->be_length;
		rb_erase(&be->be_node, root);
		nfs4_put_deviceid_node(be->be_device);
		kfree(be);
		return left;
	}

	return be;
}

static struct pnfs_block_extent *
ext_try_to_merge_right(struct rb_root *root, struct pnfs_block_extent *be)
{
	struct pnfs_block_extent *right = ext_tree_next(be);

	if (right && ext_can_merge(be, right)) {
		be->be_length += right->be_length;
		rb_erase(&right->be_node, root);
		nfs4_put_deviceid_node(right->be_device);
		kfree(right);
	}

	return be;
}

static void
__ext_tree_insert(struct rb_root *root,
		struct pnfs_block_extent *new, bool merge_ok)
{
	struct rb_node **p = &root->rb_node, *parent = NULL;
	struct pnfs_block_extent *be;

	while (*p) {
		parent = *p;
		be = ext_node(parent);

		if (new->be_f_offset < be->be_f_offset) {
			if (merge_ok && ext_can_merge(new, be)) {
				be->be_f_offset = new->be_f_offset;
				if (be->be_state != PNFS_BLOCK_NONE_DATA)
					be->be_v_offset = new->be_v_offset;
				be->be_length += new->be_length;
				be = ext_try_to_merge_left(root, be);
				goto free_new;
			}
			p = &(*p)->rb_left;
		} else if (new->be_f_offset >= ext_f_end(be)) {
			if (merge_ok && ext_can_merge(be, new)) {
				be->be_length += new->be_length;
				be = ext_try_to_merge_right(root, be);
				goto free_new;
			}
			p = &(*p)->rb_right;
		} else {
			BUG();
		}
	}

	rb_link_node(&new->be_node, parent, p);
	rb_insert_color(&new->be_node, root);
	return;
free_new:
	nfs4_put_deviceid_node(new->be_device);
	kfree(new);
}

static int
__ext_tree_remove(struct rb_root *root, sector_t start, sector_t end)
{
	struct pnfs_block_extent *be;
	sector_t len1 = 0, len2 = 0;
	sector_t orig_v_offset;
	sector_t orig_len;

	be = __ext_tree_search(root, start);
	if (!be)
		return 0;
	if (be->be_f_offset >= end)
		return 0;

	orig_v_offset = be->be_v_offset;
	orig_len = be->be_length;

	if (start > be->be_f_offset)
		len1 = start - be->be_f_offset;
	if (ext_f_end(be) > end)
		len2 = ext_f_end(be) - end;

	if (len2 > 0) {
		if (len1 > 0) {
			struct pnfs_block_extent *new;

			new = kzalloc(sizeof(*new), GFP_ATOMIC);
			if (!new)
				return -ENOMEM;

			be->be_length = len1;

			new->be_f_offset = end;
			if (be->be_state != PNFS_BLOCK_NONE_DATA) {
				new->be_v_offset =
					orig_v_offset + orig_len - len2;
			}
			new->be_length = len2;
			new->be_state = be->be_state;
			new->be_tag = be->be_tag;
			new->be_device = nfs4_get_deviceid(be->be_device);

			__ext_tree_insert(root, new, true);
		} else {
			be->be_f_offset = end;
			if (be->be_state != PNFS_BLOCK_NONE_DATA) {
				be->be_v_offset =
					orig_v_offset + orig_len - len2;
			}
			be->be_length = len2;
		}
	} else {
		if (len1 > 0) {
			be->be_length = len1;
			be = ext_tree_next(be);
		}

		while (be && ext_f_end(be) <= end) {
			struct pnfs_block_extent *next = ext_tree_next(be);

			rb_erase(&be->be_node, root);
			nfs4_put_deviceid_node(be->be_device);
			kfree(be);
			be = next;
		}

		if (be && be->be_f_offset < end) {
			len1 = ext_f_end(be) - end;
			be->be_f_offset = end;
			if (be->be_state != PNFS_BLOCK_NONE_DATA)
				be->be_v_offset += be->be_length - len1;
			be->be_length = len1;
		}
	}

	return 0;
}

int
ext_tree_insert(struct pnfs_block_layout *bl, struct pnfs_block_extent *new)
{
	struct pnfs_block_extent *be;
	struct rb_root *root;
	int err = 0;

	switch (new->be_state) {
	case PNFS_BLOCK_READWRITE_DATA:
	case PNFS_BLOCK_INVALID_DATA:
		root = &bl->bl_ext_rw;
		break;
	case PNFS_BLOCK_READ_DATA:
	case PNFS_BLOCK_NONE_DATA:
		root = &bl->bl_ext_ro;
		break;
	default:
		dprintk("invalid extent type\n");
		return -EINVAL;
	}

	spin_lock(&bl->bl_ext_lock);
retry:
	be = __ext_tree_search(root, new->be_f_offset);
	if (!be || be->be_f_offset >= ext_f_end(new)) {
		__ext_tree_insert(root, new, true);
	} else if (new->be_f_offset >= be->be_f_offset) {
		if (ext_f_end(new) <= ext_f_end(be)) {
			nfs4_put_deviceid_node(new->be_device);
			kfree(new);
		} else {
			sector_t new_len = ext_f_end(new) - ext_f_end(be);
			sector_t diff = new->be_length - new_len;

			new->be_f_offset += diff;
			new->be_v_offset += diff;
			new->be_length = new_len;
			goto retry;
		}
	} else if (ext_f_end(new) <= ext_f_end(be)) {
		new->be_length = be->be_f_offset - new->be_f_offset;
		__ext_tree_insert(root, new, true);
	} else {
		struct pnfs_block_extent *split;
		sector_t new_len = ext_f_end(new) - ext_f_end(be);
		sector_t diff = new->be_length - new_len;

		split = kmemdup(new, sizeof(*new), GFP_ATOMIC);
		if (!split) {
			err = -EINVAL;
			goto out;
		}

		split->be_length = be->be_f_offset - split->be_f_offset;
		split->be_device = nfs4_get_deviceid(new->be_device);
		__ext_tree_insert(root, split, true);

		new->be_f_offset += diff;
		new->be_v_offset += diff;
		new->be_length = new_len;
		goto retry;
	}
out:
	spin_unlock(&bl->bl_ext_lock);
	return err;
}

static bool
__ext_tree_lookup(struct rb_root *root, sector_t isect,
		struct pnfs_block_extent *ret)
{
	struct rb_node *node;
	struct pnfs_block_extent *be;

	node = root->rb_node;
	while (node) {
		be = ext_node(node);
		if (isect < be->be_f_offset)
			node = node->rb_left;
		else if (isect >= ext_f_end(be))
			node = node->rb_right;
		else {
			*ret = *be;
			return true;
		}
	}

	return false;
}

bool
ext_tree_lookup(struct pnfs_block_layout *bl, sector_t isect,
	    struct pnfs_block_extent *ret, bool rw)
{
	bool found = false;

	spin_lock(&bl->bl_ext_lock);
	if (!rw)
		found = __ext_tree_lookup(&bl->bl_ext_ro, isect, ret);
	if (!found)
		found = __ext_tree_lookup(&bl->bl_ext_rw, isect, ret);
	spin_unlock(&bl->bl_ext_lock);

	return found;
}

int ext_tree_remove(struct pnfs_block_layout *bl, bool rw,
		sector_t start, sector_t end)
{
	int err, err2;

	spin_lock(&bl->bl_ext_lock);
	err = __ext_tree_remove(&bl->bl_ext_ro, start, end);
	if (rw) {
		err2 = __ext_tree_remove(&bl->bl_ext_rw, start, end);
		if (!err)
			err = err2;
	}
	spin_unlock(&bl->bl_ext_lock);

	return err;
}

static int
ext_tree_split(struct rb_root *root, struct pnfs_block_extent *be,
		sector_t split)
{
	struct pnfs_block_extent *new;
	sector_t orig_len = be->be_length;

	new = kzalloc(sizeof(*new), GFP_ATOMIC);
	if (!new)
		return -ENOMEM;

	be->be_length = split - be->be_f_offset;

	new->be_f_offset = split;
	if (be->be_state != PNFS_BLOCK_NONE_DATA)
		new->be_v_offset = be->be_v_offset + be->be_length;
	new->be_length = orig_len - be->be_length;
	new->be_state = be->be_state;
	new->be_tag = be->be_tag;
	new->be_device = nfs4_get_deviceid(be->be_device);

	__ext_tree_insert(root, new, false);
	return 0;
}

int
ext_tree_mark_written(struct pnfs_block_layout *bl, sector_t start,
		sector_t len)
{
	struct rb_root *root = &bl->bl_ext_rw;
	sector_t end = start + len;
	struct pnfs_block_extent *be;
	int err = 0;

	spin_lock(&bl->bl_ext_lock);
	/*
	 * First remove all COW extents or holes from written to range.
	 */
	err = __ext_tree_remove(&bl->bl_ext_ro, start, end);
	if (err)
		goto out;

	/*
	 * Then mark all invalid extents in the range as written to.
	 */
	for (be = __ext_tree_search(root, start); be; be = ext_tree_next(be)) {
		if (be->be_f_offset >= end)
			break;

		if (be->be_state != PNFS_BLOCK_INVALID_DATA || be->be_tag)
			continue;

		if (be->be_f_offset < start) {
			struct pnfs_block_extent *left = ext_tree_prev(be);

			if (left && ext_can_merge(left, be)) {
				sector_t diff = start - be->be_f_offset;

				left->be_length += diff;

				be->be_f_offset += diff;
				be->be_v_offset += diff;
				be->be_length -= diff;
			} else {
				err = ext_tree_split(root, be, start);
				if (err)
					goto out;
			}
		}

		if (ext_f_end(be) > end) {
			struct pnfs_block_extent *right = ext_tree_next(be);

			if (right && ext_can_merge(be, right)) {
				sector_t diff = end - be->be_f_offset;

				be->be_length -= diff;

				right->be_f_offset -= diff;
				right->be_v_offset -= diff;
				right->be_length += diff;
			} else {
				err = ext_tree_split(root, be, end);
				if (err)
					goto out;
			}
		}

		if (be->be_f_offset >= start && ext_f_end(be) <= end) {
			be->be_tag = EXTENT_WRITTEN;
			be = ext_try_to_merge_left(root, be);
			be = ext_try_to_merge_right(root, be);
		}
	}
out:
	spin_unlock(&bl->bl_ext_lock);
	return err;
}

static size_t ext_tree_layoutupdate_size(size_t count)
{
	return sizeof(__be32) /* number of entries */ +
		PNFS_BLOCK_EXTENT_SIZE * count;
}

static void ext_tree_free_commitdata(struct nfs4_layoutcommit_args *arg,
		size_t buffer_size)
{
	if (arg->layoutupdate_pages != &arg->layoutupdate_page) {
		int nr_pages = DIV_ROUND_UP(buffer_size, PAGE_SIZE), i;

		for (i = 0; i < nr_pages; i++)
			put_page(arg->layoutupdate_pages[i]);
		kfree(arg->layoutupdate_pages);
	} else {
		put_page(arg->layoutupdate_page);
	}
}

static int ext_tree_encode_commit(struct pnfs_block_layout *bl, __be32 *p,
		size_t buffer_size, size_t *count)
{
	struct pnfs_block_extent *be;
	int ret = 0;

	spin_lock(&bl->bl_ext_lock);
	for (be = ext_tree_first(&bl->bl_ext_rw); be; be = ext_tree_next(be)) {
		if (be->be_state != PNFS_BLOCK_INVALID_DATA ||
		    be->be_tag != EXTENT_WRITTEN)
			continue;

		(*count)++;
		if (ext_tree_layoutupdate_size(*count) > buffer_size) {
			/* keep counting.. */
			ret = -ENOSPC;
			continue;
		}

		p = xdr_encode_opaque_fixed(p, be->be_device->deviceid.data,
				NFS4_DEVICEID4_SIZE);
		p = xdr_encode_hyper(p, be->be_f_offset << SECTOR_SHIFT);
		p = xdr_encode_hyper(p, be->be_length << SECTOR_SHIFT);
		p = xdr_encode_hyper(p, 0LL);
		*p++ = cpu_to_be32(PNFS_BLOCK_READWRITE_DATA);

		be->be_tag = EXTENT_COMMITTING;
	}
	spin_unlock(&bl->bl_ext_lock);

	return ret;
}

int
ext_tree_prepare_commit(struct nfs4_layoutcommit_args *arg)
{
	struct pnfs_block_layout *bl = BLK_LO2EXT(NFS_I(arg->inode)->layout);
	size_t count = 0, buffer_size = PAGE_SIZE;
	__be32 *start_p;
	int ret;

	dprintk("%s enter\n", __func__);

	arg->layoutupdate_page = alloc_page(GFP_NOFS);
	if (!arg->layoutupdate_page)
		return -ENOMEM;
	start_p = page_address(arg->layoutupdate_page);
	arg->layoutupdate_pages = &arg->layoutupdate_page;

retry:
	ret = ext_tree_encode_commit(bl, start_p + 1, buffer_size, &count);
	if (unlikely(ret)) {
		ext_tree_free_commitdata(arg, buffer_size);

		buffer_size = ext_tree_layoutupdate_size(count);
		count = 0;

		arg->layoutupdate_pages =
			kcalloc(DIV_ROUND_UP(buffer_size, PAGE_SIZE),
				sizeof(struct page *), GFP_NOFS);
		if (!arg->layoutupdate_pages)
			return -ENOMEM;

		start_p = __vmalloc(buffer_size, GFP_NOFS, PAGE_KERNEL);
		if (!start_p) {
			kfree(arg->layoutupdate_pages);
			return -ENOMEM;
		}

		goto retry;
	}

	*start_p = cpu_to_be32(count);
	arg->layoutupdate_len = ext_tree_layoutupdate_size(count);

	if (unlikely(arg->layoutupdate_pages != &arg->layoutupdate_page)) {
		void *p = start_p, *end = p + arg->layoutupdate_len;
		int i = 0;

		for ( ; p < end; p += PAGE_SIZE)
			arg->layoutupdate_pages[i++] = vmalloc_to_page(p);
	}

	dprintk("%s found %zu ranges\n", __func__, count);
	return 0;
}

void
ext_tree_mark_committed(struct nfs4_layoutcommit_args *arg, int status)
{
	struct pnfs_block_layout *bl = BLK_LO2EXT(NFS_I(arg->inode)->layout);
	struct rb_root *root = &bl->bl_ext_rw;
	struct pnfs_block_extent *be;

	dprintk("%s status %d\n", __func__, status);

	ext_tree_free_commitdata(arg, arg->layoutupdate_len);

	spin_lock(&bl->bl_ext_lock);
	for (be = ext_tree_first(root); be; be = ext_tree_next(be)) {
		if (be->be_state != PNFS_BLOCK_INVALID_DATA ||
		    be->be_tag != EXTENT_COMMITTING)
			continue;

		if (status) {
			/*
			 * Mark as written and try again.
			 *
			 * XXX: some real error handling here wouldn't hurt..
			 */
			be->be_tag = EXTENT_WRITTEN;
		} else {
			be->be_state = PNFS_BLOCK_READWRITE_DATA;
			be->be_tag = 0;
		}

		be = ext_try_to_merge_left(root, be);
		be = ext_try_to_merge_right(root, be);
	}
	spin_unlock(&bl->bl_ext_lock);
}