/*
 *  Digital Audio (PCM) abstract layer
 *  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/control.h"
#include "../include/info.h"
#include "../include/pcm.h"
#include "../include/minors.h"

#if 0
static unsigned int snd_pcm_align(snd_pcm_subchn_t * subchn, unsigned int value);
#endif

/*

 */

int snd_pcm_info(snd_pcm_t * pcm, snd_pcm_info_t * _info)
{
	snd_pcm_info_t info;
	
	memset(&info, 0, sizeof(info));
	info.type = pcm->card->type;
	info.flags = pcm->info_flags;
	strncpy(info.id, pcm->id, sizeof(info.id)-1);
	strncpy(info.name, pcm->name, sizeof(info.name)-1);
	info.playback = pcm->chn[SND_PCM_CHANNEL_PLAYBACK].subchn_count - 1;
	info.capture = pcm->chn[SND_PCM_CHANNEL_CAPTURE].subchn_count - 1;
	if (!(pcm->info_flags & SND_PCM_INFO_PLAYBACK))
		info.playback = -1;
	if (!(pcm->info_flags & SND_PCM_INFO_CAPTURE))
		info.capture = -1;
	if (copy_to_user(_info, &info, sizeof(info)))
		return -EFAULT;
	return 0;
}

int snd_pcm_channel_info(snd_pcm_subchn_t * subchn, snd_pcm_channel_info_t * _info)
{
	snd_pcm_channel_info_t info;
	snd_pcm_runtime_t * runtime;
	snd_pcm_hardware_t * hw;

	snd_debug_check(subchn == NULL, -ENODEV);
	memset(&info, 0, sizeof(info));
	info.subdevice = subchn->number;
	strncpy(info.subname, subchn->name, sizeof(info.subname)-1);
	info.channel = subchn->channel;
	runtime = subchn->runtime;
	if (runtime == NULL)
		goto __copy;
	hw = runtime->hw;
	info.mode = runtime->mode;
	info.sync = runtime->sync;
	info.flags = hw->chninfo;
	info.formats = hw->formats;
	info.rates = hw->rates;
	info.min_rate = hw->min_rate;
	info.max_rate = hw->max_rate;
	info.min_voices = hw->min_voices;
	info.max_voices = hw->max_voices;
	info.min_fragment_size = hw->min_fragment_size;
	info.max_fragment_size = hw->max_fragment_size;
	if (subchn->runtime->dma_area) {
		info.buffer_size =
		info.mmap_size = subchn->runtime->dma_area->size;
	}
	info.fragment_align = hw->fragment_align;
	info.fifo_size = hw->fifo_size;
	info.transfer_block_size = hw->transfer_block_size;
	if (runtime->dig_mask)
		memcpy(&info.dig_mask, runtime->dig_mask, sizeof(snd_pcm_digital_t));
	info.mixer_device = runtime->mixer_device;
	memcpy(&info.mixer_eid, &runtime->mixer_eid, sizeof(snd_mixer_eid_t));
	runtime->hw->ioctl(subchn->pchn->private_data, subchn,
			   SND_PCM_IOCTL1_INFO, (unsigned long *)&info);
	runtime->hw->ioctl(subchn->pchn->private_data, subchn,
			   SND_PCM_IOCTL1_MMAP_SIZE, (unsigned long *)&info.mmap_size);
      __copy:
	if (copy_to_user(_info, &info, sizeof(info)))
		return -EFAULT;
	return 0;
}

