/*
zone.c - zone memory allocation from DarkPlaces
Copyright (C) 2007 Uncle Mike

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 3 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.
*/

#include "common.h"

#define MEMCLUMPSIZE	(65536 - 1536)	// give malloc padding so we can't waste most of a page at the end
#define MEMUNIT		8		// smallest unit we care about is this many bytes
#define MEMBITS		(MEMCLUMPSIZE / MEMUNIT)
#define MEMBITINTS		(MEMBITS / 32)

#define MEMCLUMP_SENTINEL	0xABADCAFE
#define MEMHEADER_SENTINEL1	0xDEADF00D
#define MEMHEADER_SENTINEL2	0xDF

typedef struct memheader_s
{
	struct memheader_s	*next;		// next and previous memheaders in chain belonging to pool
	struct memheader_s	*prev;
	struct mempool_s	*pool;		// pool this memheader belongs to
	struct memclump_s	*clump;		// clump this memheader lives in, NULL if not in a clump
	size_t		size;		// size of the memory after the header (excluding header and sentinel2)
	const char	*filename;	// file name and line where Mem_Alloc was called
	uint		fileline;
	uint		sentinel1;	// should always be MEMHEADER_SENTINEL1

	// immediately followed by data, which is followed by a MEMHEADER_SENTINEL2 byte
} memheader_t;

typedef struct memclump_s
{
	byte		block[MEMCLUMPSIZE];// contents of the clump
	uint		sentinel1;	// should always be MEMCLUMP_SENTINEL
	int		bits[MEMBITINTS];	// if a bit is on, it means that the MEMUNIT bytes it represents are allocated, otherwise free
	uint		sentinel2;	// should always be MEMCLUMP_SENTINEL
	size_t		blocksinuse;	// if this drops to 0, the clump is freed
	size_t		largestavailable;	// largest block of memory available
	struct memclump_s	*chain;		// next clump in the chain
} memclump_t;

typedef struct mempool_s
{
	uint		sentinel1;	// should always be MEMHEADER_SENTINEL1
	struct memheader_s	*chain;		// chain of individual memory allocations
	struct memclump_s	*clumpchain;	// chain of clumps (if any)
	size_t		totalsize;	// total memory allocated in this pool (inside memheaders)
	size_t		realsize;		// total memory allocated in this pool (actual malloc total)
	size_t		lastchecksize;	// updated each time the pool is displayed by memlist
	struct mempool_s	*next;		// linked into global mempool list
	const char	*filename;	// file name and line where Mem_AllocPool was called
	int		fileline;
	char		name[64];		// name of the pool
	uint		sentinel2;	// should always be MEMHEADER_SENTINEL1
} mempool_t;

mempool_t *poolchain; // critical stuff

