/* heap.c: Glulxe code related to the dynamic allocation heap.
    Designed by Andrew Plotkin <erkyrath@eblong.com>
    http://eblong.com/zarf/glulx/index.html
*/

#include "glk.h"
#include "glulxe.h"

typedef struct heapblock_struct {
  glui32 addr;
  glui32 len;
  int isfree;
  struct heapblock_struct *next;
  struct heapblock_struct *prev;
} heapblock_t;

static glui32 heap_start = 0; /* zero for inactive heap */
static int alloc_count = 0;

/* The heap_head/heap_tail is a doubly-linked list of blocks, both
   free and allocated. It is kept in address order. It should be
   complete -- that is, the first block starts at heap_start, and each
   block ends at the beginning of the next block, until the last one,
   which ends at endmem.

   (Heap_start is never the same as end_mem; if there is no heap space,
   then the heap is inactive and heap_start is zero.)

   Adjacent free blocks may be merged at heap_alloc() time.

   ### To make alloc more efficient, we could keep a separate
   free-list. To make free more efficient, we could keep a hash
   table of allocations.
 */
static heapblock_t *heap_head = NULL;
static heapblock_t *heap_tail = NULL;

/* heap_clear():
   Set the heap state to inactive, and free the block lists. This is
   called when the game starts or restarts.
*/
void heap_clear()
{
  while (heap_head) {
    heapblock_t *blo = heap_head;
    heap_head = blo->next;
    blo->next = NULL;
    blo->prev = NULL;
    glulx_free(blo);
  }
  heap_tail = NULL;

  if (heap_start) {
    glui32 res = change_memsize(heap_start, TRUE);
    if (res)
      fatal_error_i("Unable to revert memory size when deactivating heap.",
        heap_start);
  }

  heap_start = 0;
  alloc_count = 0;
  /* heap_sanity_check(); */
}

/* heap_is_active():
   Returns whether the heap is active.
*/
int heap_is_active() {
  return (heap_start != 0);
}

/* heap_get_start():
   Returns the start address of the heap, or 0 if the heap is not active.
 */
glui32 heap_get_start() {
  return heap_start;
}

/* heap_alloc(): 
   Allocate a block. If necessary, activate the heap and/or extend memory.
   This may not be available at all; #define FIXED_MEMSIZE if you want
   the interpreter to unconditionally refuse.
   Returns the memory address of the block, or 0 if the operation failed.
*/
glui32 heap_alloc(glui32 len)
{
  heapblock_t *blo, *newblo;

#ifdef FIXED_MEMSIZE
  return 0;
#else /* FIXED_MEMSIZE */

  if (len <= 0)
    fatal_error("Heap allocation length must be positive.");

  blo = heap_head;
  while (blo) {
    if (blo->isfree && blo->len >= len)
      break;

    if (!blo->isfree) {
      blo = blo->next;
      continue;
    }

    if (!blo->next || !blo->next->isfree) {
      blo = blo->next;
      continue;
    }

    /* This is a free block, but the next block in the list is also
       free, so we "advance" by merging rather than by going to
       blo->next. */
    newblo = blo->next;
    blo->len += newblo->len;
    if (newblo->next) {
      blo->next = newblo->next;
      newblo->next->prev = blo;
    }
    else {
      blo->next = NULL;
      heap_tail = blo;
    }
    newblo->next = NULL;
    newblo->prev = NULL;
    glulx_free(newblo);
    newblo = NULL;
    continue;
  }

  if (!blo) {
    /* No free area is visible on the list. Try extending memory. How
       much? Double the heap size, or by 256 bytes, or by the memory
       length requested -- whichever is greatest. */
    glui32 res;
    glui32 extension;
    glui32 oldendmem = endmem;

    extension = 0;
    if (heap_start)
      extension = endmem - heap_start;
    if (extension < len)
      extension = len;
    if (extension < 256)
      extension = 256;
    /* And it must be rounded up to a multiple of 256. */
    extension = (extension + 0xFF) & (~(glui32)0xFF);

    res = change_memsize(endmem+extension, TRUE);
    if (res)
      return 0;

    /* If we just started the heap, note that. */
    if (heap_start == 0)
      heap_start = oldendmem;

    if (heap_tail && heap_tail->isfree) {
      /* Append the new space to the last block. */
      blo = heap_tail;
      blo->len += extension;
    }
    else {
      /* Append the new space to the block list, as a new block. */
      newblo = glulx_malloc(sizeof(heapblock_t));
      if (!newblo)
        fatal_error("Unable to allocate record for heap block.");
      newblo->addr = oldendmem;
      newblo->len = extension;
      newblo->isfree = TRUE;
      newblo->next = NULL;
      newblo->prev = NULL;

      if (!heap_tail) {
        heap_head = newblo;
        heap_tail = newblo;
      }
      else {
        blo = heap_tail;
        heap_tail = newblo;
        blo->next = newblo;
        newblo->prev = blo;
      }

      blo = newblo;
      newblo = NULL;
    }

    /* and continue forwards, using this new block (blo). */
  }

  /* Something strange happened. */
  if (!blo || !blo->isfree || blo->len < len)
    return 0;

  /* We now have a free block of size len or longer. */

  if (blo->len == len) {
    blo->isfree = FALSE;
  }
  else {
    newblo = glulx_malloc(sizeof(heapblock_t));
    if (!newblo)
      fatal_error("Unable to allocate record for heap block.");
    newblo->isfree = TRUE;
    newblo->addr = blo->addr + len;
    newblo->len = blo->len - len;
    blo->len = len;
    blo->isfree = FALSE;
    newblo->next = blo->next;
    if (newblo->next)
      newblo->next->prev = newblo;
    newblo->prev = blo;
    blo->next = newblo;
    if (heap_tail == blo)
      heap_tail = newblo;
  }

  alloc_count++;
  /* heap_sanity_check(); */
  return blo->addr;

#endif /* FIXED_MEMSIZE */
}

