/*
 *  Abstract layer for MIDI v1.0 stream
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *
 *
 *   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.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "../include/driver.h"
#include "../include/rawmidi.h"
#include "../include/info.h"
#include "../include/control.h"
#include "../include/minors.h"

static int snd_rawmidi_free(snd_rawmidi_t * rmidi);
static int snd_rawmidi_register(snd_rawmidi_t * rmidi, snd_device_t *devptr);
static int snd_rawmidi_unregister(snd_rawmidi_t * rmidi);

snd_rawmidi_t *snd_rawmidi_devices[SND_CARDS * SND_RAWMIDI_DEVICES];

static DECLARE_MUTEX(register_mutex);

static inline unsigned short snd_rawmidi_file_flags(struct file *file)
{
	switch (file->f_mode & (FMODE_READ | FMODE_WRITE)) {
	case FMODE_WRITE:
		return SND_RAWMIDI_LFLG_OUTPUT;
	case FMODE_READ:
		return SND_RAWMIDI_LFLG_INPUT;
	default:
		return SND_RAWMIDI_LFLG_OPEN;
	}
}

static inline int snd_rawmidi_input_ok(snd_rawmidi_t * rmidi)
{
	unsigned long flags;
	int result;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	spin_lock_irqsave(&pchn->lock, flags);
	result = pchn->used >= pchn->used_min;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return result;
}

static inline int snd_rawmidi_output_ok(snd_rawmidi_t * rmidi)
{
	unsigned long flags;
	int result, free;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	spin_lock_irqsave(&pchn->lock, flags);
	free = pchn->size - pchn->used;
	result = pchn->used <= pchn->used_max &&
	         free >= pchn->used_room;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return result;
}

static int snd_rawmidi_init_buffer(snd_rawmidi_t * rmidi, int channel)
{
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[channel];
	pchn->event = NULL;
	pchn->size = PAGE_SIZE;
	pchn->used_max = pchn->size - 1;
	pchn->used_room = 1;
	pchn->used_min = 1;
	pchn->buffer = NULL;
	if ((pchn->buffer = snd_kmalloc(pchn->size, GFP_KERNEL)) == NULL) {
		return -ENOMEM;
	}
	pchn->head = pchn->tail = pchn->used = 0;
	return 0;
}

int snd_rawmidi_done_buffer(snd_rawmidi_t * rmidi, int channel)
{
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[channel];
	if (pchn->buffer) {
		snd_kfree(pchn->buffer);
		pchn->buffer = NULL;
	}
	return 0;
}

int snd_rawmidi_drain_output(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	pchn->hw.trigger(rmidi, 0);
	pchn->flags = 0;
	/* interrupts aren't enabled at this moment, so spinlock isn't needed */
	pchn->used =
	pchn->head =
	pchn->tail = 0;
	return 0;
}

int snd_rawmidi_flush_output(snd_rawmidi_t * rmidi)
{
	int err;
	long timeout;
	snd_rawmidi_channel_t *pchn;
	wait_queue_head_t wait;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	err = 0;
	pchn->flags |= SND_RAWMIDI_FLG_FLUSH;
	while (pchn->used > 0) {
		timeout = interruptible_sleep_on_timeout(&pchn->sleep, 10 * HZ);
		if (signal_pending(current)) {
			err = -ERESTARTSYS;
			break;
		}
		if (pchn->used > 0 && !timeout) {
			err = -EIO;
			break;
		}
	}
	pchn->flags &= ~SND_RAWMIDI_FLG_FLUSH;
	init_waitqueue_head(&wait);
	interruptible_sleep_on_timeout(&wait, HZ / 20);
	snd_rawmidi_drain_output(rmidi);
	return err;
}

int snd_rawmidi_flush_input(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	pchn->hw.trigger(rmidi, 0);
	pchn->flags = 0;
	/* interrupts aren't enabled at this moment, so spinlock isn't needed */
	pchn->used =
	pchn->head =
	pchn->tail = 0;
	return 0;
}

