/*
 *  Virtual Raw MIDI client on Sequencer
 *
 *  Copyright (c) 2000 by Takashi Iwai <tiwai@suse.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.
 *
 */

/*
 * Virtual Raw MIDI client
 *
 * The virtual rawmidi client is a sequencer client which associate
 * a rawmidi device file.  The created rawmidi device file can be
 * accessed as a normal raw midi, but its MIDI source and destination
 * are arbitrary.  For example, a user-client software synth connected
 * to this port can be used as a normal midi device as well.
 *
 * The virtual rawmidi device accepts also multiple opens.  Each file
 * has its own input buffer, so that no conflict would occur.  The drain
 * of input/output buffer acts only to the local buffer.
 *
 * The difference from normal raw midi device is that this contains no
 * output buffer actually.  The written events are dispatched to
 * connected sequencer ports immediately.
 *
 */

#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"
#include "../../include/seq_kernel.h"
#include "../../include/seq_midi_event.h"
#include "../../include/seq_virmidi.h"

/*
 * compile flags
 */
#undef SUPPORT_SWITCHES

/*
 * prototypes
 */
static int snd_virmidi_dev_free(snd_virmidi_dev_t *rdev);
static int snd_virmidi_dev_register(snd_virmidi_dev_t *rdev, snd_device_t *devptr);
static int snd_virmidi_dev_unregister(snd_virmidi_dev_t *rdev);
static void snd_virmidi_init_event(snd_virmidi_dev_t *rdev, snd_seq_event_t *ev);

#ifdef SUPPORT_SWITCHES
int snd_virmidi_switch_add(snd_virmidi_channel_t *pchn, snd_kswitch_t *ksw);
int snd_virmidi_switch_remove(snd_virmidi_channel_t *pchn, snd_kswitch_t *ksw);
snd_kswitch_t *snd_virmidi_switch_new(snd_virmidi_channel_t *pchn, snd_kswitch_t *ksw, void *private_data);
#endif


/*
 * common variables
 */
snd_virmidi_dev_t *snd_virmidi_devices[SND_CARDS * SND_RAWMIDI_DEVICES];

static DECLARE_MUTEX(register_mutex);

/*
 * convert file flags to RAWMIDI flags
 */
static inline unsigned short snd_virmidi_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;
	}
}

/*
 * check if input buffer is readble
 */
static inline int snd_virmidi_input_ok(snd_virmidi_t *rmidi)
{
	unsigned long flags;
	int result;
	snd_virmidi_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;
}

/*
 * check if output buffer is writable
 */