static int snd_pcm_channel_params(snd_pcm_subchn_t * subchn, snd_pcm_channel_params_t * _params)
{
	snd_pcm_channel_params_t params;
	snd_pcm_runtime_t *runtime;
	int res;

	snd_debug_check(subchn == NULL, -ENODEV);
	if (copy_from_user(&params, _params, sizeof(params)))
		return -EFAULT;
	runtime = subchn->runtime;
	if (*runtime->status != SND_PCM_STATUS_NOTREADY &&
	    *runtime->status != SND_PCM_STATUS_READY)
		return -EBADFD;
	if (runtime->flags & SND_PCM_FLG_MMAP)
		return -EBADFD;
	if (params.mode == SND_PCM_MODE_BLOCK &&
	    !(runtime->hw->chninfo & SND_PCM_CHNINFO_BLOCK))
		return -EINVAL;
	if (params.format.voices < 1 || params.format.voices > 1024)
		return -EINVAL;
	if (params.format.voices > 1 && params.mode == SND_PCM_MODE_STREAM &&
	    !(runtime->hw->chninfo & SND_PCM_CHNINFO_STREAM))
		return -EINVAL;
	if (params.start_mode < 0 || params.start_mode > SND_PCM_START_GO)
		return -EINVAL;
	if (params.stop_mode < 0 || params.stop_mode > SND_PCM_STOP_ROLLOVER)
		return -EINVAL;
	runtime->mode = params.mode;
	runtime->start_mode = params.start_mode;
	runtime->stop_mode = params.stop_mode;
	runtime->flags &= ~SND_PCM_FLG_TIME;
	if (params.time)
		runtime->flags |= SND_PCM_FLG_TIME;
	memcpy(&runtime->format, &params.format, sizeof(snd_pcm_format_t));
	if (!params.format.interleave && runtime->mode == SND_PCM_MODE_STREAM)
		return -EINVAL;
 	memcpy(&runtime->digital, &params.digital, sizeof(snd_pcm_digital_t)); 
	if (params.time) {
		runtime->flags |= SND_PCM_FLG_TIMESTAMP;
	} else {
		runtime->flags &= ~SND_PCM_FLG_TIMESTAMP;
	}
	memcpy(&runtime->sync_group, &params.sync, sizeof(snd_pcm_sync_t));
	if (runtime->mode == SND_PCM_MODE_STREAM) {
		runtime->buf.stream.queue_size = params.buf.stream.queue_size;
		runtime->buf.stream.fill = params.buf.stream.fill;
		runtime->buf.stream.max_fill = params.buf.stream.max_fill;
	} else {
		runtime->buf.block.frag_size = params.buf.block.frag_size;
		runtime->buf.block.frags_min = params.buf.block.frags_min;
		runtime->buf.block.frags_max = params.buf.block.frags_max;
	}
	res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
				 SND_PCM_IOCTL1_PARAMS, (unsigned long *)&params);
	if (res < 0)
		return res;
	snd_pcm_proc_format(subchn);
	snd_pcm_timer_resolution_change(subchn);	
	*runtime->status = SND_PCM_STATUS_READY;
	return 0;
}

static int snd_pcm_channel_setup(snd_pcm_subchn_t * subchn, snd_pcm_channel_setup_t * _setup)
{
	snd_pcm_channel_setup_t setup;
	snd_pcm_runtime_t *runtime;
	int res;
	
	snd_debug_check(subchn == NULL, -ENODEV);
	if (copy_from_user(&setup, _setup, sizeof(setup)))
		return -EFAULT;
	runtime = subchn->runtime;
	memset(&setup, 0, sizeof(setup));
	setup.channel = subchn->channel;
	setup.mode = runtime->mode;
	setup.format = runtime->format;
	memcpy(&setup.digital, &runtime->digital, sizeof(snd_pcm_digital_t)); 
	if (runtime->mode == SND_PCM_MODE_STREAM) {
		setup.buf.stream.queue_size = runtime->buf.stream.queue_size;
	} else {
		setup.buf.block.frag_size = runtime->buf.block.frag_size;
		setup.buf.block.frags = runtime->frags;
		setup.buf.block.frags_min = runtime->buf.block.frags_min;
		setup.buf.block.frags_max = runtime->buf.block.frags_max;
	}
	res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
				 SND_PCM_IOCTL1_SETUP, (unsigned long *)&setup);
	if (res < 0)
		return res;
	if (copy_to_user(_setup, &setup, sizeof(setup)))
		return -EFAULT;
	return 0;
}

static int snd_pcm_channel_status(snd_pcm_subchn_t * subchn, snd_pcm_channel_status_t * _status)
{
	snd_pcm_channel_status_t status;
	snd_pcm_runtime_t *runtime;
	int res;
	
	snd_debug_check(subchn == NULL, -ENODEV);
	if (copy_from_user(&status, _status, sizeof(status)))
		return -EFAULT;
	runtime = subchn->runtime;
	if (*runtime->status == SND_PCM_STATUS_NOTREADY)
		return -EBADFD;
	memset(&status, 0, sizeof(status));
	status.channel = subchn->channel;
	status.mode = runtime->mode;
	res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
				 SND_PCM_IOCTL1_STATUS, (unsigned long *)&status);
	if (res < 0)
		return res;
	status.status = *runtime->status;
	if (copy_to_user(_status, &status, sizeof(status)))
		return -EFAULT;
	return 0;
}

static int snd_pcm_channel_prepare(snd_pcm_subchn_t * subchn)
{
	snd_pcm_runtime_t *runtime;
	int res;
	
	snd_debug_check(subchn == NULL, -ENODEV);
	runtime = subchn->runtime;
	switch (*runtime->status) {
	case SND_PCM_STATUS_NOTREADY:
		return -EBADFD;
	case SND_PCM_STATUS_RUNNING:
		return -EBUSY;
	}
	res = runtime->hw->prepare(subchn->pchn->private_data, subchn);
	if (res >= 0) {
#ifdef CONFIG_SND_OSSEMUL
		if (!subchn->oss.oss)
#endif
			snd_pcm_clear_values(subchn);
		*runtime->status = SND_PCM_STATUS_PREPARED;
	}
	return res;
}

