/* 2008-02-27: File added by Sony Corporation */
/*
 * linux/block/k3d_proc.c
 * Copyright 2007 Sony Corporation.
 *
 * 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; version 2
 * of the License.
 *
 * This program is distributed in the hope that it will 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 to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
/* k3d is based on linux/fs/xvfat/rdchk.c */

#ifdef CONFIG_PROC_FS

#include "k3d_priv.h"

#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <asm/uaccess.h>

#define POLL_SUPPORT
#ifdef POLL_SUPPORT
#include <linux/poll.h>
#endif

extern struct list_head k3d_info_list;

/*
 * "/proc/removabledisk/interval"
 */
static int proc_k3d_read_settings
	(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	char		*out = page;
	struct list_head *k3d_info_list = (struct list_head*)data;
	struct k3d_info *k3di;
	int		len;
	int limit = count - 80;
	const char *fmt_head = "%s\t%s\n";
	const char *fmt_data = "%s\t%u\n";

	out += sprintf(out, fmt_head, "name", "interval");
	out += sprintf(out, fmt_head, "----", "--------");
	list_for_each_entry(k3di, k3d_info_list, dev_list) {
	  	if (limit < out - page) {
		  	printk ("%s proc page limit over\n", __FUNCTION__);
			break;
		}
		out += sprintf(out, fmt_data,
			       k3di->name, k3di->interval);
	}
	len = out - page;
	len -= off;
	if (len < count) {
		*eof = 1;
		if (len <= 0)
			return 0;
	} else {
		len = count;
	}
	*start = page + off;
	return len;
}

static int proc_k3d_write_settings(struct file *file,
				   const char __user *buffer,
				   unsigned long count, void *data)
{
	char		name[BDEVNAME_SIZE + 1];
	int		done_check = 0;
	unsigned long	n;
	char *buf, *s;
	struct k3d_info *k3di;

	if (!capable(CAP_SYS_ADMIN))
		return -EACCES;

	if (count >= PAGE_SIZE)
		return -EINVAL;

	s = buf = (char *)__get_free_page(GFP_USER);
	if (!buf)
		return -ENOMEM;

	if (copy_from_user(buf, buffer, count)) {
		free_page((unsigned long)buf);
		return -EFAULT;
	}

	buf[count] = '\0';

	/*
	 * Skip over leading whitespace
	 */
	while (count && isspace(*s)) {
		--count;
		++s;
	}
	/*
	 * Do one full pass to verify all parameters,
	 * then do another to actually write the new settings.
	 */
	do {
		char *p = s;
		n = count;
		while (n > 0) {
			long val;
			char *q = p;

			while (n > 0 && *p != ':') {
				--n;
				p++;
			}
			if (*p != ':')
				goto parse_error;
			if (p - q > BDEVNAME_SIZE)
				goto parse_error;
			memcpy(name, q, p - q);
			name[p - q] = 0;

			if (n > 0) {
				--n;
				p++;
			} else
				goto parse_error;

			val = simple_strtol(p, &q, 10);
			if (val < 0) {
				printk("ivalid value %ld\n", val);
				goto parse_error;
			}
			n -= q - p;
			p = q;
			if (n > 0 && !isspace(*p))
				goto parse_error;
			while (n > 0 && isspace(*p)) {
				--n;
				++p;
			}

			k3di = find_k3dinfo_by_name(name);
			if (k3di == NULL)
			{
				printk("%s not find %s\n", __FUNCTION__, name);
				goto parse_error;
			}
			if (done_check)
				set_k3dinfo_interval(k3di, val);

		}
	} while (!done_check++);
	free_page((unsigned long)buf);
	return count;
parse_error:
	free_page((unsigned long)buf);
	printk("%s: parse error\n", __FUNCTION__);
	return -EINVAL;
}

/*
 * "/proc/removabledisk/status"
 */
static int
k3d_status_read_proc(char *page, char **start, off_t off, int count,
		       int *eof, void *data)
{
	struct k3d_info *k3di;
	int len = 0;
	int limit = PAGE_SIZE - 80;
	int chg, ins;
	list_for_each_entry(k3di, &k3d_info_list, dev_list) {
	  	chg = get_k3dinfo_change(k3di);
		ins = get_k3dinfo_insert(k3di);
		chg = (chg < 0) ? -1: chg;
		ins = (ins < 0) ? -1: ins;
#ifdef CONFIG_SNSC_BLOCK_K3D_STATE_CHANGE_COUNT
		len += sprintf(page+len, "%s %d %d %u\n",
			       k3di->name, ins, chg, k3di->state_change_count);
#else
		len += sprintf(page+len, "%s %d %d \n",
			       k3di->name, ins, chg);
#endif
		if (len >= limit) {
			len = -ENOSPC;
			break;
		}
	}
	*eof = 1;
	return len;
}

