/*
 *  Routines for driver control
 *  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 "minors.h"
#include "info.h"
#include "control.h"
#include "sndpersist.h"

#define SND_ATOMIC_POOL_SIZE	50	/* tunable */

struct snd_stru_control_ioctl {
	snd_control_ioctl_t fioctl;
	struct snd_stru_control_ioctl *next;
};

static struct snd_stru_control_ioctl *snd_first_ioctl = NULL;
static int snd_first_ioctl_use = 0;

snd_mutex_define_static(register);

static void snd_control_prepare_read_queue(snd_control_t * control);

static int snd_control_open(unsigned short minor, int cardnum, int device,
			    struct file *file)
{
	snd_card_t *card;
	snd_control_t *control, *control1;

	if (!(card = snd_cards[cardnum]))
		return -ENXIO;
	control = (snd_control_t *) snd_calloc(sizeof(snd_control_t));
	if (!control)
		return -ENOMEM;
	MOD_INC_USE_COUNT;
	card->use_inc(card);
	snd_spin_prepare(control, change_lock);
	snd_sleep_prepare(control, change);
	snd_spin_prepare(control, read_lock);
	snd_control_prepare_read_queue(control);
	control->card = card;
	file->private_data = control;
	snd_mutex_down(card, control);
	if (card->fcontrol) {
		for (control1 = card->fcontrol;
		     control1->next;
		     control1 = control1->next);
		control1->next = control;
	} else {
		card->fcontrol = control;
	}
	snd_mutex_up(card, control);
	return 0;
}

static int snd_control_release(unsigned short minor, int cardnum, int device,
			       struct file *file)
{
	snd_card_t *card;
	snd_control_t *control, *control1;
	snd_kctl_read_t *cread;

	if (file->private_data) {
		control = (snd_control_t *) file->private_data;
		file->private_data = NULL;
		card = control->card;
		snd_mutex_down(card, control);
		if (card->fcontrol == control) {
			card->fcontrol = control->next;
		} else {
			for (control1 = card->fcontrol;
			     control1->next != control;
			     control1 = control1->next);
			control1->next = control->next;
		}
		snd_mutex_up(card, control);
		snd_free(control, sizeof(snd_control_t));
		while (control->pool) {
			cread = control->pool;
			control->pool = cread->next;
			snd_free(cread, sizeof(*cread));
		}
		while (control->first_item) {
			cread = control->first_item;
			control->first_item = cread->next;
			snd_free(cread, sizeof(*cread));
		}
		MOD_DEC_USE_COUNT;
		control->card->use_dec(control->card);
	}
	return 0;
}

int snd_control_busy(snd_control_t * control)
{
	if (control->rebuild || control->first_item)
		return 1;
	return 0;
}

static void snd_control_prepare_read_queue(snd_control_t * control)
{
	unsigned long flags;
	snd_kctl_read_t *item;
	
	while (control->pool_size < SND_ATOMIC_POOL_SIZE) {
		item = (snd_kctl_read_t *)snd_malloc(sizeof(*item));
		if (!item) {
			snd_printk("control - atomic pool initialization failure\n");
			break;
		}
		snd_spin_lock(control, read_lock, &flags);
		item->next = control->pool;
		control->pool = item;
		control->pool_size++;
		snd_spin_unlock(control, read_lock, &flags);
	}
	while (control->pool_size > SND_ATOMIC_POOL_SIZE) {
		snd_spin_lock(control, read_lock, &flags);
		item = control->pool;
		if (item)
			control->pool = item->next;
		snd_spin_unlock(control, read_lock, &flags);
		if (item)
			snd_free(item, sizeof(*item));
	}
}

static snd_kctl_read_t *snd_control_alloc_read(snd_control_t * control, int atomic)
{
	unsigned long flags;
	snd_kctl_read_t *item;
	
	if (atomic) {
		snd_spin_lock(control, read_lock, &flags);
		item = control->pool;
		if (item) {
			control->pool = item->next;
			memset(item, 0, sizeof(*item));
		}
		snd_spin_unlock(control, read_lock, &flags);
		if (!item)
			snd_printk("control - atomic allocation failure\n");
	} else {
		item = (snd_kctl_read_t *)snd_calloc(sizeof(*item));
	}
	return item;
}