int snd_pcm_channel_go_pre(snd_pcm_subchn_t *subchn, int cmd)
{
	snd_pcm_runtime_t *runtime = subchn->runtime;

	if (cmd != SND_PCM_TRIGGER_GO)
		return 0;
	if (*runtime->status != SND_PCM_STATUS_PREPARED)
		return -EBADFD;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		if (runtime->flags & SND_PCM_FLG_MMAP) {
			*runtime->frag_tail = 0;
			if (!runtime->mmap_control->fragments[0].data)
				return -EIO;
			runtime->mmap_control->fragments[0].io = 1;
		}
	} else if (subchn->channel == SND_PCM_CHANNEL_CAPTURE) {
		if (runtime->flags & SND_PCM_FLG_MMAP) {
			*runtime->frag_head = 0;
			if (runtime->mmap_control->fragments[0].data)
				return -EIO;
			runtime->mmap_control->fragments[0].io = 1;
		}
	}
	return 0;
}

int snd_pcm_channel_go_post(snd_pcm_subchn_t *subchn, int cmd, int err)
{
	snd_pcm_runtime_t *runtime = subchn->runtime;

	if (err < 0) {
		if (cmd == SND_PCM_TRIGGER_GO && (runtime->flags & SND_PCM_FLG_MMAP)) {
			int idx;
		
			for (idx = 0; idx < 128; idx++)
				runtime->mmap_control->fragments[idx].io = 0;
		}
		return err;
	}
	if (cmd == SND_PCM_TRIGGER_GO) {
		if (runtime->flags & SND_PCM_FLG_TIME)
			do_gettimeofday(&runtime->stime);
		*runtime->status = SND_PCM_STATUS_RUNNING;
		if ((runtime->flags & SND_PCM_FLG_MMAP) && runtime->mmap_data) {
			snd_pcm_proc_write(subchn, 0,
					   runtime->mmap_data + runtime->mmap_control->fragments[0].addr,
					   runtime->mmap_control->status.frag_size, 1);
		}
	} else if (cmd == SND_PCM_TRIGGER_STOP) {
		if (runtime->flags & SND_PCM_FLG_MMAP) {
			int idx;
		
			for (idx = 0; idx < 128; idx++)
				runtime->mmap_control->fragments[idx].io = 0;
		}
	}
	return err;
}

int snd_pcm_channel_go(snd_pcm_subchn_t * subchn, int cmd)
{
	snd_pcm_runtime_t *runtime;
	int err;
	
	snd_debug_check(subchn == NULL, -ENODEV);
	runtime = subchn->runtime;
	err = snd_pcm_channel_go_pre(subchn, cmd);
	if (err < 0)
		return err;
	err = runtime->hw->trigger(subchn->pchn->private_data, subchn, cmd);
	return snd_pcm_channel_go_post(subchn, cmd, err);
}

static int snd_pcm_channel_sync_go(snd_pcm_subchn_t * subchn, snd_pcm_sync_t *_sync)
{
	snd_pcm_sync_t sync;
	snd_pcm_runtime_t *runtime;

	if (copy_from_user(&sync, _sync, sizeof(sync)))
		return -EFAULT;
	if (sync.id32[0] == 0 && sync.id32[1] == 0 &&
	    sync.id32[2] == 0 && sync.id32[3] == 0)
	    	return -EINVAL;
	runtime = subchn->runtime;
	if (subchn == NULL || memcmp(&runtime->sync_group, &sync, sizeof(sync)))
		return -ENXIO;
	return runtime->hw->trigger(subchn->pchn->private_data, subchn, SND_PCM_TRIGGER_SYNC_GO);
}

