/*
 * OSS compatible sequencer driver
 *
 * seq_oss_writeq.c - write queue
 *
 * Copyright (C) 1998,99 Takashi Iwai <iwai@ww.uni-erlangen.de>
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "seq_oss_writeq.h"
#include "seq_oss_event.h"
#include "seq_oss_timer.h"


/*
 * constants
 */
#define PENDING_TICKS	5

/*
 * Note: if SEQ_OSS_AGGRESIVE_SHARING is defined, the semaphore for
 * inode is up, and other applications can access to write.
 */
#define SEQ_OSS_AGGRESSIVE_SHARING


/*
 * queue record
 */
struct evque_t {
	evrec_t ev;
	abstime_t time;
	evque_t *next;
};


/*
 * prototypes
 */
static int enqueue(seq_oss_writeq_t *q, evrec_t *rec, abstime_t time);
static void wakeup_write_sleeper(seq_oss_writeq_t *q);
static void wakeup_sync_sleeper(seq_oss_writeq_t *dp);
static void process_oss_event(seq_oss_devinfo_t *dp, evrec_t *ev, int atomic);


/*
 * create a write queue record
 */
seq_oss_writeq_t *
snd_seq_oss_writeq_new(seq_oss_devinfo_t *dp, int maxlen)
{
	int i;
	seq_oss_writeq_t *q;
	evque_t *p;

	if ((q = snd_calloc(sizeof(*q))) == NULL) {
		snd_printk("sequencer: can't malloc write queue\n");
		return NULL;
	}

	if ((q->pool = snd_malloc(sizeof(evque_t) * maxlen)) == NULL) {
		snd_printk("sequencer: can't malloc write queue buffer\n");
		snd_free(q, sizeof(*q));
		return NULL;
	}

	q->qhead = q->qtail = NULL;
	q->maxlen = maxlen;
	q->qlen = 0;
	q->output_threshold = maxlen / 2;

	/* initialize pool linked-list */
	if (q->pool) {
		for (i = 0, p = q->pool; i < maxlen - 1; i++, p++)
			p->next = p + 1;
		p->next = NULL;
		q->empty = q->pool;
	}

	snd_spin_prepare(q, write);
	snd_sleep_prepare(q, write);
	snd_getlock(q, write) = 0;
	snd_sleep_prepare(q, sync);
	snd_getlock(q, sync) = 0;

	return q;
}

/*
 * delete the write queue
 */
void
snd_seq_oss_writeq_delete(seq_oss_writeq_t *q)
{
	if (q) {
		snd_seq_oss_writeq_clear(q);	/* to be sure */
		if (q->pool)
			snd_free(q->pool, sizeof(evque_t) * q->maxlen);
		snd_free(q, sizeof(*q));
	}
}


/*
 * reset the write queue
 */
void
snd_seq_oss_writeq_clear(seq_oss_writeq_t *q)
{
	unsigned long flags;

	snd_spin_lock(q, write, &flags);
	if (q->qtail) {
		/* relink the used queue to the empty queue */
		q->qtail->next = q->empty;
		q->empty = q->qhead;
		q->qtail = NULL;
		q->qhead = NULL;
	}
	q->qlen = 0;

	/* wake up sleepers if any */
	if (snd_getlock(q, sync) & SND_WK_SLEEP) {
		snd_getlock(q, sync) &= ~SND_WK_SLEEP;
		snd_wakeup(q, sync);
	}
	if (snd_getlock(q, write) & SND_WK_SLEEP) {
		snd_getlock(q, write) &= ~SND_WK_SLEEP;
		snd_wakeup(q, write);
	}
	snd_spin_unlock(q, write, &flags);
}

/*
 * insert event record to write queue
 * return: 0 = OK, non-zero = NG
 */