int snd_rawmidi_kernel_open(int cardnum, int device, int mode, snd_rawmidi_t ** out)
{
	snd_rawmidi_t *rmidi;
	int err;

	rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (rmidi == NULL)
		return -ENODEV;
	down(&rmidi->open_mutex);
	if ((mode & SND_RAWMIDI_LFLG_INPUT) &&
	    !(rmidi->info_flags & SND_RAWMIDI_INFO_INPUT)) {
		err = -ENXIO;
		goto _err;
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT &&
	    !(rmidi->info_flags & SND_RAWMIDI_INFO_OUTPUT)) {
		err = -ENXIO;
		goto _err;
	}
	if (mode & SND_RAWMIDI_LFLG_APPEND) {
		if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
			if (rmidi->flags & SND_RAWMIDI_LFLG_OUTPUT)
				if (!(rmidi->flags & SND_RAWMIDI_LFLG_APPEND)) {
					err = -EAGAIN;
					goto _err;
				}
			if (rmidi->flags & (mode & SND_RAWMIDI_LFLG_INPUT)) {
				err = -EAGAIN;
				goto _err;
			}
		} else {
			mode &= ~SND_RAWMIDI_LFLG_APPEND;
			goto _std;
		}
	} else {
	       _std:
		if (rmidi->flags & mode) {
			err = -EAGAIN;
			goto _err;
		}
	}
	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_rawmidi_init_buffer(rmidi, SND_RAWMIDI_CHANNEL_INPUT) < 0) {
			err = -ENOMEM;
			goto _err;
		}
		if ((err = rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].hw.open(rmidi)) < 0)
			goto _err;
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		if (rmidi->flags & SND_RAWMIDI_LFLG_OUTPUT)
			goto __skip_output;
		if (snd_rawmidi_init_buffer(rmidi, SND_RAWMIDI_CHANNEL_OUTPUT) < 0) {
			if (mode & SND_RAWMIDI_LFLG_INPUT)
				rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].hw.close(rmidi);
			err = -ENOMEM;
			goto _err;
		}
		if ((err = rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].hw.open(rmidi)) < 0) {
			if (mode & SND_RAWMIDI_LFLG_INPUT)
				rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].hw.close(rmidi);
			goto _err;
		}
	      __skip_output:
		rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].use_count++;
	}
	rmidi->flags |= mode;
	MOD_INC_USE_COUNT;
	rmidi->card->use_inc(rmidi->card);
	up(&rmidi->open_mutex);
	if (out)
		*out = rmidi;
	return 0;

 _err:
	up(&rmidi->open_mutex);
	return err;
}

static int snd_rawmidi_open(unsigned short minor, int cardnum, int device,
			    struct file *file)
{
	unsigned short fflags;
	int err;
	snd_rawmidi_t *rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (rmidi == NULL)
		return -ENODEV;

	fflags = snd_rawmidi_file_flags(file);
	if ((file->f_flags & O_APPEND) && !(file->f_flags & O_NONBLOCK))
		return -EINVAL;		/* invalid combination */
	if ((file->f_flags & O_APPEND) || minor < 256)
		fflags |= SND_RAWMIDI_LFLG_APPEND;
	while (1) {
		err = snd_rawmidi_kernel_open(cardnum, device, fflags, &rmidi);
		if (err >= 0)
			break;
		if (err == -EAGAIN) {
			if (file->f_flags & O_NONBLOCK)
				return -EBUSY;
		} else {
			return err;
		}
		interruptible_sleep_on(&rmidi->open_wait);
		if (signal_pending(current))
			return -ERESTARTSYS;
	}
	file->private_data = rmidi;
	return 0;
}

int snd_rawmidi_kernel_release(snd_rawmidi_t *rmidi, int mode)
{
	snd_debug_check(rmidi == NULL, -ENXIO);
	down(&rmidi->open_mutex);
	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].hw.trigger(rmidi, 0);
		rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].hw.close(rmidi);
		snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_CHANNEL_INPUT);
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].use_count--;
		if (rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].use_count == 0) {
			snd_rawmidi_flush_output(rmidi);
			rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].hw.close(rmidi);
			snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_CHANNEL_OUTPUT);
		}
	}
	rmidi->flags &= ~mode;
	rmidi->card->use_dec(rmidi->card);
	MOD_DEC_USE_COUNT;
	up(&rmidi->open_mutex);
	return 0;
}

static int snd_rawmidi_release(unsigned short minor, int cardnum, int device,
			       struct file *file)
{
	unsigned short fflags;
	snd_rawmidi_t *rmidi;
	int err;

	fflags = snd_rawmidi_file_flags(file);
	rmidi = snd_magic_cast(snd_rawmidi_t, file->private_data, -ENXIO);
	err = snd_rawmidi_kernel_release(rmidi, fflags);
	wake_up(&rmidi->open_wait);
	return err;
}

int snd_rawmidi_kernel_info(int cardnum, int device, snd_rawmidi_info_t *info)
{
	snd_rawmidi_t *rmidi;

	snd_debug_check(info == NULL, -ENXIO);
	rmidi = snd_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (rmidi == NULL)
		return -ENODEV;
	memset(info, 0, sizeof(*info));
	info->type = rmidi->card->type;
	info->flags = rmidi->info_flags;
	strcpy(info->id, rmidi->id);
	strcpy(info->name, rmidi->name);
	return 0;
}

static int snd_rawmidi_info(snd_rawmidi_t * rmidi, snd_rawmidi_info_t * _info)
{
	snd_rawmidi_info_t info;
	int err;

	if ((err = snd_rawmidi_kernel_info(rmidi->card->number, rmidi->device, &info)) < 0)
		return err;
	if (copy_to_user(_info, &info, sizeof(snd_rawmidi_info_t)))
		return -EFAULT;
	return 0;
}