static int snd_pcm_playback_flush(snd_pcm_subchn_t * subchn)
{
	snd_pcm_runtime_t *runtime;
	long timeout;
	int stop_mode, pause = 0;

	snd_debug_check(subchn == NULL, -ENODEV);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_PLAYBACK, -EINVAL);
	runtime = subchn->runtime;
	switch (*runtime->status) {
	case SND_PCM_STATUS_NOTREADY:
		return -EBADFD;
	case SND_PCM_STATUS_READY:
		return 0;
	case SND_PCM_STATUS_PAUSED:
		snd_pcm_kernel_playback_ioctl(subchn, SND_PCM_IOCTL_CHANNEL_PAUSE, (long)&pause);
		break;
	}
	stop_mode = runtime->stop_mode;
	runtime->stop_mode = SND_PCM_STOP_STOP;
	if (!snd_pcm_playback_empty(subchn)) {
		runtime = subchn->runtime;
		if (*runtime->status == SND_PCM_STATUS_PREPARED)
			snd_pcm_channel_go(subchn, SND_PCM_TRIGGER_GO);
		while (!snd_pcm_playback_empty(subchn) ||
		       *runtime->status == SND_PCM_STATUS_RUNNING) {
			if (runtime->mode == SND_PCM_MODE_BLOCK) {
				timeout = interruptible_sleep_on_timeout(&runtime->sleep, 10 * HZ);
				if (signal_pending(current)) {
					runtime->stop_mode = stop_mode;
					return -EINTR;
				}
				if (!timeout && !snd_pcm_playback_empty(subchn)) {
					snd_printd("playback flush error (DMA or IRQ trouble?)\n");
					runtime->stop_mode = stop_mode;
					return -EIO;
				}
			} else if (runtime->mode == SND_PCM_MODE_STREAM) {
				set_current_state(TASK_INTERRUPTIBLE);
				schedule_timeout(1);
				if (signal_pending(current)) {
					runtime->stop_mode = stop_mode;
					return -EINTR;
				}
			} else {
				runtime->stop_mode = stop_mode;
				return -EIO;
			}
		}
	}
	runtime->stop_mode = stop_mode;
	if (*runtime->status == SND_PCM_STATUS_NOTREADY)
		return -EIO;
	*runtime->status = SND_PCM_STATUS_READY;
	snd_pcm_clear_values(subchn);
	return 0;
}

static int snd_pcm_capture_flush(snd_pcm_subchn_t * subchn)
{
	snd_debug_check(subchn == NULL, -ENODEV);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_CAPTURE, -EINVAL);
	switch (*subchn->runtime->status) {
	case SND_PCM_STATUS_NOTREADY:
		return -EBADFD;
	case SND_PCM_STATUS_READY:
		return 0;
	}
	snd_pcm_transfer_stop(subchn, SND_PCM_STATUS_READY);
	snd_pcm_clear_values(subchn);
	return 0;
}

static int snd_pcm_playback_drain(snd_pcm_subchn_t *subchn)
{
	int pause = 0;

	snd_debug_check(subchn == NULL, -ENODEV);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_PLAYBACK, -EINVAL);
	switch (*subchn->runtime->status) {
	case SND_PCM_STATUS_NOTREADY:
		return -EBADFD;
	case SND_PCM_STATUS_READY:
		return 0;
	}
	snd_pcm_kernel_playback_ioctl(subchn, SND_PCM_IOCTL_CHANNEL_PAUSE, (long)&pause);
	snd_pcm_transfer_stop(subchn, SND_PCM_STATUS_READY);
	snd_pcm_clear_values(subchn);
	return 0;
}

static int snd_pcm_playback_pause(snd_pcm_subchn_t *subchn, int *_push)
{
	long push = 0;
	
	snd_debug_check(subchn == NULL, -ENODEV);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_PLAYBACK, -EINVAL);
	if (get_user(push, _push))
		return -EFAULT;
	push = push ? 1 : 0;
	return subchn->runtime->hw->ioctl(subchn->pchn->private_data, subchn,
					  SND_PCM_IOCTL1_PAUSE,
					  (unsigned long *)&push);
}

static void snd_pcm_add_file(snd_pcm_channel_t *chn,
			     snd_pcm_file_t *pcm_file)
{
	pcm_file->next = chn->files;
	chn->files = pcm_file;
}

static void snd_pcm_remove_file(snd_pcm_channel_t *chn,
				snd_pcm_file_t *pcm_file)
{
	snd_pcm_file_t * pcm_file1;
	if (chn->files == pcm_file) {
		chn->files = pcm_file->next;
	} else {
		pcm_file1 = chn->files;
		while (pcm_file1 && pcm_file1->next != pcm_file)
			pcm_file1 = pcm_file1->next;
		if (pcm_file1 != NULL)
			pcm_file1->next = pcm_file->next;
	}
}

static int snd_pcm_release_file(snd_pcm_file_t * pcm_file)
{
	snd_pcm_subchn_t * subchn;
	snd_pcm_channel_t * chn;

	snd_debug_check(pcm_file == NULL, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -ENXIO);
	chn = subchn->pchn;
	if (*subchn->runtime->status == SND_PCM_STATUS_RUNNING)
		snd_pcm_channel_go(subchn, SND_PCM_TRIGGER_STOP);
	chn->close(chn->private_data, subchn);
	subchn->ffile = NULL;
	snd_pcm_remove_file(chn, pcm_file);
	snd_pcm_release_subchn(subchn);
	if (pcm_file->private_free)
		pcm_file->private_free(pcm_file->private_data);
	snd_magic_kfree(pcm_file);
	return 0;
}

