/*
 *  Digital Audio (PCM) abstract layer
 *  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.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "minors.h"
#include "pcm.h"
#include "control.h"
#include "info.h"
#include "ulaw.h"
#include "sndpersist.h"

snd_pcm_t *snd_pcm_devices[SND_CARDS * SND_PCM_DEVICES];
static struct snd_stru_pcm_notify *snd_pcm_notify_first = NULL;
snd_mutex_define_static(register);

void snd_pcm_lock(void)
{
	snd_mutex_down_static(register);
}

void snd_pcm_unlock(void)
{
	snd_mutex_up_static(register);
}

static int snd_pcm_switch_write(snd_control_t * control, snd_pcm_t * pcm, int iface,
				snd_kswitch_list_t * klist, snd_switch_t *_sw)
{
	int result;
	
	if (snd_control_busy(control))
		return -EBUSY;
	result = snd_switch_write(klist, pcm, _sw);
	if (result > 0) {	/* change */
		snd_switch_t sw;
		
		copy_from_user(&sw, _sw, sizeof(*_sw));
		snd_control_notify_switch_value_change(control,
						       iface,
						       sw.name,
						       0);
	}
	return result;
}

static int snd_pcm_control_ioctl(snd_card_t * card,
				 snd_control_t * control,
				 unsigned int cmd, unsigned long arg)
{
	unsigned int tmp;
	int idx;
	snd_pcm_t *pcm;

	tmp = card->number * SND_PCM_DEVICES;
	switch (cmd) {
	case SND_CTL_IOCTL_HW_INFO:
		{
			struct snd_ctl_hw_info *ptr = (struct snd_ctl_hw_info *) arg;

			ptr->pcmdevs = 0;
			for (idx = SND_PCM_DEVICES - 1; idx >= 0; idx--) {
				if (snd_pcm_devices[tmp + idx]) {
					ptr->pcmdevs = idx + 1;
					break;
				}
			}
			return 0;
		}
	case SND_CTL_IOCTL_PCM_DEVICE:
		{
			int val = snd_ioctl_in((long *) arg);
			if (val < 0 || val >= SND_PCM_DEVICES)
				return -EINVAL;
			if (!snd_pcm_devices[tmp + val])
				return -EINVAL;
			control->pcm_device = val;
			return 0;
		}
	case SND_CTL_IOCTL_PCM_PSWITCH_LIST:
	case SND_CTL_IOCTL_PCM_PSWITCH_READ:
	case SND_CTL_IOCTL_PCM_PSWITCH_WRITE:
	case SND_CTL_IOCTL_PCM_RSWITCH_LIST:
	case SND_CTL_IOCTL_PCM_RSWITCH_READ:
	case SND_CTL_IOCTL_PCM_RSWITCH_WRITE:
		pcm = snd_pcm_devices[tmp + control->pcm_device];
		if (!pcm)
			return -ENOENT;
		switch (cmd) {
		case SND_CTL_IOCTL_PCM_PSWITCH_LIST:
			return snd_switch_list(&pcm->playback.switches, (snd_switch_list_t *) arg);
		case SND_CTL_IOCTL_PCM_PSWITCH_READ:
			return snd_switch_read(&pcm->playback.switches, pcm, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_PCM_PSWITCH_WRITE:
			return snd_pcm_switch_write(control, pcm, SND_CTL_IFACE_PCM_PLAYBACK, &pcm->playback.switches, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_PCM_RSWITCH_LIST:
			return snd_switch_list(&pcm->record.switches, (snd_switch_list_t *) arg);
		case SND_CTL_IOCTL_PCM_RSWITCH_READ:
			return snd_switch_read(&pcm->record.switches, pcm, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_PCM_RSWITCH_WRITE:
			return snd_pcm_switch_write(control, pcm, SND_CTL_IFACE_PCM_RECORD, &pcm->record.switches, (snd_switch_t *) arg);
		}
		break;
	}
	return -EAGAIN;
}

snd_pcm_t *snd_pcm_new_device(snd_card_t * card, char *id, snd_minor_t * reg)
{
	snd_pcm_t *pcm;

	if (!card)
		return NULL;
	pcm = (snd_pcm_t *) snd_calloc(sizeof(snd_pcm_t));
	if (!pcm)
		return NULL;
	pcm->card = card;
	if (id) {
		strncpy(pcm->id, id, sizeof(pcm->id) - 1);
	}
	pcm->reg = reg;
	snd_switch_prepare(&pcm->playback.switches);
	snd_switch_prepare(&pcm->record.switches);
	pcm->playback.pcm = pcm;
	pcm->record.pcm = pcm;
	return pcm;
}

int snd_pcm_free(snd_pcm_t * pcm)
{
	if (!pcm)
		return -EINVAL;
	snd_switch_free(&pcm->playback.switches);
	snd_switch_free(&pcm->record.switches);
	if (pcm->private_free)
		pcm->private_free(pcm->private_data);
	snd_free(pcm, sizeof(snd_pcm_t));
	return 0;
}

static void snd_pcm_store(snd_pcm_t * pcm)
{
	int size;
	long err;
	unsigned int pos;
	char *buffer;
	snd_kswitch_list_t *lpswitch, *lrswitch;
	char key[32];
	
	lpswitch = &pcm->playback.switches;
	lrswitch = &pcm->record.switches;
	snd_switch_lock(lpswitch, 0);
	snd_switch_lock(lrswitch, 0);
	size = sizeof(pcm->id) + sizeof(pcm->name) +
	       snd_switch_size(lpswitch) + snd_switch_size(lrswitch);
	buffer = (char *) vmalloc(size);
	if (!buffer) {
		snd_printk("cannot store pcm settings - no enough memory\n" );
		snd_switch_lock(lpswitch, 1);
		snd_switch_lock(lrswitch, 1);
		return;
	}
	pos = 0;
	memcpy(buffer + pos, pcm->id, sizeof(pcm->id));
	pos += sizeof(pcm->id);
	memcpy(buffer + pos, pcm->name, sizeof(pcm->name));
	pos += sizeof(pcm->name);
	err = snd_switch_store(lpswitch, pcm, buffer + pos, size - pos);
	snd_switch_lock(lpswitch, 1);
	if (err < 0) {
		snd_printk("cannot store pcm playback switch settings\n");
		snd_switch_lock(lrswitch, 1);
		vfree(buffer);
		return;
	}
	err = snd_switch_store(lrswitch, pcm, buffer + pos, size - pos);
	snd_switch_lock(lrswitch, 1);
	if (err < 0) {
		snd_printk("cannot store pcm record switch settings\n");
		vfree(buffer);
		return;
	}
	sprintf(key, "snd:%i/pcm:%i", pcm->card->number, pcm->device);
	snd_persist_store(key, buffer, size);
	vfree(buffer);
}

static void snd_pcm_restore(snd_pcm_t * pcm)
{
	int size;
	long err;
	unsigned int pos;
	char *buffer;
	char key[32];
	
	sprintf(key, "snd:%i/pcm:%i", pcm->card->number, pcm->device);
	size = snd_persist_length(key);
	if (size <= 0)
		return;
	buffer = (char *) vmalloc(size);
	if (!buffer) {
		snd_printk("snd: cannot restore pcm 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(pcm->id) + sizeof(pcm->name))) {
		vfree(buffer);
		return;
	}
	pos = 0;
	if (strcmp(buffer + pos, pcm->id)) {
		vfree(buffer);
		return;
	}
	pos += sizeof(pcm->id);
	if (strcmp(buffer + pos, pcm->name)) {
		vfree(buffer);
		return;
	}
	pos += sizeof(pcm->name);
	err = snd_switch_restore(&pcm->playback.switches, pcm, buffer + pos, size - pos);
	if (err < 0) {
		snd_printd("cannot restore pcm playback switch settings\n");
		vfree(buffer);
		return;
	}
	pos += err;
	err = snd_switch_restore(&pcm->record.switches, pcm, buffer + pos, size - pos);
	if (err < 0) {
		snd_printd("cannot restore pcm record switch settings\n");
		vfree(buffer);
		return;
	}
	vfree(buffer);
}

int snd_pcm_register(snd_pcm_t * pcm, int pcm_device)
{
	int idx, err;
	unsigned short minor;
	struct snd_stru_pcm_notify *notify;
	char str[16];

	snd_pcm_lock();
	if (pcm_device < 0 || pcm_device >= SND_PCM_DEVICES) {
		snd_pcm_unlock();
		return -EINVAL;
	}
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm_device;
	if (snd_pcm_devices[idx]) {
		snd_pcm_unlock();
		return -EBUSY;
	}
	pcm->device = pcm_device;
	snd_pcm_devices[idx] = pcm;
	minor = SND_MINOR_PCM + idx;
	sprintf(str, "pcmC%iD%i", pcm->card->number, pcm_device);
	if ((err = snd_register_device(SND_DEVICE_TYPE_PCM, pcm->card, pcm_device, pcm->reg, str)) < 0) {
		snd_pcm_devices[idx] = NULL;
		snd_pcm_unlock();
		return err;
	}
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_register)
			notify->n_register(minor, pcm);
	}
	snd_pcm_proc_init(pcm);
	snd_pcm_unlock();
	snd_pcm_restore(pcm);
	return 0;
}

int snd_pcm_unregister(snd_pcm_t * pcm)
{
	int idx;
	struct snd_stru_pcm_notify *notify;

	if (!pcm)
		return -EINVAL;
	snd_pcm_lock();
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm->device;
	if (snd_pcm_devices[idx] != pcm) {
		snd_pcm_unlock();
		return -EINVAL;
	}
	snd_pcm_store(pcm);
	snd_pcm_proc_done(pcm);
	snd_unregister_device(SND_DEVICE_TYPE_PCM, pcm->card, pcm->device);
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_unregister)
			notify->n_unregister(SND_MINOR_PCM + idx, pcm);
	}
	snd_pcm_devices[idx] = NULL;
	snd_pcm_unlock();
	return snd_pcm_free(pcm);
}