int snd_rawmidi_output_params(snd_rawmidi_t * rmidi,
			      snd_rawmidi_params_t * params)
{
	char *newbuf;
	snd_rawmidi_channel_t *pchn;

	if (rmidi->flags & SND_RAWMIDI_LFLG_APPEND)
		return -EBUSY;
	snd_rawmidi_flush_output(rmidi);
	if (params->size < 32 || params->size > 1024L * 1024L)
		return -EINVAL;
	if (params->room < 1 || params->room >= params->size)
		return -EINVAL;
	if (params->max < 1 || params->max >= params->size || params->max < params->room)
		return -EINVAL;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	if (params->size != pchn->size) {
		if ((newbuf = (char *) snd_kmalloc(params->size, GFP_KERNEL)) == NULL)
			return -ENOMEM;
		snd_kfree(pchn->buffer);
		pchn->buffer = newbuf;
		pchn->size = params->size;
	}
	pchn->used_max = params->max;
	pchn->used_room = params->room;
	return 0;
}

int snd_rawmidi_input_params(snd_rawmidi_t * rmidi,
			     snd_rawmidi_params_t * params)
{
	char *newbuf;
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];

	snd_rawmidi_flush_input(rmidi);
	if (params->size < 32 || params->size > 1024L * 1024L)
		return -EINVAL;
	if (params->min < 1 || params->min >= params->size)
		return -EINVAL;
	if (params->size != pchn->size) {
		if ((newbuf = (char *) snd_kmalloc(params->size, GFP_KERNEL)) == NULL)
			return -ENOMEM;
		snd_kfree(pchn->buffer);
		pchn->buffer = newbuf;
		pchn->size = params->size;
	}
	pchn->used_min = params->min;
	return 0;
}

static int snd_rawmidi_output_status(snd_rawmidi_t * rmidi,
				     snd_rawmidi_status_t * status)
{
	unsigned long flags;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	memset(status, 0, sizeof(*status));
	status->channel = SND_RAWMIDI_CHANNEL_OUTPUT;
	spin_lock_irqsave(&pchn->lock, flags);
	status->size = pchn->size;
	status->count = pchn->size - pchn->used;
	status->queue = pchn->used;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return 0;
}

static int snd_rawmidi_input_status(snd_rawmidi_t * rmidi,
				    snd_rawmidi_status_t * status)
{
	unsigned long flags;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	memset(status, 0, sizeof(*status));
	status->channel = SND_RAWMIDI_CHANNEL_INPUT;
	spin_lock_irqsave(&pchn->lock, flags);
	status->size = pchn->size;
	status->count = pchn->used;
	status->free = pchn->size - pchn->used;
	status->overrun = pchn->xruns;
	pchn->xruns = 0;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return 0;
}

static int snd_rawmidi_ioctl(struct file *file,
			     unsigned int cmd, unsigned long arg)
{
	snd_rawmidi_t *rmidi;
	unsigned short fflags;

	rmidi = snd_magic_cast(snd_rawmidi_t, file->private_data, -ENXIO);
	if (((cmd >> 8) & 0xff) != 'W')
		return -EINVAL;
	fflags = snd_rawmidi_file_flags(file);
	switch (cmd) {
	case SND_RAWMIDI_IOCTL_PVERSION:
		return put_user(SND_RAWMIDI_VERSION, (int *)arg) ? -EFAULT : 0;
	case SND_RAWMIDI_IOCTL_INFO:
		return snd_rawmidi_info(rmidi, (snd_rawmidi_info_t *) arg);
	case SND_RAWMIDI_IOCTL_CHANNEL_PARAMS:
	{
		snd_rawmidi_params_t params;
		if (copy_from_user(&params, (snd_rawmidi_params_t *) arg, sizeof(snd_rawmidi_params_t)))
			return -EFAULT;
		switch (params.channel) {
		case SND_RAWMIDI_CHANNEL_OUTPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
				return -EINVAL;
			return snd_rawmidi_output_params(rmidi, &params);
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			return snd_rawmidi_input_params(rmidi, &params);
		default:
			return -EINVAL;
		}
	}
	case SND_RAWMIDI_IOCTL_CHANNEL_STATUS:
	{
		int err = 0;
		snd_rawmidi_status_t status;
		if (copy_from_user(&status, (snd_rawmidi_status_t *) arg, sizeof(snd_rawmidi_status_t)))
			return -EFAULT;
		switch (status.channel) {
		case SND_RAWMIDI_CHANNEL_OUTPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
				return -EINVAL;
			err = snd_rawmidi_output_status(rmidi, &status);
			break;
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			err = snd_rawmidi_input_status(rmidi, &status);
			break;
		default:
			return -EINVAL;
		}
		if (err < 0)
			return err;
		if (copy_to_user((snd_rawmidi_status_t *) arg, &status, sizeof(snd_rawmidi_status_t)))
			return -EFAULT;
		return 0;
	}
	case SND_RAWMIDI_IOCTL_CHANNEL_DRAIN:
	{
		int val;
		if (get_user(val, (long *) arg))
			return -EFAULT;
		switch (val) {
		case SND_RAWMIDI_CHANNEL_OUTPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
				return -EINVAL;
			return snd_rawmidi_drain_output(rmidi);
		default:
			return -EINVAL;
		}
	}
	case SND_RAWMIDI_IOCTL_CHANNEL_FLUSH:
	{
		int val;
		if (get_user(val, (long *) arg))
			return -EFAULT;
		switch (val) {
		case SND_RAWMIDI_CHANNEL_OUTPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
				return -EINVAL;
			return snd_rawmidi_flush_output(rmidi);
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			return snd_rawmidi_flush_input(rmidi);
		default:
			return -EINVAL;
		}
	}
#ifdef CONFIG_SND_DEBUG
	default:
		snd_printk("rawmidi: unknown command = 0x%x\n", cmd);
#endif
	}
	return -EINVAL;
}