/* heap_free():
   Free a heap block. If necessary, deactivate the heap.
*/
void heap_free(glui32 addr)
{
  heapblock_t *blo;

  for (blo = heap_head; blo; blo = blo->next) { 
    if (blo->addr == addr)
      break;
  };
  if (!blo || blo->isfree)
    fatal_error_i("Attempt to free unallocated address from heap.", addr);

  blo->isfree = TRUE;
  alloc_count--;
  if (alloc_count <= 0) {
    heap_clear();
  }

  /* heap_sanity_check(); */
}

/* heap_get_summary():
   Create an array of words, in the VM serialization format:

     heap_start
     alloc_count
     addr of first block
     len of first block
     ...

   (Note that these are glui32 values -- native byte ordering. Also,
   the blocks will be in address order, which is a stricter guarantee
   than the VM specifies; that'll help in heap_apply_summary().)

   If the heap is inactive, store NULL. Return 0 for success;
   otherwise, the operation failed.

   The array returned in summary must be freed with glulx_free() after
   the caller uses it.
*/
int heap_get_summary(glui32 *valcount, glui32 **summary)
{
  glui32 *arr, len, pos, lx;
  heapblock_t *blo;

  *valcount = 0;
  *summary = NULL;

  if (heap_start == 0)
    return 0;

  len = 2 + 2*alloc_count;
  arr = glulx_malloc(len * sizeof(glui32));
  if (!arr)
    return 1;

  pos = 0;
  arr[pos++] = heap_start;
  arr[pos++] = alloc_count;

  for (blo = heap_head; blo; blo = blo->next) {
    if (blo->isfree)
      continue;
    arr[pos++] = blo->addr;
    arr[pos++] = blo->len;
  }

  if (pos != len)
    fatal_error("Wrong number of active blocks in heap");

  *valcount = len;
  *summary = arr;
  return 0;
}

