/*
 *  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"

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

int snd_pcm_free(snd_pcm_t * pcm);
int snd_pcm_register(snd_pcm_t * pcm, snd_device_t *devptr);
int snd_pcm_unregister(snd_pcm_t * pcm);

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

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;
			
			if (get_user(val, (int *)arg))
				return -EFAULT;
			if (val < 0 || val >= SND_PCM_DEVICES)
				return -ENXIO;
			if (snd_pcm_devices[tmp + val] == NULL)
				return -ENXIO;
			control->pcm_device = val;
			return 0;
		}
	case SND_CTL_IOCTL_PCM_CHANNEL:
		{
			int val;
			
			if (get_user(val, (int *)arg))
				return -EFAULT;
			if (val < 0 || val > 1)
				return -ENXIO;
			control->pcm_channel = val;
			return 0;
		}
	case SND_CTL_IOCTL_PCM_SUBDEVICE:
	case SND_CTL_IOCTL_PCM_PREFER_SUBDEVICE:
	case SND_CTL_IOCTL_PCM_INFO:
	case SND_CTL_IOCTL_PCM_CHANNEL_INFO:
		pcm = snd_pcm_devices[tmp + control->pcm_device];
		if (pcm == NULL)
			return -ENXIO;
		switch (cmd) {
		case SND_CTL_IOCTL_PCM_SUBDEVICE:
			{
				int val;
				if (get_user(val, (int *)arg))
					return -EFAULT;
				control->pcm_subdevice = val;
				return 0;
			}
		case SND_CTL_IOCTL_PCM_PREFER_SUBDEVICE:
			{
				int val;
				
				if (get_user(val, (int *)arg))
					return -EFAULT;
				if (val >= 0 && pcm->chn[control->pcm_channel].subchn_count <= val)
					return -ENXIO;
				pcm->chn[control->pcm_channel].open_prefer_subchn = val;
				return 0;
			}
		case SND_CTL_IOCTL_PCM_INFO:
			return snd_pcm_info(pcm, (snd_pcm_info_t *) arg);
		case SND_CTL_IOCTL_PCM_CHANNEL_INFO:
			{
				snd_pcm_channel_info_t info;
				snd_pcm_subchn_t *subchn;

				if (copy_from_user(&info, (snd_pcm_channel_info_t *) arg, sizeof(info)))
					return -EFAULT;
				
				for (subchn = pcm->chn[control->pcm_channel].subchn; subchn; subchn = subchn->next)
	                        	if (subchn->number == control->pcm_subdevice)
        	                        	break;
	        	        if (subchn == NULL)
        	        	        return -ENXIO;
	                	return snd_pcm_channel_info(subchn, (snd_pcm_channel_info_t *) arg);
			}
			break;
		}
	}
	return -ENOIOCTLCMD;
}

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

	snd_switch_prepare(pcm->card, &pchn->switches, pcm, SND_CTL_IFACE_PCM, pcm->device, channel);
#ifdef CONFIG_SND_OSSEMUL
	init_MUTEX(&pchn->oss.setup_mutex);
#endif
	pchn->channel = channel;
	pchn->pcm = pcm;
	pchn->subchn_count = subchn_count;
	pchn->reg = &snd_pcm_reg[channel];
	prev = NULL;
	for (idx = 0, prev = NULL; idx < subchn_count; idx++) {
		subchn = snd_magic_kcalloc(snd_pcm_subchn_t, 0, GFP_KERNEL);
		if (subchn == NULL)
			return -ENOMEM;
		subchn->pcm = pcm;
		subchn->pchn = pchn;
		subchn->number = idx;
		subchn->channel = channel;
		sprintf(subchn->name, "subdevice #%i", idx);
		if (prev == NULL)
			pchn->subchn = subchn;
		else
			prev->next = subchn;
		prev = subchn;
	}
	return 0;
}				

int snd_pcm_new(snd_card_t * card, char *id, int device,
	        int playback_count, int capture_count,
	        snd_pcm_t ** rpcm)
{
	snd_pcm_t *pcm;
	int err;
	static snd_device_ops_t ops = {
		(snd_dev_free_t *)snd_pcm_free,
		(snd_dev_register_t *)snd_pcm_register,
		(snd_dev_unregister_t *)snd_pcm_unregister
	};

	snd_debug_check(rpcm == NULL, -EINVAL);
	*rpcm = NULL;
	snd_debug_check(card == NULL, -ENXIO);
	pcm = snd_magic_kcalloc(snd_pcm_t, 0, GFP_KERNEL);
	if (pcm == NULL)
		return -ENOMEM;
	pcm->card = card;
	pcm->device = device;
	if (id) {
		strncpy(pcm->id, id, sizeof(pcm->id) - 1);
	}
	if ((err = snd_pcm_new_channel(pcm, &pcm->chn[SND_PCM_CHANNEL_PLAYBACK], playback_count, SND_PCM_CHANNEL_PLAYBACK)) < 0) {
		snd_pcm_free(pcm);
		return err;
	}
	if ((err = snd_pcm_new_channel(pcm, &pcm->chn[SND_PCM_CHANNEL_CAPTURE], capture_count, SND_PCM_CHANNEL_CAPTURE)) < 0) {
		snd_pcm_free(pcm);
		return err;
	}
	if ((err = snd_device_new(card, SND_DEV_PCM, pcm, device, &ops, NULL)) < 0) {
		snd_pcm_free(pcm);
		return err;
	}
	init_MUTEX(&pcm->open_mutex);
	init_waitqueue_head(&pcm->open_wait);
	*rpcm = pcm;
	return 0;
}

static void snd_pcm_free_channel(snd_pcm_channel_t * pchn)
{
	snd_pcm_subchn_t *subchn, *subchn_next;
#ifdef CONFIG_SND_OSSEMUL
	snd_pcm_oss_setup_t *setup, *setupn;
#endif

	snd_switch_free(pchn->pcm->card, &pchn->switches);
	subchn = pchn->subchn;
	while (subchn) {
		subchn_next = subchn->next;
		if (subchn->proc_entry1)
			snd_info_unregister(subchn->proc_entry1);
		snd_magic_kfree(subchn);
		subchn = subchn_next;
	}
#ifdef CONFIG_SND_OSSEMUL
	for (setup = pchn->oss.setup_list; setup; setup = setupn) {
		setupn = setup->next;
		snd_kfree(setup->task_name);
		snd_kfree(setup);
	}
#endif
	if (pchn->private_free)
		pchn->private_free(pchn->private_data);
}

int snd_pcm_free(snd_pcm_t * pcm)
{
	snd_debug_check(pcm == NULL, -ENXIO);
	if (pcm->private_free)
		pcm->private_free(pcm->private_data);
	snd_pcm_free_channel(&pcm->chn[SND_PCM_CHANNEL_PLAYBACK]);
	snd_pcm_free_channel(&pcm->chn[SND_PCM_CHANNEL_CAPTURE]);
	snd_magic_kfree(pcm);
	return 0;
}

int snd_pcm_open_subchn(snd_pcm_t *pcm, int channel,
			snd_pcm_subchn_t **rsubchn)
{
	snd_pcm_channel_t * pchn;
	snd_pcm_subchn_t * subchn;
	snd_pcm_runtime_t * runtime;

	snd_debug_check(rsubchn == NULL, -EINVAL);
	*rsubchn = NULL;
	snd_debug_check(pcm == NULL, -ENXIO);
	pchn = &pcm->chn[channel];
	if (pchn->subchn == NULL)
		return -ENODEV;

	switch (channel) {
	case SND_PCM_CHANNEL_PLAYBACK:
		if (!(pcm->info_flags & SND_PCM_INFO_PLAYBACK))
			return -ENODEV;
		if (!(pcm->info_flags & SND_PCM_INFO_DUPLEX)) {
			for (subchn = pcm->chn[SND_PCM_CHANNEL_CAPTURE].subchn; subchn; subchn = subchn->next) {
				if (SUBCHN_BUSY(subchn))
					return -EAGAIN;
			}
		}
		break;
	case SND_PCM_CHANNEL_CAPTURE:
		if (!(pcm->info_flags & SND_PCM_INFO_CAPTURE))
			return -ENODEV;
		if (!(pcm->info_flags & SND_PCM_INFO_DUPLEX)) {
			for (subchn = pcm->chn[SND_PCM_CHANNEL_PLAYBACK].subchn; subchn; subchn = subchn->next) {
				if (SUBCHN_BUSY(subchn))
					return -EAGAIN;
			}
		}
		break;
	default:
		return -EINVAL;
	}
	if (pchn->open_prefer_subchn >= 0) {
		for (subchn = pchn->subchn; subchn; subchn = subchn->next)
			if (!SUBCHN_BUSY(subchn) && subchn->number == pchn->open_prefer_subchn)
				goto __ok;
	}
	for (subchn = pchn->subchn; subchn; subchn = subchn->next)
		if (!SUBCHN_BUSY(subchn))
			break;
      __ok:
	if (subchn == NULL)
		return -EAGAIN;

	runtime = snd_kcalloc(sizeof(snd_pcm_runtime_t), GFP_KERNEL);
	if (runtime == NULL)
		return -ENOMEM;

	runtime->status = &runtime->_sstatus;
	runtime->frag_head = &runtime->_sfrag_head;
	runtime->frag_tail = &runtime->_sfrag_tail;
	init_waitqueue_head(&runtime->sleep);
	spin_lock_init(&runtime->lock);
	spin_lock_init(&runtime->sleep_lock);
	spin_lock_init(&runtime->timer_lock);

	runtime->mode = SND_PCM_MODE_BLOCK;
	*runtime->status = SND_PCM_STATUS_NOTREADY;

	subchn->runtime = runtime;
	*rsubchn = subchn;
	return 0;
}

void snd_pcm_release_subchn(snd_pcm_subchn_t *subchn)
{
	snd_pcm_runtime_t * runtime;
	subchn->file = NULL;
	runtime = subchn->runtime;
	if (runtime) {
		if (runtime->mmap_control)
			snd_vma_disconnect(runtime->mmap_control_vma);
		if (runtime->dig_mask_free)
			runtime->dig_mask_free(runtime->dig_mask);
		if (runtime->hw_free)
			runtime->hw_free(runtime->hw);
		if (runtime->private_free != NULL)
			runtime->private_free(runtime->private_data);
		snd_kfree(runtime);
		subchn->runtime = NULL;
	}
}

int snd_pcm_register(snd_pcm_t * pcm, snd_device_t *devptr)
{
	int idx, cidx, err;
	unsigned short minor;
	snd_pcm_subchn_t *subchn;
	struct snd_stru_pcm_notify *notify;
	char str[16];

	snd_debug_check(pcm == NULL || devptr == NULL, -ENXIO);
	snd_pcm_lock(0);
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm->device;
	if (snd_pcm_devices[idx]) {
		snd_pcm_lock(1);
		return -EBUSY;
	}
	snd_pcm_devices[idx] = pcm;
	for (cidx = 0; cidx < 2; cidx++) {
		int devtype;
		if (pcm->chn[cidx].subchn == NULL)
			continue;
		switch (cidx) {
		case SND_PCM_CHANNEL_PLAYBACK:
			sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->device);
			minor = SND_MINOR_PCM_PLAYBACK + idx;
			devtype = SND_DEVICE_TYPE_PCM_PLAYBACK;
			break;
		case SND_PCM_CHANNEL_CAPTURE:
			sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->device);
			minor = SND_MINOR_PCM_CAPTURE + idx;
			devtype = SND_DEVICE_TYPE_PCM_CAPTURE;
			break;
		default:
			return -EINVAL;
		}
		if ((err = snd_register_device(devtype, pcm->card, pcm->device, pcm->chn[cidx].reg, str)) < 0) {
			snd_pcm_devices[idx] = NULL;
			snd_pcm_lock(1);
			return err;
		for (subchn = pcm->chn[cidx].subchn; subchn; subchn = subchn->next)
			snd_pcm_timer_done(subchn);
		}
	}
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_register)
			notify->n_register(-1 /* idx + SND_MINOR_PCM */, pcm);
	}
	snd_pcm_proc_init(pcm);
	snd_pcm_lock(1);
	return 0;
}