static inline int snd_virmidi_output_ok(snd_virmidi_t *rmidi)
{
	unsigned long flags;
	int result, free;
	snd_virmidi_channel_t *pchn;

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

/*
 * create input/output buffer -
 * only input buffer has really allocated buffer
 */
static int snd_virmidi_init_buffer(snd_virmidi_t *rmidi, int channel, int size, int alloc_buf)
{
	snd_virmidi_channel_t *pchn;
	pchn = &rmidi->chn[channel];
	/*pchn->event = NULL;*/
	pchn->size = size;
	pchn->used_max = pchn->size - 1;
	pchn->used_room = 1;
	pchn->used_min = 1;
	pchn->buffer = NULL;
	if (alloc_buf) {
		if ((pchn->buffer = snd_kmalloc(pchn->size, GFP_KERNEL)) == NULL) {
			return -ENOMEM;
		}
	}
	pchn->head = pchn->tail = pchn->used = 0;
	return 0;
}

/*
 * free input/output buffer
 */
static int snd_virmidi_done_buffer(snd_virmidi_t *rmidi, int channel)
{
	snd_virmidi_channel_t *pchn;
	pchn = &rmidi->chn[channel];
	if (pchn->buffer) {
		snd_kfree(pchn->buffer);
		pchn->buffer = NULL;
	}
	return 0;
}

/*
 * drain output buffer - 
 * reset event parser encoder
 */
static int snd_virmidi_drain_output(snd_virmidi_t *rmidi)
{
	snd_virmidi_channel_t *pchn;
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	snd_midi_event_reset_encode(rmidi->parser);
	pchn->used = 0;
	return 0;
}

/*
 * flush output buffer -
 * just drain output buffer
 */
static int snd_virmidi_flush_output(snd_virmidi_t *rmidi)
{
	return snd_virmidi_drain_output(rmidi);
}

/*
 * flush input buffer -
 * reset event parser decoder and initialize ring-buffer pointers
 */
static int snd_virmidi_flush_input(snd_virmidi_t *rmidi)
{
	snd_virmidi_channel_t *pchn;
	unsigned long flags;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	snd_midi_event_reset_decode(rmidi->parser);
	spin_lock_irqsave(&pchn->lock, flags);
	pchn->used = pchn->head = pchn->tail = 0;
	spin_unlock_irqrestore(&pchn->lock, flags);
	return 0;
}

/*
 * open device file -
 * create a snd_virmidi instance and add it to the linked-list of
 * the associated device
 */
static int snd_virmidi_open(unsigned short minor, int cardnum, int device,
			    struct file *file)
{
	unsigned short mode;
	snd_virmidi_dev_t *rdev;
	snd_virmidi_t *rmidi;
	snd_virmidi_channel_t *ichn, *ochn;
	int obufsize;
	unsigned long flags;

	rdev = snd_virmidi_devices[(cardnum * SND_RAWMIDI_DEVICES) + device];
	if (rdev == NULL)
		return -ENODEV;
#ifdef CONFIG_SND_OSSEMUL
	if (minor < 256)
		file->f_flags |= O_NONBLOCK;
#endif
	mode = snd_virmidi_file_flags(file);
	if ((file->f_flags & O_APPEND) || minor < 256)
		mode |= SND_RAWMIDI_LFLG_APPEND;

	if ((mode & SND_RAWMIDI_LFLG_INPUT) &&
	    !(rdev->dev_flags & SND_RAWMIDI_INFO_INPUT))
		return -ENXIO;
	if (mode & SND_RAWMIDI_LFLG_OUTPUT &&
	    !(rdev->dev_flags & SND_RAWMIDI_INFO_OUTPUT))
		return -ENXIO;
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		if (rdev->write_files > 0 && !(mode & SND_RAWMIDI_LFLG_APPEND))
			return -EINVAL;
	}

	rmidi = snd_magic_kcalloc(snd_virmidi_t, 0, GFP_KERNEL);
	if (rmidi == NULL)
		return -ENOMEM;

	obufsize = (mode & SND_RAWMIDI_LFLG_OUTPUT) ? MAX_MIDI_EVENT_BUF : 0;
	if (snd_midi_event_new(obufsize, &rmidi->parser) < 0) {
		snd_magic_kfree(rmidi);
		return -ENOMEM;
	}
	rmidi->devptr = rdev;
	rmidi->flags = mode;
	rmidi->seq_mode = rdev->seq_mode;
	rmidi->client = rdev->client;
	rmidi->port = rdev->port;

	ichn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	ochn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	spin_lock_init(&ichn->lock);
	spin_lock_init(&ochn->lock);
	init_waitqueue_head(&ichn->sleep);
	init_waitqueue_head(&ochn->sleep);

	if (mode & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_virmidi_init_buffer(rmidi, SND_RAWMIDI_CHANNEL_INPUT, PAGE_SIZE, 1) < 0)
			goto _err;
	}
	if (mode & SND_RAWMIDI_LFLG_OUTPUT) {
		if (snd_virmidi_init_buffer(rmidi, SND_RAWMIDI_CHANNEL_OUTPUT, obufsize, 0) < 0)
			goto _err;
	}

	down(&rdev->open_mutex);
	MOD_INC_USE_COUNT;
	if (rdev->use_inc)
		rdev->use_inc(rdev->private_data);

	/* add to device linked-list */
	write_lock_irqsave(&rdev->list_lock, flags);
	rmidi->next = rdev->filelist;
	rdev->filelist = rmidi;
	rdev->files++;
	if (mode & SND_RAWMIDI_LFLG_INPUT)
		rdev->read_files++;
	if (mode & SND_RAWMIDI_LFLG_OUTPUT)
		rdev->write_files++;
	write_unlock_irqrestore(&rdev->list_lock, flags);

	up(&rdev->open_mutex);

	file->private_data = rmidi;
	return 0;

_err:
	snd_virmidi_done_buffer(rmidi, SND_RAWMIDI_CHANNEL_INPUT);
	snd_midi_event_free(rmidi->parser);
	snd_magic_kfree(rmidi);
	return -ENOMEM;
}

/*
 * close the device file -
 * free the instance and remove from the linked-list of associated device
 */
static int snd_virmidi_release(unsigned short minor, int cardnum, int device,
			       struct file *file)
{
	unsigned short mode;
	snd_virmidi_dev_t *rdev;
	snd_virmidi_t *rmidi, *cur, *prev;
	unsigned long flags;

	rmidi = snd_magic_cast(snd_virmidi_t, file->private_data, -ENXIO);
	mode = snd_virmidi_file_flags(file);

	if ((rdev = rmidi->devptr) == NULL)
		return -ENXIO;

	down(&rdev->open_mutex);

	/* remove from the linked-list */
	write_lock_irqsave(&rdev->list_lock, flags);
	prev = NULL;
	for (cur = rdev->filelist; cur; prev = cur, cur = cur->next) {
		if (cur == rmidi) {
			if (prev)
				prev->next = rmidi->next;
			else
				rdev->filelist = rmidi->next;
			rdev->files--;
			if (rmidi->flags & SND_RAWMIDI_LFLG_INPUT)
				rdev->read_files--;
			if (rmidi->flags & SND_RAWMIDI_LFLG_OUTPUT)
				rdev->write_files--;
			break;
		}
	}
	write_unlock_irqrestore(&rdev->list_lock, flags);