int snd_rawmidi_control_ioctl(snd_card_t * card, snd_control_t * control,
			      unsigned int cmd, unsigned long arg)
{
	unsigned int tmp;
	int idx;
	snd_rawmidi_t *rmidi;

	tmp = card->number * SND_RAWMIDI_DEVICES;
	/* ignore VIRMIDI requests */
	if (card->type == SND_CARD_TYPE_VIRMIDI)
		return -ENOIOCTLCMD;
	switch (cmd) {
	case SND_CTL_IOCTL_HW_INFO:
		{
			struct snd_ctl_hw_info *ptr = (struct snd_ctl_hw_info *) arg;

			ptr->mididevs = 0;
			for (idx = SND_RAWMIDI_DEVICES - 1; idx >= 0; idx--) {
				if (snd_rawmidi_devices[tmp + idx]) {
					ptr->mididevs = idx + 1;
					break;
				}
			}
			return 0;
		}
	case SND_CTL_IOCTL_RAWMIDI_DEVICE:
		{
			int val;
			
			if (get_user(val, (long *) arg))
				return -EFAULT;
			if (val < 0 || val >= SND_RAWMIDI_DEVICES)
				return -ENXIO;
			if (snd_rawmidi_devices[tmp + val] == NULL)
				return -ENXIO;
			control->rawmidi_device = val;
			return 0;
		}
	case SND_CTL_IOCTL_RAWMIDI_CHANNEL:
		{
			int val;
			
			if (get_user(val, (int *)arg))
				return -EFAULT;
			if (val < 0 || val > 1)
				return -ENXIO;
			control->rawmidi_channel = val;
			return 0;
		}
	case SND_CTL_IOCTL_RAWMIDI_INFO:
		rmidi = snd_rawmidi_devices[tmp + control->rawmidi_device];
		if (rmidi == NULL)
			return -ENXIO;
		switch (cmd) {
		case SND_CTL_IOCTL_RAWMIDI_INFO:
			return snd_rawmidi_info(rmidi, (snd_rawmidi_info_t *) arg);
		}
		break;
	}
	return -ENOIOCTLCMD;
}

void snd_rawmidi_receive_reset(snd_rawmidi_t * rmidi)
{
	/* TODO: reset current state */
}

int snd_rawmidi_receive(snd_rawmidi_t * rmidi, unsigned char *buffer, int count)
{
	unsigned long flags;
	int result = 0, count1;
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];

	if (pchn->buffer == NULL) {
		snd_printd("snd_rawmidi_receive: input is not active!!!\n");
		return -EINVAL;
	}
	spin_lock_irqsave(&pchn->lock, flags);
	if (count == 1) {	/* special case, faster code */
		pchn->bytes++;
		if (pchn->used < pchn->size) {
			pchn->buffer[pchn->head++] = buffer[0];
			pchn->head %= pchn->size;
			pchn->used++;
			result++;
		} else {
			pchn->xruns++;
		}
	} else {
		pchn->bytes += count;
		count1 = pchn->size - pchn->head;
		if (count1 > count)
			count1 = count;
		if (count1 > pchn->size - pchn->used)
			count1 = pchn->size - pchn->used;
		memcpy(pchn->buffer + pchn->head, buffer, count1);
		pchn->head += count1;
		pchn->head %= pchn->size;
		pchn->used += count1;
		count -= count1;
		result += count1;
		if (count > 0) {
			count1 = count;
			if (count > pchn->size - pchn->used) {
				count1 = pchn->size - pchn->used;
				pchn->xruns = count - count1;
			}
			if (count1 > 0) {
				memcpy(pchn->buffer, buffer + count1, count1);
				pchn->head = count1;
				pchn->used += count1;
				result += count1;
			}
		}
	}
	spin_unlock_irqrestore(&pchn->lock, flags);
	if (result > 0) {
		if (pchn->event) {
			pchn->event(rmidi);
		} else {
			if (snd_rawmidi_input_ok(rmidi))
				wake_up(&pchn->sleep);
		}
	}
	return result;
}