void *_Mem_Alloc( byte *poolptr, size_t size, const char *filename, int fileline )
{
	int		i, j, k, needed, endbit, largest;
	memclump_t	*clump, **clumpchainpointer;
	memheader_t	*mem;
	mempool_t		*pool = (mempool_t *)((byte *)poolptr);

	if( size <= 0 ) return NULL;
	if( poolptr == NULL ) Sys_Error( "Mem_Alloc: pool == NULL (alloc at %s:%i)\n", filename, fileline );
	pool->totalsize += size;

	if( size < 4096 )
	{
		// clumping
		needed = ( sizeof( memheader_t ) + size + sizeof( int ) + (MEMUNIT - 1)) / MEMUNIT;
		endbit = MEMBITS - needed;
		for( clumpchainpointer = &pool->clumpchain; *clumpchainpointer; clumpchainpointer = &(*clumpchainpointer)->chain )
		{
			clump = *clumpchainpointer;
			if( clump->sentinel1 != MEMCLUMP_SENTINEL )
				Sys_Error( "Mem_Alloc: trashed clump sentinel 1 (alloc at %s:%d)\n", filename, fileline );
			if( clump->sentinel2 != MEMCLUMP_SENTINEL )
				Sys_Error( "Mem_Alloc: trashed clump sentinel 2 (alloc at %s:%d)\n", filename, fileline );
			if( clump->largestavailable >= needed )
			{
				largest = 0;
				for( i = 0; i < endbit; i++ )
				{
					if( clump->bits[i>>5] & (1U << (i & 31)))
						continue;
					k = i + needed;
					for( j = i; i < k; i++ )
						if( clump->bits[i>>5] & (1U << (i & 31)))
							goto loopcontinue;
					goto choseclump;
loopcontinue:;
					if( largest < j - i )
						largest = j - i;
				}
				// since clump falsely advertised enough space (nothing wrong
				// with that), update largest count to avoid wasting time in
				// later allocations
				clump->largestavailable = largest;
			}
		}

		pool->realsize += sizeof( memclump_t );
		clump = malloc( sizeof( memclump_t ));
		if( clump == NULL ) Sys_Error( "Mem_Alloc: out of memory (alloc at %s:%i)\n", filename, fileline );
		_Q_memset( clump, 0, sizeof( memclump_t ), filename, fileline );
		*clumpchainpointer = clump;
		clump->sentinel1 = MEMCLUMP_SENTINEL;
		clump->sentinel2 = MEMCLUMP_SENTINEL;
		clump->chain = NULL;
		clump->blocksinuse = 0;
		clump->largestavailable = MEMBITS - needed;
		j = 0;
choseclump:
		mem = (memheader_t *)((byte *)clump->block + j * MEMUNIT );
		mem->clump = clump;
		clump->blocksinuse += needed;

		for( i = j + needed; j < i; j++ )
			clump->bits[j >> 5] |= (1U << (j & 31));
	}
	else
	{
		// big allocations are not clumped
		pool->realsize += sizeof( memheader_t ) + size + sizeof( int );
		mem = (memheader_t *)malloc( sizeof( memheader_t ) + size + sizeof( int ));
		if( mem == NULL ) Sys_Error( "Mem_Alloc: out of memory (alloc at %s:%i)\n", filename, fileline );
		mem->clump = NULL;
	}

	mem->filename = filename;
	mem->fileline = fileline;
	mem->size = size;
	mem->pool = pool;
	mem->sentinel1 = MEMHEADER_SENTINEL1;
	// we have to use only a single byte for this sentinel, because it may not be aligned
	// and some platforms can't use unaligned accesses
	*((byte *)mem + sizeof( memheader_t ) + mem->size ) = MEMHEADER_SENTINEL2;
	// append to head of list
	mem->next = pool->chain;
	mem->prev = NULL;
	pool->chain = mem;
	if( mem->next ) mem->next->prev = mem;
	_Q_memset((void *)((byte *)mem + sizeof( memheader_t )), 0, mem->size, filename, fileline );

	return (void *)((byte *)mem + sizeof( memheader_t ));
}

static const char *Mem_CheckFilename( const char *filename )
{
	static const char	*dummy = "<corrupted>\0";
	const char	*out = filename;
	int		i;

	if( !out ) return dummy;
	for( i = 0; i < 128; i++, out++ )
		if( *out == '\0' ) break; // valid name
	if( i == 128 ) return dummy;
	return filename;
}

