/*
 *  Information interface for ALSA driver
 *  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/minors.h"
#include "../include/info.h"
#include "../include/sndpersist.h"
#include "../include/version.h"
#include <stdarg.h>
#ifdef CONFIG_DEVFS_FS
#include <linux/devfs_fs_kernel.h>
#endif

#if LinuxVersionCode(2, 3, 27) <= LINUX_VERSION_CODE
#if LinuxVersionCode(2, 3, 29) > LINUX_VERSION_CODE
#error "Please, upgrade to 2.3.29..."
#endif
#define NO_FILL_ROUTINE
#endif

/*

 */

int snd_info_check_reserved_words(const char *str)
{
	static char *reserved[] =
	{
#ifndef CONFIG_DEVFS_FS
		"dev",
#endif
		"version",
		"meminfo",
		"memdebug",
		"detect",
		"devices",
		"oss-devices",
		"cards",
		"timers",
		"synth",
		"pcm",
		"seq",
		NULL
	};
	char **xstr = reserved;

	while (*xstr) {
		if (!strcmp(*xstr, str))
			return 0;
		xstr++;
	}
	return 1;
}

#ifdef CONFIG_PROC_FS

extern int snd_major;
extern struct file_operations snd_fops;

static DECLARE_MUTEX(info_mutex);

struct snd_info_private_data {
	snd_info_buffer_t *rbuffer;
	snd_info_buffer_t *wbuffer;
	snd_info_entry_t *entry;
	void *file_private_data;
};

static snd_info_entry_t *snd_info_entries;
static snd_info_entry_t *snd_info_devices;

static int snd_info_version_init(void);
static int snd_info_version_done(void);

/*

 */

int snd_iprintf(snd_info_buffer_t * buffer, char *fmt,...)
{
	va_list args;
	int res;
	char sbuffer[512];

	if (buffer->stop || buffer->error)
		return 0;
	va_start(args, fmt);
	res = vsprintf(sbuffer, fmt, args);
	va_end(args);
	if (buffer->size + res >= buffer->len) {
		buffer->stop = 1;
		return 0;
	}
	strcpy(buffer->curr, sbuffer);
	buffer->curr += res;
	buffer->size += res;
	return res;
}

/*

 */

struct proc_dir_entry *snd_proc_root = NULL;
#ifndef CONFIG_DEVFS_FS
struct proc_dir_entry *snd_proc_dev = NULL;
#endif
struct proc_dir_entry *snd_proc_seq = NULL;

#ifndef NO_FILL_ROUTINE
static void snd_info_fill_inode(struct inode *inode, int fill)
{
	if (fill)
		MOD_INC_USE_COUNT;
	else
		MOD_DEC_USE_COUNT;
}

static inline void snd_info_entry_prepare(struct proc_dir_entry *de)
{
	de->fill_inode = snd_info_fill_inode;
}

static inline void snd_info_entry_remove(struct proc_dir_entry *parent,
					 struct proc_dir_entry *de)
{
	if (parent && de)
		proc_unregister(parent, de->low_ino);
}
#else
static inline void snd_info_entry_prepare(struct proc_dir_entry *de)
{
	de->owner = THIS_MODULE;
}

static inline void snd_info_entry_remove(struct proc_dir_entry *parent,
					 struct proc_dir_entry *de)
{
	if (de)
		remove_proc_entry(de->name, parent);
}
#endif

static snd_info_entry_t *snd_info_find_entry(unsigned short ino)
{
	int tmp;
	snd_card_t *card;
	snd_info_entry_t *entry;

	for (entry = snd_info_entries; entry; entry = entry->next) {
		if (!entry->p)
			continue;
		if (entry->p->low_ino == ino)
			break;
	}
	if (!entry) {
		for (tmp = 0; tmp < SND_CARDS && !entry; tmp++) {
			card = snd_cards[tmp];
			if (!card)
				continue;
			for (entry = card->info_entries; entry;
							entry = entry->next) {
				if (!entry->p)
					continue;
				if (entry->p->low_ino == ino)
					break;
			}
		}
	}
	return entry;
}

