/*
 * This file contains the procedures for the handling of select
 *
 * Created for Linux based loosely upon Mathius Lattner's minix
 * patches by Peter MacDonald. Heavily edited by Linus.
 */

#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/tty.h>
#include <linux/sched.h>

#include <asm/segment.h>
#include <asm/system.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>
#include <const.h>
#include <errno.h>
#include <sys/time.h>
#include <signal.h>

/*
 * Ok, Peter made a complicated, but straightforward multiple_wait() function.
 * I have rewritten this, taking some shortcuts: This code may not be easy to
 * follow, but it should be free of race-conditions, and it's practical. If you
 * understand what I'm doing here, then you understand how the linux sleep/wakeup
 * mechanism works.
 *
 * Two very simple procedures, add_wait() and free_wait() make all the work. We
 * have to have interrupts disabled throughout the select, but that's not really
 * such a loss: sleeping automatically frees interrupts when we aren't in this
 * task.
 */

typedef struct {
	struct task_struct * old_task;
	struct task_struct ** wait_address;
} wait_entry;

typedef struct select_table_struct {
	int nr, woken;
	struct task_struct * current;
	struct select_table_struct * next_table;
	wait_entry entry[NR_OPEN*3];
} select_table;

static select_table * sel_tables = NULL;

static void add_wait(struct task_struct ** wait_address, select_table * p)
{
	int i;

	if (!wait_address)
		return;
	for (i = 0 ; i < p->nr ; i++)
		if (p->entry[i].wait_address == wait_address)
			return;
	current->next_wait = NULL;
	p->entry[p->nr].wait_address = wait_address;
	p->entry[p->nr].old_task = *wait_address;
	*wait_address = current;
	p->nr++;
}

/*
 * free_wait removes the current task from any wait-queues and then
 * wakes up the queues.
 */
static void free_one_table(select_table * p)
{
	int i;
	struct task_struct ** tpp;

	for(tpp = &LAST_TASK ; tpp > &FIRST_TASK ; --tpp)
		if (*tpp && ((*tpp)->next_wait == p->current))
			(*tpp)->next_wait = NULL;
	if (!p->nr)
		return;
	for (i = 0; i < p->nr ; i++) {
		wake_up(p->entry[i].wait_address);
		wake_up(&p->entry[i].old_task);
	}
	p->nr = 0;
}

static void free_wait(select_table * p)
{
	select_table * tmp;

	if (p->woken)
		return;
	p = sel_tables;
	sel_tables = NULL;
	while (p) {
		wake_up(&p->current);
		p->woken = 1;
		tmp = p->next_table;
		p->next_table = NULL;
		free_one_table(p);
		p = tmp;
	}
}

static struct tty_struct * get_tty(struct inode * inode)
{
	int major, minor;

	if (!S_ISCHR(inode->i_mode))
		return NULL;
	if ((major = MAJOR(inode->i_rdev)) != 5 && major != 4)
		return NULL;
	if (major == 5)
		minor = current->tty;
	else
		minor = MINOR(inode->i_rdev);
	if (minor < 0)
		return NULL;
	return TTY_TABLE(minor);
}

/*
 * The check_XX functions check out a file. We know it's either
 * a pipe, a character device or a fifo (fifo's not implemented)
 */
static int check_in(select_table * wait, struct inode * inode)
{
	struct tty_struct * tty;

	if (tty = get_tty(inode))
		if (!EMPTY(tty->secondary))
			return 1;
		else
			add_wait(&tty->secondary->proc_list, wait);
	else if (inode->i_pipe)
		if (!PIPE_EMPTY(*inode) || inode->i_count < 2)
			return 1;
		else
			add_wait(&inode->i_wait, wait);
	return 0;
}

static int check_out(select_table * wait, struct inode * inode)
{
	struct tty_struct * tty;

	if (tty = get_tty(inode))
		if (!FULL(tty->write_q))
			return 1;
		else
			add_wait(&tty->write_q->proc_list, wait);
	else if (inode->i_pipe)
		if (!PIPE_FULL(*inode))
			return 1;
		else
			add_wait(&inode->i_wait, wait);
	return 0;
}

static int check_ex(select_table * wait, struct inode * inode)
{
	struct tty_struct * tty;

	if (tty = get_tty(inode))
		if (!FULL(tty->write_q))
			return 0;
		else
			return 0;
	else if (inode->i_pipe)
		if (inode->i_count < 2)
			return 1;
		else
			add_wait(&inode->i_wait,wait);
	return 0;
}

