/*
 * malloc.c
 *
 * Very simple linked-list based malloc()/free().
 */

#include <syslinux/firmware.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <dprintf.h>
#include <minmax.h>

#include "malloc.h"
#include "thread.h"

DECLARE_INIT_SEMAPHORE(__malloc_semaphore, 1);

static void *__malloc_from_block(struct free_arena_header *fp,
				 size_t size, malloc_tag_t tag)
{
    size_t fsize;
    struct free_arena_header *nfp, *na;
    unsigned int heap = ARENA_HEAP_GET(fp->a.attrs);

    fsize = ARENA_SIZE_GET(fp->a.attrs);

    /* We need the 2* to account for the larger requirements of a free block */
    if ( fsize >= size+2*sizeof(struct arena_header) ) {
        /* Bigger block than required -- split block */
        nfp = (struct free_arena_header *)((char *)fp + size);
        na = fp->a.next;

        ARENA_TYPE_SET(nfp->a.attrs, ARENA_TYPE_FREE);
	ARENA_HEAP_SET(nfp->a.attrs, heap);
        ARENA_SIZE_SET(nfp->a.attrs, fsize-size);
        nfp->a.tag = MALLOC_FREE;
#ifdef DEBUG_MALLOC
	nfp->a.magic = ARENA_MAGIC;
#endif
        ARENA_TYPE_SET(fp->a.attrs, ARENA_TYPE_USED);
        ARENA_SIZE_SET(fp->a.attrs, size);
        fp->a.tag = tag;

        /* Insert into all-block chain */
        nfp->a.prev = fp;
        nfp->a.next = na;
        na->a.prev = nfp;
        fp->a.next = nfp;

        /* Replace current block on free chain */
        nfp->next_free = fp->next_free;
        nfp->prev_free = fp->prev_free;
        fp->next_free->prev_free = nfp;
        fp->prev_free->next_free = nfp;
    } else {
        /* Allocate the whole block */
        ARENA_TYPE_SET(fp->a.attrs, ARENA_TYPE_USED);
        fp->a.tag = tag;

        /* Remove from free chain */
        fp->next_free->prev_free = fp->prev_free;
        fp->prev_free->next_free = fp->next_free;
    }

    return (void *)(&fp->a + 1);
}

void *bios_malloc(size_t size, enum heap heap, malloc_tag_t tag)
{
    struct free_arena_header *fp;
    struct free_arena_header *head = &__core_malloc_head[heap];
    void *p = NULL;

    if (size) {
	/* Add the obligatory arena header, and round up */
	size = (size + 2 * sizeof(struct arena_header) - 1) & ARENA_SIZE_MASK;

	for ( fp = head->next_free ; fp != head ; fp = fp->next_free ) {
	    if ( ARENA_SIZE_GET(fp->a.attrs) >= size ) {
		/* Found fit -- allocate out of this block */
		p = __malloc_from_block(fp, size, tag);
		break;
	    }
        }
    }

    return p;
}

static void *_malloc(size_t size, enum heap heap, malloc_tag_t tag)
{
    void *p;

#ifdef DEBUG_MALLOC
    dprintf("_malloc(%zu, %u, %u) @ %p = ",
	size, heap, tag, __builtin_return_address(0));
#endif

    sem_down(&__malloc_semaphore, 0);
    p = firmware->mem->malloc(size, heap, tag);
    sem_up(&__malloc_semaphore);

#ifdef DEBUG_MALLOC
    dprintf("%p\n", p);
#endif
    return p;
}

__export void *malloc(size_t size)
{
    return _malloc(size, HEAP_MAIN, MALLOC_CORE);
}

__export void *lmalloc(size_t size)
{
    void *p;

    p = _malloc(size, HEAP_LOWMEM, MALLOC_CORE);
    if (!p)
	errno = ENOMEM;
    return p;
}

void *pmapi_lmalloc(size_t size)
{
    return _malloc(size, HEAP_LOWMEM, MALLOC_MODULE);
}