	/* send reset events if no more opened files */
	if (rdev->write_files <= 0 && (rdev->seq_flags & SND_RAWMIDI_INFO_OUTPUT)) {
		snd_seq_event_t ev;
		int i;

		snd_virmidi_init_event(rdev, &ev);
		ev.type = SND_SEQ_EVENT_CONTROLLER;
		for (i = 0; i < 16; i++) {
			ev.data.control.channel = i;
			ev.data.control.param = SND_MCTL_ALL_SOUNDS_OFF;
			snd_seq_kernel_client_dispatch(rdev->client, &ev, 0, 0);
			ev.data.control.param = SND_MCTL_RESET_CONTROLLERS;
			snd_seq_kernel_client_dispatch(rdev->client, &ev, 0, 0);
		}
	}
	
	if (mode & SND_RAWMIDI_LFLG_INPUT)
		snd_virmidi_done_buffer(rmidi, SND_RAWMIDI_CHANNEL_INPUT);
	if (mode & SND_RAWMIDI_LFLG_OUTPUT)
		snd_virmidi_done_buffer(rmidi, SND_RAWMIDI_CHANNEL_OUTPUT);
	snd_midi_event_free(rmidi->parser);
	snd_magic_kfree(rmidi);

	if (rdev->use_dec)
		rdev->use_dec(rdev->private_data);
	MOD_DEC_USE_COUNT;
	up(&rdev->open_mutex);

	return 0;
}


/*
 * copy rawmidi device information to user space
 */
static int snd_virmidi_dev_info(snd_virmidi_dev_t *rdev, snd_rawmidi_info_t *_info)
{
	snd_rawmidi_info_t info;

	memset(&info, 0, sizeof(info));
	info.type = rdev->card->type;
	info.flags = rdev->dev_flags;
	strcpy(info.id, rdev->id);
	strcpy(info.name, rdev->name);

	if (copy_to_user(_info, &info, sizeof(snd_rawmidi_info_t)))
		return -EFAULT;

	return 0;
}

/*
 * copy rawmidi device info of the instance to user space
 */
static int snd_virmidi_info(snd_virmidi_t *rmidi, snd_rawmidi_info_t *_info)
{
	snd_virmidi_dev_t *rdev;

	if ((rdev = rmidi->devptr) == NULL)
		return -ENXIO;
	return snd_virmidi_dev_info(rdev, _info);
}


/*
 * change output buffer parameters -
 * reset the size of buffer
 */
static int snd_virmidi_output_params(snd_virmidi_t *rmidi,
				     snd_rawmidi_params_t *params)
{
	snd_virmidi_channel_t *pchn;

	snd_virmidi_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 (snd_midi_event_resize_buffer(rmidi->parser, pchn->size) < 0)
			return -ENOMEM;
		pchn->size = params->size;
	}
	pchn->used_max = params->max;
	pchn->used_room = params->room;
	return 0;
}

/*
 * change input buffer parameters
 */
static int snd_virmidi_input_params(snd_virmidi_t *rmidi,
				    snd_rawmidi_params_t *params)
{
	char *newbuf;
	snd_virmidi_channel_t *pchn;
	unsigned long flags;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	snd_virmidi_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;
	spin_lock_irqsave(&pchn->lock, flags);
	pchn->buffer = newbuf;
	pchn->size = params->size;
	pchn->used_min = params->min;
	spin_unlock_irqrestore(&pchn->lock, flags);
	snd_kfree(pchn->buffer);
	return 0;
}

/*
 * get output status
 */
static int snd_virmidi_output_status(snd_virmidi_t *rmidi,
				     snd_rawmidi_status_t *status)
{
	unsigned long flags;
	snd_virmidi_channel_t *pchn;

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

/*
 * get input status
 */
static int snd_virmidi_input_status(snd_virmidi_t *rmidi,
				    snd_rawmidi_status_t *status)
{
	unsigned long flags;
	snd_virmidi_channel_t *pchn;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	memset(status, 0, sizeof(*status));
	spin_lock_irqsave(&pchn->lock, flags);
	status->channel = SND_RAWMIDI_CHANNEL_INPUT;
	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;
}

/*
 * ioctl handler
 */
static int snd_virmidi_ioctl(struct file *file,
			     unsigned int cmd, unsigned long arg)
{
	snd_virmidi_t *rmidi;
	unsigned short fflags;

	rmidi = snd_magic_cast(snd_virmidi_t, file->private_data, -ENXIO);
	if (((cmd >> 8) & 0xff) != 'W')
		return -EINVAL;
	fflags = snd_virmidi_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_virmidi_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_virmidi_output_params(rmidi, &params);
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			return snd_virmidi_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_virmidi_output_status(rmidi, &status);
			break;
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			err = snd_virmidi_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_virmidi_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_virmidi_flush_output(rmidi);
		case SND_RAWMIDI_CHANNEL_INPUT:
			if (!(fflags & SND_RAWMIDI_LFLG_INPUT))
				return -EINVAL;
			return snd_virmidi_flush_input(rmidi);
		default:
			return -EINVAL;
		}
	}