int
snd_seq_oss_insert_queue(seq_oss_devinfo_t *dp, evrec_t *rec)
{
	int rc;
	seq_oss_writeq_t *q = dp->writeq;
	abstime_t time;

	/* no output */
	if (! q->pool)
		return 0;

	/* if this is a timing event, process the current time */
	if (snd_seq_oss_process_timer_event(dp->timer, rec))
		return 0; /* no need to insert queue */

	time = snd_seq_oss_timer_cur_tick(dp->timer);
	if (snd_seq_oss_timer_is_realtime(dp->timer) ||
	    time <= snd_seq_oss_timer_system_tick()) {
		/* process this event now */
		process_oss_event(dp, rec, 0);
		return 0;
	} else {
		/* put it into queue */
		rc = enqueue(dp->writeq, rec, time);
		if (rc >= 0)
			snd_seq_oss_timer_set(dp->timer, time);
		return rc;
	}
}
		

/*
 * insert queue record
 */
static int
enqueue(seq_oss_writeq_t *q, evrec_t *rec, abstime_t time)
{
	evque_t *p;
	unsigned long flags;

	snd_spin_lock(q, write, &flags);
	if ((p = q->empty) == NULL) {
		snd_spin_unlock(q, write, &flags);
		return -ENOMEM;
	}
	q->empty = p->next;
		
	p->ev = *rec;
	p->time = time;
	p->next = NULL;

	/* insert the record to queue */
	if (! q->qhead) {
		/* this is the first item */
		q->qhead = p;
		q->qtail = p;
	} else if (q->qtail->time <= p->time) {
		/* append this */
		q->qtail->next = p;
		q->qtail = p;
	} else if (q->qhead->time > p->time) {
		/* prepend this */
		p->next = q->qhead;
		q->qhead = p;
	} else {
		/* search for the point */
		evque_t *prev, *next;
		for (prev = q->qhead; (next = prev->next) != NULL; prev = next) {
			if (next->time > p->time)
				break;
		}
		p->next = next;
		prev->next = p;
	}
	q->qlen++;
	snd_spin_unlock(q, write, &flags);
	return 0;
}


#ifdef SEQ_OSS_AGGRESSIVE_SHARING
#ifdef LINUX_2_1
#define semaphore_of(fp)	((fp)->f_dentry->d_inode->i_sem)
#else
#define semaphore_of(fp)	((fp)->f_inode->i_sem)
#endif
#endif

/*
 * sleep until there is enough space on the queue
 */
void
snd_seq_oss_wait_room(seq_oss_writeq_t *q, struct file *opt)
{
	unsigned long flags;
	snd_spin_lock(q, write, &flags);
#ifdef SEQ_OSS_AGGRESSIVE_SHARING
	/* allow other clients to write */
	up(&semaphore_of(opt));
#endif
	snd_getlock(q, write) |= SND_WK_SLEEP;
	snd_sleep(q, write, HZ*3600); /* enough long time */
	snd_getlock(q, write) &= ~SND_WK_SLEEP;
#ifdef SEQ_OSS_AGGRESSIVE_SHARING
	/* stop other clients for writing */
	down(&semaphore_of(opt));
#endif
	snd_spin_unlock(q, write, &flags);
}

/*
 * wake up sleepers if necessary
 */
static void
wakeup_write_sleeper(seq_oss_writeq_t *q)
{
	unsigned long flags;
	snd_spin_lock(q, write, &flags);
	if (snd_seq_oss_writeq_has_room(q) &&
	    (snd_getlock(q, write) & SND_WK_SLEEP)) {
		snd_getlock(q, write) &= ~SND_WK_SLEEP;
		snd_wakeup(q, write);
	}
	snd_spin_unlock(q, write, &flags);
}

/*
 * polling / select for writing:
 * return non-zero if queue has enough space
 */
#ifdef SND_POLL
unsigned int
snd_seq_oss_writeq_poll(seq_oss_writeq_t *q, struct file *file, poll_table *wait)
{
	unsigned long flags;
	snd_spin_lock(q, write, &flags);
	if (! snd_seq_oss_writeq_has_room(q)) {
		snd_getlock(q, write) |= SND_WK_SLEEP;
		snd_poll_wait(file, q, write, wait);
	}
	snd_spin_unlock(q, write, &flags);
	return snd_seq_oss_writeq_has_room(q);
}
#else
int
snd_seq_oss_writeq_select(seq_oss_writeq_t *q, select_table *wait)
{
	unsigned long flags;
	snd_spin_lock(q, write, &flags);
	if (! snd_seq_oss_writeq_has_room(q)) {
		snd_getlock(q, write) |= SND_WK_SLEEP;
		snd_select_wait(q, write, wait);
		snd_spin_unlock(q, write, &flags);
		return 0;
	}
	snd_getlock(q, write) &= ~SND_WK_SLEEP;
	snd_spin_unlock(q, write, &flags);
	return 1;
}
#endif