int snd_pcm_unregister(snd_pcm_t * pcm)
{
	int idx, cidx;
	snd_pcm_subchn_t *subchn;
	struct snd_stru_pcm_notify *notify;

	snd_debug_check(pcm == NULL, -ENXIO);
	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_proc_done(pcm);
	for (cidx = 0; cidx < 2; cidx++) {
		int devtype;
		if (pcm->chn[cidx].subchn == NULL)
			continue;
		switch (cidx) {
		case SND_PCM_CHANNEL_PLAYBACK:
			devtype = SND_DEVICE_TYPE_PCM_PLAYBACK;
			break;
		case SND_PCM_CHANNEL_CAPTURE:
			devtype = SND_DEVICE_TYPE_PCM_CAPTURE;
			break;
		default:
			return -EINVAL;
		}
		snd_unregister_device(devtype, pcm->card, pcm->device);
		for (subchn = pcm->chn[cidx].subchn; subchn; subchn = subchn->next)
			snd_pcm_timer_done(subchn);
	}
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_unregister)
			notify->n_unregister(-1 /* 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;

	snd_debug_check(notify == NULL || notify->n_register == NULL || notify->n_unregister == NULL, -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 == NULL) {
				snd_pcm_lock(1);
				return -ENXIO;
			}
			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(-1 /* idx + SND_MINOR_PCM */,
				             snd_pcm_devices[idx]);
		}
	} else {
		notify->next = NULL;
		if (snd_pcm_notify_first == NULL) {
			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(-1 /* idx + SND_MINOR_PCM */,
				           snd_pcm_devices[idx]);
		}
	}
	snd_pcm_lock(1);
	return 0;
}

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,
						 SND_CTL_IFACE_PCM,
						 pchn->pcm->device,
						 pchn->channel,
						 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,
						 SND_CTL_IFACE_PCM,
						 pchn->pcm->device,
						 pchn->channel,
						 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 == NULL)
		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)
{
	snd_debug_check(pchn == NULL || ksw == NULL, -ENXIO);
	snd_control_notify_switch_change(pchn->pcm->card,
					 SND_CTL_READ_SWITCH_CHANGE,
					 SND_CTL_IFACE_PCM,
					 pchn->pcm->device,
					 pchn->channel,
					 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 / SND_PCM_DEVICES,
			    idx % SND_PCM_DEVICES, pcm->id, pcm->name);
		if (pcm->chn[SND_PCM_CHANNEL_PLAYBACK].subchn)
			snd_iprintf(buffer, " : playback %i", pcm->chn[SND_PCM_CHANNEL_PLAYBACK].subchn_count);
		if (pcm->chn[SND_PCM_CHANNEL_CAPTURE].subchn)
			snd_iprintf(buffer, " : capture %i", pcm->chn[SND_PCM_CHANNEL_CAPTURE].subchn_count);
		snd_iprintf(buffer, "\n");
	}
	up(&register_mutex);
}