static void Mem_FreeBlock( memheader_t *mem, const char *filename, int fileline )
{
	int		i, firstblock, endblock;
	memclump_t	*clump, **clumpchainpointer;
	mempool_t		*pool;

	if( mem->sentinel1 != MEMHEADER_SENTINEL1 )
	{
		mem->filename = Mem_CheckFilename( mem->filename ); // make sure what we don't crash var_args
		Sys_Error( "Mem_Free: trashed header sentinel 1 (alloc at %s:%i, free at %s:%i)\n", mem->filename, mem->fileline, filename, fileline );
	}

	if( *((byte *)mem + sizeof( memheader_t ) + mem->size ) != MEMHEADER_SENTINEL2 )
	{	
		mem->filename = Mem_CheckFilename( mem->filename ); // make sure what we don't crash var_args
		Sys_Error( "Mem_Free: trashed header sentinel 2 (alloc at %s:%i, free at %s:%i)\n", mem->filename, mem->fileline, filename, fileline );
	}

	pool = mem->pool;
	// unlink memheader from doubly linked list
	if(( mem->prev ? mem->prev->next != mem : pool->chain != mem ) || ( mem->next && mem->next->prev != mem ))
		Sys_Error( "Mem_Free: not allocated or double freed (free at %s:%i)\n", filename, fileline );

	if( mem->prev ) mem->prev->next = mem->next;
	else pool->chain = mem->next;

	if( mem->next )
		mem->next->prev = mem->prev;

	// memheader has been unlinked, do the actual free now
	pool->totalsize -= mem->size;

	if(( clump = mem->clump ) != NULL )
	{
		if( clump->sentinel1 != MEMCLUMP_SENTINEL )
			Sys_Error( "Mem_Free: trashed clump sentinel 1 (free at %s:%i)\n", filename, fileline );
		if( clump->sentinel2 != MEMCLUMP_SENTINEL )
			Sys_Error( "Mem_Free: trashed clump sentinel 2 (free at %s:%i)\n", filename, fileline );
		firstblock = ((byte *)mem - (byte *)clump->block );
		if( firstblock & ( MEMUNIT - 1 ))
			Sys_Error( "Mem_Free: address not valid in clump (free at %s:%i)\n", filename, fileline );
		firstblock /= MEMUNIT;
		endblock = firstblock + ((sizeof( memheader_t ) + mem->size + sizeof( int ) + (MEMUNIT - 1)) / MEMUNIT );
		clump->blocksinuse -= endblock - firstblock;

		// could use &, but we know the bit is set
		for( i = firstblock; i < endblock; i++ )
			clump->bits[i >> 5] -= (1U << (i & 31));
		if( clump->blocksinuse <= 0 )
		{
			// unlink from chain
			for( clumpchainpointer = &pool->clumpchain; *clumpchainpointer; clumpchainpointer = &(*clumpchainpointer)->chain )
			{
				if (*clumpchainpointer == clump)
				{
					*clumpchainpointer = clump->chain;
					break;
				}
			}

			pool->realsize -= sizeof( memclump_t );
			_Q_memset( clump, 0xBF, sizeof( memclump_t ), filename, fileline );
			free( clump );
		}
		else
		{
			// clump still has some allocations
			// force re-check of largest available space on next alloc
			clump->largestavailable = MEMBITS - clump->blocksinuse;
		}
	}
	else
	{
		pool->realsize -= sizeof( memheader_t ) + mem->size + sizeof( int );
		free( mem );
	}
}

void _Mem_Free( void *data, const char *filename, int fileline )
{
	if( data == NULL ) Sys_Error( "Mem_Free: data == NULL (called at %s:%i)\n", filename, fileline );
	Mem_FreeBlock((memheader_t *)((byte *)data - sizeof( memheader_t )), filename, fileline );
}

void *_Mem_Realloc( byte *poolptr, void *memptr, size_t size, const char *filename, int fileline )
{
	char		*nb;
	memheader_t	*memhdr = NULL;

	if( size <= 0 ) return memptr; // no need to reallocate

	if( memptr )
	{
		memhdr = (memheader_t *)((byte *)memptr - sizeof( memheader_t ));
		if( size == memhdr->size ) return memptr;
	}

	nb = _Mem_Alloc( poolptr, size, filename, fileline );

	if( memptr ) // first allocate?
	{ 
		size_t	newsize;

		// get size of old block
		newsize = memhdr->size < size ? memhdr->size : size; // upper data can be trucnated!
		_Q_memcpy( nb, memptr, newsize, filename, fileline );
		_Mem_Free( memptr, filename, fileline ); // free unused old block
	}

	return (void *)nb;
}

