#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>

#include "shfs.h"
#include "shfs_fcache.h"
#include "shfs_proc.h"

int 
shfs_fcache_add(struct file *f)
{
	struct inode *inode;
	struct shfs_file *p;

	DEBUG("Adding file to rw-cache.\n");

	if (f->f_dentry && (inode = f->f_dentry->d_inode)) {
		DEBUG("ino: %lu\n", inode->i_ino);
		if (S_ISDIR(inode->i_mode)) {
			VERBOSE("not adding dir into file cache..\n");
			return -1;
		}
		p = (struct shfs_file *)inode->u.generic_ip;
		if (p) {
			DEBUG("opened: %d times before\n", p->used);
			p->used++;
			if (!p->used)
				VERBOSE("File usage counter overflow!\n");
		} else {
			p = (struct shfs_file *)KMEM_ALLOC("fcache", file_cache, GFP_KERNEL);
			if (!p) {
				VERBOSE("slab cache error!\n");
				return -1;
			}
			p->data = vmalloc(SHFS_FCACHE_SIZE);
			if (!p->data) {
				VERBOSE("slab cache error!\n");
				KMEM_FREE("fcache", file_cache, p);
				return -1;
			}
			p->type = SHFS_FCACHE_READ;
			p->new = 1;
			p->used = 1;
			p->offset = 0;
			p->count = 0;
			
			inode->u.generic_ip = p;
			DEBUG("done.\n");
		}
		return 0;
	} 
	VERBOSE("Invalid file!\n");
	return -1;
}

int 
shfs_fcache_remove(struct file *f, int *last_ref)
{
	struct inode *inode;
	struct shfs_file *p;
	int error;

	DEBUG("Removing file from rw-cache.\n");
	
	if (f->f_dentry && (inode = f->f_dentry->d_inode)) {
		DEBUG("ino: %lu\n", inode->i_ino);
		*last_ref = 0;
		if (S_ISDIR(inode->i_mode)) {
			VERBOSE("dir in file cache?\n");
			return -1;
		}
		p = (struct shfs_file *)inode->u.generic_ip;
		if (!p) {
			VERBOSE("File not found!\n");
			return -1;
		}
		if (--p->used)
			return 0;

		DEBUG("opened count 0, releasing..\n");
		*last_ref = 1;
		error = 0;
		if (p->count && p->type == SHFS_FCACHE_WRITE)
			error = shfs_proc_write(f->f_dentry, p->offset, p->count, p->data);
		vfree(p->data);

		KMEM_FREE("fcache", file_cache, p);
		inode->u.generic_ip = NULL;
		return error < 0 ? error : 0;
	}
	VERBOSE("Invalid file!\n");
	return -1;
}

int
shfs_fcache_sync(struct file *f)
{
	struct inode *inode;
	struct shfs_file *p;
	int error;

	DEBUG("Sync file in rw-cache.\n");
	
	if (f->f_dentry && (inode = f->f_dentry->d_inode)) {
		DEBUG("ino: %lu\n", inode->i_ino);
		if (S_ISDIR(inode->i_mode)) {
			VERBOSE("dir in file cache?\n");
			return -1;
		}
		p = (struct shfs_file *)inode->u.generic_ip;
		if (!p) {
			VERBOSE("File not found!\n");
			return -1;
		}
		error = 0;
		if (p->count && p->type == SHFS_FCACHE_WRITE) {
			DEBUG("Syncing..\n");
			error = shfs_proc_write(f->f_dentry, p->offset, p->count, p->data);
			p->type = SHFS_FCACHE_READ;
			p->count = 0;
		}
		return error;
	}			
	VERBOSE("Invalid file!\n");
	return -1;
}

int
shfs_fcache_clear(struct inode *inode)
{
	struct shfs_file *p;

	if (!inode || S_ISDIR(inode->i_mode)) {
		VERBOSE("invalid inode\n");
		return -1;
	}
	DEBUG("ino: %lu\n", inode->i_ino);
	p = (struct shfs_file *)inode->u.generic_ip;
	if (!p) {
		DEBUG("File not found\n");
		return -1;
	}
	if (p->count) {
		if (p->type == SHFS_FCACHE_WRITE) {
			VERBOSE("Aieeee, out of sync\n");
		} else {
			p->count = 0;
		}
	}
	return 0;
}

