/*
 * dcssblk.c -- the S/390 block driver for dcss memory
 *
 * Author: Carsten Otte
 */

#include <linux/module.h>
#include <linux/version.h>
#ifdef CONFIG_PROC_FS
#include <linux/proc_fs.h>
#endif	
#include <linux/devfs_fs_kernel.h>
#include <linux/ctype.h>  /* isdigit, isxdigit */
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/blk.h>
#include <linux/blkpg.h>
#include <linux/hdreg.h>  /* HDIO_GETGEO */
#include <linux/kdev_t.h>
#include <asm/uaccess.h>
#include <asm/dcss.h>

#define PRINT_DEBUG(x...)	printk(KERN_DEBUG DCSSBLK_NAME " debug:" x)
#define PRINT_INFO(x...)	printk(KERN_INFO DCSSBLK_NAME " info:" x)
#define PRINT_WARN(x...)	printk(KERN_WARNING DCSSBLK_NAME " warning:" x)
#define PRINT_ERR(x...)		printk(KERN_ERR DCSSBLK_NAME " error:" x)
#define DCSSBLK_NAME "dcssblk"

#ifdef CONFIG_PROC_FS
static struct proc_dir_entry *dcssblk_proc_root_entry;
static struct proc_dir_entry *dcssblk_add_entry;
static struct proc_dir_entry *dcssblk_remove_entry;
static struct proc_dir_entry *dcssblk_list_entry;
#endif
static devfs_handle_t dcssblk_devfs_dir;
unsigned int dcssblk_blksizes[1<<MINORBITS];

static int dcssblk_open (struct inode *inode, struct file *filp);
static int dcssblk_release (struct inode *inode, struct file *filp);

static struct block_device_operations dcssblk_devops =
	{
		owner:   THIS_MODULE,
		open:    dcssblk_open,
		release: dcssblk_release,
	};

typedef struct _dcss_device_t {
	struct list_head lh;
	devfs_handle_t     devfs_entry;
	unsigned int  use_count;
	unsigned long start;
	unsigned long end;
	int               segment_type;
	unsigned char save_pending;
	unsigned char is_shared;
#ifdef CONFIG_PROC_FS
	struct proc_dir_entry *dcssblk_subdir_entry;
	struct proc_dir_entry *dcssblk_save_entry;
	struct proc_dir_entry *dcssblk_shared_entry;
#endif
	char name [9];
	kdev_t             kdev;
} dcss_device_t;

typedef struct _dcssblk_proc_private_t {
	char name[8];
	int bytes_written;
} dcssblk_proc_private_t;

static int dcssblk_major;
static struct list_head dcssblk_devices = LIST_HEAD_INIT(dcssblk_devices);
static rwlock_t dcssblk_devices_lock = RW_LOCK_UNLOCKED;


MODULE_LICENSE("GPL");

/*
 * get a minor number. needs to be called with
 * write_lock(dcssblk_devices_lock) and the 
 * device needs to be enqueued before the lock is
 * freed.
 */
static int dcssblk_assign_free_minor (dcss_device_t* device) {
	unsigned int minor,found;
	struct list_head* entry;
	if (device == NULL)
		return -EINVAL;
	for (minor=0; minor< (1<<MINORBITS); minor++) {
		found = 0;
		// test if minor available
		list_for_each (entry, &dcssblk_devices)
			if (minor == MINOR((list_entry(entry, dcss_device_t, 
						       lh)->kdev)))
				found++;
		if (!found) break; // got unused minor
	}
	if (found)
		return -EBUSY;
	device->kdev = MKDEV(dcssblk_major, minor);
	return 0;
}

/*
 * get the dcss_device_t from dcssblk_devices 
 * with the struct device supplied.
 * needs to be called with read_lock(dcssblk_devices_lock)
 */
static dcss_device_t * dcssblk_get_device_by_minor (unsigned int minor) {
	struct list_head* entry;
	list_for_each (entry, &dcssblk_devices) 
		if (MINOR(list_entry(entry, dcss_device_t, lh)->kdev)==minor) {
			return list_entry(entry,dcss_device_t, lh);
		}
	return NULL;
}

/*
 * get the dcss_device_t from dcssblk_devices 
 * with the segment name supplied.
 * needs to be called with read_lock(dcssblk_devices_lock)
 */
