/*
 *  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.
 *
 */

#include "../include/driver.h"
#include "../include/minors.h"
#include "../include/midi.h"
#include "../include/control.h"
#include "../include/info.h"
#include "../include/sndpersist.h"

snd_rawmidi_t *snd_rawmidi_devices[SND_CARDS * SND_RAWMIDI_DEVICES];

static DECLARE_MUTEX(register_mutex);

int snd_rawmidi_receive(snd_rawmidi_t * rmidi, char *buffer, int count);
int snd_rawmidi_transmit(snd_rawmidi_t * rmidi, char *buffer, int count);

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;

	spin_lock_irqsave(&rmidi->input_lock, flags);
	result = rmidi->input.u.s.used >= rmidi->input.u.s.used_min;
	spin_unlock_irqrestore(&rmidi->input_lock, flags);
	return result;
}

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

	spin_lock_irqsave(&rmidi->output_lock, flags);
	free = rmidi->output.u.s.size - rmidi->output.u.s.used;
	result = rmidi->output.u.s.used <= rmidi->output.u.s.used_max &&
	         free >= rmidi->output.u.s.used_room;
	spin_unlock_irqrestore(&rmidi->output_lock, flags);
	return result;
}

int snd_rawmidi_init_buffer(snd_rawmidi_t * rmidi, int direction)
{
	struct snd_stru_rawmidi_direction *pdir;

	pdir = direction == SND_RAWMIDI_LFLG_INPUT ?
					&rmidi->input : &rmidi->output;
	pdir->mode = SND_RAWMIDI_MODE_STREAM;
	pdir->reset = NULL;
	pdir->u.s.size = PAGE_SIZE;
	pdir->u.s.used_max = pdir->u.s.size - 1;
	pdir->u.s.used_room = 1;
	pdir->u.s.used_min = 1;
	pdir->u.s.buffer = NULL;
	if ((pdir->u.s.buffer = snd_kmalloc(pdir->u.s.size, GFP_KERNEL)) == NULL) {
		return -ENOMEM;
	}
	pdir->u.s.head = pdir->u.s.tail = pdir->u.s.used = 0;
	return 0;
}

int snd_rawmidi_done_buffer(snd_rawmidi_t * rmidi, int direction)
{
	struct snd_stru_rawmidi_direction *pdir;

	pdir = direction == SND_RAWMIDI_LFLG_INPUT ?
					&rmidi->input : &rmidi->output;
	if (pdir->u.s.buffer) {
		snd_kfree(pdir->u.s.buffer);
		pdir->u.s.buffer = NULL;
	}
	return 0;
}

static void snd_rawmidi_poll_output(unsigned long data)
{
	unsigned long flags;
	snd_rawmidi_t *rmidi;

	rmidi = (snd_rawmidi_t *) data;
	rmidi->output.hw.io.write(rmidi);
	spin_lock_irqsave(&rmidi->output_lock, flags);
	if (rmidi->output.u.s.used > 0) {
		rmidi->output.flags |= SND_RAWMIDI_FLG_TIMER;
		rmidi->output.timer.expires = 1 + jiffies;
		add_timer(&rmidi->output.timer);
	} else {
		del_timer(&rmidi->output.timer);
		rmidi->output.flags &= ~SND_RAWMIDI_FLG_TIMER;
	}
	spin_unlock_irqrestore(&rmidi->output_lock, flags);
}

void snd_rawmidi_trigger_output(snd_rawmidi_t * rmidi, int up)
{
	unsigned long flags;
	int flg;

#if 0
	snd_printk("trigger_output - %i\n", up);
#endif
	if (!(rmidi->output.hw.flags & SND_RAWMIDI_HW_POLL)) {
		flg = up ? SND_RAWMIDI_FLG_TRIGGER : 0;
		spin_lock_irqsave(&rmidi->output_lock, flags);
		if ((rmidi->output.flags & SND_RAWMIDI_FLG_TRIGGER) != flg) {
			if (up) {
				rmidi->output.flags |= SND_RAWMIDI_FLG_TRIGGER;
			} else {
				rmidi->output.flags &= ~SND_RAWMIDI_FLG_TRIGGER;
			}
			spin_unlock_irqrestore(&rmidi->output_lock, flags);
			rmidi->output.hw.trigger(rmidi, up);
			return;
		}
		spin_unlock_irqrestore(&rmidi->output_lock, flags);
	} else {
		spin_lock_irqsave(&rmidi->output_lock, flags);
		if (up) {
			if (!(rmidi->output.flags & SND_RAWMIDI_FLG_TIMER)) {
				spin_unlock_irqrestore(&rmidi->output_lock, flags);
				rmidi->output.hw.io.write(rmidi);
				spin_lock_irqsave(&rmidi->output_lock, flags);
				if (rmidi->output.u.s.used > 0) {
					rmidi->output.flags |= SND_RAWMIDI_FLG_TIMER;
					rmidi->output.timer.expires = 1 + jiffies;
					add_timer(&rmidi->output.timer);
				}
			}
		} else {
			if (rmidi->output.flags & SND_RAWMIDI_FLG_TIMER) {
				rmidi->output.flags &= ~SND_RAWMIDI_FLG_TIMER;
				del_timer(&rmidi->output.timer);
			}
		}
		spin_unlock_irqrestore(&rmidi->output_lock, flags);
	}
}