/*
 * "/proc/removabledisk/event"
 */
#define EVBUF_SIZE 100
#define LINEBUF_SIZE 80

struct proc_evbuf {
	struct list_head buf_list;
	struct proc_evbuf_log {
		int chg;
		int ins;
		char name[BDEVNAME_SIZE];
#ifdef CONFIG_SNSC_BLOCK_K3D_STATE_CHANGE_COUNT
		unsigned int state_change_count;
#endif
	} log[EVBUF_SIZE];
	int head;
	int tail;
	loff_t ppos;
	wait_queue_head_t ev_wait;
	char linebuf[LINEBUF_SIZE];
	int linebuf_size;
	int linebuf_pos;
	struct semaphore ev_sem;
};

static LIST_HEAD(proc_evbuf_list);
static struct semaphore proc_evbuf_list_sem;

static void add_evbuf(struct proc_evbuf *evbuf, struct k3d_info *k3di)
{
	int next;
	int chg, ins;

	next = (evbuf->head + 1) % EVBUF_SIZE;
	if  (evbuf->tail == next) 	/* buffer full */
		evbuf->tail = (evbuf->tail + 1) % EVBUF_SIZE;

	chg = get_k3dinfo_change(k3di);
	ins = get_k3dinfo_insert(k3di);
	evbuf->log[evbuf->head].chg = (chg < 0) ? -1: chg;
	evbuf->log[evbuf->head].ins = (ins < 0) ? -1: ins;
#ifdef CONFIG_SNSC_BLOCK_K3D_STATE_CHANGE_COUNT
	evbuf->log[evbuf->head].state_change_count = k3di->state_change_count;
#endif
	strncpy(evbuf->log[evbuf->head].name, k3di->name, BDEVNAME_SIZE);
	evbuf->head = next;
}

static int k3d_event_open(struct inode * inode, struct file * file)
{
	struct proc_evbuf *evbuf;

	evbuf = (struct proc_evbuf *)
		kmalloc(sizeof(struct proc_evbuf), GFP_KERNEL);
	if (evbuf == NULL)
		return -ENOMEM;
	evbuf->head = evbuf->tail = 0;
	evbuf->ppos = 0;
	evbuf->linebuf_size = evbuf->linebuf_pos = 0;
	init_waitqueue_head(&evbuf->ev_wait);
	sema_init(&evbuf->ev_sem, 1);
	down(&proc_evbuf_list_sem);
	list_add(&evbuf->buf_list, &proc_evbuf_list);
	up(&proc_evbuf_list_sem);
	file->private_data = evbuf;
	return 0;
}

static int k3d_event_release(struct inode * inode, struct file * file)
{
	struct proc_evbuf *evbuf;

	evbuf = (struct proc_evbuf *)file->private_data;
	down(&proc_evbuf_list_sem);
	list_del(&evbuf->buf_list);
	up(&proc_evbuf_list_sem);
	kfree(evbuf);
	return 0;
}

static size_t
copy_linebuf_to_user(char *buf, size_t len, struct proc_evbuf *evbuf)
{
	int clen;
	unsigned long err;

	clen = evbuf->linebuf_size - evbuf->linebuf_pos;
	if (clen == 0)
		return 0;
	if (clen > len)
		clen = len;
	err = __copy_to_user(buf, evbuf->linebuf + evbuf->linebuf_pos, clen);
	/* Even if the error occured, linebuf_pos goes forward
	 * in order to make a user notice that some events were skipped. */
	evbuf->linebuf_pos += clen;
	return clen;
}

static int
fill_linebuf(struct proc_evbuf *evbuf)
{
	int blen, pos;

	blen = evbuf->linebuf_size - evbuf->linebuf_pos;
	if (blen > 0)
		return blen;
	if (evbuf->head == evbuf->tail)
		return 0;
	pos = evbuf->tail;

#ifdef CONFIG_SNSC_BLOCK_K3D_STATE_CHANGE_COUNT
	blen = sprintf(evbuf->linebuf, "%s %d %d %u\n",
		       evbuf->log[pos].name,
		       evbuf->log[pos].ins,
		       evbuf->log[pos].chg,
		       evbuf->log[pos].state_change_count);
#else
	blen = sprintf(evbuf->linebuf, "%s %d %d\n",
		       evbuf->log[pos].name,
		       evbuf->log[pos].ins,
		       evbuf->log[pos].chg);
#endif
	evbuf->tail = (pos + 1) % EVBUF_SIZE;
	evbuf->linebuf_size = blen;
	evbuf->linebuf_pos = 0;
	return blen;
}