static snd_card_t *snd_info_find_card(unsigned short ino)
{
	int tmp;
	snd_card_t *card;

	for (tmp = 0; tmp < SND_CARDS; tmp++) {
		card = snd_cards[tmp];
		if (!card || !card->proc_dir_link)
			continue;
		if (card->proc_dir_link->low_ino == ino)
			return card;
	}
	return NULL;
}

static loff_t snd_info_entry_lseek(struct file *file, loff_t offset, int orig)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		switch (orig) {
		case 0:	/* SEEK_SET */
			file->f_pos = offset;
			return file->f_pos;
		case 1:	/* SEEK_CUR */
			file->f_pos += offset;
			return file->f_pos;
		case 2:	/* SEEK_END */
		default:
			return -EINVAL;
		}
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.lseek)
			return entry->t.data.lseek(entry->private_data,
						   data->file_private_data,
						   file, offset, orig);
		break;
	}
	return -ENXIO;
}

static ssize_t snd_info_entry_read(struct file *file, char *buffer,
				   size_t count, loff_t * offset)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	snd_info_buffer_t *buf;
	long size = 0, size1;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		buf = data->rbuffer;
		if (!buf)
			return -EIO;
		if (file->f_pos >= buf->size)
			return 0;
		size = buf->size < count ? buf->size : count;
		size1 = buf->size - file->f_pos;
		if (size1 < size)
			size = size1;
		if (copy_to_user(buffer, buf->buffer + file->f_pos, size))
			return -EFAULT;
		file->f_pos += size;
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.read)
			return entry->t.data.read(entry->private_data,
						  data->file_private_data,
						  file, buffer, count);
		if (size > 0)
			file->f_pos += size;
		break;
	}
	return size;
}

static ssize_t snd_info_entry_write(struct file *file, const char *buffer,
				    size_t count, loff_t * offset)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	snd_info_buffer_t *buf;
	long size = 0, size1;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return -EIO;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		buf = data->wbuffer;
		if (!buf)
			return -EIO;
		if (file->f_pos < 0)
			return -EINVAL;
		if (file->f_pos >= buf->len)
			return -ENOMEM;
		size = buf->len < count ? buf->len : count;
		size1 = buf->len - file->f_pos;
		if (size1 < size)
			size = size1;
		if (copy_from_user(buf->buffer + file->f_pos, buffer, size))
			return -EFAULT;
		if (buf->size < file->f_pos + size)
			buf->size = file->f_pos + size;
		file->f_pos += size;
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.write)
			return entry->t.data.write(entry->private_data,
						   data->file_private_data,
						   file, buffer, count);
		if (size > 0)
			file->f_pos += size;
		break;
	}
	return size;
}

