/*
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 */

#define __NO_VERSION__
#include <sound/driver.h>
#include <asm/io.h>
#include <linux/time.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/info.h>
#include <sound/initval.h>

static int snd_preallocate_dma = 1;
MODULE_PARM(snd_preallocate_dma, "i");
MODULE_PARM_DESC(snd_preallocate_dma, "Preallocate DMA memory when the PCM devices are initialized.");
MODULE_PARM_SYNTAX(snd_preallocate_dma, SNDRV_BOOLEAN_TRUE_DESC);

static int snd_maximum_substreams = 4;
MODULE_PARM(snd_maximum_substreams, "i");
MODULE_PARM_DESC(snd_maximum_substreams, "Maximum substreams with preallocated DMA memory.");
MODULE_PARM_SYNTAX(snd_maximum_substreams, SNDRV_BOOLEAN_TRUE_DESC);

static int snd_minimum_buffer = 16384;


static void snd_pcm_lib_preallocate_dma_free(snd_pcm_substream_t *substream)
{
	if (substream->dma_area == NULL)
		return;
	switch (substream->dma_type) {
	case SNDRV_PCM_DMA_TYPE_CONTINUOUS:
		snd_free_pages(substream->dma_area, substream->dma_bytes);
		break;
#ifdef CONFIG_ISA
	case SNDRV_PCM_DMA_TYPE_ISA:
		snd_free_isa_pages(substream->dma_bytes, substream->dma_area, substream->dma_addr);
		break;
#endif
#ifdef CONFIG_PCI
	case SNDRV_PCM_DMA_TYPE_PCI:
		snd_free_pci_pages((struct pci_dev *)substream->dma_private, substream->dma_bytes, substream->dma_area, substream->dma_addr);
		break;
#endif
	}
	substream->dma_area = NULL;
}

int snd_pcm_lib_preallocate_free(snd_pcm_substream_t *substream)
{
	snd_pcm_lib_preallocate_dma_free(substream);
	if (substream->proc_prealloc_entry) {
		snd_info_unregister(substream->proc_prealloc_entry);
		substream->proc_prealloc_entry = NULL;
	}
	return 0;
}

int snd_pcm_lib_preallocate_free_for_all(snd_pcm_t *pcm)
{
	snd_pcm_substream_t *substream;
	int stream;

	for (stream = 0; stream < 2; stream++)
		for (substream = pcm->streams[stream].substream; substream; substream = substream->next)
			snd_pcm_lib_preallocate_free(substream);
	return 0;
}

static void snd_pcm_lib_preallocate_proc_read(snd_info_entry_t *entry,
					      snd_info_buffer_t *buffer)
{
	snd_pcm_substream_t *substream = (snd_pcm_substream_t *)entry->private_data;
	snd_iprintf(buffer, "%lu\n", (unsigned long) substream->dma_bytes / 1024);
}

static void snd_pcm_lib_preallocate_proc_write(snd_info_entry_t *entry,
					       snd_info_buffer_t *buffer)
{
	snd_pcm_substream_t *substream = (snd_pcm_substream_t *)entry->private_data;
	char line[64], str[64];
	size_t size;
	void *dma_area;
	dma_addr_t dma_addr;

	if (substream->runtime) {
		buffer->error = -EBUSY;
		return;
	}
	if (!snd_info_get_line(buffer, line, sizeof(line))) {
		snd_info_get_str(str, line, sizeof(str));
		size = simple_strtoul(str, NULL, 10) * 1024;
		if ((size != 0 && size < 8192) || size > substream->dma_max) {
			buffer->error = -EINVAL;
			return;
		}
		if (substream->dma_bytes == size)
			return;
		if (size > 0) {
			switch (substream->dma_type) {
			case SNDRV_PCM_DMA_TYPE_CONTINUOUS:
				dma_area = snd_malloc_pages(size, (unsigned int)((unsigned long)substream->dma_private & 0xffffffff));
				dma_addr = 0UL;		/* not valid */
				break;
#ifdef CONFIG_ISA
			case SNDRV_PCM_DMA_TYPE_ISA:
				dma_area = snd_malloc_isa_pages(size, &dma_addr, (unsigned int)((unsigned long)substream->dma_private & 0xffffffff));
				break;
#endif
#ifdef CONFIG_PCI
			case SNDRV_PCM_DMA_TYPE_PCI:
				dma_area = snd_malloc_pci_pages((struct pci_dev *)substream->dma_private, size, &dma_addr);
				break;
#endif
			default:
				dma_area = NULL;
				dma_addr = 0UL;
			}
			if (dma_area == NULL) {
				buffer->error = -ENOMEM;
				return;
			}
			substream->buffer_bytes_max = size;
		} else {
			dma_area = NULL;
			substream->buffer_bytes_max = UINT_MAX;
		}
		snd_pcm_lib_preallocate_dma_free(substream);
		substream->dma_area = dma_area;
		substream->dma_addr = dma_addr;
		substream->dma_bytes = size;
	} else {
		buffer->error = -EINVAL;
	}
}