#ifdef CONFIG_SND_DEBUG
	default:
		snd_printk("rawmidi: unknown command = 0x%x\n", cmd);
#endif
	}
	return -EINVAL;
}

#ifdef SUPPORT_SWITCHES
/*
 */
static int snd_virmidi_dev_switch_write(snd_control_t *control,
					snd_virmidi_dev_t *rdev,
					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, rdev, _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;
}
#endif

/*
 */
static int snd_virmidi_control_ioctl(snd_card_t *card, snd_control_t *control,
				     unsigned int cmd, unsigned long arg)
{
	unsigned int tmp;
	int idx;
	snd_virmidi_dev_t *rdev;

	tmp = card->number * SND_RAWMIDI_DEVICES;
	/* accept only 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;

			for (idx = SND_RAWMIDI_DEVICES - 1; idx >= 0; idx--) {
				if (snd_virmidi_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_virmidi_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:
		rdev = snd_virmidi_devices[tmp + control->rawmidi_device];
		if (rdev == NULL)
			return -ENXIO;
		switch (cmd) {
		case SND_CTL_IOCTL_RAWMIDI_INFO:
			return snd_virmidi_dev_info(rdev, (snd_rawmidi_info_t *) arg);
		}
		break;
	}
	return -ENOIOCTLCMD;
}


/*
 * put bytes into input buffer
 */
static int snd_virmidi_put_bytes(snd_virmidi_t *rmidi, char *buf, int count)
{
	snd_virmidi_channel_t *pchn;
	unsigned long flags;
	int count1;

	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	spin_lock_irqsave(&pchn->lock, flags);
	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, buf, count1);
	pchn->head += count1;
	pchn->head %= pchn->size;
	pchn->used += count1;
	count -= 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, buf + count1, count1);
			pchn->head = count1;
			pchn->used += count1;
		}
	}
	spin_unlock_irqrestore(&pchn->lock, flags);
	if (snd_virmidi_input_ok(rmidi))
		wake_up(&pchn->sleep);
	return 0;
}


/*
 * decode input event and put to read buffer of each opened file
 */
int snd_virmidi_dev_receive_event(snd_virmidi_dev_t *rdev, snd_seq_event_t *ev)
{
	snd_virmidi_t *rmidi;

	snd_debug_check(rdev == NULL, -EINVAL);
	if (! (rdev->seq_flags & SND_RAWMIDI_INFO_INPUT))
		return 0; /* ignored */

	read_lock(&rdev->list_lock);
	for (rmidi = rdev->filelist; rmidi; rmidi = rmidi->next) {
		if (! rmidi->flags & SND_RAWMIDI_LFLG_INPUT)
			continue;
		if (rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].buffer == NULL)
			continue;
		if (ev->type == SND_SEQ_EVENT_SYSEX) {
			if ((ev->flags & SND_SEQ_EVENT_LENGTH_MASK) != SND_SEQ_EVENT_LENGTH_VARIABLE)
				continue;
			snd_virmidi_put_bytes(rmidi, ev->data.ext.ptr, ev->data.ext.len);
		} else {
			char msg[4];
			int len;

			len = snd_midi_event_decode(rmidi->parser, msg, sizeof(msg), ev);
			if (len > 0)
				snd_virmidi_put_bytes(rmidi, msg, len);
		}
	}
	read_unlock(&rdev->list_lock);

	return 0;
}


/*
 * event_input callback from sequencer
 */
int snd_virmidi_receive(snd_seq_event_t *ev, int direct,
			void *private_data, int atomic, int hop)
{
	snd_virmidi_dev_t *rdev;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, -EINVAL);
	return snd_virmidi_dev_receive_event(rdev, ev);
}


/*
 * read callback
 */
static long snd_virmidi_read(struct file *file, char *buf, long count)
{
	long result;
	int count1;
	snd_virmidi_t *rmidi;
	snd_virmidi_channel_t *pchn;
	unsigned long flags;

	rmidi = snd_magic_cast(snd_virmidi_t, file->private_data, -ENXIO);
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
	result = 0;
	while (count > 0) {
		while (!snd_virmidi_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;
		}
		do {
			spin_lock_irqsave(&pchn->lock, flags);
			count1 = pchn->size - pchn->tail;
			if (count1 > count)
				count1 = count;
			if (count1 > pchn->used)
				count1 = pchn->used;
			if (copy_to_user(buf, pchn->buffer + pchn->tail, count1)) {
				spin_unlock_irqrestore(&pchn->lock, flags);
				return -EFAULT;
			}
			pchn->tail += count1;
			pchn->tail %= pchn->size;
			pchn->used -= count1;
			spin_unlock_irqrestore(&pchn->lock, flags);
			buf += count1;
			count -= count1;
			result += count1;
		} while (count > 0 && pchn->used);
	}
	return result;
}

