/*
 *  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	(256*1024)

typedef 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 pos;
	unsigned int packet:1,
		     active:1,
		     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;
} snd_pcm_proc_private_t;

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

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

	if (count <= 0)
		return -EINVAL;
	spin_lock_irqsave(&proc->ptr, flags);
	if (!proc->active) {
		spin_unlock_irqrestore(&proc->ptr, flags);
		return 0;
	}
	tmp = proc->size - proc->used;	/* free */
	if (tmp < count) {
		spin_unlock_irqrestore(&proc->ptr, flags);
		return -ENOMEM;
	}
	tmp = proc->size - proc->head;
	size = count;
	if (tmp < size)
		size = tmp;
	size1 = count - size;
	if (kernel) {
		memcpy(proc->buffer + proc->head, buffer, size);
	} else if (copy_from_user(proc->buffer + proc->head, buffer, size)) {
		spin_unlock_irqrestore(&proc->ptr, flags);
		return -EFAULT;
	}
	if (size1 > 0) {
		if (kernel) {
			memcpy(proc->buffer, buffer + size, size1);
		} else if (copy_from_user(proc->buffer, buffer + size, size1)) {
			spin_unlock_irqrestore(&proc->ptr, flags);
			return -EFAULT;
		}
	}
	proc->head += count;
	proc->head %= proc->size;
	proc->used += count;
	spin_unlock_irqrestore(&proc->ptr, flags);
	wake_up(&proc->read_sleep);
	return count;
}

static int snd_pcm_proc_file_silence(snd_pcm_proc_private_t *proc,
				     int count, unsigned char silencebyte)
{
	unsigned long flags;
	unsigned int tmp, size, size1;

	if (count <= 0)
		return -EINVAL;
	spin_lock_irqsave(&proc->ptr, flags);
	if (!proc->active) {
		spin_unlock_irqrestore(&proc->ptr, flags);
		return 0;
	}
	tmp = proc->size - proc->used;	/* free */
	if (tmp < count) {
		spin_unlock_irqrestore(&proc->ptr, flags);
		return -ENOMEM;
	}
	tmp = proc->size - proc->head;
	size = count;
	if (tmp < size)
		size = tmp;
	size1 = count - size;
	memset(proc->buffer + proc->head, silencebyte, size);
	if (size1 > 0)
		memset(proc->buffer, silencebyte, size1);
	proc->head += count;
	proc->head %= proc->size;
	proc->used += count;
	spin_unlock_irqrestore(&proc->ptr, flags);
	wake_up(&proc->read_sleep);
	return count;
}

static int snd_pcm_proc_file_format_write(snd_pcm_proc_private_t *proc)
{
	struct {
		snd_pcm_loopback_header_t header;
		snd_pcm_format_t format;
	} data;
	int res;

	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));
	res = snd_pcm_proc_file_write(proc, &data, sizeof(data), 1);
	proc->change = 0;
	return res;
}

static int snd_pcm_proc_file_position(snd_pcm_proc_private_t *proc)
{
	struct {
		snd_pcm_loopback_header_t header;
		unsigned int pos;
	} data;
	int res;

	data.header.size = sizeof(unsigned int);
	data.header.type = SND_PCM_LB_TYPE_POSITION;
	data.pos = proc->pos;
	res = snd_pcm_proc_file_write(proc, &data, sizeof(data), 1);
	return res;
}

void snd_pcm_proc_format(snd_pcm_subchn_t * subchn)
{
	unsigned long flags;
	snd_pcm_proc_private_t *first;

	snd_pcm_lock(0);
	if (subchn->runtime == NULL) {
		snd_pcm_lock(1);
		return;
	}
	spin_lock_irqsave(&subchn->proc_lock, flags);
	first = (snd_pcm_proc_private_t *) subchn->proc_private;
	while (first) {
		memcpy(&first->format, &subchn->runtime->format, sizeof(snd_pcm_format_t));
		first->change = 1;
		first = first->next;
	}
	spin_unlock_irqrestore(&subchn->proc_lock, flags);
	snd_pcm_lock(1);
}

