/*
 *  Digital Audio (PCM) abstract layer
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.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 "info.h"
#include "pcm.h"
#include "ulaw.h"
#include "control.h"

snd_pcm_t *snd_pcm_devices[SND_CARDS * SND_PCM_DEVICES];
static struct snd_stru_pcm_notify *snd_pcm_notify_first = NULL;

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 = NULL;

	tmp = card->number << 2;
	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 sizeof(struct snd_ctl_hw_info);
		}
	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_PSWITCHES:
	case SND_CTL_IOCTL_PCM_PSWITCH_READ:
	case SND_CTL_IOCTL_PCM_PSWITCH_WRITE:
	case SND_CTL_IOCTL_PCM_RSWITCHES:
	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_PSWITCHES:
			*(int *)arg = snd_switch_count(&pcm->playback.switches);
			return sizeof(int);
		case SND_CTL_IOCTL_PCM_PSWITCH_READ:
			if (( tmp = snd_switch_read(&pcm->playback.switches, pcm, (snd_switch_t *) arg)) < 0)
				return tmp;
			return sizeof(snd_switch_t);
		case SND_CTL_IOCTL_PCM_PSWITCH_WRITE:
			if ((tmp = snd_switch_write(&pcm->playback.switches, pcm, (snd_switch_t *) arg)) < 0)
				return tmp;
			return sizeof(snd_switch_t);
		case SND_CTL_IOCTL_PCM_RSWITCHES:
			*(int *)arg = snd_switch_count(&pcm->record.switches);
			return sizeof(int);
		case SND_CTL_IOCTL_PCM_RSWITCH_READ:
			if ((tmp = snd_switch_read(&pcm->record.switches, pcm, (snd_switch_t *) arg)) < 0)
				return tmp;
			return sizeof(snd_switch_t);
		case SND_CTL_IOCTL_PCM_RSWITCH_WRITE:
			if ((tmp = snd_switch_write(&pcm->record.switches, pcm, (snd_switch_t *) arg)) < 0)
				return tmp;
			return sizeof(snd_switch_t);
		}
		break;
	}
	return -EAGAIN;
}

snd_pcm_t *snd_pcm_new_device(snd_card_t * card, char *id, snd_io_t * io)
{
	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->io = io;
	snd_switch_prepare(&pcm->playback.switches);
	snd_switch_prepare(&pcm->record.switches);
	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 = 0;
	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 *) snd_malloc(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_free(buffer, size);
		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_free(buffer, size);
		return;
	}
	sprintf(key, "snd:%i/pcm:%i", pcm->card->number, pcm->device);
//	snd_persist_store(key, buffer, size);
	snd_free(buffer, size);
}

static void snd_pcm_restore(snd_pcm_t * pcm)
{
	int size = 0;
	long err = 0;
	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_malloc(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_free(buffer, size);
		return;
	}
	pos = 0;
	if (strcmp(buffer + pos, pcm->id)) {
		snd_free(buffer, size);
		return;
	}
	pos += sizeof(pcm->id);
	if (strcmp(buffer + pos, pcm->name)) {
		snd_free(buffer, size);
		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_free(buffer, size);
		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");
		snd_free(buffer, size);
		return;
	}
	snd_free(buffer, size);
}

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

	if (pcm_device < 0 || pcm_device >= SND_PCM_DEVICES) {
		return -EINVAL;
	}
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm_device;
	if (snd_pcm_devices[idx]) {
		return -EBUSY;
	}
	pcm->device = pcm_device;
	snd_pcm_devices[idx] = pcm;
	sprintf(str, "pcm%i%i", pcm->card->number, pcm->device);
	if ((pcm->attr = snd_register_io(pcm, str, pcm->io)) == NULL) {
		snd_pcm_devices[idx] = NULL;
		return -errno;
	}
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_register)
			notify->n_register(idx, pcm);
	}
	snd_pcm_proc_init(pcm);
	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;
	idx = (pcm->card->number * SND_PCM_DEVICES) + pcm->device;
	if (snd_pcm_devices[idx] != pcm) {
		return -EINVAL;
	}
	snd_pcm_store(pcm);
	snd_pcm_proc_done(pcm);
	snd_unregister_io(pcm->attr);
	for (notify = snd_pcm_notify_first; notify; notify = notify->next) {
		if (notify->n_unregister)
			notify->n_unregister(idx, pcm);
	}
	snd_pcm_devices[idx] = NULL;
	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;
	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) {
				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_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_pcm_devices[idx]);
		}
	}
	return 0;
}

int snd_pcm_ioctl(snd_pcm_t * pcm, unsigned int cmd, unsigned long arg)
{
	int tmp;
	
	switch (cmd) {
	case SND_PCM_IOCTL_PSWITCHES:
		*(int*)arg = snd_switch_count(&pcm->playback.switches);
		return sizeof(int);
	case SND_PCM_IOCTL_PSWITCH_READ:
		if ((tmp = snd_switch_read(&pcm->playback.switches, pcm, (snd_switch_t *) arg)) < 0)
			return tmp;
		return sizeof(snd_switch_t);
	case SND_PCM_IOCTL_PSWITCH_WRITE:
		if ((tmp = snd_switch_write(&pcm->playback.switches, pcm, (snd_switch_t *) arg)) < 0)
			return tmp;
		return sizeof(snd_switch_t);
	case SND_PCM_IOCTL_RSWITCHES:
		*(int*) arg = snd_switch_count(&pcm->record.switches);
		return sizeof(int);
	case SND_PCM_IOCTL_RSWITCH_READ:
		if ((tmp = snd_switch_read(&pcm->record.switches, pcm, (snd_switch_t *) arg)) <0)
			return tmp;
		return sizeof(snd_switch_t);
	case SND_PCM_IOCTL_RSWITCH_WRITE:
		if ((tmp = snd_switch_write(&pcm->record.switches, pcm, (snd_switch_t *) arg)) < 0)
			return tmp;
		return sizeof(snd_switch_t);
	}
	return -ENXIO;
}

int snd_pcm_switch_add(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw)
{
	return snd_switch_add(&pchn->switches, ksw);
}

int snd_pcm_switch_remove(snd_pcm_channel_t * pchn, snd_kswitch_t * ksw)
{
	return snd_switch_remove(&pchn->switches, ksw);
}

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;
}
/*
 *  Info interface
 */

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

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

static snd_info_entry_t *snd_pcm_proc_entry = NULL;

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

void snd_pcm_cleanup(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;
    }	
}