/*
 *  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 "driver.h"
#include "info.h"

/*
 *  page allocation
 */

snd_mutex_define_static(memory);
static long snd_pages;

static snd_info_entry_t *snd_memory_info_entry;
#ifdef CONFIG_SND_DEBUG
static snd_info_entry_t *snd_memory_info_entry_debug;
#endif

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++);
#if LinuxVersionCode( 2, 1, 85 ) <= LINUX_VERSION_CODE
	buf = (char *) __get_free_pages(GFP_KERNEL | (dma ? GFP_DMA : 0), pg);
#else
	buf = (char *) __get_free_pages(GFP_KERNEL, pg, dma ? MAX_DMA_ADDRESS : 0);
#endif
#if 0
	snd_printk("pg = %i, buf = 0x%x\n", pg, (int) buf);
#endif
	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_pages += 1 << pg;
	}
	*res_pg = pg;
	return buf;
}

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

	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_pages -= 1 << pg;
}

/*
 *  ok.. new allocation routines..
 */

#define BLOCKS_IN_PAGE		(PAGE_SIZE / 32)
#define HEADER_SIZE		sizeof(struct alloc_header)
#define FREE_SPACE		(PAGE_SIZE - HEADER_SIZE)
#define ALIGN_VALUE(x)		(((x)+(sizeof(void *)-1)) & ~(sizeof(void *)-1))

struct alloc_header {
	struct alloc_header *next;		/* next page */
	unsigned short sizes[BLOCKS_IN_PAGE];	/* size of block */
};

static struct alloc_header *snd_alloc_first_page;

#ifdef CONFIG_SND_DEBUG_MEMORY
static void *sndmem_alloc_ptr[1024];
static unsigned long sndmem_alloc_size[1024];
#endif

void snd_malloc_init(void)
{
	snd_pages = 0;
	snd_alloc_first_page = NULL;
#ifdef CONFIG_SND_DEBUG_MEMORY
	memset(sndmem_alloc_ptr, 0, sizeof(sndmem_alloc_ptr));
#endif
}

void snd_malloc_done(void)
{
	struct alloc_header *hdr, *next;

	if ((hdr = snd_alloc_first_page) != NULL) {
#ifdef CONFIG_SND_DEBUG
		snd_printk("malloc done - trouble\n");
		snd_memory_debug1();
#endif
		while (hdr) {
			next = hdr->next;
			snd_free_pages((char *) hdr, PAGE_SIZE);
			hdr = next;
		}
	}
#ifdef CONFIG_SND_DEBUG
	if (snd_pages > 0)
		snd_printk("malloc done - snd_pages = %li\n", snd_pages);
#endif
}

static void *snd_malloc_inspect_block(struct alloc_header *hdr,
                                      unsigned long size)
{
	int idx, i;
	unsigned short used = 0, bused, bsize;

	for (idx = 0; idx < BLOCKS_IN_PAGE; idx++) {
		bsize = hdr->sizes[idx];
		bused = bsize & 0x8000;
		bsize &= 0x7fff;
		if ((!bsize && (used + HEADER_SIZE + size <= PAGE_SIZE)) ||
		    (!bused && bsize >= size)) {	/* free & passed */
			if (bsize > size) {
				if (hdr->sizes[BLOCKS_IN_PAGE - 1] & 0x8000)
					goto __next;	/* next block */
				for (i = BLOCKS_IN_PAGE - 1; i > idx + 1; i--)
					hdr->sizes[i] = hdr->sizes[i - 1];
				hdr->sizes[idx + 1] = bsize - size;
			}
			hdr->sizes[idx] = 0x8000 | size;
			return (void *) (((char *) hdr) + HEADER_SIZE + used);
		}
	      __next:
		used += bsize;
	}
	return NULL;
}

void *snd_malloc(unsigned long size)
{
	int pg;

	if (!size)
		return NULL;
	if (size > FREE_SPACE) {
		void *res;

		res = snd_malloc_pages(size, &pg, 0);
#ifdef CONFIG_SND_DEBUG_MEMORY
		{
			int idx;

			for (idx = 0; idx < 1024; idx++)
				if (!sndmem_alloc_ptr[idx]) {
					sndmem_alloc_ptr[idx] = res;
					sndmem_alloc_size[idx] = size;
					break;
				}
		}
#endif
		return res;
	} else {
		struct alloc_header *hdr;
		struct alloc_header *phdr = NULL;
		void *result;

		snd_mutex_down_static(memory);
		hdr = snd_alloc_first_page;
		size = ALIGN_VALUE(size);
		while (hdr) {
			result = snd_malloc_inspect_block(hdr, size);
			if (result) {
				snd_mutex_up_static(memory);
				return result;
			}
			hdr = (phdr = hdr)->next;
		}
		hdr = (struct alloc_header *)
				snd_malloc_pages(PAGE_SIZE, &pg, 0);
		if (!hdr) {
			snd_mutex_up_static(memory);
			return NULL;
		}
		memset(hdr, 0, HEADER_SIZE);
		if (!phdr)
			snd_alloc_first_page = hdr;
		else
			phdr->next = hdr;
		hdr->sizes[0] = 0x8000 | size;
		snd_mutex_up_static(memory);
		return (void *) ((char *) hdr + HEADER_SIZE);
	}
}