static int snd_info_entry_open(struct inode *inode, struct file *file)
{
	snd_info_entry_t *entry;
	struct snd_info_private_data *data;
	snd_info_buffer_t *buffer;
	int mode, err;

	down(&info_mutex);
	entry = snd_info_find_entry(inode->i_ino);
	if (!entry) {
		up(&info_mutex);
		return -ENODEV;
	}
	mode = file->f_flags & O_ACCMODE;
	if (mode == O_RDONLY || mode == O_RDWR) {
		if ((entry->type == SND_INFO_ENTRY_TEXT &&
					!entry->t.text.read_size) ||
		    (entry->type == SND_INFO_ENTRY_DATA &&
		    			!entry->t.data.read) ||
		    entry->type == SND_INFO_ENTRY_DEVICE) {
			up(&info_mutex);
			return -ENODEV;
		}
	}
	if (mode == O_WRONLY || mode == O_RDWR) {
		if ((entry->type == SND_INFO_ENTRY_TEXT &&
					!entry->t.text.write_size) ||
		    (entry->type == SND_INFO_ENTRY_DATA &&
		    			!entry->t.data.write) ||
		    entry->type == SND_INFO_ENTRY_DEVICE) {
			up(&info_mutex);
			return -ENODEV;
		}
	}
	data = (struct snd_info_private_data *)
			snd_kcalloc(sizeof(struct snd_info_private_data), GFP_KERNEL);
	if (!data) {
		up(&info_mutex);
		return -ENOMEM;
	}
	data->entry = entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		if (mode == O_RDONLY || mode == O_RDWR) {
			buffer = (snd_info_buffer_t *)
				 	snd_kcalloc(sizeof(snd_info_buffer_t), GFP_KERNEL);
			if (!buffer) {
				snd_kfree(data);
				up(&info_mutex);
				return -ENOMEM;
			}
			buffer->len = (entry->t.text.read_size +
				      (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
			buffer->buffer = snd_vmalloc(buffer->len);
			if (!buffer->buffer) {
				snd_kfree(buffer);
				snd_kfree(data);
				up(&info_mutex);
				return -ENOMEM;
			}
			buffer->curr = buffer->buffer;
			data->rbuffer = buffer;
		}
		if (mode == O_WRONLY || mode == O_RDWR) {
			buffer = (snd_info_buffer_t *)
					snd_kcalloc(sizeof(snd_info_buffer_t), GFP_KERNEL);
			if (!buffer) {
				if (mode == O_RDWR) {
					snd_vfree(data->rbuffer->buffer);
					snd_kfree(data->rbuffer);
				}
				snd_kfree(data);
				up(&info_mutex);
				return -ENOMEM;
			}
			buffer->len = (entry->t.text.write_size +
				      (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
			buffer->buffer = snd_vmalloc(buffer->len);
			if (!buffer->buffer) {
				if (mode == O_RDWR) {
					snd_vfree(data->rbuffer->buffer);
					snd_kfree(data->rbuffer);
				}
				snd_kfree(buffer);
				snd_kfree(data);
				up(&info_mutex);
				return -ENOMEM;
			}
			buffer->curr = buffer->buffer;
			data->wbuffer = buffer;
		}
		break;
	case SND_INFO_ENTRY_DATA:	/* data */
		if (entry->t.data.open) {
			if ((err = entry->t.data.open(entry->private_data,
						      entry, mode,
						      &data->file_private_data)) < 0) {
				snd_kfree(data);
				up(&info_mutex);
				return err;
			}
		}
		break;
	}
	file->private_data = data;
	MOD_INC_USE_COUNT;
	if (entry->use_inc)
		entry->use_inc(entry->card);
	up(&info_mutex);
	if (entry->type == SND_INFO_ENTRY_TEXT &&
	    (mode == O_RDONLY || mode == O_RDWR)) {
		if (entry->t.text.read) {
			down(&entry->access);
			entry->t.text.read(data->rbuffer, entry->private_data);
			up(&entry->access);
		}
	}
	return 0;
}

static int snd_info_entry_release(struct inode *inode, struct file *file)
{
	snd_info_entry_t *entry;
	struct snd_info_private_data *data;
	int mode;

	mode = file->f_flags & O_ACCMODE;
	if ((data = (struct snd_info_private_data *) file->private_data) == NULL) {
		MOD_DEC_USE_COUNT;
		return -EINVAL;
	}
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_TEXT:
		if (mode == O_RDONLY || mode == O_RDWR) {
			snd_vfree(data->rbuffer->buffer);
			snd_kfree(data->rbuffer);
		}
		if (mode == O_WRONLY || mode == O_RDWR) {
			if (entry->t.text.write) {
				entry->t.text.write(data->wbuffer,
				                    entry->private_data);
				if (data->wbuffer->error) {
					snd_printk("data write error to /proc/asound/%s%s%s (%i)\n",
						entry->card ? entry->card->id : "",
						entry->card ? "/" : "", entry->name,
						data->wbuffer->error);
				}
			}
			snd_vfree(data->wbuffer->buffer);
			snd_kfree(data->wbuffer);
		}
		break;
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.release)
			entry->t.data.release(entry->private_data,
					      entry, mode,
					      data->file_private_data);
		break;
	}
	if (entry->use_dec)
		entry->use_dec(entry->card);
	MOD_DEC_USE_COUNT;
	snd_kfree(data);
	return 0;
}

static unsigned int snd_info_entry_poll(struct file *file, poll_table * wait)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;
	unsigned int mask;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	mask = 0;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.poll)
			return entry->t.data.poll(entry->private_data,
						  data->file_private_data,
						  file, wait);
		if (entry->t.data.read)
			mask |= POLLIN | POLLRDNORM;
		if (entry->t.data.write)
			mask |= POLLOUT | POLLWRNORM;
		break;
	}
	return mask;
}

