/* ``The contents of this file are subject to the Erlang Public License,
 * Version 1.1, (the "License"); you may not use this file except in
 * compliance with the License. You should have received a copy of the
 * Erlang Public License along with this software. If not, it can be
 * retrieved via the world wide web at http://www.erlang.org/.
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Initial Developer of the Original Code is Ericsson Utvecklings AB.
 * Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
 * AB. All Rights Reserved.''
 * 
 *     $Id$
 */


/*
 * Description:	
 *
 * Author: 	Rickard Green
 */

/* Headers to include ... */

#ifdef HAVE_CONFIG_H
#	include "config.h"
#endif

#include "erl_memory_trace_block_table.h"
#include <errno.h>

#undef HARD_DEBUG
#undef REALLY_HARD_DEBUG
#ifdef DEBUG
#  define HARD_DEBUG 0
#  define REALLY_HARD_DEBUG 0
#else
#  define HARD_DEBUG 0
#  define REALLY_HARD_DEBUG 0
#endif

/* Some system specific defines ... */
#if defined(__WIN32__) && !defined(__GNUC__)
#	define INLINE __forceinline
#else
#	ifdef __GNUC__
#		define INLINE __inline__
#	else
#		define INLINE
#	endif
#endif

/* Our own assert() ... */
#ifdef DEBUG
#define ASSERT(A) ((void) ((A) ? 1 : assert_failed(__FILE__, __LINE__, #A)))
#include <stdio.h>
static int assert_failed(char *f, int l, char *a)
{
    fprintf(stderr, "%s:%d: Assertion failed: %s\n", f, l, a);
    abort();
    return 0;
}

#else
#define ASSERT(A) ((void) 1)
#endif


#define EMTBT_BLOCKS_PER_POOL 1000

typedef struct emtbt_block_pool_ {
    struct emtbt_block_pool_ *next;
    emtbt_block blocks[1];
} emtbt_block_pool;

struct emtbt_table_ {
    void * (*alloc)(size_t);
    void * (*realloc)(void *, size_t);
    void   (*free)(void *);
    int is_64_bit;
    int no_blocks;
    int no_of_buckets;
    int max_used_buckets;
    int min_used_buckets;
    int used_buckets;
    int current_size_index;
    emtbt_block *blocks;
    emtbt_block ** buckets;


    /* Fixed size allocation of blocks */
    emtbt_block_pool *block_pools;
    emtbt_block *free_blocks;
    int blocks_per_pool;

};


static emtbt_block null_blk = {0};

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\
 * Block table                                                             *
 *                                                                         *
\*                                                                         */

#if HARD_DEBUG
static void check_table(emtbt_table *table);
#endif

static emtbt_block *
block_alloc_new_pool(emtbt_table *tab)
{
    size_t size;
    emtbt_block_pool *poolp;

    size = sizeof(emtbt_block_pool) - sizeof(emtbt_block);
    size += tab->blocks_per_pool*sizeof(emtbt_block);

    poolp = (*tab->alloc)(size);

    if (poolp) {
	int i;
	emtbt_block *blks;

	poolp->next = tab->block_pools;
	tab->block_pools = poolp;

	blks = (emtbt_block *) poolp->blocks;

	for (i = 1; i < tab->blocks_per_pool - 1; i++)
	    blks[i].next = &blks[i + 1];
	blks[tab->blocks_per_pool - 1].next = NULL;
	tab->free_blocks = &blks[1];

	return &blks[0];
    }
    return NULL;
}

static INLINE emtbt_block *
block_alloc(emtbt_table *tab)
{
    emtbt_block *res;
#if HARD_DEBUG
    check_table(tab);
#endif

    if (tab->free_blocks) {
	res = tab->free_blocks;
	tab->free_blocks = tab->free_blocks->next;
    }
    else {
	res = block_alloc_new_pool(tab);
    }

#ifdef DEBUG
    res->next = ((emtbt_block *) 0xfffffff0);
    res->prev = ((emtbt_block *) 0xfffffff0);
    res->bucket = ((emtbt_block **) 0xfffffff0);
#endif

#if HARD_DEBUG
    check_table(tab);
#endif

    return res;
}

