/*
 *  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 "../include/driver.h"
#include "../include/minors.h"
#include "../include/pcm.h"
#include "../include/control.h"
#include "../include/info.h"
#include "../include/ulaw.h"
#include "../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;
static DECLARE_MUTEX(register_mutex);

void snd_pcm_lock(int xup)
{
	if (!xup) {
		down(&register_mutex);
	} else {
		up(&register_mutex);
	}
}

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;
		
		if (copy_from_user(&sw, _sw, sizeof(*_sw)))
			return -EFAULT;
		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_SUBDEVICE:
	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_CSWITCH_LIST:
	case SND_CTL_IOCTL_PCM_CSWITCH_READ:
	case SND_CTL_IOCTL_PCM_CSWITCH_WRITE:
	case SND_CTL_IOCTL_PCM_PREFER_SUBDEVICE:
		pcm = snd_pcm_devices[tmp + control->pcm_device];
		if (!pcm)
			return -ENOENT;
		switch (cmd) {
		case SND_CTL_IOCTL_PCM_SUBDEVICE:
			{
				int val = snd_ioctl_in((long *) arg);
				if (val < 0)
					return -EINVAL;
				if (pcm->playback.subchn_count <= val &&
				    pcm->capture.subchn_count <= val)
					return -EINVAL;
				control->pcm_subdevice = val;
				return 0;
			}
		case SND_CTL_IOCTL_PCM_PREFER_SUBDEVICE:
			pcm->open_prefer_subchn = snd_ioctl_in((long *) arg);
			return 0;
		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_CSWITCH_LIST:
			return snd_switch_list(&pcm->capture.switches, (snd_switch_list_t *) arg);
		case SND_CTL_IOCTL_PCM_CSWITCH_READ:
			return snd_switch_read(&pcm->capture.switches, pcm, (snd_switch_t *) arg);
		case SND_CTL_IOCTL_PCM_CSWITCH_WRITE:
			return snd_pcm_switch_write(control, pcm, SND_CTL_IFACE_PCM_CAPTURE, &pcm->capture.switches, (snd_switch_t *) arg);
		}
		break;
	}
	return -EAGAIN;
}

static int snd_pcm_new_channel(snd_pcm_t *pcm,
			       snd_pcm_channel_t *pchn,
			       int subchn_count,
			       int capture)
{
	int idx;
	snd_pcm_subchn_t *subchn, *prev;

	snd_switch_prepare(&pchn->switches);
	pchn->pcm = pcm;
	pchn->subchn_count = subchn_count;
	prev = NULL;
	for (idx = 0, prev = NULL; idx < subchn_count; idx++) {
		subchn = (snd_pcm_subchn_t *) snd_kcalloc(sizeof(*subchn), GFP_KERNEL);
		if (subchn == NULL)
			return -ENOMEM;
		subchn->pcm = pcm;
		subchn->pchn = pchn;
		subchn->number = idx;
		subchn->capture = capture;
		if (prev == NULL)
			pchn->subchn = subchn;
		else
			prev->next = subchn;
		prev = subchn;
	}
	return 0;
}				

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

	if (!card)
		return NULL;
	pcm = (snd_pcm_t *) snd_kcalloc(sizeof(snd_pcm_t), GFP_KERNEL);
	if (!pcm)
		return NULL;
	pcm->card = card;
	if (id) {
		strncpy(pcm->id, id, sizeof(pcm->id) - 1);
	}
	pcm->reg = reg;
	if (snd_pcm_new_channel(pcm, &pcm->playback, playback_count, 0) < 0) {
		snd_pcm_free(pcm);
		return NULL;
	}
	if (snd_pcm_new_channel(pcm, &pcm->capture, capture_count, 1) < 0) {
		snd_pcm_free(pcm);
		return NULL;
	}
	init_MUTEX(&pcm->open_mutex);
	return pcm;
}

static void snd_pcm_free_channel(snd_pcm_channel_t * pchn)
{
	snd_pcm_subchn_t *subchn, *subchn_next;

	snd_switch_free(&pchn->switches);
	subchn = pchn->subchn;
	while (subchn) {
		subchn_next = subchn->next;
		if (subchn->proc_entry1)
			snd_info_unregister(subchn->proc_entry1);
		if (subchn->private_free)
			subchn->private_free(subchn->private_data);
		snd_kfree(subchn);
		subchn = subchn_next;
	}
	if (pchn->private_free)
		pchn->private_free(pchn->private_data);
}

int snd_pcm_free(snd_pcm_t * pcm)
{
	if (!pcm)
		return -EINVAL;
	if (pcm->private_free)
		pcm->private_free(pcm->private_data);
	snd_pcm_free_channel(&pcm->playback);
	snd_pcm_free_channel(&pcm->capture);
	snd_kfree(pcm);
	return 0;
}

int snd_pcm_open(snd_pcm_t * pcm, int playback_flag, int capture_flag,
		 snd_pcm_file_t ** rpcm_file)
{
	snd_pcm_file_t * pcm_file;
	snd_pcm_subchn_t * subchn;
	
	if (rpcm_file == NULL)
		return -EINVAL;
	*rpcm_file = NULL;
	if (!playback_flag && !capture_flag)
		return -EINVAL;
	if (pcm == NULL)
		return -EINVAL;
	pcm_file = (snd_pcm_file_t *) snd_kcalloc(sizeof(*pcm_file), GFP_KERNEL);
	if (pcm_file == NULL)
		return -ENOMEM;
	pcm_file->pcm = pcm;
	if (playback_flag) {
		if (pcm->playback.subchn == NULL) {
			snd_pcm_release(pcm_file);
			return -ENODEV;
		}
		if (pcm->open_prefer_subchn >= 0) {
			for (subchn = pcm->playback.subchn; subchn; subchn = subchn->next)
				if (subchn->file == NULL && subchn->number == pcm->open_prefer_subchn)
					goto __playback_ok;
		}
		for (subchn = pcm->playback.subchn; subchn; subchn = subchn->next)
			if (subchn->file == NULL)
				break;
	      __playback_ok:
		if (subchn == NULL) {
			snd_pcm_release(pcm_file);
			return -EBUSY;
		}
		pcm_file->playback = subchn;
		subchn->file = pcm_file;
	}
	if (capture_flag) {
		if (pcm->capture.subchn == NULL) {
			snd_pcm_release(pcm_file);
			return -ENODEV;
		}
		for (subchn = pcm->capture.subchn; subchn; subchn = subchn->next)
			if (subchn->file == NULL)
				break;
		if (subchn == NULL) {
			snd_pcm_release(pcm_file);
			return -EBUSY;
		}
		pcm_file->capture = subchn;
		subchn->file = pcm_file;
	}
	pcm_file->next = pcm->files;
	pcm->files = pcm_file;
	*rpcm_file = pcm_file;
	return 0;
}

int snd_pcm_release(snd_pcm_file_t * pcm_file)
{
	snd_pcm_t * pcm;
	snd_pcm_file_t * pcm_file_1;
	snd_pcm_subchn_t * subchn;

	if (pcm_file == NULL)
		return -EINVAL;
	pcm = pcm_file->pcm;
	if (pcm == NULL)
		return -EINVAL;
	if (pcm->files == pcm_file) {
		pcm->files = pcm_file->next;
	} else {
		pcm_file_1 = pcm->files;
		while (pcm_file_1 && pcm_file_1->next != pcm_file)
			pcm_file_1 = pcm_file_1->next;
		if (pcm_file_1 != NULL)
			pcm_file_1->next = pcm_file->next;
	}
	if ((subchn = pcm_file->playback) != NULL) {
		subchn->file = NULL;
		if (subchn->private_free)
			subchn->private_free(subchn->private_data);
		subchn->private_data = NULL;
#ifdef CONFIG_SND_OSSEMUL
		subchn->oss = 0;
#endif
	}
	if ((subchn = pcm_file->capture) != NULL) {
		subchn->file = NULL;
		if (subchn->private_free)
			subchn->private_free(subchn->private_data);
		subchn->private_data = NULL;
#ifdef CONFIG_SND_OSSEMUL
		subchn->oss = 0;
#endif
	}
	if (pcm_file->private_free)
		pcm_file->private_free(pcm_file->private_data);
	snd_kfree(pcm_file);
	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->capture.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 *) snd_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);
		snd_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");
		snd_vfree(buffer);
		return;
	}
	sprintf(key, "snd:%i/pcm:%i", pcm->card->number, pcm->device);
	snd_persist_store(key, buffer, size);
	snd_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 *) snd_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))) {
		snd_vfree(buffer);
		return;
	}
	pos = 0;
	if (strcmp(buffer + pos, pcm->id)) {
		snd_vfree(buffer);
		return;
	}
	pos += sizeof(pcm->id);
	if (strcmp(buffer + pos, pcm->name)) {
		snd_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");
		snd_vfree(buffer);
		return;
	}
	pos += err;
	err = snd_switch_restore(&pcm->capture.switches, pcm, buffer + pos, size - pos);
	if (err < 0) {
		snd_printd("cannot restore pcm record switch settings\n");
		snd_vfree(buffer);
		return;
	}
	snd_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];

	if (pcm == NULL)
		return -EINVAL;
	if (pcm_device < 0 || pcm_device >= SND_PCM_DEVICES)
		return -EINVAL;
	snd_pcm_lock(0);
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm_device;
	if (snd_pcm_devices[idx]) {
		snd_pcm_lock(1);
		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_lock(1);
		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_lock(1);
	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(0);
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm->device;
	if (snd_pcm_devices[idx] != pcm) {
		snd_pcm_lock(1);
		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_lock(1);
	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(0);
	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_lock(1);
				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_lock(1);
	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_CAPTURE,
					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_CAPTURE,
					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_CAPTURE,
				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;

	down(&register_mutex);
	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", idx >> 4, idx & 15, pcm->id, pcm->name);
		if (pcm->playback.subchn)
			snd_iprintf(buffer, " : playback %i", pcm->playback.subchn_count);
		if (pcm->capture.subchn)
			snd_iprintf(buffer, " : capture %i", pcm->capture.subchn_count);
		snd_iprintf(buffer, "\n");
	}
	up(&register_mutex);
}

/*
 *  ENTRY functions
 */

