/*
 *  Digital Audio (PCM) - /proc interface
 *  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 "driver.h"
#include "pcm.h"
#include "info.h"

#define SND_PCM_PROC_BUFFER_SIZE	(128*1024)

struct snd_pcm_proc_private {
	unsigned char *buffer;		/* allocated buffer */
	unsigned int size;
	unsigned int used;
	unsigned int head;
	unsigned int tail;
	unsigned int xrun;
	unsigned int sleep:1,
		     packet:1,
		     active:1,		/* write is active */
		     change:1;		/* format was changed */
	snd_pcm_format_t format;	/* active format */
	snd_spin_define(ptr);
	snd_spin_define(sleep);
	snd_sleep_define(read);
	snd_sleep_define(active);
	struct snd_pcm_proc_private *next;
};

static void inline snd_pcm_proc_file_reset(struct snd_pcm_proc_private *proc)
{
	proc->head = proc->tail = proc->used = 0;
}

static void snd_pcm_proc_file_write(struct snd_pcm_proc_private *proc,
				    const void *buffer, unsigned int count)
{
	unsigned long flags;
	unsigned int tmp, size, size1;

	if (!count)
		return;
	snd_spin_lock(proc, ptr, &flags);
	proc->active = 1;
	tmp = proc->size - proc->used;	/* free */
	if (tmp < count) {
		proc->xrun += count - tmp;
		count = tmp;
	}
	tmp = proc->size - proc->head;
	snd_spin_unlock(proc, ptr, &flags);
	size = count;
	if (tmp < size)
		size = tmp;
	size1 = count - size;
	copy_from_user(proc->buffer + proc->head, buffer, size);
	if (size1 > 0) {
		copy_from_user(proc->buffer, buffer + size, size1);
	}
	snd_spin_lock(proc, ptr, &flags);
	proc->head += count;
	proc->head %= proc->size;
	proc->used += count;
	proc->active = 0;
	snd_spin_unlock(proc, ptr, &flags);
	if (proc->sleep)
		snd_wakeup(proc, read);
}

static void snd_pcm_proc_file_format_write(struct snd_pcm_proc_private *proc)
{
	struct {
		snd_pcm_loopback_header_t header;
		snd_pcm_format_t format;
	} data;
	mm_segment_t fs;

	data.header.size = sizeof(snd_pcm_format_t);
	data.header.type = SND_PCM_LB_TYPE_FORMAT;
	memcpy(&data.format, &proc->format, sizeof(snd_pcm_format_t));
	fs = snd_enter_user();
	snd_pcm_proc_file_write(proc, &data, sizeof(data));
	snd_leave_user(fs);
}

void snd_pcm_proc_format(snd_pcm_channel_t * pchn,
			 snd_pcm_format_t * format)
{
	struct snd_pcm_proc_private *first;

	snd_mutex_down(pchn, proc);
	first = (struct snd_pcm_proc_private *) pchn->proc_private;
	while (first) {
		memcpy(&first->format, format, sizeof(snd_pcm_format_t));
		first->change = 1;
		first = first->next;
	}
	snd_mutex_up(pchn, proc);
}

void snd_pcm_proc_write(snd_pcm_channel_t * pchn,
			const void *buffer, unsigned int count)
{
	struct snd_pcm_proc_private *first;
	snd_pcm_loopback_header_t header;
	mm_segment_t fs;

	snd_mutex_down(pchn, proc);
	first = (struct snd_pcm_proc_private *) pchn->proc_private;
	while (first) {
		if (first->packet) {
			if (first->change) {
				snd_pcm_proc_file_format_write(first);
				first->change = 0;
			}
			header.size = count;
			header.type = SND_PCM_LB_TYPE_DATA;
			fs = snd_enter_user();
			snd_pcm_proc_file_write(first, &header, sizeof(header));
			snd_leave_user(fs);
		}
		snd_pcm_proc_file_write(first, buffer, count);
		first = first->next;
	}
	snd_mutex_up(pchn, proc);
}

static long snd_pcm_proc_read(void *private_data, void *file_private_data,
			      struct file *file, char *buf, long count)
{
	unsigned long flags;
	struct snd_pcm_proc_private *proc;
	unsigned int size, size1;
	long count1, result = 0;