byte *_Mem_AllocPool( const char *name, const char *filename, int fileline )
{
	mempool_t *pool;

	pool = (mempool_t *)malloc( sizeof( mempool_t ));
	if( pool == NULL ) Sys_Error( "Mem_AllocPool: out of memory (allocpool at %s:%i)\n", filename, fileline );
	_Q_memset( pool, 0, sizeof( mempool_t ), filename, fileline );

	// fill header
	pool->sentinel1 = MEMHEADER_SENTINEL1;
	pool->sentinel2 = MEMHEADER_SENTINEL1;
	pool->filename = filename;
	pool->fileline = fileline;
	pool->chain = NULL;
	pool->totalsize = 0;
	pool->realsize = sizeof( mempool_t );
	Q_strncpy( pool->name, name, sizeof( pool->name ));
	pool->next = poolchain;
	poolchain = pool;

	return (byte *)((mempool_t *)pool);
}

void _Mem_FreePool( byte **poolptr, const char *filename, int fileline )
{
	mempool_t	*pool = (mempool_t *)((byte *)*poolptr );
	mempool_t	**chainaddress;

	if( pool )
	{
		// unlink pool from chain
		for( chainaddress = &poolchain; *chainaddress && *chainaddress != pool; chainaddress = &((*chainaddress)->next));
		if( *chainaddress != pool) Sys_Error("Mem_FreePool: pool already free (freepool at %s:%i)\n", filename, fileline );
		if( pool->sentinel1 != MEMHEADER_SENTINEL1 ) Sys_Error("Mem_FreePool: trashed pool sentinel 1 (allocpool at %s:%i, freepool at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );
		if( pool->sentinel2 != MEMHEADER_SENTINEL1 ) Sys_Error("Mem_FreePool: trashed pool sentinel 2 (allocpool at %s:%i, freepool at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );
		*chainaddress = pool->next;

		// free memory owned by the pool
		while( pool->chain ) Mem_FreeBlock( pool->chain, filename, fileline );
		// free the pool itself
		_Q_memset( pool, 0xBF, sizeof( mempool_t ), filename, fileline );
		free( pool );
		*poolptr = NULL;
	}
}

void _Mem_EmptyPool( byte *poolptr, const char *filename, int fileline )
{
	mempool_t *pool = (mempool_t *)((byte *)poolptr);
	if( poolptr == NULL ) Sys_Error( "Mem_EmptyPool: pool == NULL (emptypool at %s:%i)\n", filename, fileline );

	if( pool->sentinel1 != MEMHEADER_SENTINEL1 ) Sys_Error( "Mem_EmptyPool: trashed pool sentinel 1 (allocpool at %s:%i, emptypool at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );
	if( pool->sentinel2 != MEMHEADER_SENTINEL1 ) Sys_Error( "Mem_EmptyPool: trashed pool sentinel 2 (allocpool at %s:%i, emptypool at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );

	// free memory owned by the pool
	while( pool->chain ) Mem_FreeBlock( pool->chain, filename, fileline );
}

qboolean Mem_CheckAlloc( mempool_t *pool, void *data )
{
	memheader_t *header, *target;

	if( pool )
	{
		// search only one pool
		target = (memheader_t *)((byte *)data - sizeof( memheader_t ));
		for( header = pool->chain; header; header = header->next )
			if( header == target ) return true;
	}
	else
	{
		// search all pools
		for( pool = poolchain; pool; pool = pool->next )
			if( Mem_CheckAlloc( pool, data ))
				return true;
	}
	return false;
}

/*
========================
Check pointer for memory
========================
*/
qboolean Mem_IsAllocatedExt( byte *poolptr, void *data )
{
	mempool_t	*pool = NULL;
	if( poolptr ) pool = (mempool_t *)((byte *)poolptr);

	return Mem_CheckAlloc( pool, data );
}

void Mem_CheckHeaderSentinels( void *data, const char *filename, int fileline )
{
	if (!data)
		Sys_Error( "Mem_CheckSentinels: data == NULL (sentinel check at %s:%i)\n", filename, fileline );
	else
	{
		memheader_t *mem = (memheader_t *)((byte *) data - sizeof(memheader_t));

		if( mem->sentinel1 != MEMHEADER_SENTINEL1 )
		{
			mem->filename = Mem_CheckFilename( mem->filename ); // make sure what we don't crash var_args
			Sys_Error( "Mem_CheckSentinels: trashed header sentinel 1 (block allocated at %s:%i, sentinel check at %s:%i)\n", mem->filename, mem->fileline, filename, fileline );
		}

		if( *((byte *) mem + sizeof(memheader_t) + mem->size) != MEMHEADER_SENTINEL2 )
		{
			mem->filename = Mem_CheckFilename( mem->filename ); // make sure what we don't crash var_args
			Sys_Error( "Mem_CheckSentinels: trashed header sentinel 2 (block allocated at %s:%i, sentinel check at %s:%i)\n", mem->filename, mem->fileline, filename, fileline );
		}
	}
}

static void Mem_CheckClumpSentinels( memclump_t *clump, const char *filename, int fileline )
{
	// this isn't really very useful
	if( clump->sentinel1 != MEMCLUMP_SENTINEL )
		Sys_Error( "Mem_CheckClumpSentinels: trashed sentinel 1 (sentinel check at %s:%i)\n", filename, fileline );
	if( clump->sentinel2 != MEMCLUMP_SENTINEL )
		Sys_Error( "Mem_CheckClumpSentinels: trashed sentinel 2 (sentinel check at %s:%i)\n", filename, fileline );
}

void _Mem_Check( const char *filename, int fileline )
{
	memheader_t	*mem;
	mempool_t		*pool;
	memclump_t	*clump;

	for( pool = poolchain; pool; pool = pool->next )
	{
		if( pool->sentinel1 != MEMHEADER_SENTINEL1 )
			Sys_Error( "Mem_CheckSentinelsGlobal: trashed pool sentinel 1 (allocpool at %s:%i, sentinel check at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );
		if( pool->sentinel2 != MEMHEADER_SENTINEL1 )
			Sys_Error( "Mem_CheckSentinelsGlobal: trashed pool sentinel 2 (allocpool at %s:%i, sentinel check at %s:%i)\n", pool->filename, pool->fileline, filename, fileline );
	}

	for( pool = poolchain; pool; pool = pool->next )
		for( mem = pool->chain; mem; mem = mem->next )
			Mem_CheckHeaderSentinels((void *)((byte *) mem + sizeof(memheader_t)), filename, fileline );

	for( pool = poolchain; pool; pool = pool->next )
		for( clump = pool->clumpchain; clump; clump = clump->chain )
			Mem_CheckClumpSentinels( clump, filename, fileline );
}

void Mem_PrintStats( void )
{
	size_t	count = 0, size = 0, realsize = 0;
	mempool_t	*pool;

	Mem_Check();
	for( pool = poolchain; pool; pool = pool->next )
	{
		count++;
		size += pool->totalsize;
		realsize += pool->realsize;
	}

	Msg( "^3%lu^7 memory pools, totalling: ^1%s\n", (long unsigned int)count, Q_memprint( size ));
	Msg( "Total allocated size: ^1%s\n", Q_memprint( realsize ));
}

void Mem_PrintList( size_t minallocationsize )
{
	mempool_t		*pool;
	memheader_t	*mem;

	Mem_Check();

	Msg( "memory pool list:\n""  ^3size                    name\n");
	for( pool = poolchain; pool; pool = pool->next )
	{
		// poolnames can contain color symbols, make sure what color is reset
		if( (pool->totalsize - pool->lastchecksize ) != 0 )
			Msg( "%5luk (%5luk actual) %s (^7%+3li byte change)\n",
				 (long unsigned int)((pool->totalsize + 1023) / 1024),
				 (long unsigned int)((pool->realsize + 1023) / 1024),
				 pool->name,
				 pool->totalsize - pool->lastchecksize );
		else Msg( "%5luk (%5luk actual) %s\n", (long unsigned int)((pool->totalsize + 1023) / 1024), (long unsigned int)((pool->realsize + 1023) / 1024), pool->name );
		pool->lastchecksize = pool->totalsize;
		for( mem = pool->chain; mem; mem = mem->next )
			if( mem->size >= minallocationsize )
				Msg( "%10lu bytes allocated at %s:%i\n", (long unsigned int)mem->size, mem->filename, mem->fileline );
	}
}