static long snd_rawmidi_kernel_read1(snd_rawmidi_t *rmidi, unsigned char *buf, long count, int kernel)
{
	unsigned long flags;
	long result = 0, count1;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	while (count > 0 && pchn->used) {
		count1 = pchn->size - pchn->tail;
		if (count1 > count)
			count1 = count;
		spin_lock_irqsave(&pchn->lock, flags);
		if (count1 > pchn->used)
			count1 = pchn->used;
		if (kernel) {
			memcpy(buf, pchn->buffer + pchn->tail, count1);
		} else {
			if (copy_to_user(buf, pchn->buffer + pchn->tail, count1)) {
				spin_unlock_irqrestore(&pchn->lock, flags);
				return result > 0 ? result : -EFAULT;
			}
		}
		pchn->tail += count1;
		pchn->tail %= pchn->size;
		pchn->used -= count1;
		spin_unlock_irqrestore(&pchn->lock, flags);
		result += count1;
		count -= count1;
	}
	return result;
}

long snd_rawmidi_kernel_read(snd_rawmidi_t *rmidi, unsigned char *buf, long count)
{
	snd_rawmidi_channel_t *pchn;
	
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	pchn->flags |= SND_RAWMIDI_FLG_TRIGGER;
	pchn->hw.trigger(rmidi, 1);
	return snd_rawmidi_kernel_read1(rmidi, buf, count, 1);
}

static long snd_rawmidi_read(struct file *file, char *buf, long count)
{
	long result;
	int count1;
	snd_rawmidi_t *rmidi;
	snd_rawmidi_channel_t *pchn;

	rmidi = snd_magic_cast(snd_rawmidi_t, file->private_data, -ENXIO);
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	pchn->flags |= SND_RAWMIDI_FLG_TRIGGER;
	pchn->hw.trigger(rmidi, 1);
	result = 0;
	while (count > 0) {
		while (!snd_rawmidi_input_ok(rmidi)) {
			if (file->f_flags & O_NONBLOCK)
				return result > 0 ? result : -EAGAIN;
			interruptible_sleep_on(&pchn->sleep);
			if (signal_pending(current))
				return result > 0 ? result : -EAGAIN;
			if (!pchn->used)
				return -EIO;
		}
		count1 = snd_rawmidi_kernel_read1(rmidi, buf, count, 0);
		result += count1;
		buf += count1;
		count -= count1;
	}
	return result;
}

void snd_rawmidi_transmit_reset(snd_rawmidi_t * rmidi)
{
	/* TODO: reset current state */
}

int snd_rawmidi_transmit_empty(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_channel_t *pchn;
	int result;
	unsigned long flags;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	if (pchn->buffer == NULL) {
		snd_printd("snd_rawmidi_transmit_empty: output is not active!!!\n");
		return 1;
	}
	spin_lock_irqsave(&pchn->lock, flags);
	result = pchn->used <= 0;
	if (result)
		pchn->flags &= ~SND_RAWMIDI_FLG_TRIGGER;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return result;		
}

int snd_rawmidi_transmit(snd_rawmidi_t * rmidi, unsigned char *buffer, int count)
{
	unsigned long flags;
	int result, count1;
	snd_rawmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	if (pchn->buffer == NULL) {
		snd_printd("snd_rawmidi_transmit: output is not active!!!\n");
		return 0;
	}
	result = 0;
	spin_lock_irqsave(&pchn->lock, flags);
	if (pchn->used == 0) {
		/* warning: lowlevel layer MUST trigger down the hardware */
		pchn->flags &= ~SND_RAWMIDI_FLG_TRIGGER;
		goto __skip;
	}
	if (count == 1) {	/* special case, faster code */
		buffer[0] = pchn->buffer[pchn->tail++];
		pchn->tail %= pchn->size;
		pchn->used--;
		pchn->bytes++;
		result++;
	} else {
		count1 = pchn->size - pchn->tail;
		if (count1 > count)
			count1 = count;
		if (count1 > pchn->used)
			count1 = pchn->used;
		memcpy(buffer, pchn->buffer + pchn->tail, count1);
		pchn->tail += count1;
		pchn->tail %= pchn->size;
		pchn->used -= count1;
		count -= count1;
		result += count1;
		if (count > 0) {
			memcpy(buffer + count1, pchn->buffer, count);
			pchn->tail = count;
			pchn->used -= count;
			result += count;
		}
		pchn->bytes += result;
	}
      __skip:
	spin_unlock_irqrestore(&pchn->lock, flags);
	if (result > 0) {
		if (pchn->event) {
			pchn->event(rmidi);
		} else {
			if (snd_rawmidi_output_ok(rmidi))
				wake_up(&pchn->sleep);
		}
	} else {
		if (pchn->flags & SND_RAWMIDI_FLG_FLUSH)
			wake_up(&pchn->sleep);
	}
	return result;
}