static int snd_pcm_open_file(struct file *file,
			     snd_pcm_t *pcm,
			     int channel,
			     snd_pcm_file_t **rpcm_file)
{
	int err = 0;
	snd_pcm_file_t *pcm_file;
	snd_pcm_subchn_t *subchn;
	snd_pcm_channel_t *chn;

	snd_debug_check(rpcm_file == NULL, -EINVAL);
	*rpcm_file = NULL;

	pcm_file = snd_magic_kcalloc(snd_pcm_file_t, 0, GFP_KERNEL);
	if (pcm_file == NULL) {
		return -ENOMEM;
	}

	if ((err = snd_pcm_open_subchn(pcm, channel, &subchn)) < 0) {
		snd_magic_kfree(pcm_file);
		return err;
	}

	chn = subchn->pchn;
	subchn->file = pcm_file;

	pcm_file->subchn = subchn;

	snd_pcm_add_file(chn, pcm_file);

	if ((err = chn->open(chn->private_data, subchn))<0) {
		snd_pcm_release_file(pcm_file);
		return err;
	}
	subchn->ffile = file;

	file->private_data = pcm_file;
	*rpcm_file = pcm_file;
	return 0;
}

int snd_pcm_open(unsigned short minor, int cardnum,
		 int device, struct file *file, int channel)
{
	int err;
	snd_pcm_t *pcm;
	snd_pcm_file_t *pcm_file;

	pcm = snd_pcm_devices[(cardnum * SND_PCM_DEVICES) + device];
	if (pcm == NULL)
		return -ENODEV;
	while (1) {
		down(&pcm->open_mutex);
		err = snd_pcm_open_file(file, pcm, channel, &pcm_file);
		if (err >= 0)
			break;
		up(&pcm->open_mutex);
		if (err == -EAGAIN) {
			if (file->f_flags & O_NONBLOCK)
				return -EBUSY;
		} else {
			return err;
		}
		interruptible_sleep_on(&pcm->open_wait);
		if (signal_pending(current))
			return -ERESTARTSYS;
	}
	MOD_INC_USE_COUNT;
	pcm->card->use_inc(pcm->card);
	up(&pcm->open_mutex);
	return 0;
}

static int snd_pcm_playback_open(unsigned short minor, int cardnum,
				 int device, struct file *file)
{
	return snd_pcm_open(minor, cardnum, device, file, 
			    SND_PCM_CHANNEL_PLAYBACK);
}

static int snd_pcm_capture_open(unsigned short minor, int cardnum,
				int device, struct file *file)
{
	return snd_pcm_open(minor, cardnum, device, file,
			    SND_PCM_CHANNEL_CAPTURE);
}

int snd_pcm_release(unsigned short minor, int cardnum,
		    int device, struct file *file)
{
	snd_pcm_t *pcm;
	snd_pcm_subchn_t *subchn;
	snd_pcm_file_t *pcm_file;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -ENXIO);
	pcm = subchn->pcm;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK)
		snd_pcm_playback_flush(subchn);  /* synchronize playback */
	down(&pcm->open_mutex);
	snd_pcm_release_file(pcm_file);
	up(&pcm->open_mutex);
	wake_up(&pcm->open_wait);
	pcm->card->use_dec(pcm->card);
	MOD_DEC_USE_COUNT;
	return 0;
}

static int snd_pcm_common_ioctl1(snd_pcm_subchn_t *subchn,
				 unsigned int cmd, unsigned long arg)
{
	snd_debug_check(subchn == NULL, -EINVAL);

	switch (cmd) {
	case SND_PCM_IOCTL_PVERSION:
		return put_user(SND_PCM_VERSION, (int *)arg) ? -EFAULT : 0;
	case SND_PCM_IOCTL_INFO:
		return snd_pcm_info(subchn->pcm, (snd_pcm_info_t *) arg);
	case SND_PCM_IOCTL_CHANNEL_INFO:
		return snd_pcm_channel_info(subchn, (snd_pcm_channel_info_t *) arg);
	case SND_PCM_IOCTL_CHANNEL_PARAMS:
		return snd_pcm_channel_params(subchn, (snd_pcm_channel_params_t *) arg);
	case SND_PCM_IOCTL_CHANNEL_SETUP:
		return snd_pcm_channel_setup(subchn, (snd_pcm_channel_setup_t *) arg);
	case SND_PCM_IOCTL_CHANNEL_STATUS:
		return snd_pcm_channel_status(subchn, (snd_pcm_channel_status_t *) arg);
	case SND_PCM_IOCTL_CHANNEL_PREPARE:
		return snd_pcm_channel_prepare(subchn);
	case SND_PCM_IOCTL_CHANNEL_GO:
		return snd_pcm_channel_go(subchn, SND_PCM_TRIGGER_GO);
	case SND_PCM_IOCTL_SYNC_GO:
		return snd_pcm_channel_sync_go(subchn, (snd_pcm_sync_t *) arg);
	}
	snd_printd("pcm: unknown command = 0x%x\n", cmd);
	return -EINVAL;
}