static INLINE void
block_free(emtbt_table *tab, emtbt_block *bp)
{

#if HARD_DEBUG
    check_table(tab);
#endif

    bp->next = tab->free_blocks;
    tab->free_blocks = bp;

#if HARD_DEBUG
    check_table(tab);
#endif


}

#define PRIME0 ((usgnd_int_32) 268438039)
#define PRIME1 ((usgnd_int_32) 268440479)
#define PRIME2 ((usgnd_int_32) 268439161)
#define PRIME3 ((usgnd_int_32) 268437017)

#define MK_HASH(H, P, IS64)						\
do {									\
    (H) = (P) & 0xff;							\
    (H) *= PRIME0;							\
    (H) += ((P) >> 8) & 0xff;						\
    (H) *= PRIME1;							\
    (H) += ((P) >> 16) & 0xff;						\
    (H) *= PRIME2;							\
    (H) += ((P) >> 24) & 0xff;						\
    (H) *= PRIME3;							\
    if ((IS64)) {							\
	(H) += ((P) >> 32) & 0xff;					\
	(H) *= PRIME0;							\
	(H) += ((P) >> 40) & 0xff;					\
	(H) *= PRIME1;							\
	(H) += ((P) >> 48) & 0xff;					\
	(H) *= PRIME2;							\
	(H) += ((P) >> 56) & 0xff;					\
	(H) *= PRIME3;							\
    }									\
} while (0)

static const int table_sizes[] = {
    3203,
    4813,
    6421,
    9643,
    12853,
    19289,
    25717,
    51437,
    102877,
    205759,
    411527,
    823117,
    1646237,
    3292489,
    6584983,
    13169977,
    26339969,
    52679969
};

#if HARD_DEBUG

static void
check_table(emtbt_table *table)
{
    int no_blocks;
    emtbt_block *block, *prev_block;

    no_blocks = 0;
    block = table->blocks;
    ASSERT(!block || !block->prev);
    prev_block = NULL;
    while (block) {
	usgnd_int_32 hash;
	MK_HASH(hash, block->pointer, table->is_64_bit);
	ASSERT(hash == block->hash);
	ASSERT(block->bucket - table->buckets
	       == hash % table->no_of_buckets);
	ASSERT(!prev_block || prev_block == block->prev);
	prev_block = block;
	block = block->next;
	no_blocks++;
	ASSERT(table->no_blocks >= no_blocks);
    }

    ASSERT(table->no_blocks == no_blocks);

#if REALLY_HARD_DEBUG
    {
	int i;
	for (i = 0; i < table->no_of_buckets; i++) {
	    int bucket_end_found;
	    emtbt_block **bucket;
	    if (!table->buckets[i])
		continue;
	    bucket_end_found = 0;
	    bucket = &table->buckets[i];
	    for (block = table->blocks; block; block = block->next) {
		if (block->bucket == bucket) {
		    if (!block->prev || block->prev->bucket != bucket)
			ASSERT(*bucket == block);
		    if (!block->next || block->next->bucket != bucket)
			bucket_end_found++;
		}
	    }
	    ASSERT(bucket_end_found);
	}
    }
#endif

}

#endif

static INLINE void
link_block(emtbt_table *table, emtbt_block **bucket, emtbt_block *block)
{
    ASSERT(bucket);

    block->bucket = bucket;
    if (*bucket) {
	block->next = *bucket;
	block->prev = (*bucket)->prev;
	if (block->prev)
	    block->prev->next = block;
	else
	    table->blocks = block;
	block->next->prev = block;
    }
    else {
	block->next = table->blocks;
	block->prev = NULL;
	if (table->blocks)
	    table->blocks->prev = block;
	table->blocks = block;
	table->used_buckets++;

    }
    *bucket = block;
    table->no_blocks++;

#if HARD_DEBUG
    check_table(table);
#endif

}

