/*
 *  ALSA sequencer Memory Manager
 *  Copyright (c) 1998 by Frank van de Pol <frank@vande-pol.demon.nl>
 *                        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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
#include "../../include/driver.h"

#include "../../include/seq_kernel.h"
#include "seq_memory.h"
#include "seq_queue.h"
#include "seq_info.h"

/* define to keep statistics on successful allocations (for debugging) */
#define TRACK_SUCCESS

/* counter for tracking memory leaks for 'external' data */
static int ext_alloc = 0;
static int ext_alloc_max = 0;
static unsigned long ext_alloc_bytes = 0;
static unsigned long ext_alloc_max_bytes = 0;
static unsigned long ext_alloc_biggest_alloc = 0;

/* keep track of failures */
static int ext_alloc_failures = 0;

#ifdef TRACK_SUCCESS
/* keep track of successfull allocations */
static int ext_alloc_success = 0;
#endif


/* release this cell, free extended data if available */
void snd_seq_cell_free(snd_seq_event_cell_t * cell)
{
	unsigned long flags;
	void *ptr;
	unsigned int len;
	pool_t *pool;

	if (cell == NULL)
		return;
	pool = cell->pool;
	if (cell->pool == NULL) {
		snd_printd("oops: snd_seq_cell called with pool == NULL?\n");
		return;
	}
	spin_lock_irqsave(&pool->lock, flags);
	if (pool->free != NULL) {
		/* normal situation */
		cell->ptr_l = pool->free;	/* chain in old element */
		cell->ptr_r = NULL;
		pool->free = cell;
	} else {
		/* first element */
		pool->free = cell;
		cell->ptr_l = NULL;
		cell->ptr_r = NULL;
	}
	pool->available++;
	if ((cell->event.flags & SND_SEQ_EVENT_LENGTH_MASK) == SND_SEQ_EVENT_LENGTH_VARIABLE) {
		ptr = cell->event.data.ext.ptr;
		len = cell->event.data.ext.len;
	} else {
		ptr = NULL;
		len = 0;
	}
	cell->event.flags = 0;	/* for sure */
	spin_unlock_irqrestore(&pool->lock, flags);
	if (ptr && len > 0)
		snd_seq_ext_free(ptr, len);
}


/* return pointer to cell. NULL on failure */
snd_seq_event_cell_t *snd_seq_cell_alloc(pool_t *pool)
{
	snd_seq_event_cell_t *cell;
	unsigned long flags;

	if (pool == NULL)
		return NULL;
	spin_lock_irqsave(&pool->lock, flags);
	if (pool->ptr == NULL) {	/* not initialized */
		pool->event_alloc_nopool++;
		spin_unlock_irqrestore(&pool->lock, flags);
		return NULL;
	}
	if (pool->free != NULL) {
		cell = pool->free;
		pool->free = cell->ptr_l;
		pool->available--;
		if (pool->available < pool->min_available)
			pool->min_available = pool->available;

		/* clear cell pointers */
		cell->ptr_l = NULL;
		cell->ptr_r = NULL;
	} else {
		/* no element available... */
		/*snd_printd("Seq: cell_alloc failed: no cells available\n");*/
		cell = NULL;
	}
	if (cell == NULL)
		pool->event_alloc_failures++;
	else
		pool->event_alloc_success++;
	spin_unlock_irqrestore(&pool->lock, flags);

	return cell;
}