void *snd_calloc(unsigned long size)
{
	void *result;

	if ((result = snd_malloc(size)) == NULL)
		return NULL;
	memset(result, 0, size);
	return result;
}

static void snd_free_join_blocks(struct alloc_header *hdr, int idx)
{
	int i;

	hdr->sizes[idx] += hdr->sizes[idx + 1];
	for (i = idx + 1; hdr->sizes[i + 1] != 0 && i + 1 < BLOCKS_IN_PAGE; i++)
		hdr->sizes[i] = hdr->sizes[i + 1];
	hdr->sizes[i] = 0;
}

static void snd_free_block(struct alloc_header *hdr,
			   struct alloc_header *phdr,
			   int idx)
{
	unsigned short used;		/* zero if we can free this page */

	hdr->sizes[idx] &= 0x7fff;
	if (idx == BLOCKS_IN_PAGE - 1 ||
	    (idx + 1 < BLOCKS_IN_PAGE && hdr->sizes[idx + 1] == 0))
		hdr->sizes[idx] = 0;
      __again:
	for (idx = used = 0; hdr->sizes[idx] != 0 && idx < BLOCKS_IN_PAGE; idx++) {
		if (idx + 1 < BLOCKS_IN_PAGE &&
		    !(hdr->sizes[idx] & 0x8000) &&	/* join blocks */
		    !(hdr->sizes[idx + 1] & 0x8000) &&
		    hdr->sizes[idx + 1] != 0) {
			snd_free_join_blocks(hdr, idx);
			goto __again;
		}
		if (hdr->sizes[idx] & 0x8000) used++;
	}
	if (idx > 0 && !(hdr->sizes[idx - 1] & 0x8000))
		hdr->sizes[idx - 1] = 0;
	if (!used) {
		if (phdr)
			phdr->next = hdr->next;
		else
			snd_alloc_first_page = hdr->next;
		snd_free_pages((char *) hdr, PAGE_SIZE);
	}
}

static int snd_free_inspect_block(struct alloc_header *hdr,
				  struct alloc_header *phdr,
				  void *obj, unsigned long size)
{
	int idx;
	unsigned short used = 0;

	for (idx = 0; hdr->sizes[idx] != 0 && idx < BLOCKS_IN_PAGE; idx++) {
		if (((char *) hdr) + HEADER_SIZE + used == obj) {
			snd_free_block(hdr, phdr, idx);
			return 1;
		}
		used += hdr->sizes[idx] & 0x7fff;
	}
	return 0;
}

void snd_free(void *obj, unsigned long size)
{
	if (!size)
		return;
	if (!obj) {
#ifdef CONFIG_SND_DEBUG_MEMORY
		snd_printk("snd_free - NULL?\n");
#endif
		return;
	}
#if 0
	snd_printk("freeeeeeeeeeee 0x%x, %li\n", obj, size);
#endif
	if (size > FREE_SPACE) {
		snd_free_pages(obj, size);
#ifdef CONFIG_SND_DEBUG_MEMORY
		{
			int idx;

			for (idx = 0; idx < 1024; idx++)
				if (sndmem_alloc_ptr[idx] == obj) {
					sndmem_alloc_ptr[idx] = NULL;
					return;
				}
			snd_printk("snd_free (1) - free of unallocated block at 0x%lx size %li\n", (long) obj, size);
		}
#endif
	} else {
		struct alloc_header *hdr;
		struct alloc_header *phdr = NULL;	/* previous header */

		snd_mutex_down_static(memory);
		hdr = snd_alloc_first_page;
		while (hdr) {
			if (snd_free_inspect_block(hdr, phdr, obj, size)) {
				snd_mutex_up_static(memory);
				return;
			}
			hdr = (phdr = hdr)->next;
		}
#ifdef CONFIG_SND_DEBUG_MEMORY
		snd_printk("snd_free (2) - free of unallocated block at 0x%lx size %li\n", (long) obj, size);
#endif
		snd_mutex_up_static(memory);
	}
}