static int
resize_table(emtbt_table *table, int new_no_of_buckets)
{
#ifdef DEBUG
    int org_no_blocks;
#endif
    int i;
    emtbt_block *block;
    emtbt_block **buckets;

    if (new_no_of_buckets < table->no_of_buckets) {
	/* shrink never fails */
	buckets = (emtbt_block **) (*table->realloc)(table->buckets,
						     (sizeof(emtbt_block *)
						      * new_no_of_buckets));
	if (!buckets)
	    return 1;
    }
    else if (new_no_of_buckets > table->no_of_buckets) {
	(*table->free)((void *) table->buckets);
	buckets = (emtbt_block **) (*table->alloc)((sizeof(emtbt_block *)
						    * new_no_of_buckets));
	if (!buckets)
	    return 0;
    }
    else
	return 1;

    table->buckets = buckets;
    table->no_of_buckets = new_no_of_buckets;
    table->max_used_buckets = (4*new_no_of_buckets)/5;
    table->min_used_buckets = new_no_of_buckets/5;
    table->used_buckets = 0;

#ifdef DEBUG
    org_no_blocks = table->no_blocks;
#endif

    table->no_blocks = 0;
    

    for (i = 0; i < new_no_of_buckets; i++)
	buckets[i] = NULL;

    block = table->blocks;
    table->blocks = NULL;

    while (block) {
	emtbt_block *next_block = block->next;
	link_block(table,&table->buckets[block->hash%new_no_of_buckets],block);
	block = next_block;
    }

    ASSERT(org_no_blocks == table->no_blocks);

    return 1;
}

static INLINE int
grow_table(emtbt_table *table)
{
    if (table->current_size_index < sizeof(table_sizes)/sizeof(int)) {
	int new_size;
	table->current_size_index++;
	new_size = table_sizes[table->current_size_index];
	ASSERT(new_size > 0);
	return resize_table(table, new_size);
    }
    return 1;
}

static INLINE void
shrink_table(emtbt_table *table)
{
    if (table->current_size_index > 0) {
	int new_size;
	table->current_size_index--;
	new_size = table_sizes[table->current_size_index];
	ASSERT(new_size > 0);
	(void) resize_table(table, new_size);
    }
}

static INLINE emtbt_block *
peek_block(emtbt_table *table, usgnd_int_max ptr)
{
    emtbt_block **bucket;
    emtbt_block *block;
    usgnd_int_32 hash;

    MK_HASH(hash, ptr, table->is_64_bit);

    bucket = &table->buckets[hash % table->no_of_buckets];
    block = *bucket;
    if (!block)
	return NULL;

    while (block->bucket == bucket) {
	ASSERT(block);
	if (block->pointer == ptr)
	    return block;
	if (!block->next)
	    break;
	block = block->next;
    }
    return NULL;
}

static INLINE int
insert_block(emtbt_table *table, emtbt_block *block)
{
    emtbt_block **bucket;
    emtbt_block *tmp_block;
    usgnd_int_32 hash;
    usgnd_int_max p;

#if HARD_DEBUG
    check_table(table);
#endif

    if (table->used_buckets >= table->max_used_buckets) {
	if(!grow_table(table))
	    return -1;
    }

    p = block->pointer;

    MK_HASH(hash, p, table->is_64_bit);
    block->hash = hash;

    bucket = &table->buckets[hash % table->no_of_buckets];
    tmp_block = *bucket;
    if (tmp_block) {
	while (tmp_block->bucket == bucket) {
	    if (tmp_block->pointer == p)
		return 0;
	    if (!tmp_block->next)
		break;
	    tmp_block = tmp_block->next;
	}
    }

    link_block(table, bucket, block);

    ASSERT(block == peek_block(table, p));


    return 1;
}