/* duplicate event, NULL on failure */
extern snd_seq_event_cell_t *snd_seq_event_dup(pool_t *pool, snd_seq_event_t * event, int atomic)
{
	snd_seq_event_cell_t *new_cell;
	char *ptr;

	/* FIXME: VARIPC not accepted yet.. */
	if (event == NULL ||
	    (event->flags & SND_SEQ_EVENT_LENGTH_MASK) == SND_SEQ_EVENT_LENGTH_VARIPC)
		return NULL;

	new_cell = snd_seq_cell_alloc(pool);
	if (new_cell == NULL)
		return NULL;

	memcpy(&new_cell->event, event, sizeof(snd_seq_event_t));
	switch (event->flags & SND_SEQ_EVENT_LENGTH_MASK) {
	case SND_SEQ_EVENT_LENGTH_VARIABLE:
		/* duplicate data */
		ptr = snd_seq_ext_malloc(event->data.ext.len, atomic);
		new_cell->event.data.ext.ptr = ptr;
		if (!ptr) {
			snd_seq_cell_free(new_cell);
			return NULL;
		}
		memcpy(ptr, event->data.ext.ptr, event->data.ext.len);
		break;
	case SND_SEQ_EVENT_LENGTH_VARUSR:
		/* convert type to LENGTH_VARIABLE */
		ptr = snd_seq_ext_malloc(event->data.ext.len, atomic);
		new_cell->event.flags &= ~SND_SEQ_EVENT_LENGTH_MASK;
		new_cell->event.flags |= SND_SEQ_EVENT_LENGTH_VARIABLE;
		/* copy data to kernel space */
		new_cell->event.data.ext.ptr = ptr;
		if (!ptr) {
			snd_seq_cell_free(new_cell);
			return NULL;
		}
		if (copy_from_user(ptr, event->data.ext.ptr, event->data.ext.len)) {
			snd_seq_cell_free(new_cell);
			return NULL;
		}
		break;
	}
	return new_cell;
}


/* duplicate event cell, NULL on failure */
extern snd_seq_event_cell_t *snd_seq_cell_dup(pool_t *pool, snd_seq_event_cell_t * cell, int atomic)
{
	return snd_seq_event_dup(pool, &cell->event, atomic);
}


/* return number of unused (free) cells */
int snd_seq_unused_cells(pool_t *pool)
{
	if (pool == NULL)
		return 0;
	return (pool->available);
}


/* return total number of allocated cells */
int snd_seq_total_cells(pool_t *pool)
{
	if (pool == NULL)
		return 0;
	return (pool->total_elements);
}


/* allocate room specified number of events */
int snd_seq_pool_init(pool_t *pool, int events)
{
	int cell;
	snd_seq_event_cell_t *ptr, *cellptr;
	unsigned long flags;

	if (pool == NULL)
		return -EINVAL;
	if (pool->ptr)			/* should be atomic? */
		return 0;
	/* alloc memory */
	ptr = snd_kcalloc(sizeof(snd_seq_event_cell_t) * events, GFP_KERNEL);
	if (ptr) {

		/* add the new cell's to the free cell list by calling the free()
		   function for each one */
		for (cell = 0; cell < events; cell++) {
			cellptr = &ptr[cell];
			cellptr->pool = pool;
			snd_seq_cell_free(cellptr);
		}
		/* init statistics */
		spin_lock_irqsave(&pool->lock, flags);
		if (!pool->min_available)
			pool->min_available = pool->available;
		pool->total_elements = events;
		pool->ptr = ptr;
		spin_unlock_irqrestore(&pool->lock, flags);
	} else {
		snd_printd("Seq: malloc for sequencer events failed\n");
		return -ENOMEM;
	}
	return 0;
}

/* remove events */
int snd_seq_pool_done(pool_t *pool)
{
	unsigned long flags;
	snd_seq_event_cell_t *ptr;	
	unsigned int total_elements;

	if (pool == NULL)
		return -EINVAL;
	spin_lock_irqsave(&pool->lock, flags);
	if (pool->available != pool->total_elements) {
		spin_unlock_irqrestore(&pool->lock, flags);
		snd_printd("seq: snd_seq_pool_done() - pool isn't free!!\n");
		return -EBUSY;
	}
	ptr = pool->ptr;
	total_elements = pool->total_elements;
	pool->ptr = NULL;
	pool->free = NULL;
	pool->total_elements = 0;
	pool->available = 0;
	spin_unlock_irqrestore(&pool->lock, flags);
	if (!ptr)
		return 0;
	snd_kfree(ptr);
	return 0;
}

/* init new memory pool */
pool_t *snd_seq_pool_new(void)
{
	pool_t *pool;

	/* create pool block */
	pool = snd_kcalloc(sizeof(pool_t), GFP_KERNEL);
	if (!pool) {
		snd_printd("seq: malloc failed for pool\n");
		return NULL;
	}
	pool->lock = SPIN_LOCK_UNLOCKED;
	pool->ptr = NULL;
	pool->free = NULL;
	pool->total_elements = 0;
	pool->available = 0;
	
	/* init statistics */
	pool->min_available = pool->available;
	return pool;
}