int snd_pcm_notify(struct snd_stru_pcm_notify *notify, int nfree)
{
	int idx;
	struct snd_stru_pcm_notify *tnotify;

	if (!notify || !notify->n_register || !notify->n_unregister)
		return -EINVAL;
	snd_pcm_lock();
	if (nfree) {
		tnotify = snd_pcm_notify_first;
		if (tnotify == notify) {
			snd_pcm_notify_first = notify->next;
		} else {
			while (tnotify && tnotify->next != notify)
				tnotify = tnotify->next;
			if (!tnotify) {
				snd_pcm_unlock();
				return -ENOENT;
			}
			tnotify->next = tnotify->next->next;
		}
		for (idx = 0; idx < SND_CARDS * SND_PCM_DEVICES; idx++) {
			if (snd_pcm_devices[idx] == NULL)
				continue;
			notify->n_unregister(idx + SND_MINOR_PCM,
				             snd_pcm_devices[idx]);
		}
	} else {
		notify->next = NULL;
		if (!snd_pcm_notify_first) {
			snd_pcm_notify_first = notify;
		} else {
			for (tnotify = snd_pcm_notify_first;
			     tnotify->next;
			     tnotify = tnotify->next);
			tnotify->next = notify;
		}
		for (idx = 0; idx < SND_CARDS * SND_PCM_DEVICES; idx++) {
			if (snd_pcm_devices[idx] == NULL)
				continue;
			notify->n_register(idx + SND_MINOR_PCM,
				           snd_pcm_devices[idx]);
		}
	}
	snd_pcm_unlock();
	return 0;
}