/*
 * initialize an event record
 */
static void snd_virmidi_init_event(snd_virmidi_dev_t *rdev, snd_seq_event_t *ev)
{
	memset(ev, 0, sizeof(*ev));
	ev->source.client = rdev->client;
	ev->source.port = rdev->port;
	switch (rdev->seq_mode) {
	case SND_VIRMIDI_SEQ_DISPATCH:
		ev->dest.client = SND_SEQ_ADDRESS_SUBSCRIBERS;
		break;
	case SND_VIRMIDI_SEQ_ATTACH:
		/* FIXME: source and destination are same - not good.. */
		ev->dest.client = rdev->client;
		ev->dest.port = rdev->port;
		break;
	}
	ev->flags = SND_SEQ_TIME_STAMP_TICK | SND_SEQ_TIME_MODE_ABS;
	ev->queue = SND_SEQ_QUEUE_DIRECT;
}

/*
 * dispatch an event record
 */
static int snd_virmidi_dispatch_event(snd_virmidi_t *rmidi,
				      snd_seq_event_t *ev, struct file *file)
{
	int result;

#if 0
	if (file->f_flags & O_NONBLOCK)
		result = snd_seq_kernel_client_enqueue(rmidi->client, ev, 0, 0);
	else
		result = snd_seq_kernel_client_enqueue_blocking(rmidi->client, ev, file, 0, 0);
#else
	result = snd_seq_kernel_client_dispatch(rmidi->client, ev, 0, 0);
#endif

	return result;
}


/*
 * write callback
 */
static long snd_virmidi_write(struct file *file, const char *_buf, long count)
{
	long result;
	int err;
	snd_virmidi_t *rmidi;
	snd_seq_event_t ev;
	snd_virmidi_channel_t *pchn;
	unsigned long flags;
	const unsigned char *buf = (const unsigned char *)_buf;

	rmidi = snd_magic_cast(snd_virmidi_t, file->private_data, -ENXIO);
	result = 0;
	err = 0;
	snd_virmidi_init_event(rmidi->devptr, &ev);
	pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
	while (count > 0) {
		err = snd_midi_event_encode_byte(rmidi->parser, *buf, &ev);
		if (err < 0)
			break;
		else if (err == 0) {
			spin_lock_irqsave(&pchn->lock, flags);
			pchn->used++;
			spin_unlock_irqrestore(&pchn->lock, flags);
		} else {	/* event packet is ready */
			spin_lock_irqsave(&pchn->lock, flags);
			pchn->bytes += pchn->used + 1;
			pchn->used = 0;
			spin_unlock_irqrestore(&pchn->lock, flags);
			err = snd_virmidi_dispatch_event(rmidi, &ev, file);
			/* release sysex data */
			if (snd_seq_ev_is_variable(&ev) && ev.data.ext.ptr)
				snd_seq_ext_free(ev.data.ext.ptr, ev.data.ext.len);
			if (err < 0)
				break;
			/* re-initialize event record */
			snd_virmidi_init_event(rmidi->devptr, &ev);
		}
		result++;
		buf++;
		count--;
	}

	return result > 0 ? result : err;
}


/*
 * poll callback - no write check yet
 */
static unsigned int snd_virmidi_poll(struct file *file, poll_table *wait)
{
	snd_virmidi_t *rmidi;
	unsigned int mask;
	unsigned short fflags;

	fflags = snd_virmidi_file_flags(file);
	rmidi = snd_magic_cast(snd_virmidi_t, file->private_data, 0);

	if (fflags & SND_RAWMIDI_LFLG_INPUT)
		poll_wait(file, &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT].sleep, wait);
#if 0
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT)
		snd_seq_kernel_client_write_poll(rmidi->client, file, wait);
#endif
	mask = 0;
	if (fflags & SND_RAWMIDI_LFLG_INPUT) {
		if (snd_virmidi_input_ok(rmidi))
			mask |= POLLIN | POLLRDNORM;
	}
	if (fflags & SND_RAWMIDI_LFLG_OUTPUT) {
		if (snd_virmidi_output_ok(rmidi))
			mask |= POLLOUT | POLLWRNORM;
	}
	return mask;
}


/*
 * proc entry
 */