static dcss_device_t * dcssblk_get_device_by_name (char* name) {
	struct list_head* entry;
	list_for_each (entry, &dcssblk_devices) 
		if (!strcmp (name, list_entry (entry, dcss_device_t, lh)
			     ->name)) {
			return list_entry(entry,dcss_device_t, lh);
		}
	return NULL;
}

static void dcssblk_do_save (dcss_device_t* device) {
	segment_replace (device->name);
}

/*
 * device attribute for switching shared/nonshared
 * operation 
 */
static int dcssblk_shared_store  (struct file *file, 
				      const char *buffer,
				      unsigned long count, void *data) {
	dcss_device_t* device = data;
	char* buf;
	int rc;
	long value;

	if (device == NULL) {
		rc = -EINVAL;
		goto out_nobuf;
	}
	write_lock(&dcssblk_devices_lock);
	if (device->use_count) {
		rc = -EBUSY;
		write_unlock (&dcssblk_devices_lock);
		goto out_nobuf;
	}

	/*
	 * fetch buffer from userland
	 */
	buf = kmalloc(count,GFP_ATOMIC);
	if (buf == NULL) {
		rc = -ENOMEM;
		write_unlock(&dcssblk_devices_lock);
		goto out_nobuf;
	}
	if (copy_from_user(buf, buffer, count)) {
		rc = -EFAULT;
		write_unlock(&dcssblk_devices_lock);
		goto out;	
	}
	value = simple_strtoul(buf, &buf, 10);
	
	if (value) {
		// reload segment in shared mode
		segment_unload (device->name);
		rc = segment_load (device->name, SEGMENT_SHARED_RO, 
				   &device->start, &device->end);
		if (rc < 0) {
			PRINT_WARN ("SEGMENT %s NOT RELOADED RC=%d\n",
				    device->name, rc);
			goto removeseg;
		}
		device->segment_type = rc;
		device->is_shared = 1;
		rc = count;
	} else {
		// reload segment in exclusive mode
		segment_unload (device->name);
		rc = segment_load (device->name, SEGMENT_EXCLUSIVE_RW, 
				   &device->start, &device->end);
		if (rc < 0) {
			PRINT_WARN("SEGMENT %s NOT RELOADED RC=%d\n",
				   device->name, rc);
			goto removeseg;
		}
		device->segment_type = rc;
		device->is_shared = 0;
		rc = count;
	}
	switch (device->segment_type) {
	case SEGMENT_SHARED_RO:
	case SEGMENT_EXCLUSIVE_RO:
		set_device_ro (device->kdev, 1);
		break;
	case SEGMENT_SHARED_RW:
	case SEGMENT_EXCLUSIVE_RW:
		set_device_ro (device->kdev, 0);
		break;
	}
	if (value && ((device->segment_type == SEGMENT_EXCLUSIVE_RO) ||
		      (device->segment_type == SEGMENT_EXCLUSIVE_RW))) {
		PRINT_WARN( 
			   "dcssblk: could not get shared copy of segment %s\n",
			   device->name);
		device->is_shared = 0;
		rc = -EPERM;
	}
	if ((value == 0) && ((device->segment_type == SEGMENT_SHARED_RO) ||
			     (device->segment_type == SEGMENT_SHARED_RW))) {
		PRINT_WARN(
			   "dcssblk: could not get exclusive copy of segment %s\n",
			   device->name);
		device->is_shared = 1;
		rc = -EPERM;
	}
	write_unlock(&dcssblk_devices_lock);
	goto out;

 removeseg:
	PRINT_WARN (
		    "dcssblk: could not reload segment %s, removing it!\n",
		    device->name);
	list_del(&device->lh);
	write_unlock(&dcssblk_devices_lock);
#ifdef CONFIG_PROC_FS
	remove_proc_entry("save", device->dcssblk_subdir_entry);
	remove_proc_entry("shared", device->dcssblk_subdir_entry);
	remove_proc_entry(device->name, dcssblk_proc_root_entry);
#endif
	devfs_unregister(device->devfs_entry );

	kfree (device);	
	MOD_DEC_USE_COUNT; // permanent
 out:
	kfree (buf);
 out_nobuf:
	return rc;

}

/*
 * device attribute for showing status of "shared / non-shared"
 */