void snd_pcm_proc_write(snd_pcm_subchn_t * subchn, unsigned int pos,
			const void *buffer, long count, int kernel)
{
	snd_pcm_proc_private_t *first;
	unsigned long flags;
	snd_pcm_loopback_header_t header;
	int res;

	spin_lock_irqsave(&subchn->proc_lock, flags);
	first = (snd_pcm_proc_private_t *) subchn->proc_private;
	while (first) {
		if (first->packet) {
			if (first->change) {
				if (snd_pcm_proc_file_format_write(first) < 0) {
					first->xrun += count;
					goto __next;
				}
			}
			if (pos != first->pos) {
				first->pos = pos;
				if (snd_pcm_proc_file_position(first) < 0) {
					first->xrun += count;
					goto __next;
				}
			}
			header.size = count;
			header.type = SND_PCM_LB_TYPE_DATA;
			res = snd_pcm_proc_file_write(first, &header, sizeof(header), 1);
			if (res < 0) {
				first->xrun += count;
				goto __next;
			}
		}
		if (snd_pcm_proc_file_write(first, buffer, count, kernel) >= 0) {
			first->pos += count;
		} else {
			first->xrun += count;
		}
	      __next:
		first = first->next;
	}
	spin_unlock_irqrestore(&subchn->proc_lock, flags);
}