static int snd_pcm_playback_ioctl1(snd_pcm_subchn_t *subchn,
				   unsigned int cmd, unsigned long arg)
{
	snd_debug_check(subchn == NULL, -EINVAL);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_PLAYBACK, -EINVAL);
	switch (cmd) {
	case SND_PCM_IOCTL_CHANNEL_FLUSH:
		return snd_pcm_playback_flush(subchn);
	case SND_PCM_IOCTL_CHANNEL_DRAIN:
		return snd_pcm_playback_drain(subchn);
	case SND_PCM_IOCTL_CHANNEL_PAUSE:
		return snd_pcm_playback_pause(subchn, (int *)arg);
	}
	return snd_pcm_common_ioctl1(subchn, cmd, arg);
}

static int snd_pcm_capture_ioctl1(snd_pcm_subchn_t *subchn,
				  unsigned int cmd, unsigned long arg)
{
	snd_debug_check(subchn == NULL, -EINVAL);
	snd_debug_check(subchn->channel != SND_PCM_CHANNEL_CAPTURE, -EINVAL);
	switch (cmd) {
	case SND_PCM_IOCTL_CHANNEL_FLUSH:
		return snd_pcm_capture_flush(subchn);
	}
	return snd_pcm_common_ioctl1(subchn, cmd, arg);
}

static int snd_pcm_playback_ioctl(struct file *file,
				  unsigned int cmd, unsigned long arg)
{
	snd_pcm_file_t *pcm_file;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);

	if (((cmd >> 8) & 0xff) != 'A')
		return -EINVAL;

	return snd_pcm_playback_ioctl1(pcm_file->subchn, cmd, arg);
}

static int snd_pcm_capture_ioctl(struct file *file,
				  unsigned int cmd, unsigned long arg)
{
	snd_pcm_file_t *pcm_file;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);

	if (((cmd >> 8) & 0xff) != 'A')
		return -EINVAL;

	return snd_pcm_capture_ioctl1(pcm_file->subchn, cmd, arg);
}

int snd_pcm_kernel_playback_ioctl(snd_pcm_subchn_t *subchn,
				  unsigned int cmd, unsigned long arg)
{
	mm_segment_t fs;
	int result;
	
	fs = snd_enter_user();
	result = snd_pcm_playback_ioctl1(subchn, cmd, arg);
	snd_leave_user(fs);
	return result;
}

int snd_pcm_kernel_capture_ioctl(snd_pcm_subchn_t *subchn,
				 unsigned int cmd, unsigned long arg)
{
	mm_segment_t fs;
	int result;
	
	fs = snd_enter_user();
	result = snd_pcm_capture_ioctl1(subchn, cmd, arg);
	snd_leave_user(fs);
	return result;
}

int snd_pcm_kernel_ioctl(snd_pcm_subchn_t *subchn,
			 unsigned int cmd, unsigned long arg)
{
	switch (subchn->channel) {
	case SND_PCM_CHANNEL_PLAYBACK:
		return snd_pcm_kernel_playback_ioctl(subchn, cmd, arg);
	case SND_PCM_CHANNEL_CAPTURE:
		return snd_pcm_kernel_capture_ioctl(subchn, cmd, arg);
	default:
		return -EINVAL;
	}
}

static long snd_pcm_read(struct file *file, char *buf, long count)
{
	snd_pcm_file_t * pcm_file;
	snd_pcm_subchn_t * subchn;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -EIO);
	return snd_pcm_lib_read(subchn, buf, count);
}

static long snd_pcm_readv(struct file *file, const struct iovec *vector,
			  unsigned long count)
{
	snd_pcm_file_t * pcm_file;
	snd_pcm_subchn_t * subchn;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -EIO);
	return snd_pcm_lib_readv(subchn, vector, count);
}