static void snd_rawmidi_poll_input(unsigned long data)
{
	unsigned long flags;
	snd_rawmidi_t *rmidi;

	rmidi = (snd_rawmidi_t *) data;
	rmidi->output.hw.io.read(rmidi);
	spin_lock_irqsave(&rmidi->input_lock, flags);
	rmidi->input.flags |= SND_RAWMIDI_FLG_TIMER;
	rmidi->input.timer.data = (unsigned long) rmidi;
	rmidi->input.timer.expires = 1 + jiffies;
	add_timer(&rmidi->input.timer);
	spin_unlock_irqrestore(&rmidi->input_lock, flags);
}

void snd_rawmidi_trigger_input(snd_rawmidi_t * rmidi, int up)
{
	unsigned long flags;
	int flg;

#if 0
	snd_printk("trigger_input - %i\n", up);
#endif
	if (!(rmidi->input.hw.flags & SND_RAWMIDI_HW_POLL)) {
		flg = up ? SND_RAWMIDI_FLG_TRIGGER : 0;
		spin_lock_irqsave(&rmidi->input_lock, flags);
		if ((rmidi->input.flags & SND_RAWMIDI_FLG_TRIGGER) != flg) {
			if (up) {
				rmidi->input.flags |= SND_RAWMIDI_FLG_TRIGGER;
			} else {
				rmidi->input.flags &= ~SND_RAWMIDI_FLG_TRIGGER;
			}
			spin_unlock_irqrestore(&rmidi->input_lock, flags);
			rmidi->input.hw.trigger(rmidi, up);
			return;
		}
		spin_unlock_irqrestore(&rmidi->input_lock, flags);
	} else {
		spin_lock_irqsave(&rmidi->input_lock, flags);
		if (up) {
			if (!(rmidi->input.flags & SND_RAWMIDI_FLG_TIMER)) {
				spin_unlock_irqrestore(&rmidi->input_lock, flags);
				rmidi->input.hw.io.read(rmidi);
				spin_lock_irqsave(&rmidi->input_lock, flags);
				rmidi->input.flags |= SND_RAWMIDI_FLG_TIMER;
				rmidi->input.timer.expires = 1 + jiffies;
				add_timer(&rmidi->input.timer);
			}
		} else {
			if (rmidi->input.flags & SND_RAWMIDI_FLG_TIMER) {
				rmidi->input.flags &= ~SND_RAWMIDI_FLG_TIMER;
				del_timer(&rmidi->input.timer);
			}
		}
		spin_unlock_irqrestore(&rmidi->input_lock, flags);
	}
}

int snd_rawmidi_drain_output(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_trigger_output(rmidi, 0);
	/* interrupts aren't enabled at this moment, so spinlock isn't needed */
	rmidi->output.u.s.used =
	  rmidi->output.u.s.head =
	  rmidi->output.u.s.tail = 0;
	return 0;
}

int snd_rawmidi_flush_output(snd_rawmidi_t * rmidi)
{
	int err;
	long timeout;

	err = 0;
	while (rmidi->output.u.s.used > 0) {
#if 0
		snd_printk("flush - used = %i\n", rmidi->output.u.s.used);
#endif
		timeout = interruptible_sleep_on_timeout(&rmidi->output_sleep, 10 * HZ);
		if (signal_pending(current)) {
			err = -EINTR;
			break;
		}
		if (rmidi->output.u.s.used > 0 && !timeout) {
			err = -EIO;
			break;
		}
	}
	snd_rawmidi_drain_output(rmidi);
	return err;
}

static int snd_rawmidi_flush_input(snd_rawmidi_t * rmidi)
{
	snd_rawmidi_trigger_input(rmidi, 0);
	/* interrupts aren't enabled at this moment, so spinlock isn't needed */
	rmidi->input.u.s.used =
	  rmidi->input.u.s.head =
	  rmidi->input.u.s.tail = 0;
	return 0;
}

static int snd_rawmidi_open(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_rawmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (!rmidi)
		return -ENODEV;
	down(&rmidi->open_mutex);
	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		if (!(rmidi->info_flags & SND_RAWMIDI_INFO_INPUT)) {
			up(&rmidi->open_mutex);
			return -ENXIO;
		}
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT) {
		if (!(rmidi->info_flags & SND_RAWMIDI_INFO_OUTPUT)) {
			up(&rmidi->open_mutex);
			return -ENXIO;
		}
	}
	if (rmidi->flags & fflags) {
		up(&rmidi->open_mutex);
		return -EBUSY;
	}