static long snd_rawmidi_kernel_write1(snd_rawmidi_t * rmidi, const unsigned char *buf, long count, int kernel)
{
	unsigned long flags;
	long count1, result;
	snd_rawmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];

	result = 0;
	spin_lock_irqsave(&pchn->lock, flags);
	if (rmidi->flags & SND_RAWMIDI_LFLG_APPEND) {
		if (pchn->size - pchn->used < count) {
			spin_unlock_irqrestore(&pchn->lock, flags);
			return -EAGAIN;
		}
	}
	while (count > 0 && pchn->used < pchn->size) {
		count1 = pchn->size - pchn->head;
		if (count1 > count)
			count1 = count;
		if (count1 > pchn->size - pchn->used)
			count1 = pchn->size - pchn->used;
		if (kernel) {
			memcpy(pchn->buffer + pchn->head, buf, count1);
		} else {
			if (copy_from_user(pchn->buffer + pchn->head, buf, count1)) {
				result = result > 0 ? result : -EFAULT;
				goto __end;
			}
		}
		pchn->head += count1;
		pchn->head %= pchn->size;
		pchn->used += count1;
		result += count1;
		buf += count1;
		count -= count1;
	}
      __end:
	if (result > 0)
		pchn->flags |= SND_RAWMIDI_FLG_TRIGGER;
	spin_unlock_irqrestore(&pchn->lock, flags);
	pchn->hw.trigger(rmidi, 1);
	return result;
}

long snd_rawmidi_kernel_write(snd_rawmidi_t * rmidi, const unsigned char *buf, long count)
{
	return snd_rawmidi_kernel_write1(rmidi, buf, count, 1);
}

static long snd_rawmidi_write(struct file *file, const char *buf, long count)
{
	long result, timeout;
	int count1;
	snd_rawmidi_t *rmidi;
	snd_rawmidi_channel_t *pchn;

	rmidi = snd_magic_cast(snd_rawmidi_t, file->private_data, -ENXIO);
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	result = 0;
	while (count > 0) {
		while (!snd_rawmidi_output_ok(rmidi)) {
			if (file->f_flags & O_NONBLOCK)
				return result > 0 ? result : -EAGAIN;
			timeout = interruptible_sleep_on_timeout(&pchn->sleep, 30 * HZ);
			if (signal_pending(current))
				return -ERESTARTSYS;
			if (pchn->used >= pchn->size && !timeout)
				return -EIO;
		}
		count1 = snd_rawmidi_kernel_write1(rmidi, buf, count, 0);
		result += count1;
		buf += count1;
		count -= count1;
		if (count1 < count && (file->f_flags & O_NONBLOCK))
			break;
	}
	return result;
}

static unsigned int snd_rawmidi_poll(struct file *file, poll_table * wait)
{
	snd_rawmidi_t *rmidi;
	unsigned int mask;
	unsigned short fflags;

	fflags = snd_rawmidi_file_flags(file);
	rmidi = snd_magic_cast(snd_rawmidi_t, file->private_data, 0);

	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		snd_rawmidi_channel_t *pchn;
		pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
		pchn->flags |= SND_RAWMIDI_FLG_TRIGGER;
		pchn->hw.trigger(rmidi, 1);
		poll_wait(file, &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].sleep, wait);
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT)
		poll_wait(file, &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT].sleep, wait);
	mask = 0;
	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_rawmidi_input_ok(rmidi))
			mask |= POLLIN | POLLRDNORM;
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT) {
		if (snd_rawmidi_output_ok(rmidi))
			mask |= POLLOUT | POLLWRNORM;
	}
	return mask;
}

/*

 */

static void snd_rawmidi_proc_info_read(snd_info_buffer_t * buffer,
				       void *private_data)
{
	snd_rawmidi_t *rmidi;

	rmidi = snd_magic_cast(snd_rawmidi_t, private_data, );
	snd_iprintf(buffer, "%s\n\n", rmidi->name);
	if (rmidi->info_flags & SND_RAWMIDI_INFO_OUTPUT) {
		snd_rawmidi_channel_t *pchn;
		pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
		snd_iprintf(buffer,
			    "Output\n"
			    "  Tx bytes     : %u\n",
			    pchn->bytes);
		if (rmidi->flags & SND_RAWMIDI_LFLG_OUTPUT) {
			snd_iprintf(buffer,
				    "  Mode         : %s\n"
				    "  Buffer size  : %d\n",
				    pchn->flags & SND_RAWMIDI_FLG_OSS ?
				    		"OSS compatible" : "native",
				    pchn->size);
		}
	}
	if (rmidi->info_flags & SND_RAWMIDI_INFO_INPUT) {
		snd_rawmidi_channel_t *pchn;
		pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
		snd_iprintf(buffer,
			    "Input\n"
			    "  Rx bytes     : %u\n",
			    pchn->bytes);
		if (rmidi->flags & SND_RAWMIDI_LFLG_INPUT) {
			snd_iprintf(buffer,
				    "  Buffer size  : %d\n"
				    "  Overruns     : %d\n",
				    pchn->size,
				    pchn->xruns);
		}
	}
}