static void snd_control_empty_read_queue(snd_control_t * control, int rebuild, int atomic)
{
	unsigned long flags;
	snd_kctl_read_t *read1, *read2;
	
	snd_spin_lock(control, read_lock, &flags);
	read1 = control->first_item;
	control->first_item = control->last_item = NULL;
	control->rebuild = rebuild ? 1 : 0;
	snd_spin_unlock(control, read_lock, &flags);
	while (read1) {
		read2 = read1->next;
		if (atomic) {
			snd_spin_lock(control, read_lock, &flags);
			read1->next = control->pool;
			control->pool = read1;
			control->pool_size++;
			snd_spin_unlock(control, read_lock, &flags);
		} else {
			snd_free(read1, sizeof(snd_kctl_read_t));
		}
		read1 = read2;
	}
}

static void snd_control_notify_change(snd_control_t * control, snd_ctl_read_t * cread, int all, int atomic)
{
	unsigned long flags;
	snd_control_t *control1;
	snd_card_t *card;
	snd_kctl_read_t *nread;
	
	if (!control || !cread)
		return;
	card = control->card;
	/* TODO: lock card->fcontrol list */
	for (control1 = card->fcontrol; control1; control1 = control1->next) {
		if (all || control1 != control) {
			if (!control1->read_active) {
				control1->rebuild = 1;
				continue;
			}
			nread = snd_control_alloc_read(control1, atomic);
			if (!nread) {
				snd_control_empty_read_queue(control1, 1, atomic);
			} else {
				memcpy(&nread->data, cread, sizeof(nread->data));
				snd_spin_lock(control1, read_lock, &flags);
				if (!control1->first_item) {
					control1->first_item = control1->last_item = nread;
				} else {
					control1->last_item->next = nread;
				}
				snd_spin_lock(control1, change_lock, &flags);
				if (snd_getlock(control1, change) & SND_WK_SLEEP) {
					snd_getlock(control1, change) &= ~SND_WK_SLEEP;
					snd_wakeup(control1, change);
				}
				snd_spin_unlock(control1, change_lock, &flags);
			}
		}
	}
	if (!atomic)
 		snd_control_prepare_read_queue(control);
}

void snd_control_notify_structure_change(snd_control_t * control, snd_ctl_read_t * read)
{
	snd_control_notify_change(control, read, 1, 0);
}

void snd_control_notify_value_change(snd_control_t * control, snd_ctl_read_t * read, int atomic)
{
	snd_control_notify_change(control, read, 0, atomic);
}

void snd_control_notify_switch_change(snd_card_t * card, int cmd, int iface, char *name)
{
	snd_ctl_read_t r;
	
	if (!card || !name)
		return;
	memset(&r, 0, sizeof(r));
	r.cmd = cmd;
	r.data.sw.iface = iface;
	strncpy(r.data.sw.switem.name, name, sizeof(r.data.sw.switem.name));
	snd_control_notify_structure_change(card->fcontrol, &r);
}

void snd_control_notify_switch_value_change(snd_control_t * control, int iface, char *name, int atomic)
{
	snd_ctl_read_t r;
	
	memset(&r, 0, sizeof(r));
	r.cmd = SND_CTL_READ_SWITCH_VALUE;
	r.data.sw.iface = iface;
	strncpy(r.data.sw.switem.name, name, sizeof(r.data.sw.switem.name));
	snd_control_notify_value_change(control, &r, atomic);
}

static int snd_control_hw_info(snd_card_t * card, snd_control_t * control,
			       unsigned int cmd, unsigned long arg)
{
	struct snd_stru_control_ioctl *p;
	struct snd_ctl_hw_info info;

	memset(&info, 0, sizeof(info));
	if (verify_area(VERIFY_WRITE, (void *) arg,
			sizeof(struct snd_ctl_hw_info)))
		 return -EFAULT;
	snd_mutex_down(card, control);
	info.type = card->type;
	strncpy(info.id, card->id, 8);
	strncpy(info.abbreviation, card->abbreviation, sizeof(info.abbreviation) - 1);
	strncpy(info.name, card->shortname, sizeof(info.name) - 1);
	strncpy(info.longname, card->longname, sizeof(info.longname) - 1);
	info.switches = snd_switch_count(&card->switches);
	for (p = snd_first_ioctl; p; p = p->next)
		p->fioctl(card, control, cmd, (unsigned long) &info);
	snd_mutex_up(card, control);
	copy_to_user((void *) arg, &info, sizeof(struct snd_ctl_hw_info));
	return 0;
}

