#include <stdlib.h>
#include <string.h>
#include "kerbind.h"
#include "gs_descr.h"


#define POOLSIZE_DEFAULT	100000L
#define POOLSIZE_VAR		"ALLOCMEM"
#define MIN_CHUNK		(sizeof(chunk_header) + 8)
#define ALLOCED			0xAB000000UL
#define FREED			0xFB000000UL

#define round(n)		(((n) + 7) & ~7)
#define is_alloced(H)		(((H)->size & 0xFF000000L) == ALLOCED)
#define is_freed(H)		(((H)->size & 0xFF000000L) == FREED)
#define chunksize(H)		((H)->size & 0x00FFFFFFL)


typedef struct chunk_header {
  struct chunk_header *next;
  unsigned long size;
} chunk_header;

static unsigned char *pool = 0;
static unsigned long poolsize = POOLSIZE_DEFAULT;
static chunk_header *arena;


int init_mem(void)
{
  register char *s = gs_getvstr(POOLSIZE_VAR);

  if (s)
    poolsize = strtoul(s, NULL, 0);
  if (poolsize <= MIN_CHUNK) {
    c_conws("alloc pool size too small - using default size\r\n");
    poolsize = POOLSIZE_DEFAULT;
  }
  pool = (unsigned char *)m_xalloc(poolsize, 0x2B);
  if (!pool) {
    c_conws("Unable to allocate alloc pool\r\n");
    return 0;
  }
  arena = (chunk_header *)pool;
  arena->next = 0;
  arena->size = (poolsize - sizeof(chunk_header)) | FREED;
  return 1;
}


/* join_next() -- Joins chunk |H| with the next block and sets its state to
   |state|.  WARNING:  Assumes |H->next| is not NULL. */
static void join_next(register chunk_header *H, unsigned long state)
{
  register chunk_header *H2 = H->next;
  H->next = H2->next;
  H->size = (chunksize(H) + chunksize(H2) + sizeof(chunk_header)) | state;
}

/* try_split() -- split chunk |block| into two chunks, the first of size
   |newsize|, if there's room for a second chunk.  The new chunk (if
   any) is marked FREED; the (possibly smaller) original block is marked
   ALLOCED.  WARNING:  assumes |newsize| is already round()'ed. */
static void try_split(chunk_header *block, unsigned long newsize)
{
  register chunk_header *H;

  if (chunksize(block) - newsize >= MIN_CHUNK) {
    H = (chunk_header *)((unsigned char *)(block + 1) + newsize);
    H->next = block->next;
    H->size = (chunksize(block) - newsize - sizeof(chunk_header)) | FREED;
    block->next = H;
    block->size = newsize | ALLOCED;

    /* if the next block is free, merge the new block into it */
    if (H->next && is_freed(H->next))
      join_next(H, FREED);
  } else {
    block->size = chunksize(block) | ALLOCED;
  }
}


void* gs_mem_alloc(int32 size)
{
  register chunk_header *H;

  size = round(size);
  if (size >= 0x01000000L || size == 0) {
    LOG(DBG_ERROR, "KRmalloc(%l) returns 0", size);
    return 0;
  }

  for (H = arena; H; H = H->next) {
    if (is_alloced(H) || chunksize(H) < size)
      continue;
    if (!is_freed(H)) {
      LOG(DBG_ERROR, "In KRmalloc(%l):  possible arena corruption at %p",
	  size, (void *)H);
    }
    try_split(H, size);
    LOG(DBG_SYSCALL, "KRmalloc(%l) returns %p", size, (void *)(H+1));
    return (char *)(H + 1);
  }

  LOG(DBG_ERROR, "KRmalloc(%l) returns 0", size);
  return 0;
}