static INLINE void
delete_block(emtbt_table *table, emtbt_block *block)
{
    emtbt_block **bucket;

    if (!block)
	return;

#if HARD_DEBUG
    check_table(table);
#endif

    bucket = block->bucket;
    ASSERT(bucket);

    if (block->prev)
	block->prev->next = block->next;
    else
	table->blocks = block->next;

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

    if (block == *bucket) {
	ASSERT(!block->prev || block->prev->bucket != bucket);
	if (block->next && block->next->bucket == bucket)
	    *bucket = block->next;
	else {
	    ASSERT(table->used_buckets > 0);
	    *bucket = NULL;
	    table->used_buckets--;
	}
    }
#ifdef DEBUG

    block->next = ((emtbt_block *) 0xfffffff0);
    block->prev = ((emtbt_block *) 0xfffffff0);
    block->bucket = ((emtbt_block **) 0xfffffff0);
#endif

    ASSERT(table->no_blocks > 0);
    table->no_blocks--;

    if (table->used_buckets < table->min_used_buckets)
	shrink_table(table);

#if HARD_DEBUG
    check_table(table);
#endif

}

static INLINE emtbt_block *
fetch_block(emtbt_table *table, usgnd_int_max ptr)
{
    emtbt_block *block;

    block = peek_block(table, ptr);
    delete_block(table, block);
    return block;
}


const char *emtbt_error_string(int error)
{
    switch (error) {
    case EMTBT_ALLOC_XBLK_ERROR:
	return "Allocation to an already existing block";
    case EMTBT_REALLOC_NOBLK_ERROR:
	return "Reallocation of non-existing block";
    case EMTBT_REALLOC_XBLK_ERROR:
	return "Reallocation to an already existing block";
    case EMTBT_REALLOC_BLK_TYPE_MISMATCH:
	return "Block types mismatch when reallocating";
    case EMTBT_FREE_NOBLK_ERROR:
	return "Deallocation of non-existing block";
    case EMTBT_FREE_BLK_TYPE_MISMATCH:
	return "Block types mismatch when deallocating";
    case EMTBT_INTERNAL_ERROR:
	return "Block table internal error";
    default:
	return NULL;
    }


}


emtbt_table *
emtbt_new_table(int is_64_bit,
		void * (*alloc)(size_t),
		void * (*realloc)(void *, size_t),
		void   (*free)(void *))
{
    emtbt_table *tab = (*alloc)(sizeof(emtbt_table));
    if (tab) {
	tab->alloc = alloc;
	tab->realloc = realloc;
	tab->free = free;
	tab->is_64_bit = is_64_bit;
	tab->no_blocks = 0;
	tab->no_of_buckets = 0;
	tab->max_used_buckets = 0;
	tab->min_used_buckets = 0;
	tab->used_buckets = 0;
	tab->current_size_index = 0;
	tab->blocks = NULL;
	tab->buckets = NULL;

	tab->block_pools = NULL;
	tab->free_blocks = NULL;
	tab->blocks_per_pool = EMTBT_BLOCKS_PER_POOL;

    }
    return tab;
}

void
emtbt_destroy_table(emtbt_table *tab)
{
    void (*freep)(void *);
    emtbt_block_pool *poolp1, *poolp2;

    freep = tab->free;

    /* Free block pools */
    poolp1 = tab->block_pools;
    while (poolp1) {
	poolp2 = poolp1;
	poolp1 = poolp1->next;
	(*freep)((void *) poolp2);
    }

    if (tab->buckets)
	(*freep)((void *) tab->buckets);

    (*freep)((void *) tab);
}


#define CP_BLK(TO, FROM)						\
do {									\
    (TO)->time.secs	= (FROM)->time.secs;				\
    (TO)->time.usecs	= (FROM)->time.usecs;				\
    (TO)->type		= (FROM)->type;					\
    (TO)->pointer	= (FROM)->pointer;				\
    (TO)->size		= (FROM)->size;					\
} while (0)

int
emtbt_alloc_op(emtbt_table *tab, emtp_operation *op)
{
    int res;
    emtbt_block *blk;

    blk = block_alloc(tab);
    if (!blk)
	return ENOMEM;
	
    blk->time.secs	= op->time.secs;
    blk->time.usecs	= op->time.usecs;
    blk->type		= op->u.block.type;
    blk->pointer	= op->u.block.new_ptr;
    blk->size		= op->u.block.new_size;

    res = insert_block(tab, blk);
    if (res < 0)
	return ENOMEM;
    else if (res == 0)
	return EMTBT_ALLOC_XBLK_ERROR;
    return 0;
}