static void snd_virmidi_dev_proc_info_read(snd_info_buffer_t * buffer,
					   void *private_data)
{
	snd_virmidi_dev_t *rdev;
	snd_virmidi_t *rmidi;
	int i;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, );

	down(&rdev->open_mutex);
	snd_iprintf(buffer, "%s\n\n", rdev->name);
	snd_iprintf(buffer, "Sequencer Port = %d:%d\n", rdev->client, rdev->port);
	read_lock(&rdev->list_lock);
	snd_iprintf(buffer, "Files = %d (Read = %d / Write = %d)\n",
		    rdev->files, rdev->read_files, rdev->write_files);
	i = 0;
	for (rmidi = rdev->filelist; rmidi; rmidi = rmidi->next, i++) {
		snd_iprintf(buffer, "[File %d]\n", i);
		if (rmidi->flags & SND_RAWMIDI_LFLG_OUTPUT) {
			snd_virmidi_channel_t *pchn;
			pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_OUTPUT];
			snd_iprintf(buffer,
				    "Output\n"
				    "  Tx bytes     : %u\n",
				    pchn->bytes);
			snd_iprintf(buffer,
				    "  Mode         : %s\n"
				    "  Buffer size  : %d\n",
				    pchn->flags & SND_RAWMIDI_FLG_OSS ?
				    "OSS compatible" : "native",
				    pchn->size);
		}
		if (rmidi->flags & SND_RAWMIDI_LFLG_INPUT) {
			snd_virmidi_channel_t *pchn;
			pchn = &rmidi->chn[SND_RAWMIDI_CHANNEL_INPUT];
			snd_iprintf(buffer,
				    "Input\n"
				    "  Rx bytes     : %u\n",
				    pchn->bytes);
			snd_iprintf(buffer,
				    "  Buffer size  : %d\n"
				    "  Overruns     : %d\n",
				    pchn->size,
				    pchn->xruns);
			/*
			snd_iprintf(buffer,
				    "  Buffer used  : %d\n"
				    "  Buffer min   : %d\n"
				    "  Buffer head  : %d\n"
				    "  Buffer tail  : %d\n",
				    pchn->used,
				    pchn->used_min,
				    pchn->head,
				    pchn->tail);
				    */
		}
	}
	read_unlock(&rdev->list_lock);
	up(&rdev->open_mutex);
}


/*
 * subscribe callback - allow output to rawmidi device
 */
static int snd_virmidi_subscribe(void *private_data, snd_seq_port_subscribe_t *info)
{
	snd_virmidi_dev_t *rdev;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, -EINVAL);
	/*printk("virmidi: subscribed\n");*/
	/*MOD_INC_USE_COUNT;*/
	rdev->seq_flags |= SND_RAWMIDI_INFO_OUTPUT;
	return 0;
}

/*
 * unsubscribe callback - disallow output to rawmidi device
 */
static int snd_virmidi_unsubscribe(void *private_data, snd_seq_port_subscribe_t *info)
{
	snd_virmidi_dev_t *rdev;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, -EINVAL);
	/*printk("virmidi: unsubscribed\n");*/
	rdev->seq_flags &= ~SND_RAWMIDI_INFO_OUTPUT;
	/*MOD_DEC_USE_COUNT;*/
	return 0;
}


/*
 * use callback - allow input to rawmidi device
 */
static int snd_virmidi_use(void *private_data, snd_seq_port_subscribe_t *info)
{
	snd_virmidi_dev_t *rdev;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, -EINVAL);
	/*printk("virmidi: used\n");*/
	/*MOD_INC_USE_COUNT;*/
	rdev->seq_flags |= SND_RAWMIDI_INFO_INPUT;
	return 0;
}

/*
 * unuse callback - disallow input to rawmidi device
 */
static int snd_virmidi_unuse(void *private_data, snd_seq_port_subscribe_t *info)
{
	snd_virmidi_dev_t *rdev;

	rdev = snd_magic_cast(snd_virmidi_dev_t, private_data, -EINVAL);
	/*printk("virmidi: unused\n");*/
	rdev->seq_flags &= ~SND_RAWMIDI_INFO_INPUT;
	/*MOD_DEC_USE_COUNT;*/
	return 0;
}


/*
 *  Register functions
 */
static snd_minor_t snd_virmidi_reg =
{
	comment:	"seq-virmidi",
	read:		snd_virmidi_read,
	write:		snd_virmidi_write,
	open:		snd_virmidi_open,
	release:	snd_virmidi_release,
	poll:		snd_virmidi_poll,
	ioctl:		snd_virmidi_ioctl,
};


/*
 * create a new device
 */