static long snd_pcm_write(struct file *file, const char *buf, long count)
{
	snd_pcm_file_t *pcm_file;
	snd_pcm_subchn_t * subchn;
	long result;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -EIO);
	up(&file->f_dentry->d_inode->i_sem);
	result = snd_pcm_lib_write(subchn, buf, count);
	down(&file->f_dentry->d_inode->i_sem);
	return result;
}

static long snd_pcm_writev(struct file *file, const struct iovec *vector,
			   unsigned long count)
{
	snd_pcm_file_t *pcm_file;
	snd_pcm_subchn_t * subchn;
	long result;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -EIO);
	up(&file->f_dentry->d_inode->i_sem);
	result = snd_pcm_lib_writev(subchn, vector, count);
	down(&file->f_dentry->d_inode->i_sem);
	return result;
}

unsigned int snd_pcm_playback_poll(struct file *file, poll_table * wait)
{
	snd_pcm_file_t *pcm_file;
	unsigned int mask;
	snd_pcm_subchn_t *subchn;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, 0);

	subchn = pcm_file->subchn;
	if (subchn == NULL)
		return 0;
	
	poll_wait(file, &subchn->runtime->sleep, wait);
	mask = 0;
	if (*subchn->runtime->status != SND_PCM_STATUS_RUNNING ||
	    snd_pcm_playback_ok(subchn))
		mask |= POLLOUT | POLLWRNORM;
	return mask;
}

unsigned int snd_pcm_capture_poll(struct file *file, poll_table * wait)
{
	snd_pcm_file_t *pcm_file;
	unsigned int mask;
	snd_pcm_subchn_t *subchn;

	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, 0);

	subchn = pcm_file->subchn;
	if (subchn == NULL)
		return 0;
	
	poll_wait(file, &subchn->runtime->sleep, wait);
	mask = 0;
	if (*subchn->runtime->status != SND_PCM_STATUS_RUNNING ||
            snd_pcm_capture_ok(subchn))
		mask |= POLLIN | POLLRDNORM;
	return mask;
}

static void snd_pcm_vma_notify_control(void *client, void *data)
{
	snd_pcm_subchn_t *subchn = snd_magic_cast(snd_pcm_subchn_t, client, );
	snd_pcm_runtime_t *runtime;
	
	if (subchn == NULL)
		return;
	runtime = subchn->runtime;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		snd_pcm_playback_drain(subchn);
	} else {
		snd_pcm_capture_flush(subchn);
	}
	if (runtime) {
		runtime->_sstatus = runtime->mmap_control->status.status;
		runtime->status = &runtime->_sstatus;
		runtime->frag_tail = &runtime->_sfrag_tail;
		*runtime->frag_tail = 0;
		runtime->frag_head = &runtime->_sfrag_head;
		*runtime->frag_head = 0;
		snd_free_pages(runtime->mmap_control, sizeof(snd_pcm_mmap_control_t));
		runtime->mmap_control = NULL;
		runtime->mmap_control_vma = NULL;
		runtime->flags &= ~SND_PCM_FLG_MMAP;
	}
	MOD_DEC_USE_COUNT;
}

static void snd_pcm_vma_notify_data(void *client, void *data)
{
	snd_pcm_subchn_t *subchn = snd_magic_cast(snd_pcm_subchn_t, client, );
	snd_pcm_runtime_t *runtime;
	
	if (subchn == NULL)
		return;
	runtime = subchn->runtime;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		snd_pcm_playback_drain(subchn);
	} else {
		snd_pcm_capture_flush(subchn);
	}
	if (runtime) {
		runtime->mmap_data = NULL;
		runtime->mmap_data_vma = NULL;
	}
	MOD_DEC_USE_COUNT;
}

static int snd_pcm_mmap(struct inode *inode, struct file *file,
			struct vm_area_struct *area)
{
	snd_pcm_file_t * pcm_file;
	snd_pcm_subchn_t * subchn;
	snd_pcm_runtime_t * runtime;
	snd_vma_t * vma;
	long size, size1;
	char *ptr;
	int res;
	unsigned long page;
	
	pcm_file = snd_magic_cast(snd_pcm_file_t, file->private_data, -ENXIO);
	subchn = pcm_file->subchn;
	snd_debug_check(subchn == NULL, -ENXIO);
	if ((area->vm_flags & (VM_READ|VM_WRITE)) == 0)
		return -EINVAL;
	if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
		if (!(area->vm_flags & VM_WRITE))
			return -EINVAL;
	} else {
		if (!(area->vm_flags & VM_READ))
			return -EINVAL;
	}
	runtime = subchn->runtime;
	snd_debug_check(runtime == NULL, -EAGAIN);
	if (*runtime->status != SND_PCM_STATUS_READY)
		return -EBADFD;
	size = area->vm_end - area->vm_start;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 3, 25)
	page = area->vm_pgoff << PAGE_SHIFT;