static int dcssblk_shared_status(char *buffer, char **start, off_t offset,
				int count, int *eof, void *data) {
	dcss_device_t* device = data;
	
	*eof = 1;
	return sprintf(buffer, device->is_shared ? "1\n" : "0\n");
}

/*
 * device attribute for save operation on current copy
 * of the segment. If the segment is busy, saving will
 * become pending until it gets released which can be
 * undone by storing a non-true value to this entry
 */
static int dcssblk_save_store  (struct file *file, 
				    const char *buffer,
				    unsigned long count, void *data) {
	dcss_device_t* device = data;
	char* buf;
	int rc,value;

	if (device == NULL) {
		rc = -EINVAL;
		goto out_nobuf;
	}
	read_lock(&dcssblk_devices_lock);
	/*
	 * fetch buffer from userland
	 */
	buf = kmalloc(count,GFP_ATOMIC);
	if (buf == NULL) {
		rc = -ENOMEM;
		write_unlock(&dcssblk_devices_lock);
		goto out_nobuf;
	}
	if (copy_from_user(buf, buffer, count)) {
		rc = -EFAULT;
		write_unlock(&dcssblk_devices_lock);
		goto out;	
	}
	value = simple_strtoul(buf, &buf, 10);
	if (value) {
		if (device->use_count == 0) {
			/* device is idle => we save immediately */
			PRINT_WARN ("saving segment %s\n", device->name);
			dcssblk_do_save (device);
		}  else {
			/* device is busy => we save it when it becomes
			   idle in dcssblk_release */
			PRINT_WARN ("segment %s is currently busy\n",
				    device->name);
			PRINT_WARN ("segment %s will be saved when it becomes idle\n",
				    device->name);
			device->save_pending = 1;
		}
	} else {
		if (device->save_pending) {
			/* device is busy & the user wants to undo his save
			   request */
			device->save_pending = 0;
			PRINT_WARN ("deactivating pending save for segment %s\n",
				    device->name);
		}
	}
	read_unlock(&dcssblk_devices_lock);
	rc = count;
 out:
	kfree (buf);
 out_nobuf:
	return rc;
}

/*
 * device attribute for showing status of "save pending"
 */
static int dcssblk_save_status(char *buffer, char **start, off_t offset,
				int count, int *eof, void *data) {
	dcss_device_t* device = data;
	
	*eof = 1;
	return sprintf(buffer, device->save_pending ? "1\n" : "0\n");
}

/*
 * device attribute for adding devices
 */