int snd_virmidi_new(snd_card_t *card, int device, snd_virmidi_dev_t **rrdev)
{
	snd_virmidi_dev_t *rdev;
	int err;
	static snd_device_ops_t ops = {
		(snd_dev_free_t *)snd_virmidi_dev_free,
		(snd_dev_register_t *)snd_virmidi_dev_register,
		(snd_dev_unregister_t *)snd_virmidi_dev_unregister
	};

	snd_debug_check(rrdev == NULL, -EINVAL);
	*rrdev = NULL;
	snd_debug_check(card == NULL, -ENXIO);
	rdev = snd_magic_kcalloc(snd_virmidi_dev_t, 0, GFP_KERNEL);
	if (rdev == NULL)
		return -ENOMEM;
	rdev->card = card;
	rdev->device = device;
	rdev->client = -1;
	rdev->port = -1;
	init_MUTEX(&rdev->open_mutex);
	rdev->list_lock = RW_LOCK_UNLOCKED;
	rdev->dev_flags = SND_RAWMIDI_INFO_OUTPUT |
		SND_RAWMIDI_INFO_INPUT |
		SND_RAWMIDI_INFO_DUPLEX;
	rdev->seq_flags = 0;
	rdev->files = 0;
	rdev->read_files = 0;
	rdev->write_files = 0;
	rdev->filelist = NULL;
	
	snd_switch_prepare(card, &rdev->switches[SND_RAWMIDI_CHANNEL_INPUT], rdev, SND_CTL_IFACE_RAWMIDI, device, SND_RAWMIDI_CHANNEL_INPUT);
	snd_switch_prepare(card, &rdev->switches[SND_RAWMIDI_CHANNEL_OUTPUT], rdev, SND_CTL_IFACE_RAWMIDI, device, SND_RAWMIDI_CHANNEL_OUTPUT);
	if ((err = snd_device_new(card, SND_DEV_RAWMIDI, rdev, device, &ops, NULL)) < 0) {
		snd_virmidi_dev_free(rdev);
		return err;
	}
	*rrdev = rdev;
	return 0;
}


/*
 * release the device resources
 */
static int snd_virmidi_dev_free(snd_virmidi_dev_t *rdev)
{
	snd_debug_check(rdev == NULL, -ENXIO);
	snd_switch_free(rdev->card, &rdev->switches[SND_RAWMIDI_CHANNEL_INPUT]);
	snd_switch_free(rdev->card, &rdev->switches[SND_RAWMIDI_CHANNEL_OUTPUT]);
	if (rdev->private_free)
		rdev->private_free(rdev->private_data);
	snd_magic_kfree(rdev);
	return 0;
}


/*
 * create a sequencer client and a port
 */
static int snd_virmidi_dev_attach_seq(snd_virmidi_dev_t *rdev)
{
	int client;
	snd_seq_client_callback_t callbacks;
	snd_seq_port_callback_t pcallbacks;
	snd_seq_client_info_t info;
	snd_seq_port_info_t pinfo;
	int err;

	if (rdev->client >= 0)
		return -EBUSY;

	memset(&callbacks, 0, sizeof(callbacks));
	callbacks.private_data = rdev;
	callbacks.allow_input = 1;
	callbacks.allow_output = 1;
	client = snd_seq_create_kernel_client(rdev->card, rdev->device, &callbacks);
	if (client < 0)
		return client;
	rdev->client = client;

	/* set client name */
	memset(&info, 0, sizeof(info));
	info.client = client;
	info.type = KERNEL_CLIENT;
	sprintf(info.name, "%s %d:%d", rdev->name, rdev->card->number, rdev->device);
	strcpy(info.group, SND_SEQ_GROUP_DEVICE);
	snd_seq_kernel_client_ctl(client, SND_SEQ_IOCTL_SET_CLIENT_INFO, &info);

	/* create a port */
	memset(&pinfo, 0, sizeof(pinfo));
	pinfo.client = client;
	strcpy(pinfo.name, info.name);
	strcpy(pinfo.group, SND_SEQ_GROUP_DEVICE);
	/* set all capabilities */
	pinfo.capability |= SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SYNC_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
	pinfo.capability |= SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SYNC_READ | SND_SEQ_PORT_CAP_SUBS_READ;
	pinfo.capability |= SND_SEQ_PORT_CAP_DUPLEX;
	pinfo.type = SND_SEQ_PORT_TYPE_MIDI_GENERIC;
	pinfo.midi_channels = 16;
	memset(&pcallbacks, 0, sizeof(pcallbacks));
	pcallbacks.private_data = rdev;
	pcallbacks.subscribe = snd_virmidi_subscribe;
	pcallbacks.unsubscribe = snd_virmidi_unsubscribe;
	pcallbacks.use = snd_virmidi_use;
	pcallbacks.unuse = snd_virmidi_unuse;
	pcallbacks.event_input = snd_virmidi_receive;
	pinfo.kernel = &pcallbacks;
	err = snd_seq_kernel_client_ctl(client, SND_SEQ_IOCTL_CREATE_PORT, &pinfo);
	if (err < 0) {
		snd_seq_delete_kernel_client(client);
		rdev->client = -1;
		return err;
	}

	rdev->port = pinfo.port;
	return 0;	/* success */
}


/*
 * release the sequencer client
 */
static void snd_virmidi_dev_detach_seq(snd_virmidi_dev_t *rdev)
{
	if (rdev->client >= 0) {
		snd_seq_delete_kernel_client(rdev->client);
		rdev->client = -1;
	}
}


/*
 * register the device
 */