char *snd_malloc_strdup(char *string)
{
	int len;
	char *res;

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

void snd_free_str(char *string)
{
	if (string)
		snd_free(string, strlen(string) + 1);
}

static void snd_memory_info_read(snd_info_buffer_t * buffer, void *private_data)
{
	snd_iprintf(buffer, "The driver is using %i page%s (%li bytes) of the kernel memory for data.\n",
		    snd_pages,
		    snd_pages > 1 ? "s" : "",
		    snd_pages * PAGE_SIZE);
}

#ifdef CONFIG_SND_DEBUG_MEMORY
static void snd_memory_info_debug_read(snd_info_buffer_t * buffer, void *private_data)
{
	int i, used, free, total_blocks, total_used;
	struct alloc_header *hdr;

	snd_mutex_down_static(memory);
	snd_iprintf(buffer, "Memory allocation table:\n");
	for (i = used = 0; i < 1024; i++)
		if (sndmem_alloc_ptr[i]) {
			used++;
			snd_iprintf(buffer, "  %04i: 0x%08lx [%08i] = %8li\n",
				    i,
				    sndmem_alloc_ptr[i],
				    MAP_NR(sndmem_alloc_ptr[i]),
				    sndmem_alloc_size[i]);
		}
	for (hdr = snd_alloc_first_page; hdr; hdr = hdr->next, i++)
		snd_iprintf(buffer, "  %04i: 0x%08lx [%08i] = %8li (internal)\n", i, (long) hdr, MAP_NR(hdr), PAGE_SIZE);
	if (!used && !snd_alloc_first_page)
		snd_iprintf(buffer, "  -none-\n");
	snd_iprintf(buffer, "\nMemory allocation table (internal):\n");
	hdr = snd_alloc_first_page;
	if (!hdr)
		snd_iprintf(buffer, "  -none-\n");
	else {
		total_blocks = total_used = 0;
		while (hdr) {
			snd_iprintf(buffer, "  page at 0x%lx:\n", (long) hdr);
			for (i = used = free = 0; i < BLOCKS_IN_PAGE; i++) {
				unsigned short bsize, bused;

				bsize = hdr->sizes[i];
				if (!bsize)
					continue;
				bused = bsize & 0x8000;
				bsize &= 0x7fff;
				total_blocks++;
				snd_iprintf(buffer, "    %04i: 0x%08lx = %4li (%s)\n", i, (long) ((char *) hdr + HEADER_SIZE + used), bsize, bused ? "allocated" : "free");
				if (!bused)
					free += bsize;
				else
					total_used += bsize;
				used += bsize;
			}
			snd_iprintf(buffer, "    sum : size = %li, used = %li, free = %li, unused = %li\n", PAGE_SIZE - HEADER_SIZE, used - free, free, PAGE_SIZE - HEADER_SIZE - used);
			hdr = hdr->next;
		}
		snd_iprintf(buffer, "    total: blocks = %i, used = %i, recomended block allocation = %i\n",
			    total_blocks, total_used,
			    PAGE_SIZE / (total_used / total_blocks));
	}
	snd_mutex_up_static(memory);
}
#endif

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;
#ifdef CONFIG_SND_DEBUG_MEMORY
	entry = snd_info_create_entry(NULL, "memdebug");
	if (entry) {
		entry->t.text.read_size = 512 * 1024;
		entry->t.text.read = snd_memory_info_debug_read;
		if (snd_info_register(entry) < 0) {
			snd_info_unregister(entry);
			entry = NULL;
		}
	}
	snd_memory_info_entry_debug = entry;
#endif
	return 0;
}

int snd_memory_info_done(void)
{
	if (snd_memory_info_entry)
		snd_info_unregister(snd_memory_info_entry);
#ifdef CONFIG_SND_DEBUG_MEMORY
	if (snd_memory_info_entry_debug)
		snd_info_unregister(snd_memory_info_entry_debug);
#endif
	return 0;
}