#else
	page = area->vm_offset;
#endif
	switch (page) {
	case SND_PCM_MMAP_OFFSET_CONTROL:
		if (size != PAGE_ALIGN(sizeof(snd_pcm_mmap_control_t)))
			return -EINVAL;
		if (runtime->mmap_control)
			return -EBUSY;
		vma = snd_kcalloc(sizeof(snd_vma_t), GFP_KERNEL);
		if (vma == NULL)
			return -ENOMEM;
		vma->notify = snd_pcm_vma_notify_control;
		vma->notify_client = subchn;
		runtime->mmap_control = (snd_pcm_mmap_control_t *) snd_malloc_pages(sizeof(snd_pcm_mmap_control_t), NULL, 0);
		if (runtime->mmap_control == NULL) {
			snd_kfree(vma);
			return -ENOMEM;
		}
		memset(runtime->mmap_control, 0, sizeof(snd_pcm_mmap_control_t));
		vma->area = area;
		vma->notify_data = runtime->mmap_control;
		vma->notify_size = sizeof(snd_pcm_mmap_control_t);
		if (remap_page_range(area->vm_start,
				     virt_to_phys(runtime->mmap_control),
				     PAGE_ALIGN(sizeof(snd_pcm_mmap_control_t)),
				     area->vm_page_prot)) {
			snd_free_pages(runtime->mmap_control, sizeof(snd_pcm_mmap_control_t));
			snd_kfree(vma);
			return -EAGAIN;
		}
		area->vm_file = file;
		res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
					 SND_PCM_IOCTL1_MMAP_CTRL, 0);
		if (res < 0) {
			snd_free_pages(runtime->mmap_control, sizeof(snd_pcm_mmap_control_t));
			snd_kfree(vma);
			return res;
		}
		snd_vma_add(vma);
		MOD_INC_USE_COUNT;
		runtime->mmap_control_vma = vma;
		runtime->flags |= SND_PCM_FLG_MMAP;
		runtime->status = &runtime->mmap_control->status.status;
		*runtime->status = SND_PCM_STATUS_READY;
		if (subchn->channel == SND_PCM_CHANNEL_PLAYBACK) {
			runtime->frag_tail = &runtime->mmap_control->status.frag_io;
		} else {
			runtime->frag_head = &runtime->mmap_control->status.frag_io;
		}
		break;
	case SND_PCM_MMAP_OFFSET_DATA:
		res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
					 SND_PCM_IOCTL1_MMAP_SIZE, &size1);
		if (res < 0)
			return res;
		if (size != size1)
			return -EINVAL;
		if (runtime->mmap_control == NULL)
			return -EBADFD;
		vma = snd_kcalloc(sizeof(snd_vma_t), GFP_KERNEL);
		if (vma == NULL)
			return -ENOMEM;
		vma->area = area;
		vma->notify = snd_pcm_vma_notify_data;
		vma->notify_client = subchn;
		res = runtime->hw->ioctl(subchn->pchn->private_data, subchn,
					 SND_PCM_IOCTL1_MMAP_PTR, (long *)&ptr);
		if (res < 0) {
			snd_kfree(vma);
			return res;
		}
		vma->notify_data = ptr;
		vma->notify_size = size1;
		if (remap_page_range(area->vm_start,
				     virt_to_phys(ptr),
				     size1,
				     area->vm_page_prot)) {
			snd_kfree(vma);
			return -EAGAIN;
		}
		runtime->mmap_data = ptr;
		runtime->mmap_data_vma = vma;
		area->vm_file = file;
		snd_vma_add(vma);
		MOD_INC_USE_COUNT;
		break;
	default:
		return -EINVAL;
	}
	return 0;
}

/*
 *  Register section
 */

snd_minor_t snd_pcm_reg[2] =
{
	{
		comment:	"digital audio playback",
		write:		snd_pcm_write,
		writev:		snd_pcm_writev,
		open:		snd_pcm_playback_open,
		release:	snd_pcm_release,
		poll:		snd_pcm_playback_poll,
		ioctl:		snd_pcm_playback_ioctl,
		mmap:		snd_pcm_mmap,
	},
	{
		comment:	"digital audio capture",
		read:		snd_pcm_read,
		readv:		snd_pcm_readv,
		open:		snd_pcm_capture_open,
		release:	snd_pcm_release,
		poll:		snd_pcm_capture_poll,
		ioctl:		snd_pcm_capture_ioctl,
		mmap:		snd_pcm_mmap,
	}
};