/*
 *  Register functions
 */

static snd_minor_t snd_rawmidi_reg =
{
	comment:	"raw midi",
	read:		snd_rawmidi_read,
	write:		snd_rawmidi_write,
	open:		snd_rawmidi_open,
	release:	snd_rawmidi_release,
	poll:		snd_rawmidi_poll,
	ioctl:		snd_rawmidi_ioctl,
};

int snd_rawmidi_new(snd_card_t * card, char *id, int device, snd_rawmidi_t ** rrawmidi)
{
	snd_rawmidi_t *rmidi;
	int err;
	static snd_device_ops_t ops = {
		(snd_dev_free_t *)snd_rawmidi_free,
		(snd_dev_register_t *)snd_rawmidi_register,
		(snd_dev_unregister_t *)snd_rawmidi_unregister
	};
	snd_rawmidi_channel_t *ichn, *ochn;

	snd_debug_check(rrawmidi == NULL, -EINVAL);
	*rrawmidi = NULL;
	snd_debug_check(card == NULL, -ENXIO);
	rmidi = snd_magic_kcalloc(snd_rawmidi_t, 0, GFP_KERNEL);
	if (rmidi == NULL)
		return -ENOMEM;
	ichn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	ochn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	rmidi->card = card;
	rmidi->device = device;
	spin_lock_init(&ichn->lock);
	spin_lock_init(&ochn->lock);
	init_waitqueue_head(&ichn->sleep);
	init_waitqueue_head(&ochn->sleep);
	init_MUTEX(&rmidi->open_mutex);
	init_waitqueue_head(&rmidi->open_wait);
	snd_switch_prepare(card, &ichn->switches, rmidi, SND_CTL_IFACE_RAWMIDI, device, SND_RAWMIDI_CHANNEL_INPUT);
	snd_switch_prepare(card, &ochn->switches, rmidi, SND_CTL_IFACE_RAWMIDI, device, SND_RAWMIDI_CHANNEL_OUTPUT);
	if (id) {
		strncpy(rmidi->id, id, sizeof(rmidi->id) - 1);
	}
	if ((err = snd_device_new(card, SND_DEV_RAWMIDI, rmidi, device, &ops, NULL)) < 0) {
		snd_rawmidi_free(rmidi);
		return err;
	}
	*rrawmidi = rmidi;
	return 0;
}

static int snd_rawmidi_free(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_channel_t *ichn, *ochn;
	snd_debug_check(rmidi == NULL, -ENXIO);
	ichn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	ochn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	snd_switch_free(rmidi->card, &ochn->switches);
	snd_switch_free(rmidi->card, &ichn->switches);
	if (ochn->hw.private_free)
		ochn->hw.private_free(ochn->hw.private_data);
	if (ichn->hw.private_free)
		ichn->hw.private_free(ichn->hw.private_data);
	if (rmidi->private_free)
		rmidi->private_free(rmidi->private_data);
	snd_magic_kfree(rmidi);
	return 0;
}