static int dcssblk_add_store (struct file *file, 
				  const char *buffer,
				  unsigned long count, void *data)
{
	int rc=0, i;
	char* buf;
	dcss_device_t* dcssdev;
	struct list_head* entry;

	MOD_INC_USE_COUNT; // released at end of func

	/*
	 * fetch buffer from userland
	 */
	buf = kmalloc(count+1,GFP_KERNEL);
	if (buf == NULL) {
		rc = -ENOMEM;
		goto out_nobuf;
	}
	if (copy_from_user(buf, buffer, count)) {
		rc = -EFAULT;
		goto out;
	}
	

	/*
	 * check input parameters
	 */
	for (i=0; ((*(buf+i)!='\0') && (*(buf+i)!='\n') && i<count); i++) {
		buf[i] = toupper(buf[i]);
	}
	*(buf+i) = '\0';
	if ((i==0) || (i>8)) {
		rc = -ENAMETOOLONG;
		goto out;
	}
	/*
	 * already loaded?
	 */
	read_lock(&dcssblk_devices_lock);
	list_for_each (entry, &dcssblk_devices) {
		if (!strcmp(buf, list_entry(entry, dcss_device_t, lh)->name)) {
			read_unlock(&dcssblk_devices_lock);
			PRINT_WARN ("SEGMENT %s ALREADY LOADED!\n", buf);
			rc = -EEXIST;
			goto out;
		}
	}
	read_unlock(&dcssblk_devices_lock);
	/*
	 * try to get the dcss
	 */
	dcssdev = kmalloc (sizeof(dcss_device_t),GFP_KERNEL);
	if (dcssdev == NULL) {
		rc = -ENOMEM;
		goto out;
	}
	memset (dcssdev, 0, sizeof(dcss_device_t));
	memcpy (dcssdev->name, buf, i);
	dcssdev->name[i+1] = '\0';
	INIT_LIST_HEAD(&dcssdev->lh);
	/*
	 * load the segment
	 */
	rc = segment_load (dcssdev->name, SEGMENT_SHARED_RO, 
			   &dcssdev->start, &dcssdev->end);
	if (rc < 0) {
		PRINT_WARN ("SEGMENT %s NOT LOADED RC=%d\n",
			    dcssdev->name, rc);
		goto free_dcssdev;
	}
	if (rc == SEGMENT_EXCLUSIVE_RO || rc == SEGMENT_EXCLUSIVE_RW)
		dcssdev->is_shared = 0;
	else
		dcssdev->is_shared = 1;
	PRINT_WARN ("LOADED SEGMENT %s from %08lx to %08lx\n",dcssdev->name,dcssdev->start,dcssdev->end);
	dcssdev->segment_type = rc;
	dcssdev->save_pending = 0;
	/*
	 * get minor
	 */
	write_lock(&dcssblk_devices_lock);
	rc = dcssblk_assign_free_minor(dcssdev);
	if (rc) {
		write_unlock (&dcssblk_devices_lock);
		goto unload_seg;
	}
	/*
	 * create devfs device node
	 */
	dcssdev->devfs_entry = devfs_register (dcssblk_devfs_dir,dcssdev->name,
					       DEVFS_FL_DEFAULT,
					       MAJOR(dcssdev->kdev),
					       MINOR(dcssdev->kdev),
					       S_IFBLK | S_IRUSR | S_IWUSR,
					       &dcssblk_devops,NULL);
	/*
	 * create procfs subdirectory
	 */
#ifdef CONFIG_PROC_FS
	dcssdev->dcssblk_subdir_entry =  proc_mkdir (dcssdev->name, 
						     dcssblk_proc_root_entry);
	dcssdev->dcssblk_shared_entry = 
		create_proc_entry ("shared",
				   S_IRUSR|S_IWUSR,
				   dcssdev->dcssblk_subdir_entry);
	dcssdev->dcssblk_save_entry =
		create_proc_entry ("save",
				   S_IRUSR|S_IWUSR,
				   dcssdev->dcssblk_subdir_entry);
	dcssdev->dcssblk_shared_entry->write_proc = 
		dcssblk_shared_store;
	dcssdev->dcssblk_shared_entry->read_proc = 
		dcssblk_shared_status;
	dcssdev->dcssblk_save_entry->write_proc =
		dcssblk_save_store;
	dcssdev->dcssblk_save_entry->read_proc =
		dcssblk_save_status;
	dcssdev->dcssblk_shared_entry->data =
		dcssdev;
	dcssdev->dcssblk_save_entry->data =
		dcssdev;
#endif

	/*
	 * enqueue
	 */
	list_add_tail (&dcssdev->lh, &dcssblk_devices);
	write_unlock(&dcssblk_devices_lock);
	switch (dcssdev->segment_type) {
	case SEGMENT_SHARED_RO:
	case SEGMENT_EXCLUSIVE_RO:
		set_device_ro (dcssdev->kdev, 1);
		break;
	case SEGMENT_SHARED_RW:
	case SEGMENT_EXCLUSIVE_RW:
		set_device_ro (dcssdev->kdev, 0);
		break;
	}
	PRINT_DEBUG ("SEGMENT %s loaded successfully\n",
		     dcssdev->name);
	rc = count;
	MOD_INC_USE_COUNT; // second time -> permanent
	goto out;
 unload_seg:
	segment_unload (dcssdev->name);
 free_dcssdev:
	kfree (dcssdev);
 out:
	kfree (buf);
 out_nobuf:
	MOD_DEC_USE_COUNT;
	return rc;
}

/*
 * device attribute for removing devices
 */