#ifdef CONFIG_SND_OSSEMUL
	if (minor < 256)
		file->f_flags |= O_NONBLOCK;
#endif
	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_rawmidi_init_buffer(rmidi, SND_RAWMIDI_LFLG_INPUT) < 0) {
			up(&rmidi->open_mutex);
			return -ENOMEM;
		}
		if ((err = rmidi->input.hw.open(rmidi)) < 0) {
			snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_INPUT);
			up(&rmidi->open_mutex);
			return err;
		}
		rmidi->input.data = snd_rawmidi_receive;
#ifdef CONFIG_SND_OSSEMUL
		if (minor < 256)
			rmidi->input.flags |= SND_RAWMIDI_FLG_OSS;
		else
			rmidi->input.flags &= ~SND_RAWMIDI_FLG_OSS;
#endif
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT) {
		if (snd_rawmidi_init_buffer(rmidi, SND_RAWMIDI_LFLG_OUTPUT) < 0) {
			if (fflags & SND_RAWMIDI_LFLG_INPUT) {
				rmidi->input.hw.close(rmidi);
				snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_INPUT);
				rmidi->input.data = NULL;
			}
			up(&rmidi->open_mutex);
			return -ENOMEM;
		}
		if ((err = rmidi->output.hw.open(rmidi)) < 0) {
			if (fflags & SND_RAWMIDI_LFLG_INPUT) {
				rmidi->input.hw.close(rmidi);
				snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_INPUT);
				rmidi->input.data = NULL;
			}
			up(&rmidi->open_mutex);
			return err;
		}
		rmidi->output.data = snd_rawmidi_transmit;
#ifdef CONFIG_SND_OSSEMUL
		if (minor < 256)
			rmidi->output.flags |= SND_RAWMIDI_FLG_OSS;
		else
			rmidi->output.flags &= ~SND_RAWMIDI_FLG_OSS;
#endif
	}
	rmidi->flags |= fflags;
	file->private_data = rmidi;
	MOD_INC_USE_COUNT;
	rmidi->card->use_inc(rmidi->card);
	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;

	fflags = snd_rawmidi_file_flags(file);
	rmidi = (snd_rawmidi_t *) file->private_data;
	if (!rmidi)
		return -ENODEV;
	down(&rmidi->open_mutex);
	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		snd_rawmidi_trigger_input(rmidi, 0);
		rmidi->input.hw.close(rmidi);
		rmidi->input.data = NULL;
		snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_INPUT);
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT) {
		if (!(file->f_flags & O_NONBLOCK))
			snd_rawmidi_flush_output(rmidi);
		snd_rawmidi_trigger_output(rmidi, 0);
		rmidi->output.hw.close(rmidi);
		rmidi->output.data = NULL;
		snd_rawmidi_done_buffer(rmidi, SND_RAWMIDI_LFLG_OUTPUT);
	}
	rmidi->flags &= ~fflags;
	rmidi->card->use_dec(rmidi->card);
	MOD_DEC_USE_COUNT;
	up(&rmidi->open_mutex);
	return 0;
}

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

	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);
	if (copy_to_user(_info, &info, sizeof(snd_rawmidi_info_t)))
		return -EFAULT;
	return 0;
}

static int snd_rawmidi_output_info(snd_rawmidi_t * rmidi,
				   snd_rawmidi_output_info_t * _info)
{
	snd_rawmidi_output_info_t info;

	memset(&info, 0, sizeof(info));
	info.switches = snd_switch_count(&rmidi->output.switches);
	if (copy_to_user(_info, &info, sizeof(snd_rawmidi_output_info_t)))
		return -EFAULT;
	return 0;
}

static int snd_rawmidi_input_info(snd_rawmidi_t * rmidi,
				  snd_rawmidi_input_info_t * _info)
{
	snd_rawmidi_input_info_t info;

	memset(&info, 0, sizeof(info));
	info.switches = snd_switch_count(&rmidi->input.switches);
	if (copy_to_user(_info, &info, sizeof(snd_rawmidi_input_info_t)))
		return -EFAULT;
	return 0;
}

static int snd_rawmidi_output_params(snd_rawmidi_t * rmidi,
				     snd_rawmidi_output_params_t * _params)
{
	snd_rawmidi_output_params_t params;
	char *newbuf;

	if (copy_from_user(&params, _params, sizeof(snd_rawmidi_output_params_t)))
		return -EFAULT;
	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;
	if ((newbuf = (char *) snd_kmalloc(params.size, GFP_KERNEL)) == NULL)
		return -ENOMEM;
	snd_kfree(rmidi->output.u.s.buffer);
	rmidi->output.u.s.buffer = newbuf;
	rmidi->output.u.s.size = params.size;
	rmidi->output.u.s.used_max = params.max;
	rmidi->output.u.s.used_room = params.room;
	return 0;
}