	proc = (struct snd_pcm_proc_private *) file_private_data;
	if (count > proc->size)
		count = proc->size;
	if (!count)
		return result;
	while (count > 0) {
		while (!proc->used) {
			if (file->f_flags & O_NONBLOCK)
				return result;
			snd_spin_lock(proc, sleep, &flags);
			proc->sleep = 1;
			snd_sleep(proc, read, 10 * HZ);
			proc->sleep = 0;
			snd_spin_unlock(proc, sleep, &flags);
			if (snd_sleep_abort(proc, read)) {
				return -EINTR;
			}
		}
		count1 = count;
		snd_spin_lock(proc, ptr, &flags);
		if (count1 > proc->used)
			count1 = proc->used;
		size = count1;
		if (proc->size - proc->tail < size)
			size = proc->size - proc->tail;
		size1 = count1 - size;
		snd_spin_unlock(proc, ptr, &flags);
		if (verify_area(VERIFY_WRITE, buf, count1))
			return -EFAULT;
		copy_to_user(buf, proc->buffer + proc->tail, size);
		buf += size;
		if (size1 > 0) {
			copy_to_user(buf, proc->buffer, size1);
			buf += size1;
		}
		snd_spin_lock(proc, ptr, &flags);
		proc->tail += count1;
		proc->tail %= proc->size;
		proc->used -= count1;
		snd_spin_unlock(proc, ptr, &flags);
		count -= count1;
		result += count1;
	}
	return result;
}

static int snd_pcm_proc_open(void *private_data, snd_info_entry_t * entry,
			     unsigned short mode, void **file_private_data)
{
	struct snd_pcm_proc_private *proc, *proc1;
	snd_pcm_t *pcm;
	snd_pcm_channel_t *pchn;
	int record;

	if (mode == O_RDWR || mode == O_WRONLY)
		return -EIO;
	proc = (struct snd_pcm_proc_private *)
			snd_calloc(sizeof(struct snd_pcm_proc_private));
	if (!proc)
		return -ENOMEM;
	proc->buffer = vmalloc(proc->size = SND_PCM_PROC_BUFFER_SIZE);
	if (!proc->buffer) {
		snd_free(proc, sizeof(struct snd_pcm_proc_private));
		return -ENOMEM;
	}
	snd_spin_prepare(proc, ptr);
	snd_spin_prepare(proc, sleep);
	snd_sleep_prepare(proc, read);
	snd_sleep_prepare(proc, active);
	pcm = (snd_pcm_t *) private_data;
	record = entry->name[strlen(entry->name) - 1] == 'r';
	pchn = !record ? &pcm->playback : &pcm->record;
	snd_mutex_down(pchn, proc);
	proc1 = (struct snd_pcm_proc_private *) pchn->proc_private;
	if (!proc1) {
		pchn->proc_private = proc;
	} else {
		while (proc1->next)
			proc1 = proc1->next;
		proc1->next = proc;
	}
	snd_mutex_up(pchn, proc);
	*file_private_data = proc;
	MOD_INC_USE_COUNT;
	return 0;
}

static int snd_pcm_proc_release(void *private_data, snd_info_entry_t * entry,
			        unsigned short mode, void *file_private_data)
{
	struct snd_pcm_proc_private *proc, *proc1;
	snd_pcm_t *pcm;
	snd_pcm_channel_t *pchn;
	int record;

	proc = (struct snd_pcm_proc_private *) file_private_data;
	if (!proc) {
		MOD_DEC_USE_COUNT;
		return -EIO;
	}
	pcm = (snd_pcm_t *) private_data;
	record = entry->name[strlen(entry->name) - 1] == 'r';
	pchn = !record ? &pcm->playback : &pcm->record;
	snd_mutex_down(pchn, proc);
	proc1 = (struct snd_pcm_proc_private *) pchn->proc_private;
	if (proc == proc1) {
		pchn->proc_private = proc->next;
	} else {
		while (proc1->next != proc)
			proc1 = proc1->next;
		proc1->next = proc->next;
	}
	snd_mutex_up(pchn, proc);
	vfree(proc->buffer);
	snd_free(proc, sizeof(struct snd_pcm_proc_private));
	MOD_DEC_USE_COUNT;
	return 0;
}