static int snd_pcm_lib_preallocate_pages1(snd_pcm_substream_t *substream,
					  size_t size, size_t max)
{
	unsigned long rsize = 0;
	void *dma_area = NULL;
	dma_addr_t dma_addr = 0UL;
	snd_info_entry_t *entry;

	if (!size || !snd_preallocate_dma || substream->number >= snd_maximum_substreams) {
		size = 0;
	} else {
		switch (substream->dma_type) {
		case SNDRV_PCM_DMA_TYPE_CONTINUOUS:
			dma_area = snd_malloc_pages_fallback(size, (unsigned int)((unsigned long)substream->dma_private & 0xffffffff), &rsize);
			dma_addr = 0UL;		/* not valid */
			break;
#ifdef CONFIG_ISA
		case SNDRV_PCM_DMA_TYPE_ISA:
			dma_area = snd_malloc_isa_pages_fallback(size, &dma_addr, (unsigned int)((unsigned long)substream->dma_private & 0xffffffff), &rsize);
			break;
#endif
#ifdef CONFIG_PCI
		case SNDRV_PCM_DMA_TYPE_PCI:
			dma_area = snd_malloc_pci_pages_fallback((struct pci_dev *)substream->dma_private, size, &dma_addr, &rsize);
			break;
#endif
		default:
			size = 0;
		}
		if (rsize < snd_minimum_buffer) {
			snd_pcm_lib_preallocate_dma_free(substream);
			size = 0;
		}
	}
	substream->dma_area = dma_area;
	substream->dma_addr = dma_addr;
	substream->dma_bytes = rsize;
	if (rsize > 0)
		substream->buffer_bytes_max = rsize;
	substream->dma_max = max;
	if ((entry = snd_info_create_card_entry(substream->pcm->card, "prealloc", substream->proc_root)) != NULL) {
		entry->c.text.read_size = 64;
		entry->c.text.read = snd_pcm_lib_preallocate_proc_read;
		entry->c.text.write_size = 64;
		entry->c.text.write = snd_pcm_lib_preallocate_proc_write;
		entry->private_data = substream;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_prealloc_entry = entry;
	return 0;
}

int snd_pcm_lib_preallocate_pages(snd_pcm_substream_t *substream,
				      size_t size, size_t max,
				      unsigned int flags)
{
	substream->dma_type = SNDRV_PCM_DMA_TYPE_CONTINUOUS;
	substream->dma_private = (void *)(unsigned long)flags;
	return snd_pcm_lib_preallocate_pages1(substream, size, max);
}

int snd_pcm_lib_preallocate_pages_for_all(snd_pcm_t *pcm,
					  size_t size, size_t max,
					  unsigned int flags)
{
	snd_pcm_substream_t *substream;
	int stream, err;

	for (stream = 0; stream < 2; stream++)
		for (substream = pcm->streams[stream].substream; substream; substream = substream->next)
			if ((err = snd_pcm_lib_preallocate_pages(substream, size, max, flags)) < 0)
				return err;
	return 0;
}

#ifdef CONFIG_ISA
int snd_pcm_lib_preallocate_isa_pages(snd_pcm_substream_t *substream,
				      size_t size, size_t max,
				      unsigned int flags)
{
	substream->dma_type = SNDRV_PCM_DMA_TYPE_ISA;
	substream->dma_private = (void *)(unsigned long)flags;
	return snd_pcm_lib_preallocate_pages1(substream, size, max);
}

int snd_pcm_lib_preallocate_isa_pages_for_all(snd_pcm_t *pcm,
					      size_t size, size_t max,
					      unsigned int flags)
{
	snd_pcm_substream_t *substream;
	int stream, err;

	for (stream = 0; stream < 2; stream++)
		for (substream = pcm->streams[stream].substream; substream; substream = substream->next)
			if ((err = snd_pcm_lib_preallocate_isa_pages(substream, size, max, flags)) < 0)
				return err;
	return 0;
}
#endif /* CONFIG_ISA */

int snd_pcm_lib_malloc_pages(snd_pcm_substream_t *substream, size_t size)
{
	snd_pcm_runtime_t *runtime;
	void *dma_area = NULL;
	dma_addr_t dma_addr = 0UL;

	snd_assert(substream != NULL, return -EINVAL);
	runtime = substream->runtime;
	snd_assert(runtime != NULL, return -EINVAL);	
	if (runtime->dma_area != NULL) {
		/* perphaps, we might free the large DMA memory region
		   to save some space here, but the actual solution
		   costs us less time */
		if (runtime->dma_bytes >= size)
			return 0;	/* ok, do not change */
      		snd_pcm_lib_free_pages(substream);
	}
	if (substream->dma_area != NULL && substream->dma_bytes >= size) {
		dma_area = substream->dma_area;
		dma_addr = substream->dma_addr;
	} else {
		switch (substream->dma_type) {
#ifdef CONFIG_ISA
		case SNDRV_PCM_DMA_TYPE_ISA:
			dma_area = snd_malloc_isa_pages(size, &dma_addr, (unsigned int)((unsigned long)substream->dma_private & 0xffffffff)); 
			break;
#endif
#ifdef CONFIG_PCI
		case SNDRV_PCM_DMA_TYPE_PCI:
			dma_area = snd_malloc_pci_pages((struct pci_dev *)substream->dma_private, size, &dma_addr);
			break;
#endif
		default:
			return -ENXIO;
		}
	}
	if (! dma_area)
		return -ENOMEM;
	runtime->dma_area = dma_area;
	runtime->dma_addr = dma_addr;
	runtime->dma_bytes = size;
	return 1;			/* area was changed */
}

int snd_pcm_lib_free_pages(snd_pcm_substream_t *substream)
{
	snd_pcm_runtime_t *runtime;

	snd_assert(substream != NULL, return -EINVAL);
	runtime = substream->runtime;
	snd_assert(runtime != NULL, return -EINVAL);
	if (runtime->dma_area == NULL)
		return 0;
	if (runtime->dma_area != substream->dma_area) {
		switch (substream->dma_type) {
#ifdef CONFIG_ISA
		case SNDRV_PCM_DMA_TYPE_ISA:
			snd_free_isa_pages(runtime->dma_bytes, runtime->dma_area, runtime->dma_addr);
			break;
#endif
#ifdef CONFIG_PCI
		case SNDRV_PCM_DMA_TYPE_PCI:
			snd_free_pci_pages((struct pci_dev *)substream->dma_private, runtime->dma_bytes, runtime->dma_area, runtime->dma_addr);
			break;
#endif
		}
	}
	runtime->dma_area = NULL;
	runtime->dma_addr = 0UL;
	runtime->dma_bytes = 0;
	return 0;
}

#ifdef CONFIG_PCI

int snd_pcm_lib_preallocate_pci_pages(struct pci_dev *pci,
				      snd_pcm_substream_t *substream,
				      size_t size, size_t max)
{
	substream->dma_type = SNDRV_PCM_DMA_TYPE_PCI;
	substream->dma_private = pci;
	return snd_pcm_lib_preallocate_pages1(substream, size, max);
}

int snd_pcm_lib_preallocate_pci_pages_for_all(struct pci_dev *pci,
					      snd_pcm_t *pcm,
					      size_t size, size_t max)
{
	snd_pcm_substream_t *substream;
	int stream, err;

	for (stream = 0; stream < 2; stream++)
		for (substream = pcm->streams[stream].substream; substream; substream = substream->next)
			if ((err = snd_pcm_lib_preallocate_pci_pages(pci, substream, size, max)) < 0)
				return err;
	return 0;
}

#endif /* CONFIG_PCI */