static snd_info_entry_t *snd_pcm_proc_entry = NULL;

int init_module(void)
{
	snd_info_entry_t *entry;

	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;
	}
}

EXPORT_SYMBOL(snd_pcm_lock);
EXPORT_SYMBOL(snd_pcm_devices);
EXPORT_SYMBOL(snd_pcm_new_device);
EXPORT_SYMBOL(snd_pcm_free);
EXPORT_SYMBOL(snd_pcm_register);
EXPORT_SYMBOL(snd_pcm_unregister);
EXPORT_SYMBOL(snd_pcm_open);
EXPORT_SYMBOL(snd_pcm_release);
EXPORT_SYMBOL(snd_pcm_notify);
EXPORT_SYMBOL(snd_pcm_ioctl);
EXPORT_SYMBOL(snd_pcm_switch_add);
EXPORT_SYMBOL(snd_pcm_switch_remove);
EXPORT_SYMBOL(snd_pcm_switch_new);
EXPORT_SYMBOL(snd_pcm_switch_change);
  /* pcm_proc.c */
EXPORT_SYMBOL(snd_pcm_proc_format);
EXPORT_SYMBOL(snd_pcm_proc_write);
  /* ulaw.c */
EXPORT_SYMBOL(snd_ulaw_dsp);
EXPORT_SYMBOL(snd_ulaw_dsp_loud);
EXPORT_SYMBOL(snd_dsp_ulaw);
EXPORT_SYMBOL(snd_dsp_ulaw_loud);
EXPORT_SYMBOL(snd_translate_to_user);
EXPORT_SYMBOL(snd_translate_from_user);