int
emtbt_realloc_op(emtbt_table *tab, emtp_operation *op, emtbt_block *old_blk)
{
    int res;
    emtbt_block *blk;

    if (!op->u.block.new_size) {
	/* freed block */

	blk = fetch_block(tab, op->u.block.prev_ptr);
	if (!blk)
	    return EMTBT_REALLOC_NOBLK_ERROR;

	CP_BLK(old_blk, blk);
	block_free(tab, blk);
    }
    else {

	if (!op->u.block.new_ptr) {
	    /* failed operation */
	    if (!op->u.block.prev_ptr)
		CP_BLK(old_blk, &null_blk);
	    else {
		blk = peek_block(tab, op->u.block.prev_ptr);
		if (!blk)
		    return EMTBT_REALLOC_NOBLK_ERROR;
		CP_BLK(old_blk, blk);
#if 0
		if (blk->type != op->u.block.type)
		    return EMTBT_REALLOC_BLK_TYPE_MISMATCH;
#endif
	    }
	}
	else if (!op->u.block.prev_ptr) {
	    /* new block */

	    CP_BLK(old_blk, &null_blk);
	    blk = block_alloc(tab);
	    if (!blk)
		return ENOMEM;
	    blk->type		= op->u.block.type;
	    blk->pointer	= op->u.block.new_ptr;
	    blk->time.secs	= op->time.secs;
	    blk->time.usecs	= op->time.usecs;
	    blk->size		= op->u.block.new_size;

	    res = insert_block(tab, blk);
	    if (res < 0)
		return ENOMEM;
	    else if (res == 0)
		return EMTBT_REALLOC_XBLK_ERROR;
	}
	else if (op->u.block.new_ptr == op->u.block.prev_ptr) {
	    /* resized block */
	    blk = peek_block(tab, op->u.block.prev_ptr);
	    if (!blk)
		return EMTBT_REALLOC_NOBLK_ERROR;
	    CP_BLK(old_blk, blk);
	    blk->time.secs	= op->time.secs;
	    blk->time.usecs	= op->time.usecs;
	    blk->size		= op->u.block.new_size;
#if 0
	    if (blk->type != op->u.block.type)
		return EMTBT_REALLOC_BLK_TYPE_MISMATCH;
#endif
	}
	else {
	    /* moved block */
	    blk = fetch_block(tab, op->u.block.prev_ptr);
	    if (!blk)
		return EMTBT_REALLOC_NOBLK_ERROR;
	    CP_BLK(old_blk, blk);
	    blk->time.secs	= op->time.secs;
	    blk->time.usecs	= op->time.usecs;
	    blk->pointer 	= op->u.block.new_ptr;
	    blk->size		= op->u.block.new_size;
	    res = insert_block(tab, blk);
	    if (res < 0)
		return ENOMEM;
	    else if (res == 0)
		return EMTBT_REALLOC_XBLK_ERROR;
#if 0
	    if (blk->type != op->u.block.type)
		return EMTBT_REALLOC_BLK_TYPE_MISMATCH;
#endif
	}
    }   
    return 0;

}


int
emtbt_free_op(emtbt_table *tab, emtp_operation *op, emtbt_block *old_blk)
{
    emtbt_block *blk;

    if (!op->u.block.prev_ptr)
	CP_BLK(old_blk, &null_blk);
    else {

	blk = fetch_block(tab, op->u.block.prev_ptr);
	if (!blk)
	    return EMTBT_FREE_NOBLK_ERROR;

	CP_BLK(old_blk, blk);
	block_free(tab, blk);
#if 0
	if (blk->type != op->u.block.type)
	    return EMTBT_FREE_BLK_TYPE_MISMATCH;
#endif
    }
    return 0;
}