static int snd_pcm_proc_ioctl(void * private_data,
			      void * file_private_data,
			      struct file * file,
			      unsigned int cmd,
			      unsigned long arg)
{
	struct snd_pcm_proc_private *proc;
	unsigned long flags;

	proc = (struct snd_pcm_proc_private *) file_private_data;
	printk("cmd = 0x%x (%c)\n", cmd, (cmd >> 8) & 0xff);
	if (((cmd >> 8) & 0xff) != 'L') return -ENXIO;
	switch (cmd) {
	case SND_PCM_LB_IOCTL_PVERSION:
		return snd_ioctl_out((long *) arg, SND_PCM_LB_VERSION);
	case SND_PCM_LB_IOCTL_STREAM_MODE:
		snd_spin_lock(proc, ptr, &flags);
		while (proc->active) {
			snd_spin_unlock(proc, ptr, &flags);
			snd_schedule(proc, active, 1);
			snd_spin_lock(proc, ptr, &flags);
		}
		switch (snd_ioctl_in((long *) arg)) {
			case SND_PCM_LB_STREAM_MODE_PACKET:
				proc->packet=1;
			default:
				proc->packet=0;
		}
		snd_pcm_proc_file_reset(proc);
		snd_spin_unlock(proc, ptr, &flags);
		return 0;
	case SND_PCM_LB_IOCTL_FORMAT:
		if (verify_area(VERIFY_WRITE, (void *) arg, sizeof(snd_pcm_format_t)))
			return -EFAULT;
		copy_to_user((void *) arg, &proc->format, sizeof(snd_pcm_format_t));
		return 0;
	}
	return -ENXIO;
}

#ifdef LINUX_2_1
static unsigned int snd_pcm_proc_poll(void *private_data,
				      void *file_private_data,
				      struct file *file,
				      poll_table * wait)
{
	unsigned int mask;
	unsigned long flags;
	struct snd_pcm_proc_private *proc;

	proc = (struct snd_pcm_proc_private *) file_private_data;

	snd_spin_lock(proc, sleep, &flags);
	proc->sleep = 1;
	snd_poll_wait(file, proc, read, wait);
	snd_spin_unlock(proc, sleep, &flags);

	mask = 0;
	if (proc->used)
		mask |= POLLIN | POLLRDNORM;
	return mask;
}
#else
static int snd_pcm_proc_select(void *private_data, void *file_private_data,
			       struct file *file, int sel_type,
			       select_table * wait)
{
	unsigned long flags;
	struct snd_pcm_proc_private *proc;

	proc = (struct snd_pcm_proc_private *) file_private_data;
	if (sel_type == SEL_IN) {
		snd_spin_lock(proc, sleep, &flags);
		if (!proc->used) {
			proc->sleep = 1;
			snd_select_wait(proc, read, wait);
			snd_spin_unlock(proc, sleep, &flags);
			return 0;
		}
		proc->sleep = 0;
		snd_spin_unlock(proc, sleep, &flags);
	}
	return 0;
}
#endif

static void snd_pcm_proc_init_direction(snd_pcm_t * pcm, int direction)
{
	char name[16];
	snd_info_entry_t *entry;
	snd_pcm_channel_t *pchn;

	pchn = !direction ? &pcm->playback : &pcm->record;
	sprintf(name, "pcm%d%s", pcm->device, !direction ? "p" : "r");
	entry = snd_info_create_entry(pcm->card, name);
	if (entry) {
		entry->type = SND_INFO_ENTRY_DATA;
		entry->private_data = pcm;
		entry->t.data.open = snd_pcm_proc_open;
		entry->t.data.release = snd_pcm_proc_release;
		entry->t.data.read = snd_pcm_proc_read;
		entry->t.data.ioctl = snd_pcm_proc_ioctl;
#ifdef LINUX_2_1
		entry->t.data.poll = snd_pcm_proc_poll;
#else
		entry->t.data.select = snd_pcm_proc_select;
#endif
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	pchn->proc_entry = entry;
}


void snd_pcm_proc_init(snd_pcm_t * pcm)
{
	snd_mutex_prepare(&pcm->playback, proc);
	snd_mutex_prepare(&pcm->record, proc);
	if (pcm->info_flags & SND_PCM_INFO_PLAYBACK) {
		snd_pcm_proc_init_direction(pcm, 0);
	}
	if (pcm->info_flags & SND_PCM_INFO_RECORD) {
		snd_pcm_proc_init_direction(pcm, 1);
	}
}

void snd_pcm_proc_done(snd_pcm_t * pcm)
{
	if ((pcm->info_flags & SND_PCM_INFO_RECORD) && pcm->record.proc_entry) {
		snd_info_unregister((snd_info_entry_t *) pcm->record.proc_entry);
		pcm->record.proc_entry = NULL;
	}
	if ((pcm->info_flags & SND_PCM_INFO_PLAYBACK) && pcm->playback.proc_entry) {
		snd_info_unregister((snd_info_entry_t *) pcm->playback.proc_entry);
		pcm->playback.proc_entry = NULL;
	}
}