void *bios_realloc(void *ptr, size_t size)
{
    struct free_arena_header *ah, *nah;
    struct free_arena_header *head;

    void *newptr;
    size_t newsize, oldsize, xsize;

    if (!ptr)
	return malloc(size);

    if (size == 0) {
	free(ptr);
	return NULL;
    }

    ah = (struct free_arena_header *)
	((struct arena_header *)ptr - 1);

	head = &__core_malloc_head[ARENA_HEAP_GET(ah->a.attrs)];

#ifdef DEBUG_MALLOC
    if (ah->a.magic != ARENA_MAGIC)
	dprintf("failed realloc() magic check: %p\n", ptr);
#endif

    /* Actual size of the old block */
    //oldsize = ah->a.size;
    oldsize = ARENA_SIZE_GET(ah->a.attrs);

    /* Add the obligatory arena header, and round up */
    newsize = (size + 2 * sizeof(struct arena_header) - 1) & ARENA_SIZE_MASK;

    if (oldsize >= newsize && newsize >= (oldsize >> 2) &&
	oldsize - newsize < 4096) {
	/* This allocation is close enough already. */
	return ptr;
    } else {
	xsize = oldsize;

	nah = ah->a.next;
	if ((char *)nah == (char *)ah + ARENA_SIZE_GET(ah->a.attrs) &&
		ARENA_TYPE_GET(nah->a.attrs) == ARENA_TYPE_FREE &&
		ARENA_SIZE_GET(nah->a.attrs) + oldsize >= newsize) {
	    //nah->a.type == ARENA_TYPE_FREE &&
	    //oldsize + nah->a.size >= newsize) {
	    /* Merge in subsequent free block */
	    ah->a.next = nah->a.next;
	    ah->a.next->a.prev = ah;
	    nah->next_free->prev_free = nah->prev_free;
	    nah->prev_free->next_free = nah->next_free;
	    ARENA_SIZE_SET(ah->a.attrs, ARENA_SIZE_GET(ah->a.attrs) +
			   ARENA_SIZE_GET(nah->a.attrs));
	    xsize = ARENA_SIZE_GET(ah->a.attrs);
	}

	if (xsize >= newsize) {
	    /* We can reallocate in place */
	    if (xsize >= newsize + 2 * sizeof(struct arena_header)) {
		/* Residual free block at end */
		nah = (struct free_arena_header *)((char *)ah + newsize);
		ARENA_TYPE_SET(nah->a.attrs, ARENA_TYPE_FREE);
		ARENA_SIZE_SET(nah->a.attrs, xsize - newsize);
		ARENA_SIZE_SET(ah->a.attrs, newsize);
		ARENA_HEAP_SET(nah->a.attrs, ARENA_HEAP_GET(ah->a.attrs));

#ifdef DEBUG_MALLOC
		nah->a.magic = ARENA_MAGIC;
#endif

		//nah->a.type = ARENA_TYPE_FREE;
		//nah->a.size = xsize - newsize;
		//ah->a.size = newsize;

		/* Insert into block list */
		nah->a.next = ah->a.next;
		ah->a.next = nah;
		nah->a.next->a.prev = nah;
		nah->a.prev = ah;

		/* Insert into free list */
		if (newsize > oldsize) {
		    /* Hack: this free block is in the path of a memory object
		       which has already been grown at least once.  As such, put
		       it at the *end* of the freelist instead of the beginning;
		       trying to save it for future realloc()s of the same block. */
		    nah->prev_free = head->prev_free;
		    nah->next_free = head;
		    head->prev_free = nah;
		    nah->prev_free->next_free = nah;
		} else {
		    nah->next_free = head->next_free;
		    nah->prev_free = head;
		    head->next_free = nah;
		    nah->next_free->prev_free = nah;
		}
   	    }
	    /* otherwise, use up the whole block */
	    return ptr;
	} else {
	    /* Last resort: need to allocate a new block and copy */
	    oldsize -= sizeof(struct arena_header);
	    newptr = malloc(size);
	    if (newptr) {
		memcpy(newptr, ptr, min(size, oldsize));
		free(ptr);
	    }
	    return newptr;
	}
    }
}

__export void *realloc(void *ptr, size_t size)
{
    return firmware->mem->realloc(ptr, size);
}

__export void *zalloc(size_t size)
{
    void *ptr;

    ptr = malloc(size);
    if (ptr)
	memset(ptr, 0, size);

    return ptr;
}