/* remove memory pool */
int snd_seq_pool_delete(pool_t **ppool)
{
	pool_t *pool = *ppool;

	*ppool = NULL;
	if (pool == NULL)
		return 0;
	if (snd_seq_pool_done(pool) == -EBUSY) {
		pool->available = pool->total_elements;
		snd_seq_pool_done(pool);
	}
	snd_kfree(pool);
	return 0;
}

/* initialize sequencer memory */
void snd_sequencer_memory_init(void)
{
}

/* release sequencer memory */
void snd_sequencer_memory_done(void)
{
	if (ext_alloc > 0) {
		snd_printd("seq: memory leak alert, still %d blocks of external data allocated\n", ext_alloc);
	}
}


/* wrapper for allocating and freeing 'external' data (eg. sysex, meta
   events etc.) for now it is just passed to the kmalloc and kfree
   calls, but in a later stadium a different allocation method could be
   used. */

void *snd_seq_ext_malloc(unsigned long size, int atomic)
{
	void *obj;

	obj = snd_kmalloc(size, atomic ? GFP_ATOMIC : GFP_KERNEL);
	if (obj) {
		ext_alloc++;
		ext_alloc_bytes += size;
		if (ext_alloc > ext_alloc_max)
			ext_alloc_max = ext_alloc;
		if (ext_alloc_bytes > ext_alloc_max_bytes)
			ext_alloc_max_bytes = ext_alloc_bytes;
		if (size > ext_alloc_biggest_alloc)
			ext_alloc_biggest_alloc = size;
#ifdef TRACK_SUCCESS
		ext_alloc_success++;
#endif
	} else {
		ext_alloc_failures++;
	}

	return obj;
}

void snd_seq_ext_free(void *obj, unsigned long size)
{
	ext_alloc--;
	ext_alloc_bytes -= size;

	if (ext_alloc < 0) {
		snd_printk("seq: whoops, more ext data free()s than malloc()...\n");
	}
	snd_kfree(obj);
}


/* exported to seq_clientmgr.c */
void snd_seq_info_pool(snd_info_buffer_t * buffer, pool_t *pool, char *space)
{
	if (!pool)
		return;
	snd_iprintf(buffer, "%sPool size          : %d\n", space, snd_seq_total_cells(pool));
	snd_iprintf(buffer, "%sAvailable cells    : %d\n", space, snd_seq_unused_cells(pool));
	snd_iprintf(buffer, "%sCells in use       : %d\n", space, snd_seq_total_cells(pool) - snd_seq_unused_cells(pool));
	snd_iprintf(buffer, "%sPeak cells in use  : %d\n", space, snd_seq_total_cells(pool) - pool->min_available);
	snd_iprintf(buffer, "%sAlloc success      : %d\n", space, pool->event_alloc_success);
	snd_iprintf(buffer, "%sAlloc failures     : %d\n", space, pool->event_alloc_failures);
	snd_iprintf(buffer, "%sAlloc no-pool      : %d\n", space, pool->event_alloc_nopool);
}

/* exported to seq_info.c */
void snd_seq_info_memory_read(snd_info_buffer_t * buffer, void *private_data)
{
	/*
	int c;
	queue_t *q;

	for (c = 0; c < SND_SEQ_MAX_QUEUES; c++) {
		q = queueptr(c);
		if (!q)
			continue;
		snd_iprintf(buffer, "Event pool %i\n", c);
		snd_seq_info_pool(buffer, q->pool, "  ");
		snd_iprintf(buffer, "\n");
	}
	*/
	snd_iprintf(buffer, "External data\n");
	snd_iprintf(buffer, "  Blocks in use      : %d\n", ext_alloc);
	snd_iprintf(buffer, "  Bytes in use       : %d\n", ext_alloc_bytes);
	snd_iprintf(buffer, "  Peak blocks in use : %d\n", ext_alloc_max);
	snd_iprintf(buffer, "  Peak bytes in use  : %d\n", ext_alloc_max_bytes);
	snd_iprintf(buffer, "  Largest allocation : %d\n", ext_alloc_biggest_alloc);
#ifdef TRACK_SUCCESS
	snd_iprintf(buffer, "  Alloc success      : %d\n", ext_alloc_success);
#endif
	snd_iprintf(buffer, "  Alloc failures     : %d\n", ext_alloc_failures);
}