static ssize_t
k3d_event_read(struct file *file, char *buf, size_t len, loff_t *ppos)
{
	struct proc_evbuf *evbuf;
	size_t ulen;
	int rval;

	evbuf = (struct proc_evbuf *)file->private_data;
	k3d_printk2("%s: len=%d ppos=%d(%d)\n", __FUNCTION__,
		      len, (int)*ppos, (int)evbuf->ppos);
	if (*ppos != evbuf->ppos)
		return -EIO;
	if (!access_ok(VERIFY_READ, buf, len))
		return -EFAULT;

	down(&evbuf->ev_sem);
	ulen = copy_linebuf_to_user(buf, len, evbuf);
	while (ulen < len) {
		rval = fill_linebuf(evbuf);
		k3d_printk2("%s: ulen=%d fill=%d\n", __FUNCTION__,
			      ulen, rval);
		if (rval > 0)
			ulen += copy_linebuf_to_user(buf + ulen, len - ulen,
						     evbuf);
		else if (ulen > 0)
			break;
		else {
			up(&evbuf->ev_sem);
			if (file->f_flags & O_NONBLOCK)
				return -EAGAIN;
			k3d_printk2("%s: sleep\n", __FUNCTION__);
			interruptible_sleep_on(&evbuf->ev_wait);
			k3d_printk2("%s: wakeup\n", __FUNCTION__);
			if (signal_pending(current))
				return -ERESTARTSYS;
			down(&evbuf->ev_sem);
		}
	}
	*ppos += ulen;
	evbuf->ppos = *ppos;
	up(&evbuf->ev_sem);
	k3d_printk2("%s: read_size=%d\n", __FUNCTION__, ulen);
	return ulen;
}

#ifdef POLL_SUPPORT
static unsigned int k3d_event_poll(struct file *file, poll_table * wait)
{
	struct proc_evbuf *evbuf;

	evbuf = (struct proc_evbuf *)file->private_data;

	poll_wait(file, &evbuf->ev_wait, wait);
	if (evbuf->head != evbuf->tail)
		return POLLIN | POLLRDNORM;
	return 0;
}
#endif

static struct file_operations event_fops =
{
	read:		k3d_event_read,
	open:		k3d_event_open,
	release:	k3d_event_release,
#ifdef POLL_SUPPORT
	poll:		k3d_event_poll,
#endif
};


/*
 * create and destroy proc entry "/proc/removabledisk"
 */
static struct proc_dir_entry *proc_rd_root;
static struct proc_dir_entry *proc_status;
static struct proc_dir_entry *proc_event;

static void proc_k3d_interval_create(void)
{
	struct proc_dir_entry *ent;

	if (!proc_rd_root)
		return;

	ent = create_proc_entry("interval",
				S_IFREG|S_IRUSR|S_IWUSR,
				proc_rd_root);
	if (!ent) return;
	ent->nlink = 1;
	ent->data = &k3d_info_list;
	ent->read_proc  = proc_k3d_read_settings;
	ent->write_proc = proc_k3d_write_settings;
}

static void proc_k3d_interval_destroy(void)
{
	if (!proc_rd_root)
		return;

	remove_proc_entry("interval", proc_rd_root);
}

int __init k3d_proc_init(void)
{
	sema_init(&proc_evbuf_list_sem, 1);
	proc_rd_root = proc_mkdir("removabledisk", 0);
	if (proc_rd_root) {
		proc_status = create_proc_read_entry("status", S_IRUGO,
						     proc_rd_root,
						     k3d_status_read_proc,
						     NULL);
		proc_event = create_proc_entry("event", S_IRUGO,
					       proc_rd_root);
		if (proc_event)
			proc_event->proc_fops = &event_fops;
	}

	proc_k3d_interval_create();
	return 0;
}

void k3d_proc_exit(void)
{
	if (proc_rd_root) {
		remove_proc_entry("status", proc_rd_root);
		remove_proc_entry("event", proc_rd_root);
		remove_proc_entry("removabledisk", NULL);
		proc_k3d_interval_destroy();
	}
}

void k3d_proc_notify(struct k3d_info *k3di)
{
	struct proc_evbuf *evbuf;

	k3d_printk("%s: dev=%s  chg_cap=%ld ins_cap=%ld chg=%ld ins=%ld\n",
		     __FUNCTION__, k3di->name,
		     k3di_capability(k3di, K3D_S_CHANGE),
		     k3di_capability(k3di, K3D_S_INSERT),
		     k3di_state(k3di, K3D_S_CHANGE),
		     k3di_state(k3di, K3D_S_INSERT));

	down(&proc_evbuf_list_sem);
	list_for_each_entry(evbuf, &proc_evbuf_list, buf_list) {
		down(&evbuf->ev_sem);
		add_evbuf(evbuf, k3di);
		up(&evbuf->ev_sem);
		wake_up(&evbuf->ev_wait);
	}
	up(&proc_evbuf_list_sem);
}

#endif /* CONFIG_PROC_FS */