static int snd_control_switch_read(snd_control_t * control, snd_switch_t *_sw)
{
	return snd_switch_read(&control->card->switches, control->card, _sw);
}

static int snd_control_switch_write(snd_control_t * control, snd_switch_t *_sw)
{
	static char *root_only[] =
	{
		SND_CTL_SW_JOYSTICK,
		SND_CTL_SW_JOYSTICK_ADDRESS,
		NULL
	};
	snd_card_t *card;
	snd_switch_t sw;
	char **root_str = root_only;
	int result;

	if (snd_control_busy(control))
		return -EBUSY;
	card = control->card;
	memset(&sw, 0, sizeof(sw));
	if (verify_area(VERIFY_READ, (void *) _sw, sizeof(snd_switch_t)))
		 return -EFAULT;
	copy_from_user(&sw, _sw, sizeof(sw));
	while (*root_str) {
		if (!strcmp(sw.name, *root_str)) {
			if (!suser()) {
				snd_mutex_up(card, control);
				return -EPERM;
			}
		}
		root_str++;
	}
	result = snd_switch_write(&card->switches, card, _sw);
	if (result > 0)	/* change */
		snd_control_notify_switch_value_change(control,
						       SND_CTL_IFACE_CONTROL,
						       sw.name,
						       0);
	return result;
}

static int snd_control_ioctl(struct file *file,
			     unsigned int cmd,
			     unsigned long arg)
{
	snd_control_t *control;
	snd_card_t *card;
	struct snd_stru_control_ioctl *p;
	int err;

	control = (snd_control_t *) file->private_data;
	if (!control)
		return -EINVAL;
	card = control->card;
	if (!card)
		return -EINVAL;
	switch (cmd) {
	case SND_CTL_IOCTL_PVERSION:
		return snd_ioctl_out((long *) arg, SND_CTL_VERSION);
	case SND_CTL_IOCTL_HW_INFO:
		return snd_control_hw_info(card, control, cmd, arg);
	case SND_CTL_IOCTL_SWITCH_LIST:
		return snd_switch_list(&card->switches, (snd_switch_list_t *) arg);
	case SND_CTL_IOCTL_SWITCH_READ:
		return snd_control_switch_read(control, (snd_switch_t *) arg);
	case SND_CTL_IOCTL_SWITCH_WRITE:
		return snd_control_switch_write(control, (snd_switch_t *) arg);
	}
	snd_mutex_down(card, control);
	for (p = snd_first_ioctl; p; p = p->next) {
		err = p->fioctl(card, control, cmd, arg);
		if (err != -EAGAIN) {
			snd_mutex_up(card, control);
			return err;
		}
	}
	snd_mutex_up(card, control);
	return -ENXIO;
}

static long snd_control_read(struct file *file, char *buffer, long count)
{
	unsigned long flags;
	snd_control_t *control;
	snd_kctl_read_t *cread;
	snd_ctl_read_t rebuild;
	long result;

	control = (snd_control_t *) file->private_data;
	if (!control || !control->card)
		return -ENOENT;
	snd_spin_lock(control, read_lock, &flags);
	control->read_active = 1;
	snd_spin_unlock(control, read_lock, &flags);
	result = 0;
	while (count >= sizeof(snd_ctl_read_t)) {
		snd_spin_lock(control, read_lock, &flags);
		if (control->rebuild) {
			control->rebuild = 0;
			snd_spin_unlock(control, read_lock, &flags);
			memset(&rebuild, 0, sizeof(rebuild));
			rebuild.cmd = SND_CTL_READ_REBUILD;
			copy_to_user(buffer, &rebuild, sizeof(rebuild));
			snd_spin_unlock(control, read_lock, &flags);
			goto __move;
		} else {
			cread = control->first_item;
			if (cread) {
				control->first_item = cread->next;
				if (!control->first_item)
					control->last_item = NULL;
			}
		}
		snd_spin_unlock(control, read_lock, &flags);
		if (cread) {
			copy_to_user(buffer, &cread->data, sizeof(snd_ctl_read_t));
			snd_free(cread, sizeof(*cread));
		      __move:
			buffer += sizeof(snd_ctl_read_t);
			count -= sizeof(snd_ctl_read_t);
			result += sizeof(snd_ctl_read_t);
		} else {
			break;
		}
	}
	return result;
}