void snd_pcm_proc_write_silence(snd_pcm_subchn_t * subchn,
				unsigned int pos, long count)
{
	snd_pcm_proc_private_t *first;
	unsigned long flags;
	snd_pcm_loopback_header_t header;
	int res;

	spin_lock_irqsave(&subchn->proc_lock, flags);
	first = (snd_pcm_proc_private_t *) subchn->proc_private;
	while (first) {
		if (first->packet) {
			if (first->change) {
				if (snd_pcm_proc_file_format_write(first) < 0) {
					first->xrun += count;
					goto __next;
				}
			}
			if (pos != first->pos) {
				first->pos = pos;
				if (snd_pcm_proc_file_position(first) < 0) {
					first->xrun += count;
					goto __next;
				}
			}
			header.size = count;
			header.type = SND_PCM_LB_TYPE_SILENCE;
			res = snd_pcm_proc_file_write(first, &header, sizeof(header), 1);
			if (res < 0) {
				first->xrun += count;
				goto __next;
			}
			first->pos += count;
			goto __next;
		}
		if (snd_pcm_proc_file_silence(first, count, snd_pcm_format_silence(subchn->runtime->format.format)) >= 0) {
			first->pos += count;
		} else {
			first->xrun += count;
		}
	      __next:
		first = first->next;
	}
	spin_unlock_irqrestore(&subchn->proc_lock, flags);
}

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

	proc = snd_magic_cast(snd_pcm_proc_private_t, file_private_data, -ENXIO);
	proc->active = 1;
	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) {
			if (copy_to_user(buf, proc->buffer, size1))
				return -EFAULT;
			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)
{
	unsigned long flags;
	snd_pcm_proc_private_t *proc, *proc1;
	snd_pcm_subchn_t *subchn;

	if (mode == O_RDWR || mode == O_WRONLY)
		return -EIO;
	proc = 	snd_magic_kcalloc(snd_pcm_proc_private_t, 0, GFP_KERNEL);
	if (proc == NULL)
		return -ENOMEM;
	proc->buffer = snd_vmalloc(proc->size = SND_PCM_PROC_BUFFER_SIZE);
	if (proc->buffer == NULL) {
		snd_magic_kfree(proc);
		return -ENOMEM;
	}
	spin_lock_init(&proc->ptr);
	init_waitqueue_head(&proc->read_sleep);
	init_waitqueue_head(&proc->active_sleep);
	snd_pcm_proc_file_reset(proc);
	subchn = snd_magic_cast(snd_pcm_subchn_t, private_data, -ENXIO);
	spin_lock_irqsave(&subchn->proc_lock, flags);
	proc1 = (snd_pcm_proc_private_t *) subchn->proc_private;
	if (proc1 == NULL) {
		subchn->proc_private = proc;
	} else {
		while (proc1->next)
			proc1 = proc1->next;
		proc1->next = proc;
	}
	spin_unlock_irqrestore(&subchn->proc_lock, flags);
	snd_pcm_lock(0);
	if (subchn->runtime)
		memcpy(&proc->format, &subchn->runtime->format, sizeof(snd_pcm_format_t));
	snd_pcm_lock(1);
	*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)
{
	unsigned long flags;
	snd_pcm_proc_private_t *proc, *proc1;
	snd_pcm_subchn_t *subchn;

	proc = snd_magic_cast(snd_pcm_proc_private_t, file_private_data, -ENXIO);
	if (proc == NULL) {
		MOD_DEC_USE_COUNT;
		return -EIO;
	}
	subchn = snd_magic_cast(snd_pcm_subchn_t, private_data, -ENXIO);
	spin_lock_irqsave(&subchn->proc_lock, flags);
	proc1 = (snd_pcm_proc_private_t *) subchn->proc_private;
	if (proc == proc1) {
		subchn->proc_private = proc->next;
	} else {
		while (proc1->next != proc)
			proc1 = proc1->next;
		proc1->next = proc->next;
	}
	spin_unlock_irqrestore(&subchn->proc_lock, flags);
	snd_vfree(proc->buffer);
	snd_magic_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)
{
	snd_pcm_proc_private_t *proc = snd_magic_cast(snd_pcm_proc_private_t, file_private_data, -ENXIO);
	snd_pcm_subchn_t *subchn = snd_magic_cast(snd_pcm_subchn_t, private_data, -ENXIO);
	unsigned long flags;

	if (((cmd >> 8) & 0xff) != 'L') return -ENXIO;
	switch (cmd) {
	case SND_PCM_LB_IOCTL_PVERSION:
		return put_user(SND_PCM_LB_VERSION, (int *)arg) ? -EFAULT : 0;
	case SND_PCM_LB_IOCTL_STREAM_MODE:
		spin_lock_irqsave(&subchn->proc_lock, flags);
		{
			int val;
			
			if (get_user(val, (int *)arg))
				return -EFAULT;
			switch (val) {
			case SND_PCM_LB_STREAM_MODE_PACKET:
				proc->packet=1;
				break;
			default:
				proc->packet=0;
			}
		}
		snd_pcm_proc_file_reset(proc);
		spin_unlock_irqrestore(&subchn->proc_lock, 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;
	case SND_PCM_LB_IOCTL_STATUS:
		{
			snd_pcm_loopback_status_t status;
			
			memset(&status, 0, sizeof(status));
			snd_pcm_lock(0);
			spin_lock_irqsave(&subchn->proc_lock, flags);
			status.lost = proc->xrun;
			spin_unlock_irqrestore(&subchn->proc_lock, flags);
			if (SUBCHN_BUSY(subchn))
				snd_pcm_kernel_ioctl(subchn, SND_PCM_IOCTL_CHANNEL_STATUS, (long)&status.status);
			snd_pcm_lock(1);
		}
	}
	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;
	snd_pcm_proc_private_t *proc;

	proc = snd_magic_cast(snd_pcm_proc_private_t, file_private_data, -ENXIO);

	mask = 0;
	proc->active = 1;
	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;

	spin_lock_init(&subchn->proc_lock);
	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->chn[SND_PCM_CHANNEL_PLAYBACK].subchn; subchn; subchn = subchn->next)
			snd_pcm_proc_init_direction(subchn, 'p');
	}
	if (pcm->info_flags & SND_PCM_INFO_CAPTURE) {
		for (subchn = pcm->chn[SND_PCM_CHANNEL_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->chn[SND_PCM_CHANNEL_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->chn[SND_PCM_CHANNEL_PLAYBACK].subchn; subchn; subchn = subchn->next)
			if (subchn->proc_entry)
				snd_info_unregister((snd_info_entry_t *) subchn->proc_entry);
	}
}