static int dcssblk_remove_store  (struct file *file, 
				      const char *buffer,
				      unsigned long count, void *data) {
	dcss_device_t* device;
	char * buf;
	int rc=count,i;

	MOD_INC_USE_COUNT;
	/*
	 * fetch buffer from userland
	 */
	buf = kmalloc(count,GFP_KERNEL);
	if (buf == NULL) {
		rc = -ENOMEM;
		goto out_nobuf;
	}
	if (copy_from_user(buf, buffer, count)) {
		rc = -EFAULT;
		goto out;
	}
	for (i=0; ((*(buf+i)!='\0') && (*(buf+i)!='\n') && i<count); i++) {
			buf[i] = toupper(buf[i]);
	}
	*(buf+i) = '\0';
	write_lock(&dcssblk_devices_lock);
	device = dcssblk_get_device_by_name (buf);
	if (device == NULL) {
		rc = -ENODEV;
		write_unlock (&dcssblk_devices_lock);
		goto out;
	}
	if (device->use_count != 0) {
		rc = -EBUSY;
		write_unlock (&dcssblk_devices_lock);
		goto out;
	}
	list_del(&device->lh);
	write_unlock(&dcssblk_devices_lock);
	segment_unload (device->name);
#ifdef CONFIG_PROC_FS
	remove_proc_entry("save", device->dcssblk_subdir_entry);
	remove_proc_entry("shared", device->dcssblk_subdir_entry);
	remove_proc_entry(device->name, dcssblk_proc_root_entry);
#endif
	devfs_unregister(device->devfs_entry );
	
	PRINT_DEBUG ("SEGMENT %s unloaded successfully\n",
		     device->name);
	kfree (device);	
	rc = count;
	MOD_DEC_USE_COUNT; // permanent
 out:
	kfree (buf);
 out_nobuf:
	MOD_DEC_USE_COUNT;
	return rc;
}

/*
 * device attribute for listing devices
 */
static int dcssblk_list_store(char *buffer, char **start, off_t offset,
				int count, int *eof, void *data)
{
	unsigned int minor, len;
	struct list_head* entry;

	len = 0;
	read_lock(&dcssblk_devices_lock);
	for (minor=0; minor< (1<<MINORBITS); minor++) {
		// test if minor available
		list_for_each (entry, &dcssblk_devices)
			if (minor == MINOR((list_entry(entry, dcss_device_t, 
						       lh)->kdev))) {
				len += sprintf(buffer + len, "%i\t%s\n", minor,
					list_entry(entry, dcss_device_t,
							lh)->name);
			}
	}
	read_unlock(&dcssblk_devices_lock);
	*eof = 1;
	return len;
}

static int dcssblk_open (struct inode *inode, struct file *filp)
{
	dcss_device_t *dcssdev;
	int rc;
	write_lock(&dcssblk_devices_lock);
	if ((dcssdev=dcssblk_get_device_by_minor(MINOR(inode->i_rdev)))
	    == NULL) {		
		rc=-ENODEV;
		goto out_unlock;
	}
	dcssdev->use_count ++;
	rc = 0;
 out_unlock:
	write_unlock(&dcssblk_devices_lock);
      	return rc;
}

static int dcssblk_release (struct inode *inode, struct file *filp)
{
	dcss_device_t *dcssdev;
	int rc;
	write_lock(&dcssblk_devices_lock);
	if ((dcssdev=dcssblk_get_device_by_minor(MINOR(inode->i_rdev)))
	    == NULL) {
		rc=-ENODEV;
		goto out_unlock;
	}
	dcssdev->use_count --;
	if ((dcssdev->use_count==0) && (dcssdev->save_pending)) {
		PRINT_WARN ("Segment %s became idle and is being saved\n",
			    dcssdev->name);
		dcssblk_do_save (dcssdev);
		dcssdev->save_pending = 0;
	}
	rc = 0;
 out_unlock:
	write_unlock(&dcssblk_devices_lock);
	return rc;
}

static int dcssblk_make_request (request_queue_t * q, int rw, 
				 struct buffer_head * bh) {
	dcss_device_t *dcssdev;
	unsigned long index;
	unsigned long page_addr;
	unsigned long source_addr;
	unsigned long bytes;

	read_lock(&dcssblk_devices_lock);
	dcssdev = dcssblk_get_device_by_minor (MINOR(bh->b_rdev));
	read_unlock(&dcssblk_devices_lock);

	if (dcssdev == NULL)
		/* No such device. */
		goto fail;
	if ((bh->b_rsector & 3) != 0 || (bh->b_size & 4095) != 0)
		/* Request is not page-aligned. */
		goto fail;
	if ((bh->b_size + (bh->b_rsector<<9)) 
	    > (dcssdev->end - dcssdev->start + 1))
		/* Request beyond end of DCSS segment. */
		goto fail;     
	index = (bh->b_rsector >> 3);
	page_addr = (unsigned long) bh->b_data;
	source_addr = dcssdev->start + (index<<12);
	bytes = bh->b_size;
	if ((page_addr & 4095) != 0 || (bytes & 4095) != 0)
		/* More paranoia. */
		goto fail;
	if ((rw == READ) || (rw == READA)) {
		memcpy ((void*)page_addr, (void*)source_addr, 
			bytes);
	} else {
		memcpy ((void*)source_addr, (void*)page_addr, 
			bytes);
	}
	bh->b_end_io(bh, 1);
	return 0;
 fail:
	bh->b_end_io(bh, 0);
	return 0;
}