#ifdef SND_POLL
static unsigned int snd_control_poll(struct file *file, poll_table * wait)
{
	unsigned long flags;
	unsigned int mask;
	snd_control_t *control;

	control = (snd_control_t *) file->private_data;
	if (!control)
		return -EIO;

	snd_spin_lock(control, read_lock, &flags);
	control->read_active = 1;
	snd_spin_unlock(control, read_lock, &flags);

	snd_spin_lock(control, change_lock, &flags);
	snd_getlock(control, change) |= SND_WK_SLEEP;
	snd_spin_unlock(control, change_lock, &flags);
	snd_poll_wait(file, control, change, wait);

	mask = 0;
	if (control->first_item)
		mask |= POLLIN | POLLRDNORM;

	return mask;
}
#else
static int snd_control_select(struct file *file,
                              int sel_type,
                              select_table * wait)
{
	unsigned long flags;
	snd_control_t *control;

	control = (snd_control_t *) file->private_data;
	if (!control)
		return -EIO;

	switch (sel_type) {
	case SEL_IN:
		snd_spin_lock(control, read_lock, &flags);
		control->read_active = 1;
		snd_spin_unlock(control, read_lock, &flags);
		snd_spin_lock(control, change_lock, &flags);
		if (!control->first_item) {
			snd_getlock(control, change) |= SND_WK_SLEEP;
			snd_spin_unlock(control, change_lock, &flags);
			snd_select_wait(control, change, wait);
			return 0;
		}
		snd_getlock(control, change) &= ~SND_WK_SLEEP;
		snd_spin_unlock(control, change_lock, &flags);
		return 1;
	case SEL_OUT:
		break;
	case SEL_EX:
		break;
	}
	return 0;
}
#endif

int snd_control_register_ioctl(snd_control_ioctl_t fcn)
{
	struct snd_stru_control_ioctl *p, *pn;

	pn = (struct snd_stru_control_ioctl *)
			snd_calloc(sizeof(struct snd_stru_control_ioctl));
	if (!pn)
		return -ENOMEM;
	pn->fioctl = fcn;
	snd_mutex_down_static(register);
	if (!snd_first_ioctl) {
		snd_first_ioctl = pn;
	} else {
		for (p = snd_first_ioctl; p->next; p = p->next);
		p->next = pn;
	}
	snd_mutex_up_static(register);
	return 0;
}

int snd_control_unregister_ioctl(snd_control_ioctl_t fcn)
{
	struct snd_stru_control_ioctl *p, *pn, *po;

	snd_mutex_down_static(register);
	if (!snd_first_ioctl) {
		snd_mutex_up_static(register);
		return -EINVAL;
	}
	p = snd_first_ioctl;
	if (p->fioctl == fcn) {
		snd_first_ioctl = p->next;
		snd_mutex_up_static(register);
		snd_free(p, sizeof(struct snd_stru_control_ioctl));
		return 0;
	}
	for (; p; p = pn) {
		pn = p->next;
		if (pn && pn->fioctl == fcn) {
			po = pn;
			p->next = pn = pn->next;
			snd_mutex_up_static(register);
			snd_free(po, sizeof(struct snd_stru_control_ioctl));
			return 0;
		}
	}
	snd_mutex_up_static(register);
	return -EINVAL;
}

int snd_control_switch_add(snd_card_t * card, snd_kswitch_t * ksw)
{
	int err = snd_switch_add(&card->switches, ksw);
	if (err >= 0)
		snd_control_notify_switch_change(card,
						 SND_CTL_READ_SWITCH_ADD,
						 SND_CTL_IFACE_CONTROL,
						 ksw->name);
	return err;
}

int snd_control_switch_remove(snd_card_t * card, snd_kswitch_t * ksw)
{
	int err = snd_switch_remove(&card->switches, ksw);
	if (err >= 0)
		snd_control_notify_switch_change(card,
						 SND_CTL_READ_SWITCH_REMOVE,
						 SND_CTL_IFACE_CONTROL,
						 ksw->name);
	return err;
}

snd_kswitch_t *snd_control_switch_new(snd_card_t * card, snd_kswitch_t * ksw, void *private_data)
{
	snd_kswitch_t *sw;
	
	sw = snd_switch_new(ksw);
	if (!sw)
		return NULL;
	if (snd_control_switch_add(card, sw) < 0) {
		snd_switch_free_one(sw);
		return NULL;
	}
	sw->private_data = private_data;
	return sw;
}