static int snd_info_entry_ioctl(struct inode *inode, struct file *file,
				unsigned int cmd, unsigned long arg)
{
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.ioctl)
			return entry->t.data.ioctl(entry->private_data,
						   data->file_private_data,
						   file, cmd, arg);
		break;
	}
	return -ENOTTY;
}

static int snd_info_entry_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct inode *inode = file->f_dentry->d_inode;
	struct snd_info_private_data *data;
	struct snd_info_entry *entry;

	data = (struct snd_info_private_data *) file->private_data;
	if (!data)
		return 0;
	entry = data->entry;
	switch (entry->type) {
	case SND_INFO_ENTRY_DATA:
		if (entry->t.data.mmap)
			return entry->t.data.mmap(entry->private_data,
						  data->file_private_data,
						  inode, file, vma);
		break;
	}
	return -ENXIO;
}

static struct file_operations snd_info_entry_operations =
{
	snd_info_entry_lseek,	/* lseek */
	snd_info_entry_read,	/* read */
	snd_info_entry_write,	/* write */
	NULL,			/* readdir */
	snd_info_entry_poll,	/* poll */
	snd_info_entry_ioctl,	/* ioctl - default */
	snd_info_entry_mmap,	/* mmap */
	snd_info_entry_open,	/* open */
	NULL,			/* flush */
	snd_info_entry_release,	/* release */
	NULL,			/* can't fsync */
	NULL,			/* fasync */
	NULL,			/* check_media_change */
	NULL,			/* revalidate */
	NULL,			/* lock */
};

static struct inode_operations snd_info_entry_inode_operations =
{
	&snd_info_entry_operations,	/* default sound info directory file-ops */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	NULL,			/* readlink */
	NULL,			/* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};

#ifndef CONFIG_DEVFS_FS
static struct inode_operations snd_info_device_inode_operations =
{
	&snd_fops,		/* default sound info directory file-ops */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	NULL,			/* readlink */
	NULL,			/* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};
#endif

static int snd_info_card_readlink(struct dentry *dentry,
				  char *buffer, int buflen)
{
	int len;
	snd_card_t *card;

	down(&info_mutex);
	card = snd_info_find_card(dentry->d_inode->i_ino);
	if (!card) {
		up(&info_mutex);
		return -ENOENT;
	}
	len = strlen(card->id) + 1;
	if (buflen < len)
		len = buflen;
	if (copy_to_user(buffer, card->id, len))
		return -EFAULT;
	up(&info_mutex);
	return len;
}

static struct dentry *snd_info_card_followlink(struct dentry *dentry,
					       struct dentry *base,
					       unsigned int follow)
{
	snd_card_t *card;
	char tmp[16];

	down(&info_mutex);
	card = snd_info_find_card(dentry->d_inode->i_ino);
	if (!card || !card->proc_dir) {
		up(&info_mutex);
		return NULL;
	}
	strcpy(tmp, card->id);
	up(&info_mutex);
	return lookup_dentry(tmp, base, follow);
}

static struct file_operations snd_info_card_link_operations =
{
	NULL,			/* lseek - default */
	NULL,			/* read - bad */
	NULL,			/* write - bad */
	NULL,			/* readdir - bad */
	NULL,			/* select - default */
	NULL,			/* ioctl - default */
	NULL,			/* mmap */
	NULL,			/* very special open code */
	NULL,			/* no special release code */
	NULL			/* can't fsync */
};

struct inode_operations snd_info_card_link_inode_operations =
{
	&snd_info_card_link_operations,		/* file-operations */
	NULL,			/* create */
	NULL,			/* lookup */
	NULL,			/* link */
	NULL,			/* unlink */
	NULL,			/* symlink */
	NULL,			/* mkdir */
	NULL,			/* rmdir */
	NULL,			/* mknod */
	NULL,			/* rename */
	snd_info_card_readlink,	/* readlink */
	snd_info_card_followlink, /* follow_link */
	NULL,			/* readpage */
	NULL,			/* writepage */
	NULL,			/* bmap */
	NULL,			/* truncate */
	NULL			/* permission */
};