/*
 *  ENTRY functions
 */

static snd_info_entry_t *snd_pcm_proc_entry = NULL;

static int __init alsa_pcm_init(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;
}

static void __exit alsa_pcm_exit(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;
	}
}

module_init(alsa_pcm_init)
module_exit(alsa_pcm_exit)

EXPORT_SYMBOL(snd_pcm_lock);
EXPORT_SYMBOL(snd_pcm_devices);
EXPORT_SYMBOL(snd_pcm_new);
EXPORT_SYMBOL(snd_pcm_notify);
EXPORT_SYMBOL(snd_pcm_open_subchn);
EXPORT_SYMBOL(snd_pcm_release_subchn);
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_native.c */
EXPORT_SYMBOL(snd_pcm_channel_go);
EXPORT_SYMBOL(snd_pcm_channel_go_pre);
EXPORT_SYMBOL(snd_pcm_channel_go_post);
EXPORT_SYMBOL(snd_pcm_kernel_playback_ioctl);
EXPORT_SYMBOL(snd_pcm_kernel_capture_ioctl);
EXPORT_SYMBOL(snd_pcm_kernel_ioctl);
EXPORT_SYMBOL(snd_pcm_open);
EXPORT_SYMBOL(snd_pcm_release);
EXPORT_SYMBOL(snd_pcm_playback_poll);
EXPORT_SYMBOL(snd_pcm_capture_poll);
  /* pcm_proc.c */
EXPORT_SYMBOL(snd_pcm_proc_format);
EXPORT_SYMBOL(snd_pcm_proc_write);
EXPORT_SYMBOL(snd_pcm_proc_write_silence);
 /* pcm_misc.c */
EXPORT_SYMBOL(snd_pcm_format_signed);
EXPORT_SYMBOL(snd_pcm_format_unsigned);
EXPORT_SYMBOL(snd_pcm_format_linear);
EXPORT_SYMBOL(snd_pcm_format_little_endian);
EXPORT_SYMBOL(snd_pcm_format_big_endian);
EXPORT_SYMBOL(snd_pcm_format_width);
EXPORT_SYMBOL(snd_pcm_format_physical_width);
EXPORT_SYMBOL(snd_pcm_format_size);
EXPORT_SYMBOL(snd_pcm_format_silence);
EXPORT_SYMBOL(snd_pcm_build_linear_format);