static int snd_virmidi_dev_register(snd_virmidi_dev_t *rdev, snd_device_t *devptr)
{
	int idx, err;
	snd_info_entry_t *entry;
	char name[16];

	snd_debug_check(devptr == NULL, -ENXIO);

	switch (rdev->seq_mode) {
	case SND_VIRMIDI_SEQ_DISPATCH:
		err = snd_virmidi_dev_attach_seq(rdev);
		if (err < 0)
			return err;
		break;
	case SND_VIRMIDI_SEQ_ATTACH:
		if (rdev->client == 0)
			return -EINVAL;
		/* should check presence of port more strictly.. */
		break;
	default:
		snd_printk("seq_virmidi: seq_mode is not set: %d\n", rdev->seq_mode);
		return -EINVAL;
	}

	down(&register_mutex);
	idx = (rdev->card->number * SND_RAWMIDI_DEVICES) + rdev->device;
	if (snd_virmidi_devices[idx]) {
		up(&register_mutex);
		if (rdev->seq_mode == SND_VIRMIDI_SEQ_DISPATCH)
			snd_virmidi_dev_detach_seq(rdev);
		return -EBUSY;
	}
	snd_virmidi_devices[idx] = rdev;
	sprintf(name, "midiC%iD%i", rdev->card->number, rdev->device);
	if ((err = snd_register_device(SND_DEVICE_TYPE_RAWMIDI,
				       rdev->card, rdev->device,
				       &snd_virmidi_reg, name)) < 0) {
		snd_virmidi_devices[idx] = NULL;
		if (rdev->seq_mode == SND_VIRMIDI_SEQ_DISPATCH)
			snd_virmidi_dev_detach_seq(rdev);
		up(&register_mutex);
		return err;
	}
#ifdef CONFIG_SND_OSSEMUL
	rdev->ossreg = 0;
	if (rdev->device < 2) {
		if (snd_register_oss_device(SND_OSS_DEVICE_TYPE_MIDI,
					    rdev->card, rdev->device,
					    &snd_virmidi_reg, name) < 0) {
			snd_printk("unable to register OSS rawmidi device %i:%i\n", rdev->card->number, rdev->device);
		} else {
			rdev->ossreg = 1;
		}
	}
#endif
	up(&register_mutex);
	sprintf(name, "midiD%d", rdev->device);
	entry = snd_info_create_entry(rdev->card, name);
	if (entry) {
		entry->private_data = rdev;
		entry->t.text.read_size = 1024;
		entry->t.text.read = snd_virmidi_dev_proc_info_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	rdev->proc_entry = entry;
#ifdef CONFIG_SND_OSSEMUL
	if (rdev->device == 0)
		snd_oss_info_register(SND_OSS_INFO_DEV_MIDI, rdev->card->number, rdev->name);
#endif

	return 0;
}


/*
 * unregister the device -
 */
static int snd_virmidi_dev_unregister(snd_virmidi_dev_t *rdev)
{
	int idx;

	snd_debug_check(rdev == NULL, -ENXIO);
	down(&register_mutex);
	idx = (rdev->card->number * SND_RAWMIDI_DEVICES) + rdev->device;
	if (snd_virmidi_devices[idx] != rdev) {
		up(&register_mutex);
		return -EINVAL;
	}
#ifdef CONFIG_SND_OSSEMUL
	if (rdev->device == 0)
		snd_oss_info_unregister(SND_OSS_INFO_DEV_MIDI, rdev->card->number);
#endif
	if (rdev->proc_entry) {
		snd_info_unregister(rdev->proc_entry);
		rdev->proc_entry = NULL;
	}
#ifdef CONFIG_SND_OSSEMUL
	if (rdev->ossreg)
		snd_unregister_oss_device(SND_OSS_DEVICE_TYPE_MIDI, rdev->card, rdev->device);
#endif
	snd_unregister_device(SND_DEVICE_TYPE_RAWMIDI, rdev->card, rdev->device);
	snd_virmidi_devices[idx] = NULL;
	if (rdev->seq_mode == SND_VIRMIDI_SEQ_DISPATCH)
		snd_virmidi_dev_detach_seq(rdev);
	up(&register_mutex);
	return snd_virmidi_dev_free(rdev);
}


/*
 * switch support
 */
#ifdef SUPPORT_SWITCHES
int snd_virmidi_switch_add(snd_virmidi_channel_t *pchn, snd_kswitch_t *ksw)
{
	return snd_switch_add(&pchn->switches, ksw);
}

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

snd_kswitch_t *snd_virmidi_switch_new(snd_virmidi_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_virmidi_switch_add(pchn, sw) < 0) {
		snd_switch_free_one(sw);
		return NULL;
	}
	sw->private_data = private_data;
	return sw;
}
#endif

/*
 *  ENTRY functions
 */

static int __init alsa_virmidi_init(void)
{
	snd_control_register_ioctl(snd_virmidi_control_ioctl);
	return 0;
}

static void __exit alsa_virmidi_exit(void)
{
	snd_control_unregister_ioctl(snd_virmidi_control_ioctl);
}

module_init(alsa_virmidi_init)
module_exit(alsa_virmidi_exit)

EXPORT_SYMBOL(snd_virmidi_new);