/* heap_apply_summary():
   Given an array of words in the above format, set up the heap to
   contain it. As noted above, the caller must ensure that the blocks
   are in address order. When this is called, the heap must be
   inactive.

   Return 0 for success. Otherwise the operation failed (and, most
   likely, caused a fatal error).
*/
int heap_apply_summary(glui32 valcount, glui32 *summary)
{
  glui32 lx, jx, lastend;

  if (heap_start)
    fatal_error("Heap active when heap_apply_summary called");

  if (valcount == 0 || summary == NULL)
    return 0;
  if (valcount == 2 && summary[0] == 0 && summary[1] == 0)
    return 0;

#ifdef FIXED_MEMSIZE
  return 1;
#else /* FIXED_MEMSIZE */

  lx = 0;
  heap_start = summary[lx++];
  alloc_count = summary[lx++];

  for (jx=lx; jx+2<valcount; jx+=2) {
    if (summary[jx] >= summary[jx+2])
      fatal_error("Heap block summary is out of order.");
  }

  lastend = heap_start;

  while (lx < valcount || lastend < endmem) {
    heapblock_t *blo;

    blo = glulx_malloc(sizeof(heapblock_t));
    if (!blo)
      fatal_error("Unable to allocate record for heap block.");

    if (lx >= valcount) {
      blo->addr = lastend;
      blo->len = endmem - lastend;
      blo->isfree = TRUE;
    }
    else {
      if (lastend < summary[lx]) {
        blo->addr = lastend;
        blo->len = summary[lx] - lastend;
        blo->isfree = TRUE;
      }
      else {
        blo->addr = summary[lx++];
        blo->len = summary[lx++];
        blo->isfree = FALSE;
      }
    }

    blo->prev = NULL;
    blo->next = NULL;

    if (!heap_head) {
      heap_head = blo;
      heap_tail = blo;
    }
    else {
      heap_tail->next = blo;
      blo->prev = heap_tail;
      heap_tail = blo;
    }

    lastend = blo->addr + blo->len;
  }

  /* heap_sanity_check(); */

  return 0;
#endif /* FIXED_MEMSIZE */
}

#if 0
#include <stdio.h>

static void heap_dump(void);

/* heap_dump():
   Print out the heap list (using printf). This exists for debugging,
   which is why it's ifdeffed out.
*/
static void heap_dump()
{
  heapblock_t *blo;

  if (heap_start == 0) {
    printf("# Heap is inactive.\n");
    return;    
  }

  printf("# Heap active: %d outstanding blocks\n", alloc_count);
  printf("# Heap start: %ld\n", heap_start);

  for (blo = heap_head; blo; blo = blo->next) {
    printf("#  %s at %ld..%ld, len %ld\n", 
      (blo->isfree ? " free" : "*used"),
      blo->addr, blo->addr+blo->len, blo->len);
  }

  printf("# Heap end: %ld\n", endmem);
}

/* heap_sanity_check():
   Check the validity of the heap. Throw a fatal error if anything is
   wrong.
*/
void heap_sanity_check()
{
  heapblock_t *blo, *last;
  int livecount;

  heap_dump();

  if (heap_start == 0) {
    if (heap_head || heap_tail)
      fatal_error("Heap sanity: nonempty list when heap is inactive.");
    if (alloc_count)
      fatal_error_i("Heap sanity: outstanding blocks when heap is inactive.",
        alloc_count);
    return;
  }

#ifdef FIXED_MEMSIZE
  fatal_error("Heap sanity: heap is active, but interpreter is compiled with no allocation.");
#endif /* FIXED_MEMSIZE */

  /* When the heap is active there may, briefly, be no heapblocks on the
     list. */

  last = NULL;
  livecount = 0;

  for (blo = heap_head; blo; last = blo, blo = blo->next) {
    glui32 lastend;

    if (blo->prev != last)
      fatal_error("Heap sanity: prev pointer mismatch.");
    if (!last) 
      lastend = heap_start;
    else
      lastend = last->addr + last->len;
    if (lastend != blo->addr)
      fatal_error("Heap sanity: addr+len mismatch.");

    if (!blo->isfree)
      livecount++;
  }

  if (!last) {
    if (heap_start != endmem)
      fatal_error_i("Heap sanity: empty list, but endmem is not heap start.",
        heap_start);
    if (heap_tail)
      fatal_error("Heap sanity: empty list, but heap tail exists.");
  }
  else {
    if (last->addr + last->len != endmem)
      fatal_error_i("Heap sanity: last block does not end at endmem.",
        last->addr + last->len);
    if (last != heap_tail)
      fatal_error("Heap sanity: heap tail points wrong.");
  }

  if (livecount != alloc_count)
    fatal_error_i("Heap sanity: wrong number of live blocks.", livecount);
}

#endif /* 0 */