void gs_mem_free(void *mem)
{
  register chunk_header *H = (chunk_header *)mem;

  LOG(DBG_SYSCALL, "Entering KRfree(%p)", mem);
  if (!mem)
    return;

  H--;		/* step back to the header */
  if (!is_alloced(H)) {
    LOG(DBG_ERROR, "In KRfree(%p):  block not allocated by KR{m,re}alloc()",
	mem);
    return;
  }

  H->size = chunksize(H) | FREED;

  if (H->next && is_freed(H->next)) {
    /* next block is free; merge with it */
    join_next(H, FREED);
  }

  if (H != arena) {
    /* there is a previous chunk; merge with it if it's freed */
    register chunk_header *H2;

    for (H2 = arena; H2 && H2->next != H; H2 = H2->next) {
      if (!is_freed(H2) && !is_alloced(H2))
	LOG(DBG_ERROR, "In KRfree(%p):  possible arena corruption at %p",
	    mem, (void *)H2);
      continue;
    }
    if (!H2) {	/* this shouldn't happen */
      LOG(DBG_ERROR, "In KRfree(%p):  cannot find block in arena", mem);
    } else if (is_freed(H2)) {
      join_next(H2, FREED);
      H = H2;
    } else if (!is_alloced(H2)) {
      LOG(DBG_ERROR, "In KRfree(%p):  possible arena corruption at %p",
	  mem, (void *)H2);
    }
  }

  LOG(DBG_SYSCALL, "Exiting KRfree(%p)", mem);
}

int32 gs_mem_getfree(int16 flag)
{
  register chunk_header *H;
  register unsigned long size = 0;

  LOG(DBG_SYSCALL, "Entering KRgetfree(%d)", flag);

  if (flag) {
    for (H = arena; H; H = H->next) {
      if (is_freed(H) && chunksize(H) > size)
	size = chunksize(H);
      else if (!is_alloced(H)) {
	LOG(DBG_ERROR, "In KRgetfree(%d):  possible arena corruption at %p",
	    flag, (void *)H);
      }
    }
  } else {
    for (H = arena; H; H = H->next) {
      if (is_freed(H))
	size += chunksize(H);
      else if (!is_alloced(H)) {
	LOG(DBG_ERROR, "In KRgetfree(%d):  possible arena corruption at %p",
	    flag, (void *)H);
      }
    }
  }

  LOG(DBG_SYSCALL, "KRgetfree(%d) returns %l", flag, size);
  return size;
}

void* gs_mem_realloc(void *mem, int32 newsize)
{
  register chunk_header *H = (chunk_header *)mem;

  LOG(DBG_SYSCALL, "Entering KRrealloc(%p, %l)", mem, newsize);
  if (!mem) {
    void *newmem = gs_mem_alloc(newsize);
    if (newmem)
      memset(newmem, 0, newsize);
    LOG((newmem ? DBG_SYSCALL : DBG_ERROR),
	"KRrealloc(%p, %l) returns %p", mem, newsize, newmem);
    return newmem;
  }

  if (newsize == 0) {
    gs_mem_free(mem);
    LOG(DBG_SYSCALL, "KRrealloc(%p, %l) returns 0", mem, newsize);
    return 0;
  }

  H--;		/* step back to the header */
  if (!is_alloced(H)) {
    LOG(DBG_ERROR,
	"In KRrealloc(%p, %l):  block not allocated by KR{m,re}alloc()",
	mem, newsize);
    return 0;
  }

  newsize = round(newsize);
  if (newsize <= chunksize(H)) {
    /* if we're downsizing, we may have room to split the chunk */
    try_split(H, newsize);
    LOG(DBG_SYSCALL, "KRrealloc(%p, %l) returns %p", mem, newsize, mem);
    return mem;
  } else if (H->next && is_freed(H->next) &&
             (chunksize(H) + chunksize(H->next) +
					sizeof(chunk_header)) >= newsize) {
    /* Next chunk is free and big enough to accommodate the new size;
       join it with the current chunk */
    join_next(H, ALLOCED);
    /* We may even have room to split off part of the newly joined chunk */
    try_split(H, newsize);
    LOG(DBG_SYSCALL, "KRrealloc(%p, %l) returns %p", mem, newsize, mem);
    return mem;
  } else {
    register char *newmem = gs_mem_alloc(newsize);

    if (!newmem) {
      LOG(DBG_ERROR, "KRrealloc(%p, %l) returns 0", mem, newsize);
      return 0;
    }
    memcpy(newmem, mem, chunksize(H));
    gs_mem_free(mem);
    LOG(DBG_SYSCALL, "KRrealloc(%p, %l) returns %p", mem, newsize, newmem);
    return newmem;
  }
}


void cleanup_mem()
{
  if (pool) m_free(pool);
}