/*
 * setup the block device related things
 */
static int __init dcssblk_setup_blkdev(void)
{
	request_queue_t *q;
	int i,rc;

	for (i=0; i < (1<<MINORBITS); i++)
		dcssblk_blksizes[i] = PAGE_SIZE;

	/*
	 * Register blockdev
	 */
	rc = register_blkdev(0, DCSSBLK_NAME, &dcssblk_devops);
	if (rc < 0) {
		PRINT_ERR("Can't get register major\n");
		return rc;
	}
	dcssblk_major = rc;
	blksize_size[dcssblk_major] = dcssblk_blksizes;
	hardsect_size[dcssblk_major] = dcssblk_blksizes;

	
	/*
	 * Assign the other needed values: make request function, sizes and
	 * hardsect size. All the minor devices feature the same value.
	 */

	q = BLK_DEFAULT_QUEUE(dcssblk_major);
	blk_queue_make_request (q, dcssblk_make_request);
	return 0;
}

static void dcssblk_unregister_blkdev(void) {
	int rc;
	rc = unregister_blkdev (dcssblk_major, DCSSBLK_NAME);
	if (rc) {
		PRINT_ERR ("Can't unregister blockdev\n");
	}	
} 

/* 
 * Register procfs entries
 */
static int dcssblk_register_procfs(void)
{
#ifdef CONFIG_PROC_FS
	dcssblk_proc_root_entry =  proc_mkdir ("dcssblk", &proc_root);
	dcssblk_add_entry = create_proc_entry ("add",
					       S_IFREG | S_IWUSR,
					       dcssblk_proc_root_entry);

	dcssblk_remove_entry = create_proc_entry ("remove",
						  S_IFREG | S_IWUSR,
						  dcssblk_proc_root_entry);

	dcssblk_list_entry = create_proc_entry ("list",
						  S_IFREG | S_IRUGO,
						  dcssblk_proc_root_entry);

	dcssblk_add_entry->write_proc = dcssblk_add_store;
	dcssblk_add_entry->owner = THIS_MODULE;
	dcssblk_remove_entry->write_proc = dcssblk_remove_store;
	dcssblk_remove_entry->owner = THIS_MODULE;
	dcssblk_list_entry->read_proc = dcssblk_list_store;
	dcssblk_list_entry->owner = THIS_MODULE;
#endif
	dcssblk_devfs_dir = devfs_mk_dir (NULL, 
					  "dcssblk", 
					  NULL);
	return 0;

}

static void dcssblk_unregister_procfs(void) {
	devfs_unregister(dcssblk_devfs_dir);
#ifdef CONFIG_PROC_FS
	remove_proc_entry ("list", dcssblk_proc_root_entry);
	remove_proc_entry ("remove", dcssblk_proc_root_entry);
	remove_proc_entry ("add", dcssblk_proc_root_entry);
	remove_proc_entry ("dcssblk", NULL);
#endif
}

/*
 * Finally, the init/exit functions.
 */
static void __exit dcssblk_exit(void)
{
	dcssblk_unregister_procfs();
	dcssblk_unregister_blkdev();
}

static int __init dcssblk_init(void)
{
	int rc;
	PRINT_DEBUG ("DCSSBLOCK INIT\n");
	rc = dcssblk_register_procfs();
	if (rc) goto out;
	rc = dcssblk_setup_blkdev();
	if (rc) {
		dcssblk_unregister_procfs();
		goto out;
	}
 out:
	return rc;
}

module_init(dcssblk_init);
module_exit(dcssblk_exit);
