/*
 *  Copyright (c) by Jaroslav Kysela <perex@suse.cz>
 *  Memory allocation routines.
 *
 *
 *   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 "../include/driver.h"
#include "../include/info.h"

/*
 *  page allocation
 */

static long snd_alloc_pages;
static long snd_alloc_kmalloc;
static long snd_alloc_vmalloc;

static snd_info_entry_t *snd_memory_info_entry;

char *snd_malloc_pages(unsigned long size, int *res_pg, int dma)
{
	int pg;
	char *buf;

	for (pg = 0; PAGE_SIZE * (1 << pg) < size; pg++);
	buf = (char *) __get_free_pages(GFP_KERNEL | (dma ? GFP_DMA : 0), pg);
	if (buf) {
		int page, last_page;

		page = MAP_NR(buf);
		last_page = page + (1 << pg);
		while (page < last_page)
			set_bit(PG_reserved, &mem_map[page++].flags);
		snd_alloc_pages += 1 << pg;
	}
	*res_pg = pg;
	return buf;
}

void snd_free_pages(char *ptr, unsigned long size)
{
	int pg, page, last_page;

	if (ptr == NULL)
		return;
	for (pg = 0; PAGE_SIZE * (1 << pg) < size; pg++);
	page = MAP_NR(ptr);
	last_page = page + (1 << pg);
	while (page < last_page)
		clear_bit(PG_reserved, &mem_map[page++].flags);
	free_pages((unsigned long) ptr, pg);
	snd_alloc_pages -= 1 << pg;
}

void snd_memory_init(void)
{
	snd_alloc_pages = 0;
	snd_alloc_kmalloc = 0;
	snd_alloc_vmalloc = 0;
}

void snd_memory_done(void)
{
	if (snd_alloc_pages > 0)
		snd_printk("snd_memory_done - snd_alloc_pages = %li\n", snd_alloc_pages);
	if (snd_alloc_kmalloc > 0)
		snd_printk("snd_memory_done - snd_alloc_kmalloc = %li\n", snd_alloc_kmalloc);
	if (snd_alloc_vmalloc > 0)
		snd_printk("snd_memory_done - snd_alloc_vmalloc = %li\n", snd_alloc_vmalloc);
}

void *snd_kmalloc(size_t size, int flags)
{
	void *result;
	
	result = kmalloc(size + sizeof(size_t), flags);
	if (result != NULL) {
		*(size_t *)result = size;
		snd_alloc_kmalloc += size;
		return result + sizeof(size_t);
	}
	return NULL;
}

void snd_kfree(void *obj)
{
	if (obj == NULL)
		return;
	snd_alloc_kmalloc -= *(size_t *)(obj - sizeof(size_t));
	kfree(obj - sizeof(size_t));
}

void *snd_kcalloc(size_t size, int flags)
{
	void *result;
	
	result = snd_kmalloc(size, flags);
	if (result)
		memset(result, 0, size);
	return result;
}

char *snd_kmalloc_strdup(const char *string, int flags)
{
	int len;
	char *res;

	if (!string)
		return NULL;
	len = strlen(string) + 1;
	if ((res = snd_kmalloc(len, flags)) == NULL)
		return NULL;
	memcpy(res, string, len);
	return res;
}

void *snd_vmalloc(unsigned long size)
{
	void *result;
	
	result = vmalloc(size + sizeof(size_t));
	if (result != NULL) {
		*(size_t *)result = size;
		snd_alloc_vmalloc += size;
		return result + sizeof(size_t);
	}
	return NULL;
}

void snd_vfree(void *obj)
{
	if (obj == NULL)
		return;
	snd_alloc_vmalloc -= *(size_t *)(obj - sizeof(size_t));
	vfree(obj - sizeof(size_t));
}

static void snd_memory_info_read(snd_info_buffer_t * buffer, void *private_data)
{
	snd_iprintf(buffer, "pages  : %li bytes (%li pages)\n", snd_alloc_pages * PAGE_SIZE, snd_alloc_pages);
	snd_iprintf(buffer, "kmalloc: %li bytes\n", snd_alloc_kmalloc);
	snd_iprintf(buffer, "vmalloc: %li bytes\n", snd_alloc_vmalloc);
}