static int snd_rawmidi_input_params(snd_rawmidi_t * rmidi,
				    snd_rawmidi_input_params_t * _params)
{
	snd_rawmidi_input_params_t params;
	char *newbuf;

	if (copy_from_user(&params, _params, sizeof(snd_rawmidi_input_params_t)))
		return -EFAULT;
	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 ((newbuf = (char *) snd_kmalloc(params.size, GFP_KERNEL)) == NULL)
		return -ENOMEM;
	snd_kfree(rmidi->input.u.s.buffer);
	rmidi->input.u.s.buffer = newbuf;
	rmidi->input.u.s.size = params.size;
	rmidi->input.u.s.used_min = params.min;
	return 0;
}

static int snd_rawmidi_output_status(snd_rawmidi_t * rmidi,
				     snd_rawmidi_output_status_t * _status)
{
	unsigned long flags;
	snd_rawmidi_output_status_t status;

	memset(&status, 0, sizeof(status));
	spin_lock_irqsave(&rmidi->output_lock, flags);
	status.size = rmidi->output.u.s.size;
	status.count = rmidi->output.u.s.size - rmidi->output.u.s.used;
	status.queue = rmidi->output.u.s.used;
	spin_unlock_irqrestore(&rmidi->output_lock, flags);
	if (copy_to_user(_status, &status, sizeof(snd_rawmidi_output_status_t)))
		return -EFAULT;
	return 0;
}

static int snd_rawmidi_input_status(snd_rawmidi_t * rmidi,
				    snd_rawmidi_input_status_t * _status)
{
	unsigned long flags;
	snd_rawmidi_input_status_t status;

	memset(&status, 0, sizeof(status));
	spin_lock_irqsave(&rmidi->input_lock, flags);
	status.size = rmidi->input.u.s.size;
	status.count = rmidi->input.u.s.used;
	status.free = rmidi->input.u.s.size - rmidi->input.u.s.used;
	status.overrun = rmidi->input.u.s.xruns;
	rmidi->input.u.s.xruns = 0;
	spin_unlock_irqrestore(&rmidi->input_lock, flags);
	if (copy_to_user(_status, &status, sizeof(snd_rawmidi_input_status_t)))
		return -EFAULT;
	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_rawmidi_t *) file->private_data;
	if (!rmidi)
		return -EIO;
	if (((cmd >> 8) & 0xff) != 'W')
		return -EIO;
	fflags = snd_rawmidi_file_flags(file);
	switch (cmd) {
	case SND_RAWMIDI_IOCTL_PVERSION:
		return snd_ioctl_out((long *) arg, SND_RAWMIDI_VERSION);
	case SND_RAWMIDI_IOCTL_INFO:
		return snd_rawmidi_info(rmidi, (snd_rawmidi_info_t *) arg);
	case SND_RAWMIDI_IOCTL_OUTPUT_INFO:
		if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
			return -EIO;
		return snd_rawmidi_output_info(rmidi, (snd_rawmidi_output_info_t *) arg);
	case SND_RAWMIDI_IOCTL_INPUT_INFO:
		if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
			return -EIO;
		return snd_rawmidi_input_info(rmidi, (snd_rawmidi_input_info_t *) arg);
	case SND_RAWMIDI_IOCTL_OUTPUT_PARAMS:
		if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
			return -EIO;
		return snd_rawmidi_output_params(rmidi, (snd_rawmidi_output_params_t *) arg);
	case SND_RAWMIDI_IOCTL_INPUT_PARAMS:
		if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
			return -EIO;
		return snd_rawmidi_input_params(rmidi, (snd_rawmidi_input_params_t *) arg);
	case SND_RAWMIDI_IOCTL_OUTPUT_STATUS:
		if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
			return -EIO;
		return snd_rawmidi_output_status(rmidi, (snd_rawmidi_output_status_t *) arg);
	case SND_RAWMIDI_IOCTL_INPUT_STATUS:
		if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
			return -EIO;
		return snd_rawmidi_input_status(rmidi, (snd_rawmidi_input_status_t *) arg);
	case SND_RAWMIDI_IOCTL_DRAIN_OUTPUT:
		if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
			return -EIO;
		return snd_rawmidi_drain_output(rmidi);
	case SND_RAWMIDI_IOCTL_FLUSH_OUTPUT:
		if (!(fflags & SND_RAWMIDI_LFLG_OUTPUT))
			return -EIO;
		return snd_rawmidi_flush_output(rmidi);
	case SND_RAWMIDI_IOCTL_FLUSH_INPUT:
		if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
			return -EIO;
		return snd_rawmidi_flush_input(rmidi);
#ifdef CONFIG_SND_DEBUG
	default:
		snd_printk("rawmidi: unknown command = 0x%x\n", cmd);
#endif
	}
	return -EIO;
}