static int snd_rawmidi_register(snd_rawmidi_t * rmidi, snd_device_t *devptr)
{
	int idx, err;
	snd_info_entry_t *entry;
	char name[16];

	snd_debug_check(devptr == NULL, -ENXIO);
	if (rmidi->device >= SND_RAWMIDI_DEVICES)
		return -ENOMEM;
	down(&register_mutex);
	idx = (rmidi->card->number * SND_RAWMIDI_DEVICES) + rmidi->device;
	if (snd_rawmidi_devices[idx] != NULL) {
		up(&register_mutex);
		return -EBUSY;
	}
	snd_rawmidi_devices[idx] = rmidi;
	sprintf(name, "midiC%iD%i", rmidi->card->number, rmidi->device);
	if ((err = snd_register_device(SND_DEVICE_TYPE_RAWMIDI,
				       rmidi->card, rmidi->device,
				       &snd_rawmidi_reg, name)) < 0) {
		snd_printk("unable to register rawmidi device %i:%i\n", rmidi->card->number, rmidi->device);
		snd_rawmidi_devices[idx] = NULL;
		up(&register_mutex);
		return err;
	}
#ifdef CONFIG_SND_OSSEMUL
	rmidi->ossreg = 0;
	if (rmidi->device < 2) {
		if (snd_register_oss_device(SND_OSS_DEVICE_TYPE_MIDI,
				rmidi->card, rmidi->device, &snd_rawmidi_reg, name) < 0) {
			snd_printk("unable to register OSS rawmidi device %i:%i\n", rmidi->card->number, rmidi->device);
		} else {
			rmidi->ossreg = 1;
		}
	}
#endif
	up(&register_mutex);
	sprintf(name, "midiD%d", rmidi->device);
	entry = snd_info_create_entry(rmidi->card, name);
	if (entry) {
		entry->private_data = rmidi;
		entry->t.text.read_size = 1024;
		entry->t.text.read = snd_rawmidi_proc_info_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	rmidi->proc_entry = entry;
#ifdef CONFIG_SND_OSSEMUL
	if (rmidi->device == 0)
		snd_oss_info_register(SND_OSS_INFO_DEV_MIDI, rmidi->card->number, rmidi->name);
#endif
#ifdef CONFIG_SND_SEQUENCER
	if (snd_seq_device_new(rmidi->card, rmidi->device, SND_SEQ_DEV_ID_MIDISYNTH, 0, &rmidi->seq_dev) >= 0) {
		sprintf(rmidi->seq_dev->name, "MIDI %d-%d", rmidi->card->number, rmidi->device);
		snd_device_register(rmidi->card, rmidi->seq_dev);
	}
#endif
	return 0;
}

static int snd_rawmidi_unregister(snd_rawmidi_t * rmidi)
{
	int idx;

	snd_debug_check(rmidi == NULL, -ENXIO);
	down(&register_mutex);
	idx = (rmidi->card->number * SND_RAWMIDI_DEVICES) + rmidi->device;
	if (snd_rawmidi_devices[idx] != rmidi) {
		up(&register_mutex);
		return -EINVAL;
	}
#ifdef CONFIG_SND_OSSEMUL
	if (rmidi->device == 0)
		snd_oss_info_unregister(SND_OSS_INFO_DEV_MIDI, rmidi->card->number);
#endif
	if (rmidi->proc_entry) {
		snd_info_unregister(rmidi->proc_entry);
		rmidi->proc_entry = NULL;
	}
#ifdef CONFIG_SND_OSSEMUL
	if (rmidi->ossreg)
		snd_unregister_oss_device(SND_OSS_DEVICE_TYPE_MIDI, rmidi->card, rmidi->device);
#endif
	snd_unregister_device(SND_DEVICE_TYPE_RAWMIDI, rmidi->card, rmidi->device);
	snd_rawmidi_devices[idx] = NULL;
	up(&register_mutex);
#ifdef CONFIG_SND_SEQUENCER
	if (rmidi->seq_dev) {
		snd_device_free(rmidi->card, rmidi->seq_dev);
		rmidi->seq_dev = NULL;
	}
#endif
	return snd_rawmidi_free(rmidi);
}

int snd_rawmidi_switch_add(snd_rawmidi_channel_t * pchn, snd_kswitch_t * ksw)
{
	return snd_switch_add(&pchn->switches, ksw);
}

int snd_rawmidi_switch_remove(snd_rawmidi_channel_t * pchn, snd_kswitch_t * ksw)
{
	return snd_switch_remove(&pchn->switches, ksw);
}

snd_kswitch_t *snd_rawmidi_switch_new(snd_rawmidi_channel_t * pchn, snd_kswitch_t * ksw, void *private_data)
{
	snd_kswitch_t *sw;
	
	sw = snd_switch_new(ksw);
	if (sw == NULL)
		return NULL;
	if (snd_rawmidi_switch_add(pchn, sw) < 0) {
		snd_switch_free_one(sw);
		return NULL;
	}
	sw->private_data = private_data;
	return sw;
}

/*
 *
 */

/*
 *  ENTRY functions
 */

static int __init alsa_rawmidi_init(void)
{
	snd_control_register_ioctl(snd_rawmidi_control_ioctl);
	return 0;
}

static void __exit alsa_rawmidi_exit(void)
{
	snd_control_unregister_ioctl(snd_rawmidi_control_ioctl);
}

module_init(alsa_rawmidi_init)
module_exit(alsa_rawmidi_exit)

EXPORT_SYMBOL(snd_rawmidi_output_params);
EXPORT_SYMBOL(snd_rawmidi_input_params);
EXPORT_SYMBOL(snd_rawmidi_drain_output);
EXPORT_SYMBOL(snd_rawmidi_flush_output);
EXPORT_SYMBOL(snd_rawmidi_flush_input);
EXPORT_SYMBOL(snd_rawmidi_receive_reset);
EXPORT_SYMBOL(snd_rawmidi_receive);
EXPORT_SYMBOL(snd_rawmidi_transmit_reset);
EXPORT_SYMBOL(snd_rawmidi_transmit_empty);
EXPORT_SYMBOL(snd_rawmidi_transmit);
EXPORT_SYMBOL(snd_rawmidi_new);
EXPORT_SYMBOL(snd_rawmidi_switch_add);
EXPORT_SYMBOL(snd_rawmidi_switch_remove);
EXPORT_SYMBOL(snd_rawmidi_switch_new);
EXPORT_SYMBOL(snd_rawmidi_info);
EXPORT_SYMBOL(snd_rawmidi_kernel_info);
EXPORT_SYMBOL(snd_rawmidi_kernel_open);
EXPORT_SYMBOL(snd_rawmidi_kernel_release);
EXPORT_SYMBOL(snd_rawmidi_kernel_read);
EXPORT_SYMBOL(snd_rawmidi_kernel_write);