int 
shfs_fcache_read(struct file *f, unsigned offset, unsigned count, char *buffer)
{
	unsigned readahead, c, o, x, y, z, read = 0;
	struct inode *inode;
	struct shfs_file *p;
	int res;

	DEBUG("Reading file: offset: %u, count: %u\n", offset, count);
	if (!f->f_dentry || !(inode = f->f_dentry->d_inode)) {
		VERBOSE("invalid inode\n");
		return -1;
	}
	DEBUG("ino: %lu\n", inode->i_ino);
	if (S_ISDIR(inode->i_mode)) {
		VERBOSE("dir in file cache?\n");
		return -1;
	}

	p = (struct shfs_file *)inode->u.generic_ip;
	if (!p) {
		VERBOSE("File not found!\n");
		return -1;
	}

	/* hit? */
	if (offset >= p->offset && offset < (p->offset + p->count)) {
		o = offset - p->offset;
		c = count > p->count - o ? p->count - o : count;
		memcpy(buffer, p->data + o, c);
		buffer += c;
		offset += c;
		count -= c;
		read += c;
	}

	if (count && p->type == SHFS_FCACHE_WRITE) {
		shfs_proc_write(f->f_dentry, p->offset, p->count, p->data);
		p->type = SHFS_FCACHE_READ;
		p->offset = offset;
		p->count = 0;
	}

	while (count) {
		o = offset & PAGE_MASK;
		x = offset - o;
		if (p->offset + p->count == offset)
			readahead = p->count;
		else {
			p->offset = o;
			p->count = 0;
			readahead = 0;
		}
		if (readahead < (x + count))
			readahead = x + count;
		readahead = (readahead+PAGE_SIZE-1) & PAGE_MASK;
		if (readahead > SHFS_FCACHE_SIZE)
			readahead = SHFS_FCACHE_SIZE;
		if (o % readahead) {	 /* HD */
			y = o, z = readahead;
			while (y && z)
				if (y > z) y %= z; else z %= y;
			readahead = y > z ? y : z;
		}
		if (p->count + readahead > SHFS_FCACHE_SIZE) {
			p->offset = o;
			p->count = 0;
		}
		DEBUG("rw cache read: offset: %u, size %u\n", o, readahead);
		res = shfs_proc_read(f->f_dentry, o, readahead, p->data+p->count, 0);
		if (res < 0) {
			p->count = 0;
			return res;
		}

		c = res - x;
		if (c > count)
			c = count;
		memcpy(buffer, p->data + p->count + x, c);
		buffer += c;
		offset += c;
		count -= c;
		read += c;
		p->count += res;
	}
	return read;
}

int 
shfs_fcache_write(struct file *f, unsigned offset, unsigned count, char *buffer)
{
	unsigned o, c, wrote = 0;
	struct inode *inode;
	struct shfs_file *p;
	int res = 0;

	DEBUG("Writing to file: offset: %u, count: %u\n", offset, count);
	if (!f->f_dentry || !(inode = f->f_dentry->d_inode)) {
		VERBOSE("invalid inode\n");
		return -1;
	}
	DEBUG("ino: %lu\n", inode->i_ino);
	if (S_ISDIR(inode->i_mode)) {
		VERBOSE("dir in file cache?\n");
		return -1;
	}

	p = (struct shfs_file *)inode->u.generic_ip;
	if (!p) {
		VERBOSE("File not found!\n");
		return -1;
	}

	if (p->type == SHFS_FCACHE_READ) {
		p->offset = offset;
		p->count = 0;
		p->type = SHFS_FCACHE_WRITE;
	}

	DEBUG("1 cache: offset: %u, count: %u\n", p->offset, p->count);
	if (offset >= p->offset && offset < (p->offset + p->count)) {
		o = offset - p->offset;
		c = count > SHFS_FCACHE_SIZE - o ? SHFS_FCACHE_SIZE - o : count;
		memcpy(p->data + o, buffer, c);
		if (o + c > p->count)
			p->count = o + c;
		buffer += c;
		offset += c;
		count -= c;
		wrote += c;
	}
	DEBUG("2 cache: offset: %u, count: %u\n", p->offset, p->count);
	if (count && offset == p->offset+p->count) {
		o = offset - p->offset;
		c = count > SHFS_FCACHE_SIZE - p->count ? SHFS_FCACHE_SIZE - p->count : count;
		memcpy(p->data + o, buffer, c);
		p->count += c;
		buffer += c;
		offset += c;
		count -= c;
		wrote += c;
	}
	DEBUG("3 cache: offset: %u, count: %u\n", p->offset, p->count);
	while (count) {
		if ((res = shfs_proc_write(f->f_dentry, p->offset, p->count, p->data)) < 0)
			break;
		c = count > SHFS_FCACHE_SIZE ? SHFS_FCACHE_SIZE : count;
		memcpy(p->data, buffer, c);
		p->offset = offset;
		p->count = c;
		buffer += c;
		offset += c;
		count -= c;
		wrote += c;
	}
	DEBUG("4 cache: offset: %u, count: %u\n", p->offset, p->count);

	if (p->new && wrote) {
		res = shfs_proc_write(f->f_dentry, p->offset, 1, p->data);
		p->new = 0;
	}

	return res >= 0 ? wrote : res;
}