static int snd_rawmidi_switch_write(snd_control_t * control,
				    snd_rawmidi_t * rmidi,
				    snd_kswitch_list_t * klist,
				    int iface,
				    snd_switch_t * _sw)
{
	int result;
	
	if (snd_control_busy(control))
		return -EBUSY;
	result = snd_switch_write(klist, rmidi, _sw);
	if (result > 0) {
		snd_switch_t sw;

		if (copy_from_user(&sw, _sw, sizeof(*_sw)))
			return -EFAULT;
		snd_control_notify_switch_value_change(control,
						       iface,
						       sw.name,
						       0);		
	}
	return -EIO;
}

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;
	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 = snd_ioctl_in((long *) arg);
			if (val < 0 || val >= SND_RAWMIDI_DEVICES)
				return -EINVAL;
			if (!snd_rawmidi_devices[tmp + val])
				return -EINVAL;
			control->rawmidi_device = val;
			return 0;
		}
	case SND_CTL_IOCTL_RAWMIDI_INFO:
	case SND_CTL_IOCTL_RAWMIDI_OUTPUT_INFO:
	case SND_CTL_IOCTL_RAWMIDI_INPUT_INFO:
	case SND_CTL_IOCTL_RAWMIDI_OSWITCH_LIST:
	case SND_CTL_IOCTL_RAWMIDI_OSWITCH_READ:
	case SND_CTL_IOCTL_RAWMIDI_OSWITCH_WRITE:
	case SND_CTL_IOCTL_RAWMIDI_ISWITCH_LIST:
	case SND_CTL_IOCTL_RAWMIDI_ISWITCH_READ:
	case SND_CTL_IOCTL_RAWMIDI_ISWITCH_WRITE:
		rmidi = snd_rawmidi_devices[tmp + control->rawmidi_device];
		if (!rmidi)
			return -ENOENT;
		switch (cmd) {
		case SND_CTL_IOCTL_RAWMIDI_INFO:
			return snd_rawmidi_info(rmidi, (snd_rawmidi_info_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_OUTPUT_INFO:
			return snd_rawmidi_output_info(rmidi, (snd_rawmidi_output_info_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_INPUT_INFO:
			return snd_rawmidi_input_info(rmidi, (snd_rawmidi_input_info_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_OSWITCH_LIST:
			return snd_switch_list(&rmidi->output.switches, (snd_switch_list_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_OSWITCH_READ:
			return snd_switch_read(&rmidi->output.switches, rmidi, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_OSWITCH_WRITE:
			return snd_rawmidi_switch_write(control, rmidi, &rmidi->output.switches, SND_CTL_IFACE_RAWMIDI_OUTPUT, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_ISWITCH_LIST:
			return snd_switch_list(&rmidi->input.switches, (snd_switch_list_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_ISWITCH_READ:
			return snd_switch_read(&rmidi->input.switches, rmidi, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_RAWMIDI_ISWITCH_WRITE:
			return snd_rawmidi_switch_write(control, rmidi, &rmidi->input.switches, SND_CTL_IFACE_RAWMIDI_INPUT, (snd_switch_t *) arg);
		}
		break;
	}
	return -EAGAIN;
}

int snd_rawmidi_receive(snd_rawmidi_t * rmidi, char *buffer, int count)
{
	unsigned long flags;
	int count1;

#if 0
	snd_printk("rx: count = %i\n", count);
#endif
	if (!rmidi->input.u.s.buffer) {
		snd_printd("snd_rawmidi_receive: input isn't active!!!\n");
		return -EINVAL;
	}
	spin_lock_irqsave(&rmidi->input_lock, flags);
	rmidi->input.bytes += count;
	count1 = rmidi->input.u.s.size - rmidi->input.u.s.head;
	if (count1 > count)
		count1 = count;
	if (count1 > rmidi->input.u.s.size - rmidi->input.u.s.used) {
		count1 = rmidi->input.u.s.size - rmidi->input.u.s.used;
	}
	memcpy(rmidi->input.u.s.buffer + rmidi->input.u.s.head, buffer, count1);
	rmidi->input.u.s.head += count1;
	rmidi->input.u.s.head %= rmidi->input.u.s.size;
	rmidi->input.u.s.used += count1;
	count -= count1;
	if (count > 0) {
		count1 = count;
		if (count > rmidi->input.u.s.size - rmidi->input.u.s.used) {
			count1 = rmidi->input.u.s.size - rmidi->input.u.s.used;
			rmidi->input.u.s.xruns = count - count1;
		}
		if (count1 > 0) {
			memcpy(rmidi->input.u.s.buffer, buffer + count1, count1);
			rmidi->input.u.s.head = count1;
			rmidi->input.u.s.used += count1;
		}
	}
	spin_unlock_irqrestore(&rmidi->input_lock, flags);
	if (snd_rawmidi_input_ok(rmidi))
		wake_up(&rmidi->input_sleep);
#if 0
	snd_printk("rx: head = %i, tail = %i, used = %i, size = %i\n", rmidi->input.head, rmidi->input.tail, rmidi->input.used, rmidi->input.size);
#endif
	return 0;
}

static long snd_rawmidi_read(struct file *file, char *buf, long count)
{
	unsigned long flags;
	long result, timeout;
	int count1;
	snd_rawmidi_t *rmidi;

	rmidi = (snd_rawmidi_t *) file->private_data;
	if (!rmidi)
		return -EIO;
	result = 0;
	snd_rawmidi_trigger_input(rmidi, 1);
	while (count > 0) {
		while (!snd_rawmidi_input_ok(rmidi)) {
			if (file->f_flags & O_NONBLOCK)
				return result;
			timeout = interruptible_sleep_on_timeout(&rmidi->input_sleep, 10 * HZ);
			if (signal_pending(current))
				return result;
			if (!rmidi->input.u.s.used && !timeout)
				return -EIO;
		}
		while (count > 0 && rmidi->input.u.s.used) {
			count1 = rmidi->input.u.s.size - rmidi->input.u.s.tail;
			if (count1 > count)
				count1 = count;
			spin_lock_irqsave(&rmidi->input_lock, flags);
			if (count1 > rmidi->input.u.s.used)
				count1 = rmidi->input.u.s.used;
			spin_unlock_irqrestore(&rmidi->input_lock, flags);
			if (copy_to_user(buf, rmidi->input.u.s.buffer + rmidi->input.u.s.tail, count1))
				return -EFAULT;
			spin_lock_irqsave(&rmidi->input_lock, flags);
			rmidi->input.u.s.tail += count1;
			rmidi->input.u.s.tail %= rmidi->input.u.s.size;
			rmidi->input.u.s.used -= count1;
			spin_unlock_irqrestore(&rmidi->input_lock, flags);
			result += count1;
			buf += count1;
			count -= count1;
		}
	}
	return result;
}

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

	result = 0;
#if 0
	snd_printk("transmit: count = %i\n", count);
#endif
	if (!rmidi->output.u.s.buffer) {
		snd_printd("snd_rawmidi_transmit: output isn't active!!!\n");
		return 0;
	}
	spin_lock_irqsave(&rmidi->output_lock, flags);
	count1 = rmidi->output.u.s.size - rmidi->output.u.s.tail;
	if (count1 > count)
		count1 = count;
	if (count1 > rmidi->output.u.s.used)
		count1 = rmidi->output.u.s.used;
	if (!count1) {
		spin_unlock_irqrestore(&rmidi->output_lock, flags);
		snd_rawmidi_trigger_output(rmidi, 0);
		wake_up(&rmidi->output_sleep);
		return 0;
	}
	memcpy(buffer, rmidi->output.u.s.buffer + rmidi->output.u.s.tail, count1);
	rmidi->output.u.s.tail += count1;
	rmidi->output.u.s.tail %= rmidi->output.u.s.size;
	rmidi->output.u.s.used -= count1;
	count -= count1;
	result += count1;
	if (count > 0) {
		memcpy(buffer + count1, rmidi->output.u.s.buffer, count);
		rmidi->output.u.s.tail = count;
		rmidi->output.u.s.used -= count;
		result += count;
	}
	if (!rmidi->output.u.s.used) {
		spin_unlock_irqrestore(&rmidi->output_lock, flags);
		if (!result)
			snd_rawmidi_trigger_output(rmidi, 0);
	} else {
		spin_unlock_irqrestore(&rmidi->output_lock, flags);
	}
	if (snd_rawmidi_output_ok(rmidi))
		wake_up(&rmidi->output_sleep);
#if 0
	snd_printk("write1: head = %i, tail = %i, used = %i, size = %i\n", rmidi->output.u.s.head, rmidi->output.u.s.tail, rmidi->output.u.s.used, rmidi->output.u.s.size);
	snd_printk("transmit result = %i\n", result);
#endif
	rmidi->output.bytes += result;
	return result;
}

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

	rmidi = (snd_rawmidi_t *) file->private_data;
	if (!rmidi)
		return -EIO;
	result = 0;
	while (count > 0) {
		while (!snd_rawmidi_output_ok(rmidi)) {
			if (file->f_flags & O_NONBLOCK)
				return result;
			timeout = interruptible_sleep_on_timeout(&rmidi->output_sleep, 10 * HZ);
			if (signal_pending(current))
				return -EINTR;
			if (rmidi->output.u.s.used >= rmidi->output.u.s.size &&
			    !timeout)
				return -EIO;
		}
		while (count > 0 && rmidi->output.u.s.used < rmidi->output.u.s.size) {
			count1 = rmidi->output.u.s.size - rmidi->output.u.s.head;
			if (count1 > count)
				count1 = count;
			spin_lock_irqsave(&rmidi->output_lock, flags);
			if (count1 > rmidi->output.u.s.size - rmidi->output.u.s.used)
				count1 = rmidi->output.u.s.size - rmidi->output.u.s.used;
			spin_unlock_irqrestore(&rmidi->output_lock, flags);
			if (copy_from_user(rmidi->output.u.s.buffer + rmidi->output.u.s.head, buf, count1))
				return -EFAULT;
			spin_lock_irqsave(&rmidi->output_lock, flags);
			rmidi->output.u.s.head += count1;
			rmidi->output.u.s.head %= rmidi->output.u.s.size;
			rmidi->output.u.s.used += count1;
			spin_unlock_irqrestore(&rmidi->output_lock, flags);
			result += count1;
			buf += count1;
			count -= count1;
		}
		snd_rawmidi_trigger_output(rmidi, 1);
	}
	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_rawmidi_t *) file->private_data;
	if (!rmidi)
		return 0;

	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		snd_rawmidi_trigger_input(rmidi, 1);
		poll_wait(file, &rmidi->input_sleep, wait);
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT)
		poll_wait(file, &rmidi->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;
}

/*

 */

void snd_rawmidi_command_reset(snd_rawmidi_t * rmidi, int direction)
{
	struct snd_stru_rawmidi_direction *pdir;

	pdir = direction == SND_RAWMIDI_LFLG_INPUT ? &rmidi->input : &rmidi->output;
	pdir->u.p.cused = 0;
	pdir->u.p.cprev = 0;
}

/*

 */

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

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

/*
 *  Register functions
 */

static snd_minor_t snd_rawmidi_reg =
{
	"raw midi",		/* comment */
	NULL,			/* reserved */

	NULL,			/* unregister */

	NULL,			/* lseek */
	snd_rawmidi_read,	/* read */
	snd_rawmidi_write,	/* write */
	snd_rawmidi_open,	/* open */
	snd_rawmidi_release,	/* release */
	snd_rawmidi_poll,	/* poll */
	snd_rawmidi_ioctl,	/* ioctl */
	NULL,			/* mmap */
};

snd_rawmidi_t *snd_rawmidi_new_device(snd_card_t * card, char *id)
{
	snd_rawmidi_t *rmidi;

	if (!card)
		return NULL;
	rmidi = (snd_rawmidi_t *) snd_kmalloc(sizeof(snd_rawmidi_t), GFP_KERNEL);
	if (!rmidi)
		return NULL;
	memset(rmidi, 0, sizeof(snd_rawmidi_t));
	rmidi->card = card;
	rmidi->input_lock = SPIN_LOCK_UNLOCKED;
	rmidi->output_lock = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&rmidi->input_sleep);
	init_waitqueue_head(&rmidi->output_sleep);
	init_MUTEX(&rmidi->open_mutex);
	snd_switch_prepare(&rmidi->input.switches);
	snd_switch_prepare(&rmidi->output.switches);
	rmidi->input.timer.data = (unsigned long) rmidi;
	rmidi->input.timer.function = snd_rawmidi_poll_input;
	rmidi->output.timer.data = (unsigned long) rmidi;
	rmidi->output.timer.function = snd_rawmidi_poll_output;
	if (id) {
		strncpy(rmidi->id, id, sizeof(rmidi->id) - 1);
	}
	return rmidi;
}

int snd_rawmidi_free(snd_rawmidi_t * rmidi)
{
	if (!rmidi)
		return -EINVAL;
	snd_switch_free(&rmidi->output.switches);
	snd_switch_free(&rmidi->input.switches);
	if (rmidi->output.hw.private_free)
		rmidi->output.hw.private_free(rmidi->output.hw.private_data);
	if (rmidi->input.hw.private_free)
		rmidi->input.hw.private_free(rmidi->input.hw.private_data);
	if (rmidi->private_free)
		rmidi->private_free(rmidi->private_data);
	snd_kfree(rmidi);
	return 0;
}

static void snd_rawmidi_store(snd_rawmidi_t * rmidi)
{
	int size;
	long err;
	unsigned int pos;
	char *buffer;
	snd_kswitch_list_t *loswitch, *liswitch;
	char key[32];
	
	loswitch = &rmidi->output.switches;
	liswitch = &rmidi->input.switches;
	snd_switch_lock(loswitch, 0);
	snd_switch_lock(liswitch, 0);
	size = sizeof(rmidi->id) + sizeof(rmidi->name) +
	       snd_switch_size(loswitch) + snd_switch_size(liswitch);
	buffer = (char *) snd_vmalloc(size);
	if (!buffer) {
		snd_printk("cannot store rawmidi settings - no enough memory\n" );
		snd_switch_lock(loswitch, 1);
		snd_switch_lock(liswitch, 1);
		return;
	}
	pos = 0;
	memcpy(buffer + pos, rmidi->id, sizeof(rmidi->id));
	pos += sizeof(rmidi->id);
	memcpy(buffer + pos, rmidi->name, sizeof(rmidi->name));
	pos += sizeof(rmidi->name);
	err = snd_switch_store(loswitch, rmidi, buffer + pos, size - pos);
	snd_switch_lock(loswitch, 1);
	if (err < 0) {
		snd_printd("cannot store rawmidi output switch settings\n");
		snd_switch_lock(liswitch, 1);
		snd_vfree(buffer);
		return;
	}
	pos += err;
	err = snd_switch_store(liswitch, rmidi, buffer + pos, size - pos);
	snd_switch_lock(liswitch, 1);
	if (err < 0) {
		snd_printd("cannot store rawmidi input switch settings\n");
		snd_vfree(buffer);
		return;
	}
	sprintf(key, "snd:%i/rawmidi:%i", rmidi->card->number, rmidi->device);
	snd_persist_store(key, buffer, size);
	snd_vfree(buffer);
}

static void snd_rawmidi_restore(snd_rawmidi_t * rmidi)
{
	int size;
	long err;
	unsigned int pos;
	char *buffer;
	char key[32];
	
	sprintf(key, "snd:%i/rawmidi:%i", rmidi->card->number, rmidi->device);
	size = snd_persist_length(key);
	if (size <= 0)
		return;
	buffer = (char *) snd_vmalloc(size);
	if (!buffer) {
		snd_printk("cannot restore rawmidi settings - no enough memory\n" );
		return;
	}
	size = snd_persist_restore(key, buffer, size);
	/* ok. I'm very paranoid in bellow code */
	if (size < (int)(sizeof(rmidi->id) + sizeof(rmidi->name))) {
		snd_vfree(buffer);
		return;
	}
	pos = 0;
	if (strcmp(buffer + pos, rmidi->id)) {
		snd_vfree(buffer);
		return;
	}
	pos += sizeof(rmidi->id);
	if (strcmp(buffer + pos, rmidi->name)) {
		snd_vfree(buffer);
		return;
	}
	pos += sizeof(rmidi->name);
	err = snd_switch_restore(&rmidi->output.switches, rmidi, buffer + pos, size - pos);
	if (err < 0) {
		snd_printk("cannot restore rawmidi output switch settings\n");
		snd_vfree(buffer);
		return;
	}
	pos += err;
	err = snd_switch_restore(&rmidi->input.switches, rmidi, buffer + pos, size - pos);
	if (err < 0)
		snd_printk("cannot restore rawmidi input switch settings\n");
	snd_vfree(buffer);
}

int __snd_rawmidi_register(snd_rawmidi_t * rmidi, int rawmidi_device)
{
	int idx, err;
	snd_info_entry_t *entry;
	char name[16];

	down(&register_mutex);
	if (rawmidi_device < 0 || rawmidi_device >= SND_RAWMIDI_DEVICES) {
		up(&register_mutex);
		return -EINVAL;
	}
	idx = (rmidi->card->number * SND_RAWMIDI_DEVICES) + rawmidi_device;
	if (snd_rawmidi_devices[idx]) {
		up(&register_mutex);
		return -EBUSY;
	}
	rmidi->device = rawmidi_device;
	snd_rawmidi_devices[idx] = rmidi;
	sprintf(name, "midiC%iD%i", rmidi->card->number, rawmidi_device);
	if ((err = snd_register_device(SND_DEVICE_TYPE_RAWMIDI,
				rmidi->card, rawmidi_device, &snd_rawmidi_reg, name)) < 0) {
		snd_rawmidi_devices[idx] = NULL;
		up(&register_mutex);
		return err;
	}
#ifdef CONFIG_SND_OSSEMUL
	rmidi->ossreg = 0;
	if (rawmidi_device < 2) {
		if (snd_register_oss_device(SND_OSS_DEVICE_TYPE_MIDI,
				rmidi->card, rawmidi_device, &snd_rawmidi_reg, name) < 0) {
			snd_printk("unable to register OSS rawmidi device %i:%i\n", rmidi->card->number, rawmidi_device);
		} else {
			rmidi->ossreg = 1;
		}
	}
#endif
	up(&register_mutex);
	sprintf(name, "midiD%d", rawmidi_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
	snd_oss_info_register(SND_OSS_INFO_DEV_MIDI, rmidi->card->number, rmidi->name);
#endif
	snd_rawmidi_restore(rmidi);
	return 0;
}

int __snd_rawmidi_unregister(snd_rawmidi_t * rmidi)
{
	int idx;

	if (!rmidi)
		return -EINVAL;
	down(&register_mutex);
	idx = (rmidi->card->number * SND_RAWMIDI_DEVICES) + rmidi->device;
	if (snd_rawmidi_devices[idx] != rmidi) {
		up(&register_mutex);
		return -EINVAL;
	}
	snd_rawmidi_store(rmidi);
#ifdef CONFIG_SND_OSSEMUL
	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);
	return snd_rawmidi_free(rmidi);
}

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

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

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