int snd_memory_info_init(void)
{
	snd_info_entry_t *entry;

	entry = snd_info_create_entry(NULL, "meminfo");
	if (entry) {
		entry->t.text.read_size = 256;
		entry->t.text.read = snd_memory_info_read;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	snd_memory_info_entry = entry;
	return 0;
}

int snd_memory_info_done(void)
{
	if (snd_memory_info_entry)
		snd_info_unregister(snd_memory_info_entry);
	return 0;
}

/*
 *  DMA allocation routines
 */

static int snd_dma_is_multi(snd_dma_t * dma, char * owner)
{
	if (!dma->multi)
		return 0;
	if (dma->multi_match[0] == NULL)
		return 1;
	if (dma->areas) {
		if (dma->areas->owner == NULL ||
		    strcmp(dma->areas->owner, owner))
			return 0;
	}
	if (!strcmp(dma->multi_match[0], owner) ||
	    (dma->multi_match[1] && !strcmp(dma->multi_match[1], owner)))
	    	return 1;
	return 0;
}

int snd_dma_malloc(snd_card_t * card, snd_dma_t * dma, char *owner, snd_dma_area_t ** rarea)
{
	unsigned char *buf;
	long size;
	int pg, add_area = 0;
	snd_dma_area_t *area;

	*rarea = NULL;
	if (card == NULL || dma == NULL)
		return -EINVAL;
	if (dma->type == SND_DMA_TYPE_HARDWARE)
		return -EINVAL;
	down(&dma->mutex);
	if (!snd_dma_is_multi(dma, owner) && dma->areas) {
		area = dma->areas;
		if (area->owner) {
			if (area->mmaped) {
				area->mmap_free = 0;
				up(&dma->mutex);
				return 0;
			} else {
				up(&dma->mutex);
				return -EBUSY;
			}
		}
	} else {
		area = (snd_dma_area_t *)snd_kcalloc(sizeof(*area), GFP_KERNEL);
		if (area == NULL) {
			up(&dma->mutex);
			return -ENOMEM;
		}
		add_area = 1;
	}
	area->dma = dma;
	area->mmap_free = 0;
	buf = NULL;
	size = dma->rsize;
	pg = 0;
	while (!buf && size >= PAGE_SIZE) {
		buf = snd_malloc_pages(size, &pg, dma->type == SND_DMA_TYPE_ISA || dma->type == SND_DMA_TYPE_PCI_16MB ? 1 : 0);
		if (!buf)
			size >>= 1;
	}
	if (buf) {
		area->size = (1 << pg) * PAGE_SIZE;
		area->buf = buf;
		area->owner = owner;
		if (add_area) {
			area->next = dma->areas;
			dma->areas = area;
		}
		*rarea = area;
	} else {
		if (add_area)
			snd_kfree(area);
	}
	up(&dma->mutex);
	return buf == NULL ? -ENOMEM : 0;
}

static void snd_dma_free1(snd_card_t * card, snd_dma_t * dma, snd_dma_area_t * area)
{
	if (area->mmaped) {	/* wait */
		area->mmap_free = 1;
		return;
	}
	area->mmap_free = 0;
	area->owner = NULL;
	if (area->buf) {
		/* free DMA buffer */
		if (dma->type == SND_DMA_TYPE_ISA &&
		    dma->areas == area && area->next == NULL)
			snd_dma_disable(dma->dma);
		snd_free_pages(area->buf, area->size);
		area->buf = NULL;
		area->size = 0;
		if (dma->areas == area) {
			dma->areas = area->next;
		} else {
			snd_dma_area_t *a = dma->areas;
			while (a->next && a->next != area)
				a = a->next;
			if (a->next == NULL) {
				snd_printk("snd_dma_free1: INTERNAL ERROR\n");
			} else {
				a->next = area->next;
			}
		}
		snd_kfree(area);
	}
}

void snd_dma_free(snd_card_t * card, snd_dma_area_t * area)
{
	snd_dma_t *dma;

	if (card == NULL || area == NULL)
		return;
	dma = area->dma;
	if (dma == NULL)
		return;
	if (dma->type == SND_DMA_TYPE_HARDWARE)
		return;
#if 0
	snd_printk("snd_dma_free: dmanum = %i, mmaped = %i\n", dma->dma, area->mmaped);
#endif
	down(&dma->mutex);
	snd_dma_free1(card, dma, area);
	up(&dma->mutex);
}

void snd_dma_notify_vma_close(struct vm_area_struct *vma)
{
	int idx;
	snd_card_t *card;
	snd_dma_t *dma;
	snd_dma_area_t *area;

	for (idx = 0; idx < snd_ecards_limit; idx++) {
		if (!(snd_cards_bitmap & (1 << idx)))
			continue;
		card = snd_cards[idx];
		if (!card)
			continue;
		for (dma = card->dmas; dma; dma = dma->next) {
			down(&dma->mutex);
			for (area = dma->areas; area; area = area->next) {
				if (area->mmaped && area->vma == vma) { /* yep! */
					area->vma = NULL;
					area->mmaped = 0;
					if (area->mmap_free)
						snd_dma_free1(card, dma, area);
				}
			}
			up(&dma->mutex);
		}
	}
}