int snd_info_init(void)
{
	struct proc_dir_entry *p;

	snd_info_entries = NULL;
	snd_info_devices = NULL;
	p = create_proc_entry("asound", S_IFDIR | S_IRUGO | S_IXUGO, &proc_root);
	if (!p)
		return -ENOMEM;
	snd_info_entry_prepare(p);
	snd_proc_root = p;
#ifndef CONFIG_DEVFS_FS
	p = create_proc_entry("dev", S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	snd_info_entry_prepare(p);
	snd_proc_dev = p;
#endif
	p = create_proc_entry("seq", S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	snd_info_entry_prepare(p);
	snd_proc_seq = p;
	snd_info_version_init();
#ifdef CONFIG_SND_MEMORY_DEBUG
	snd_memory_info_init();
#endif
	snd_minor_info_init();
#ifdef CONFIG_SND_OSSEMUL
	snd_minor_info_oss_init();
#endif
	snd_card_info_init();
	return 0;
}

int snd_info_done(void)
{
	snd_card_info_done();
#ifdef CONFIG_SND_OSSEMUL
	snd_minor_info_oss_done();
#endif
	snd_minor_info_done();
#ifdef CONFIG_SND_MEMORY_DEBUG
	snd_memory_info_done();
#endif
	snd_info_version_done();
#ifdef CONFIG_SND_DEBUG
	if (snd_info_devices) {
		snd_printd("Oops.. Info device entries still allocated!!!\n");
	}
	if (snd_info_entries) {
		snd_printd("Oops.. Info entries still allocated!!!\n");
	}
#endif
	if (snd_proc_root) {
		snd_info_entry_remove(snd_proc_root, snd_proc_seq);
#ifndef CONFIG_DEVFS_FS
		snd_info_entry_remove(snd_proc_root, snd_proc_seq);
#endif
		snd_info_entry_remove(&proc_root, snd_proc_root);
	}
	return 0;
}

/*

 */


int snd_info_card_register(snd_card_t * card)
{
	char str[4];
	struct proc_dir_entry *p;

	if (!card)
		return -EINVAL;
	p = create_proc_entry(card->id, S_IFDIR | S_IRUGO | S_IXUGO, snd_proc_root);
	if (!p)
		return -ENOMEM;
	snd_info_entry_prepare(p);
	card->proc_dir = p;
	sprintf(str, "%i", card->number);
	p = create_proc_entry(str, S_IFLNK | S_IRUGO | S_IWUGO | S_IXUGO, snd_proc_root);
	if (!p) {
		snd_info_entry_remove(snd_proc_root, card->proc_dir);
		card->proc_dir = NULL;
		return -ENOMEM;
	}
	snd_info_entry_prepare(p);
	p->ops = &snd_info_card_link_inode_operations;
	card->proc_dir_link = p;
	return 0;
}

int snd_info_card_unregister(snd_card_t * card)
{
	if (!card)
		return -EINVAL;
	if (card->proc_dir_link)
		snd_info_entry_remove(snd_proc_root, card->proc_dir_link);
	if (card->proc_dir)
		snd_info_entry_remove(snd_proc_root, card->proc_dir);
	card->proc_dir = NULL;
	return 0;
}

/*

 */

int snd_info_get_line(snd_info_buffer_t * buffer, char *line, int len)
{
	int c = -1;

	if (len <= 0 || buffer->stop || buffer->error)
		return 1;
	while (--len > 0) {
		c = *buffer->curr++;
		if (c == '\n') {
			if ((buffer->curr - buffer->buffer) >= buffer->size) {
				buffer->stop = 1;
			}
			break;
		}
		*line++ = c;
		if ((buffer->curr - buffer->buffer) >= buffer->size) {
			buffer->stop = 1;
			break;
		}
	}
	while (c != '\n' && !buffer->stop) {
		c = *buffer->curr++;
		if ((buffer->curr - buffer->buffer) >= buffer->size) {
			buffer->stop = 1;
		}
	}
	*line = '\0';
	return 0;
}

char *snd_info_get_str(char *dest, char *src, int len)
{
	int c;

	while (*src == ' ' || *src == '\t')
		src++;
	if (*src == '"' || *src == '\'') {
		c = *src++;
		while (--len > 0 && *src && *src != c) {
			*dest++ = *src++;
		}
		if (*src == c)
			src++;
	} else {
		while (--len > 0 && *src && *src != ' ' && *src != '\t') {
			*dest++ = *src++;
		}
	}
	*dest = 0;
	while (*src == ' ' || *src == '\t')
		src++;
	return src;
}

snd_info_entry_t *snd_info_create_entry(snd_card_t * card, const char *name)
{
	snd_info_entry_t *entry;

	entry = (snd_info_entry_t *) snd_kcalloc(sizeof(snd_info_entry_t), GFP_KERNEL);
	if (!entry)
		return NULL;
	entry->name = snd_kmalloc_strdup(name, GFP_KERNEL);
	if (!entry->name) {
		snd_kfree(entry);
		return NULL;
	}
	entry->mode = S_IFREG | S_IRUGO;
	entry->type = SND_INFO_ENTRY_TEXT;
	entry->card = card;
	if (card) {
		entry->use_inc = card->use_inc;
		entry->use_dec = card->use_dec;
	}
	init_MUTEX(&entry->access);
	return entry;
}

void snd_info_free_entry(snd_info_entry_t * entry)
{
	if (!entry)
		return;
	if (entry->name)
		snd_kfree((char *)entry->name);
	if (entry->private_free)
		entry->private_free(entry->private_data);
	snd_kfree(entry);
}

#if !defined(CONFIG_DEVFS_FS) && !defined(NO_FILL_ROUTINE)
static void snd_info_device_fill_inode(struct inode *inode, int fill)
{
	struct proc_dir_entry *de;
	snd_info_entry_t *entry;

	if (!fill) {
		MOD_DEC_USE_COUNT;
		return;
	}
	MOD_INC_USE_COUNT;
	de = (struct proc_dir_entry *) inode->u.generic_ip;
	if (!de)
		return;
	entry = (snd_info_entry_t *) de->data;
	if (!entry)
		return;
	inode->i_rdev = MKDEV(entry->t.device.major, entry->t.device.minor);
}

static inline void snd_info_device_entry_prepare(struct proc_dir_entry *de, snd_info_entry_t *entry)
{
	de->fill_inode = snd_info_device_fill_inode;
}
#else
static inline void snd_info_device_entry_prepare(struct proc_dir_entry *de, snd_info_entry_t *entry)
{
	de->rdev = MKDEV(entry->t.device.major, entry->t.device.minor);
	de->owner = THIS_MODULE;
}
#endif

snd_info_entry_t *snd_info_create_device(const char *name, unsigned int number, unsigned int mode)
{
#ifdef CONFIG_DEVFS_FS
	char dname[32];
#endif
	unsigned short major = number >> 16;
	unsigned short minor = (unsigned short) number;
	snd_info_entry_t *entry, *e;
#ifndef CONFIG_DEVFS_FS
	struct proc_dir_entry *p = NULL;
#endif

	if (!major)
		major = snd_major;
	if (!mode)
		mode = S_IFCHR | S_IRUGO | S_IWUGO;
	mode &= (snd_device_mode & (S_IRUGO | S_IWUGO)) | S_IFCHR | S_IFBLK;
	entry = snd_info_create_entry(NULL, name);
	if (!entry)
		return NULL;
	entry->type = SND_INFO_ENTRY_DEVICE;
	entry->mode = mode;
	entry->t.device.major = major;
	entry->t.device.minor = minor;
	down(&info_mutex);
#ifndef CONFIG_DEVFS_FS
	p = create_proc_entry(entry->name, entry->mode, snd_proc_dev);
	if (p) {
		snd_info_device_entry_prepare(p, entry);
		p->ops = &snd_info_device_inode_operations;
	} else {
		up(&info_mutex);
		snd_info_free_entry(entry);
		return NULL;
	}
	p->gid = snd_device_gid;
	p->uid = snd_device_uid;
	p->data = (void *) entry;
	entry->p = p;
#else
	entry->p = NULL;
#endif
	if (!(e = snd_info_devices)) {
		snd_info_devices = entry;
	} else {
		while (e->next)
			e = e->next;
		e->next = entry;
	}
	up(&info_mutex);
#ifdef CONFIG_DEVFS_FS
	sprintf(dname, "snd/%s", name);
	devfs_register(dname, 0, DEVFS_FL_TOLERANT,
			major, minor, mode, 0, 0,
			&snd_fops, NULL);
#endif
	return entry;
}

void snd_info_free_device(snd_info_entry_t * entry)
{
#ifdef CONFIG_DEVFS_FS
	char dname[32];
	devfs_handle_t master;
#endif
	snd_info_entry_t *e;

#ifndef CONFIG_DEVFS_FS
	if (!entry || !entry->p)
#else
	if (!entry)
#endif
		return;
	down(&info_mutex);
#ifndef CONFIG_DEVFS_FS
	snd_info_entry_remove(snd_proc_dev, entry->p);
#endif
	if (entry == (e = snd_info_devices)) {
		snd_info_devices = snd_info_devices->next;
	} else {
		while (e->next && e->next != entry)
			e = e->next;
		if (e->next)
			e->next = e->next->next;
	}
	up(&info_mutex);
#ifdef CONFIG_DEVFS_FS
	sprintf(dname, "snd/%s", entry->name);
	master = devfs_find_handle(dname, 0, 0, 0, DEVFS_SPECIAL_CHR, 0);
	devfs_unregister(master);
#endif
	snd_info_free_entry(entry);
}

static struct proc_dir_entry *snd_info_get_root(snd_info_entry_t * entry)
{
	struct proc_dir_entry *root;

	root = snd_proc_root;
	if (entry->card)
		root = entry->card->proc_dir;
#ifndef CONFIG_DEVFS_FS
	if (entry->type == SND_INFO_ENTRY_DATA &&
	    entry->subtype == SND_INFO_ENTRY_SDEVICE)
		root = snd_proc_dev;
#endif
	if (entry->subtype == SND_INFO_ENTRY_SEQUENCER)
		root = snd_proc_seq;
	return root;
}

int snd_info_register(snd_info_entry_t * entry)
{
	snd_info_entry_t *e;
	struct proc_dir_entry *root, *p = NULL;
	snd_card_t *card;
	int len;

	if (!entry)
		return -EINVAL;
	entry->next = NULL;
#if 0
	snd_printk("snd_info_register: %s, 0x%lx\n",
				entry->name, (long) entry->card);
#endif
	len = strlen(entry->name);
	card = entry->card;
	root = snd_info_get_root(entry);
	down(&info_mutex);
	p = create_proc_entry(entry->name, entry->mode, root);
	if (p) {
		snd_info_entry_prepare(p);
		p->ops = &snd_info_entry_inode_operations;
		p->size = entry->size;
	} else {
		up(&info_mutex);
		return -ENOMEM;
	}
	entry->p = p;
	if (!card) {
		if (!(e = snd_info_entries)) {
			snd_info_entries = entry;
		} else {
			while (e->next)
				e = e->next;
			e->next = entry;
		}
	} else {
		if (!(e = card->info_entries)) {
			card->info_entries = entry;
		} else {
			while (e->next)
				e = e->next;
			e->next = entry;
		}
	}
	up(&info_mutex);
	return 0;
}

int snd_info_unregister(snd_info_entry_t * entry)
{
	struct proc_dir_entry *root;
	snd_info_entry_t *e;

	if (!entry || !entry->p)
		return -EINVAL;
	root = snd_info_get_root(entry);
	down(&info_mutex);
	snd_info_entry_remove(root, entry->p);
	if (!entry->card) {
		if (entry == (e = snd_info_entries)) {
			snd_info_entries = snd_info_entries->next;
		} else {
			while (e->next && e->next != entry)
				e = e->next;
			if (e->next)
				e->next = e->next->next;
		}
	} else {
		if (entry == (e = entry->card->info_entries)) {
			entry->card->info_entries = entry->card->info_entries->next;
		} else {
			while (e->next && e->next != entry)
				e = e->next;
			if (e->next)
				e->next = e->next->next;
		}
	}
	up(&info_mutex);
	snd_info_free_entry(entry);
	return 0;
}

/*

 */

static snd_info_entry_t *snd_info_version_entry = NULL;

static void snd_info_version_read(snd_info_buffer_t * buffer, void *private_data)
{
	static char *kernel_version = UTS_RELEASE;

	snd_iprintf(buffer,
		    "Advanced Linux Sound Architecture Driver Version " CONFIG_SND_VERSION ".\n"
		    "Compiled on " __DATE__ " for kernel %s"
#ifdef __SMP__
		    " (SMP)"
#endif
#ifdef MODVERSIONS
		    " with versioned symbols"
#endif
		    ".\n", kernel_version);
}

static int snd_info_version_init(void)
{
	snd_info_entry_t *entry;

	entry = snd_info_create_entry(NULL, "version");
	if (!entry)
		return -ENOMEM;
	entry->t.text.read_size = 256;
	entry->t.text.read = snd_info_version_read;
	if (snd_info_register(entry) < 0) {
		snd_info_free_entry(entry);
		return -ENOMEM;
	}
	snd_info_version_entry = entry;
	return 0;
}

static int snd_info_version_done(void)
{
	if (snd_info_version_entry)
		snd_info_unregister(snd_info_version_entry);
	return 0;
}

/*
 *  store/restore settings via persistant module
 */

int snd_info_store_text(snd_info_entry_t * entry)
{
	int result = 0;
	snd_info_buffer_t ibuffer;
	char key[128];

	if (!entry)
		return -EINVAL;
	if (entry->type != SND_INFO_ENTRY_TEXT)
		return -EINVAL;
	memset(&ibuffer, 0, sizeof(ibuffer));
	ibuffer.len = (entry->t.text.read_size + (PAGE_SIZE - 1)) &
							 ~(PAGE_SIZE - 1);
	ibuffer.buffer = ibuffer.curr = snd_vmalloc(ibuffer.len);
	if (!ibuffer.buffer)
		return -ENOMEM;
	down(&entry->access);
	entry->t.text.read(&ibuffer, entry->private_data);
	result = ibuffer.error;
	if (!result) {
		if (entry->card)
			sprintf(key, "snd:%i/%s", entry->card->number,
						  entry->name);
		else
			sprintf(key, "snd:%s", entry->name);
		if (ibuffer.size > 0) {
			result = snd_persist_store(key,
						   ibuffer.buffer,
						   ibuffer.size);
		} else {
			result = snd_persist_remove(key);
		}
	}
	up(&entry->access);
	snd_vfree(ibuffer.buffer);
	return result;
}

int snd_info_restore_text(snd_info_entry_t * entry)
{
	int result = 0;
	snd_info_buffer_t ibuffer;
	char key[128];

	if (!entry)
		return -EINVAL;
	if (entry->type != SND_INFO_ENTRY_TEXT)
		return -EINVAL;
	memset(&ibuffer, 0, sizeof(ibuffer));
	ibuffer.len = (entry->t.text.write_size + (PAGE_SIZE - 1)) &
							~(PAGE_SIZE - 1);
	ibuffer.buffer = ibuffer.curr = snd_vmalloc(ibuffer.len);
	if (!ibuffer.buffer)
		return -ENOMEM;
	down(&entry->access);
	if (entry->card)
		sprintf(key, "snd:%i/%s", entry->card->number, entry->name);
	else
		sprintf(key, "snd:%s", entry->name);
	result = snd_persist_restore(key, ibuffer.buffer, ibuffer.len);
	if (result > 0) {
		ibuffer.size = result;
		result = 0;
		entry->t.text.write(&ibuffer, entry->private_data);
		result = ibuffer.error;
	}
	up(&entry->access);
	snd_vfree(ibuffer.buffer);
	return result;
}

#endif /* CONFIG_PROC_FS */