int snd_control_switch_change(snd_card_t * card, snd_kswitch_t * ksw)
{
	if (!card || !ksw)
		return -EINVAL;
	snd_control_notify_switch_change(card,
					 SND_CTL_READ_SWITCH_CHANGE,
					 SND_CTL_IFACE_CONTROL,
					 ksw->name);
	return 0;
}

/*
 *  INIT PART
 */

static snd_minor_t snd_control_reg =
{
	"control",		/* comment */
	NULL,			/* reserved */

	NULL,			/* unregister */

	NULL,			/* lseek */
	snd_control_read,	/* read */
	NULL,			/* write */
	snd_control_open,	/* open */
	snd_control_release,	/* release */
#ifdef SND_POLL
	snd_control_poll,	/* poll */
#else
	snd_control_select,	/* select */
#endif
	snd_control_ioctl,	/* ioctl */
	NULL			/* mmap */
};

void snd_control_store(snd_card_t * card)
{
	int size;
	unsigned int pos;
	long err;
	char *buffer;
	snd_kswitch_list_t *lswitch;
	char key[32];
	
	lswitch = &card->switches;
	snd_switch_lock(lswitch, 0);
	size = sizeof(unsigned) +
	       sizeof(card->id) + sizeof(card->longname) +
	       snd_switch_size(lswitch);
	buffer = (char *) vmalloc(size);
	if (!buffer) {
		snd_printk("snd: cannot store control settings - no enough memory\n" );
		snd_switch_lock(lswitch, 1);
		return;
	}
	pos = 0;
	memcpy(buffer + pos, card->id, sizeof(card->id));
	pos += sizeof(card->id);
	memcpy(buffer + pos, card->longname, sizeof(card->longname));
	pos += sizeof(card->longname);
	err = snd_switch_store(lswitch, card, buffer + pos, size - pos);
	snd_switch_lock(lswitch, 1);
	if (err < 0) {
		snd_printd("cannot store control switch settings\n");
		vfree(buffer);
		return;
	}
	sprintf(key, "snd:%i/control", card->number);
	snd_persist_store(key, buffer, size);
	vfree(buffer);
}

void snd_control_restore(snd_card_t * card)
{
	int size;
	unsigned int pos;
	long err;
	char *buffer;
	snd_kswitch_list_t *lswitch;
	char key[32];
	
	lswitch = &card->switches;
	sprintf(key, "snd:%i/control", card->number);
	size = snd_persist_length(key);
	if (size <= 0)
		return;
	buffer = (char *) vmalloc(size);
	if (!buffer) {
		snd_printk("cannot restore control settings - no enough memory\n" );
		return;
	}
	size = snd_persist_restore(key, buffer, size);
	/* ok. I'm very paranoid in bellow code */
	if (size < (int)(sizeof(card->id) + sizeof(card->longname))) {
		vfree(buffer);
		return;
	}
	pos = 0;
	if (strcmp(buffer + pos, card->id)) {
		vfree(buffer);
		return;
	}
	pos += sizeof(card->id);
	if (strcmp(buffer + pos, card->longname)) {
		vfree(buffer);
		return;
	}
	pos += sizeof(card->longname);
	snd_switch_lock(lswitch, 0);
	err = snd_switch_restore(lswitch, card, buffer + pos, size - pos);
	snd_switch_lock(lswitch, 1);
	if (err < 0)
		snd_printd("cannot restore control switch settings\n");
	vfree(buffer);
}

int snd_control_register(snd_card_t *card)
{
	int err, cardnum;
	char name[16];

	if (!card)
		return -EINVAL;
	cardnum = card->number;
	if (cardnum < 0 || cardnum >= SND_CARDS)
		return -EINVAL;
	sprintf(name, "controlC%i", cardnum);
	if ((err = snd_register_device(SND_DEVICE_TYPE_CONTROL,
					card, 0, &snd_control_reg, name)) < 0)
		return err;
	snd_first_ioctl_use++;
	return 0;
}

int snd_control_unregister(snd_card_t *card)
{
	int err, cardnum;

	if (!card)
		return -EINVAL;
	cardnum = card->number;
	if (cardnum < 0 || cardnum >= SND_CARDS)
		return -EINVAL;
	if ((err = snd_unregister_device(SND_DEVICE_TYPE_CONTROL, card, 0)) < 0)
		return err;
	snd_first_ioctl_use--;
	return 0;
}