#ifdef CONFIG_SND_DEBUG
void snd_memory_debug1(void)
{
	int i, used, free, total_blocks, total_used;
	struct alloc_header *hdr;

	snd_mutex_down_static(memory);
	printk("Memory allocation table:\n");
#ifdef CONFIG_SND_DEBUG_MEMORY
	for (i = used = 0; i < 1024; i++)
		if (sndmem_alloc_ptr[i]) {
			used++;
			printk("  %04i: 0x%08lx [%08li] = %8li\n",
			       i,
			       (long) sndmem_alloc_ptr[i],
			       (long) MAP_NR(sndmem_alloc_ptr[i]),
			       sndmem_alloc_size[i]);
		}
#endif
	for (hdr = snd_alloc_first_page; hdr; hdr = hdr->next, i++)
		printk("  %04i: 0x%08lx [%08li] = %8li (internal)\n", i, (long) hdr, (long) MAP_NR(hdr), (long) PAGE_SIZE);
	if (!used && !snd_alloc_first_page)
		printk("  -none-\n");
	printk("Memory allocation table (internal):\n");
	hdr = snd_alloc_first_page;
	if (!hdr)
		printk("  -none-\n");
	else {
		total_blocks = total_used = 0;
		while (hdr) {
			printk("  page at 0x%lx:\n", (long) hdr);
			for (i = used = free = 0; i < BLOCKS_IN_PAGE; i++) {
				unsigned short bsize, bused;

				bsize = hdr->sizes[i];
				if (!bsize)
					continue;
				bused = bsize & 0x8000;
				bsize &= 0x7fff;
				total_blocks++;
				printk("    %04i: 0x%08lx = %4i (%s)\n", i, (long) ((char *) hdr + HEADER_SIZE + used), bsize, bused ? "allocated" : "free");
				if (!bused)
					free += bsize;
				else
					total_used += bsize;
				used += bsize;
			}
			printk("    sum : size = %li, used = %i, free = %li, unused = %li\n", PAGE_SIZE - HEADER_SIZE, used - free, (long) free, PAGE_SIZE - HEADER_SIZE - used);
			hdr = hdr->next;
		}
		printk("    total: blocks = %i, used = %i, recomended block allocation = %li\n",
		       total_blocks, total_used,
		       (long) (PAGE_SIZE / (total_used / total_blocks)));
	}
	snd_mutex_up_static(memory);
}
#endif

/*
 *  DMA allocation routines
 */

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

	if (card == NULL || dma == NULL)
		return -EINVAL;
	if (dma->type == SND_DMA_TYPE_HARDWARE)
		return -EINVAL;
	snd_mutex_down(dma, mutex);
	if (!soft) {
		if (dma->owner) {
			if (dma->mmaped) {
				dma->mmap_free = 0;
				snd_mutex_up(dma, mutex);
				return 0;
			} else {
				snd_mutex_up(dma, mutex);
				return -EBUSY;
			}
		}
		dma->mmap_free = 0;
	} else {
		if (dma->soft_owner) {
			snd_mutex_up(dma, mutex);
			return -EBUSY;
		}
	}
	if (dma->buf) {		/* allocated */
		if (!soft) {
			snd_mutex_down(dma, lock);	/* this waits until DMA isn't free and locks it again */
			dma->owner = owner;
		} else {
			dma->soft_owner = owner;
		}
		snd_mutex_up(dma, mutex);
		return 0;	/* ok... dma is allocated */
	}
	buf = NULL;
	size = dma->rsize;
	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) {
		dma->size = (1 << pg) * PAGE_SIZE;
		dma->buf = buf;
		if (!soft) {
			snd_mutex_down(dma, lock);	/* lock this dma channel */
			dma->owner = owner;
		} else {
			dma->soft_owner = owner;
		}
	}
	snd_mutex_up(dma, mutex);
	return buf == NULL ? -ENOMEM : 0;
}

void snd_dma_free(snd_card_t * card, snd_dma_t * dma, int soft)
{
	unsigned long flags;

	if (card == NULL || dma == NULL)
		return;
	if (dma->type == SND_DMA_TYPE_HARDWARE)
		return;
#if 0
	snd_printk("snd_dma_free: dmanum = %i, soft = %i, mmaped = %i\n", dmanum, soft, dma->mmaped);
#endif
	snd_mutex_down(dma, mutex);
	if (!soft || soft == -1) {
		if (dma->mmaped) {	/* wait */
			dma->mmap_free = 1;
			snd_mutex_up(dma, mutex);
			return;
		}
		dma->mmap_free = 0;
		dma->owner = NULL;
		snd_mutex_up(dma, lock);
	} else {
		dma->soft_owner = NULL;
	}
	if (dma->buf && !dma->owner && !dma->soft_owner) {
		/* free DMA buffer */
		if (dma->type == SND_DMA_TYPE_ISA) {
			snd_cli(&flags);
			disable_dma(dma->dma);
			clear_dma_ff(dma->dma);
			snd_sti(&flags);
		}
		snd_free_pages(dma->buf, dma->size);
		dma->buf = NULL;
		dma->size = 0;
	}
	snd_mutex_up(dma, mutex);
}

int snd_dma_soft_grab(snd_card_t * card, snd_dma_t * dma)
{
	return -EBUSY;
}

void snd_dma_soft_release(snd_card_t * card, snd_dma_t * dma)
{
}

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

	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) {
			if (dma->mmaped && dma->vma == area) {	/* yep! */
				snd_mutex_down(dma, mutex);
				dma->vma = NULL;
				dma->mmaped = 0;
				if (dma->mmap_free) {
					snd_mutex_up(dma, mutex);
					snd_dma_free(card, dma, -1);
				} else {
					snd_mutex_up(dma, mutex);
				}
			}
		}
	}
}