int snd_pcm_ioctl(snd_pcm_t * pcm, unsigned int cmd, unsigned long arg)
{
	return -ENXIO;
}

int snd_pcm_switch_add(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw)
{
	int err = snd_switch_add(&pchn->switches, ksw);
	if (err >= 0)
		snd_control_notify_switch_change(pchn->pcm->card,
					SND_CTL_READ_SWITCH_ADD,
					pchn == &pchn->pcm->playback ?
						SND_CTL_IFACE_PCM_PLAYBACK :
						SND_CTL_IFACE_PCM_RECORD,
					ksw->name);
	return err;
}

int snd_pcm_switch_remove(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw)
{
	int err = snd_switch_remove(&pchn->switches, ksw);
	if (err >= 0)
		snd_control_notify_switch_change(pchn->pcm->card,
					SND_CTL_READ_SWITCH_REMOVE,
					pchn == &pchn->pcm->playback ?
						SND_CTL_IFACE_PCM_PLAYBACK :
						SND_CTL_IFACE_PCM_RECORD,
					ksw->name);
	return err;
}

snd_kswitch_t *snd_pcm_switch_new(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw, void *private_data)
{
	snd_kswitch_t * sw;
	
	sw = snd_switch_new(ksw);
	if (!sw)
		return NULL;
	if (snd_pcm_switch_add(pchn, sw) < 0) {
		snd_switch_free_one(sw);
		return NULL;
	}
	sw->private_data = private_data;
	return sw;
}

int snd_pcm_switch_change(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw)
{
	if (!pchn || !ksw)
		return -EINVAL;
	snd_control_notify_switch_change(pchn->pcm->card,
				SND_CTL_READ_SWITCH_CHANGE,
				pchn == &pchn->pcm->playback ?
					SND_CTL_IFACE_PCM_PLAYBACK :
					SND_CTL_IFACE_PCM_RECORD,
				ksw->name);
	return 0;
}

/*
 *  Info interface
 */

static void snd_pcm_proc_read(snd_info_buffer_t * buffer, void *private_data)
{
	int idx;
	snd_pcm_t *pcm;

	snd_mutex_down_static(register);
	for (idx = 0; idx < SND_CARDS * SND_PCM_DEVICES; idx++) {
		pcm = snd_pcm_devices[idx];
		if (pcm == NULL)
			continue;
		snd_iprintf(buffer, "%02i-%02i: %s : %s\n", idx >> 2, idx & 3, pcm->id, pcm->name);
	}
	snd_mutex_up_static(register);
}

/*
 *  ENTRY functions
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_pcm_export;
#endif

static snd_info_entry_t *snd_pcm_proc_entry = NULL;

int init_module(void)
{
	snd_info_entry_t *entry;

#ifndef LINUX_2_1
	if (register_symtab(&snd_symbol_table_pcm_export) < 0)
		return -ENOMEM;
#endif
	snd_control_register_ioctl(snd_pcm_control_ioctl);
	if ((entry = snd_info_create_entry(NULL, "pcm")) != NULL) {
		entry->t.text.read_size = SND_CARDS * SND_PCM_DEVICES * 128;
		entry->t.text.read = snd_pcm_proc_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	snd_pcm_proc_entry = entry;
	return 0;
}

void cleanup_module(void)
{
	snd_control_unregister_ioctl(snd_pcm_control_ioctl);
	if (snd_pcm_proc_entry) {
		snd_info_unregister(snd_pcm_proc_entry);
		snd_pcm_proc_entry = NULL;
	}
}