int do_select(fd_set in, fd_set out, fd_set ex,
	fd_set *inp, fd_set *outp, fd_set *exp)
{
	int count;
	select_table wait_table;
	int i;
	fd_set mask;

	mask = in | out | ex;
	for (i = 0 ; i < NR_OPEN ; i++,mask >>= 1) {
		if (!(mask & 1))
			continue;
		if (!current->filp[i])
			return -EBADF;
		if (!current->filp[i]->f_inode)
			return -EBADF;
		if (current->filp[i]->f_inode->i_pipe)
			continue;
		if (S_ISCHR(current->filp[i]->f_inode->i_mode))
			continue;
		if (S_ISFIFO(current->filp[i]->f_inode->i_mode))
			continue;
		return -EBADF;
	}
repeat:
	wait_table.nr = 0;
	wait_table.woken = 0;
	wait_table.current = current;
	wait_table.next_table = sel_tables;
	sel_tables = &wait_table;
	*inp = *outp = *exp = 0;
	count = 0;
	mask = 1;
	for (i = 0 ; i < NR_OPEN ; i++, mask += mask) {
		if (mask & in)
			if (check_in(&wait_table,current->filp[i]->f_inode)) {
				*inp |= mask;
				count++;
			}
		if (mask & out)
			if (check_out(&wait_table,current->filp[i]->f_inode)) {
				*outp |= mask;
				count++;
			}
		if (mask & ex)
			if (check_ex(&wait_table,current->filp[i]->f_inode)) {
				*exp |= mask;
				count++;
			}
	}
	if (!(current->signal & ~current->blocked) &&
	    current->timeout && !count) {
		current->state = TASK_INTERRUPTIBLE;
		sti();
		schedule();
		cli();
		free_wait(&wait_table);
		goto repeat;
	}
	free_wait(&wait_table);
	return count;
}

/*
 * Note that we cannot return -ERESTARTSYS, as we change our input
 * parameters. Sad, but there you are. We could do some tweaking in
 * the library function ...
 */
int sys_select( unsigned long *buffer )
{
/* Perform the select(nd, in, out, ex, tv) system call. */
	int i;
	fd_set res_in, in = 0, *inp;
	fd_set res_out, out = 0, *outp;
	fd_set res_ex, ex = 0, *exp;
	fd_set mask;
	struct timeval *tvp;
	unsigned long timeout;

	mask = get_fs_long(buffer++);
	if (mask >= 32)
		mask = ~0;
	else
		mask = ~((~0) << mask);
	inp = (fd_set *) get_fs_long(buffer++);
	outp = (fd_set *) get_fs_long(buffer++);
	exp = (fd_set *) get_fs_long(buffer++);
	tvp = (struct timeval *) get_fs_long(buffer);

	if (inp)
		in = mask & get_fs_long(inp);
	if (outp)
		out = mask & get_fs_long(outp);
	if (exp)
		ex = mask & get_fs_long(exp);
	timeout = 0xffffffff;
	if (tvp) {
		timeout = get_fs_long((unsigned long *)&tvp->tv_usec)/(1000000/HZ);
		timeout += get_fs_long((unsigned long *)&tvp->tv_sec) * HZ;
		timeout += jiffies;
	}
	current->timeout = timeout;
	cli();
	i = do_select(in, out, ex, &res_in, &res_out, &res_ex);
	if (current->timeout > jiffies)
		timeout = current->timeout - jiffies;
	else
		timeout = 0;
	sti();
	current->timeout = 0;
	if (i < 0)
		return i;
	if (inp) {
		verify_area(inp, 4);
		put_fs_long(res_in,inp);
	}
	if (outp) {
		verify_area(outp,4);
		put_fs_long(res_out,outp);
	}
	if (exp) {
		verify_area(exp,4);
		put_fs_long(res_ex,exp);
	}
	if (tvp) {
		verify_area(tvp, sizeof(*tvp));
		put_fs_long(timeout/HZ, (unsigned long *) &tvp->tv_sec);
		timeout %= HZ;
		timeout *= (1000000/HZ);
		put_fs_long(timeout, (unsigned long *) &tvp->tv_usec);
	}
	if (i)
		return i;
	if (current->signal & ~current->blocked)
		return -EINTR;
	return 0;
}
