/*
 *  Copyright (C) 2000 Takashi Iwai <tiwai@suse.de>
 *
 *  Memory management routines for control of EMU WaveTable chip
 *
 *   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/emux_mem.h"

MODULE_AUTHOR("Takashi Iwai");

/*
 * create a new memory manager
 */
snd_emux_memhdr_t *
snd_emux_memhdr_new(int memsize)
{
	snd_emux_memhdr_t *hdr;

	hdr = snd_kcalloc(sizeof(*hdr), GFP_KERNEL);
	if (hdr == NULL)
		return NULL;
	hdr->size = memsize;
	init_MUTEX(&hdr->block_mutex);

	return hdr;
}

/*
 * free a memory manager
 */
void snd_emux_memhdr_free(snd_emux_memhdr_t *hdr)
{
	snd_emux_memblk_t *blk, *next;

	snd_debug_check(hdr == NULL,);
	/* release all blocks */
	for (blk = hdr->block; blk; blk = next) {
		next = blk->next;
		snd_kfree(blk);
	}
	snd_kfree(hdr);
}

/*
 * allocate a memory block (without mutex)
 */
snd_emux_memblk_t *
__snd_emux_mem_alloc(snd_emux_memhdr_t *hdr, int size, snd_emux_memblk_t **prevp)
{
	snd_emux_memblk_t *blk, *prev;
	snd_emux_unit_t units, prev_offset;

	snd_debug_check(hdr == NULL, NULL);
	snd_debug_check(size <= 0, NULL);

	if (prevp)
		*prevp = NULL;

	/* word alignment */
	units = size;
	if (units & 1)
		units++;
	if (units > hdr->size)
		return NULL;

	/* look for empty block */
	prev = NULL;
	prev_offset = 0;
	for (blk = hdr->block; blk; prev = blk, blk = blk->next) {
		if (blk->offset - prev_offset > units)
			goto __found;
		prev_offset = blk->offset + blk->size;
	}
	if (hdr->size - prev_offset < units)
		return NULL;

__found:
	if (prevp)
		*prevp = prev;

	return __snd_emux_memblk_new(hdr, units, prev);
}


/*
 * create a new memory block with the given size
 * the block is linked next to prev
 */
snd_emux_memblk_t *
__snd_emux_memblk_new(snd_emux_memhdr_t *hdr, snd_emux_unit_t units,
		      snd_emux_memblk_t *prev)
{
	snd_emux_memblk_t *blk;

	blk = snd_kmalloc(sizeof(snd_emux_memblk_t) + hdr->block_extra_size, GFP_KERNEL);
	if (blk == NULL)
		return NULL;

	if (prev) {
		blk->next = prev->next;
		prev->next = blk;
		blk->offset = prev->offset + prev->size;
	} else {
		blk->next = hdr->block;
		hdr->block = blk;
		blk->offset = 0;
	}
	blk->size = units;
	hdr->nblocks++;
	hdr->used += units;
	return blk;
}


/*
 * allocate a memory block (with mutex)
 */
snd_emux_memblk_t *
snd_emux_mem_alloc(snd_emux_memhdr_t *hdr, int size)
{
	snd_emux_memblk_t *blk;
	down(&hdr->block_mutex);
	blk = __snd_emux_mem_alloc(hdr, size, NULL);
	up(&hdr->block_mutex);
	return blk;
}


/*
 * find the previous block
 * return non-zero if failed.
 */
int
__snd_emux_mem_find_prev(snd_emux_memhdr_t *hdr, snd_emux_memblk_t *blk,
			 snd_emux_memblk_t **prevp)
{
	snd_emux_memblk_t *prev = NULL, *cur;
	for (cur = hdr->block; cur; prev = cur, cur = cur->next) {
		if (cur == blk) {
			*prevp = prev;
			return 0;
		}
	}
	return 1;
}

/*
 * remove the block from linked-list and free resource
 * (without mutex)
 */
void
__snd_emux_mem_free(snd_emux_memhdr_t *hdr, snd_emux_memblk_t *blk, snd_emux_memblk_t *prev)
{
	if (prev)
		prev->next = blk->next;
	else
		hdr->block = blk->next;
	hdr->nblocks--;
	hdr->used -= blk->size;
	snd_kfree(blk);
}

/*
 * free a memory block (with mutex)
 */
int snd_emux_mem_free(snd_emux_memhdr_t *hdr, snd_emux_memblk_t *blk)
{
	snd_emux_memblk_t *prev;

	snd_debug_check(hdr == NULL, -EINVAL);
	snd_debug_check(blk == NULL, -EINVAL);

	down(&hdr->block_mutex);
	if (__snd_emux_mem_find_prev(hdr, blk, &prev)) {
		up(&hdr->block_mutex);
		snd_printk("emux: invalid block found (offset=0x%x, size=0x%x)\n", blk->offset, blk->size);
		return -EINVAL;
	}
	__snd_emux_mem_free(hdr, blk, prev);
	up(&hdr->block_mutex);
	return 0;
}

/*
 * return available memory size
 */
int snd_emux_mem_avail(snd_emux_memhdr_t *hdr)
{
	unsigned int size;
	down(&hdr->block_mutex);
	size = hdr->size - hdr->used;
	up(&hdr->block_mutex);
	return size;
}


EXPORT_SYMBOL(snd_emux_memhdr_new);
EXPORT_SYMBOL(snd_emux_memhdr_free);
EXPORT_SYMBOL(snd_emux_mem_alloc);
EXPORT_SYMBOL(snd_emux_mem_free);
EXPORT_SYMBOL(snd_emux_mem_avail);
EXPORT_SYMBOL(__snd_emux_mem_alloc);
EXPORT_SYMBOL(__snd_emux_mem_find_prev);
EXPORT_SYMBOL(__snd_emux_mem_free);
EXPORT_SYMBOL(__snd_emux_memblk_new);

/*
 *  INIT part
 */

static int __init alsa_emux_mem_init(void)
{
	return 0;
}

static void __exit alsa_emux_mem_exit(void)
{
}

module_init(alsa_emux_mem_init)
module_exit(alsa_emux_mem_exit)