/*
 * wait until the write buffer becomes empty
 */
int
snd_seq_oss_writeq_sync(seq_oss_writeq_t *q)
{
	unsigned long flags;

	snd_spin_lock(q, write, &flags);
 	if (q->qlen > 0) {
		/* sleep until the queue is empty or time out */
		snd_getlock(q, sync) |= SND_WK_SLEEP;
		snd_sleep(q, sync, HZ);
		snd_getlock(q, sync) &= SND_WK_SLEEP;
		if (snd_sleep_abort(q, sync)) {
			/* interrupted - return 0 to finish sync */
			snd_spin_unlock(q, write, &flags);
			return 0;
		}
	}
	snd_spin_unlock(q, write, &flags);
	return q->qlen;
}


/*
 * wake up sync sleepers if necessary
 */
static void
wakeup_sync_sleeper(seq_oss_writeq_t *q)
{
	unsigned long flags;
	snd_spin_lock(q, write, &flags);
	if (q->qlen <= 0 && (snd_getlock(q, sync) & SND_WK_SLEEP)) {
		snd_getlock(q, sync) &= ~SND_WK_SLEEP;
		snd_wakeup(q, sync);
	}
	snd_spin_unlock(q, write, &flags);
}


/*
 * timer callback;
 * pick up records from queue and process them if within time
 */
void
snd_seq_oss_process_queue(seq_oss_devinfo_t *dp, abstime_t time)
{
	seq_oss_writeq_t *q = dp->writeq;

	if (! q || !q->pool)
		return;

	for (;;) {
		evque_t *p;
		evrec_t ev; /* temporary event record */
		unsigned long flags;

		snd_spin_lock(q, write, &flags);
		/* pick up queue */
		if ((p = q->qhead) == NULL) {
			/* no record more */
			snd_spin_unlock(q, write, &flags);
			break;
		}
		if (time < p->time) {
			/* the record is out of time;
			 * request the next interrupt time and quit
			 */
			snd_seq_oss_timer_set(dp->timer, p->time);
			snd_spin_unlock(q, write, &flags);
			return;
		}
		/* release this record from queue */
		q->qhead = p->next;
		if (! q->qhead)
			q->qtail = NULL;

		/* copy the event record to the temporary buffer */
		ev = p->ev;

		/* re-link this record to the empty list */
		p->next = q->empty;
		q->empty = p;

		q->qlen--;
		snd_spin_unlock(q, write, &flags);

		/* process this event */
		process_oss_event(dp, &ev, 1);
	}
}


/*
 * process OSS event record
 */
static void
process_oss_event(seq_oss_devinfo_t *dp, evrec_t *ev, int atomic)
{
	if (snd_seq_oss_play_event(dp, ev, atomic)) {
		/* the event was not processed */
		if (dp->writeq->pool) {
			/* process this again at next moment */
			unsigned int time = snd_seq_oss_timer_system_tick() + PENDING_TICKS;
			if (enqueue(dp->writeq, ev, time) >= 0)
				snd_seq_oss_timer_set(dp->timer, time);
		}
		return;
	}

	/* wake up sleepers if any */
	wakeup_sync_sleeper(dp->writeq);
	wakeup_write_sleeper(dp->writeq);
}


/*
 * proc interface
 */
void
snd_seq_oss_writeq_info_read(seq_oss_writeq_t *q, snd_info_buffer_t *buf)
{
	unsigned long flags;
	snd_iprintf(buf, "  write queue length %d\n", q->qlen);
	snd_iprintf(buf, "  output threshold %d\n", q->output_threshold);
	snd_spin_lock(q, write, &flags);
	if (snd_getlock(q, write) & SND_WK_SLEEP)
		snd_iprintf(buf, "  write sleeping\n");
	if (snd_getlock(q, sync) & SND_WK_SLEEP)
		snd_iprintf(buf, "  sync sleeping\n");
	snd_spin_unlock(q, write, &flags);
}
