/*
 *  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 "../include/driver.h"
#include "../include/pcm.h"
#include "../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 packet:1,
		     active:1,		/* write is active */
		     change:1;		/* format was changed */
	snd_pcm_format_t format;	/* active format */
	spinlock_t ptr;
	wait_queue_head_t read_sleep;
	wait_queue_head_t active_sleep;
	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;
	spin_lock_irqsave(&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;
	spin_unlock_irqrestore(&proc->ptr, flags);
	size = count;
	if (tmp < size)
		size = tmp;
	size1 = count - size;
	if (copy_from_user(proc->buffer + proc->head, buffer, size))
		return;
	if (size1 > 0) {
		if (copy_from_user(proc->buffer, buffer + size, size1))
			return;
	}
	spin_lock_irqsave(&proc->ptr, flags);
	proc->head += count;
	proc->head %= proc->size;
	proc->used += count;
	proc->active = 0;
	spin_unlock_irqrestore(&proc->ptr, flags);
	wake_up(&proc->read_sleep);
}

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_subchn_t * subchn,
			 snd_pcm_format_t * format)
{
	struct snd_pcm_proc_private *first;

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

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

	down(&subchn->proc);
	first = (struct snd_pcm_proc_private *) subchn->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;
	}
	up(&subchn->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;
			interruptible_sleep_on(&proc->read_sleep);
			if (signal_pending(current))
				return -EINTR;
		}
		count1 = count;
		spin_lock_irqsave(&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;
		spin_unlock_irqrestore(&proc->ptr, flags);
		if (copy_to_user(buf, proc->buffer + proc->tail, size))
			return -EFAULT;
		buf += size;
		if (size1 > 0) {
			copy_to_user(buf, proc->buffer, size1);
			buf += size1;
		}
		spin_lock_irqsave(&proc->ptr, flags);
		proc->tail += count1;
		proc->tail %= proc->size;
		proc->used -= count1;
		spin_unlock_irqrestore(&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_subchn_t *subchn;

	if (mode == O_RDWR || mode == O_WRONLY)
		return -EIO;
	proc = (struct snd_pcm_proc_private *)
			snd_kcalloc(sizeof(struct snd_pcm_proc_private), GFP_KERNEL);
	if (!proc)
		return -ENOMEM;
	proc->buffer = snd_vmalloc(proc->size = SND_PCM_PROC_BUFFER_SIZE);
	if (!proc->buffer) {
		snd_kfree(proc);
		return -ENOMEM;
	}
	proc->ptr = SPIN_LOCK_UNLOCKED;
	init_waitqueue_head(&proc->read_sleep);
	init_waitqueue_head(&proc->active_sleep);
	subchn = (snd_pcm_subchn_t *) private_data;
	down(&subchn->proc);
	proc1 = (struct snd_pcm_proc_private *) subchn->proc_private;
	if (!proc1) {
		subchn->proc_private = proc;
	} else {
		while (proc1->next)
			proc1 = proc1->next;
		proc1->next = proc;
	}
	up(&subchn->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_subchn_t *subchn;

	proc = (struct snd_pcm_proc_private *) file_private_data;
	if (!proc) {
		MOD_DEC_USE_COUNT;
		return -EIO;
	}
	subchn = (snd_pcm_subchn_t *) private_data;
	down(&subchn->proc);
	proc1 = (struct snd_pcm_proc_private *) subchn->proc_private;
	if (proc == proc1) {
		subchn->proc_private = proc->next;
	} else {
		while (proc1->next != proc)
			proc1 = proc1->next;
		proc1->next = proc->next;
	}
	up(&subchn->proc);
	snd_vfree(proc->buffer);
	snd_kfree(proc);
	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:
		spin_lock_irqsave(&proc->ptr, flags);
		while (proc->active) {
			spin_unlock_irqrestore(&proc->ptr, flags);
			current->state = TASK_INTERRUPTIBLE;
			schedule_timeout(1);
			spin_lock_irqsave(&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);
		spin_unlock_irqrestore(&proc->ptr, flags);
		return 0;
	case SND_PCM_LB_IOCTL_FORMAT:
		if (copy_to_user((void *) arg, &proc->format, sizeof(snd_pcm_format_t)))
			return -EFAULT;
		return 0;
	}
	return -ENXIO;
}

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

	proc = (struct snd_pcm_proc_private *) file_private_data;

	mask = 0;
	if (proc->used)
		mask |= POLLIN | POLLRDNORM;
	return mask;
}

static void snd_pcm_proc_init_direction(snd_pcm_subchn_t *subchn, char direction)
{
	char name[16];
	snd_info_entry_t *entry;

	init_MUTEX(&subchn->proc);
	sprintf(name, "pcmloopD%dS%i%c", subchn->pcm->device, subchn->number, direction);
	entry = snd_info_create_entry(subchn->pcm->card, name);
	if (entry) {
		entry->type = SND_INFO_ENTRY_DATA;
		entry->private_data = subchn;
		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;
		entry->t.data.poll = snd_pcm_proc_poll;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	subchn->proc_entry = entry;
}


void snd_pcm_proc_init(snd_pcm_t * pcm)
{
	snd_pcm_subchn_t *subchn;

	if (pcm->info_flags & SND_PCM_INFO_PLAYBACK) {
		for (subchn = pcm->playback.subchn; subchn; subchn = subchn->next)
			snd_pcm_proc_init_direction(subchn, 'p');
	}
	if (pcm->info_flags & SND_PCM_INFO_CAPTURE) {
		for (subchn = pcm->capture.subchn; subchn; subchn = subchn->next)
			snd_pcm_proc_init_direction(subchn, 'c');
	}
}

void snd_pcm_proc_done(snd_pcm_t * pcm)
{
	snd_pcm_subchn_t *subchn;

	if (pcm->info_flags & SND_PCM_INFO_CAPTURE) {
		for (subchn = pcm->capture.subchn; subchn; subchn = subchn->next)
			if (subchn->proc_entry)
				snd_info_unregister((snd_info_entry_t *) subchn->proc_entry);
	}
	if (pcm->info_flags & SND_PCM_INFO_PLAYBACK) {
		for (subchn = pcm->playback.subchn; subchn; subchn = subchn->next)
			if (subchn->proc_entry)
				snd_info_unregister((snd_info_entry_t *) subchn->proc_entry);
	}
}
