/*
 *  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 2 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

/*
 * smalloc - small memory allocator for dosemu.
 *
 * Author: Stas Sergeev
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include "smalloc.h"

#define POOL_USED(p) (p->mn.used || p->mn.next)
#ifndef _min
#define _min(x, y) ((x) < (y) ? (x) : (y))
#endif
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
#define _PAGE_MASK (~(PAGE_SIZE-1))
#ifndef PAGE_ALIGN
#define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&_PAGE_MASK)
#endif

static void smerror_dummy(int prio, const char *fmt, ...) FORMAT(printf, 2, 3);

static void (*smerr)(int prio, const char *fmt, ...)
	FORMAT(printf, 2, 3) = smerror_dummy;

static void smerror_dummy(int prio, const char *fmt, ...)
{
}

#define smerror(mp, ...) mp->smerr(3, __VA_ARGS__)

static int do_dump(struct mempool *mp, char *buf, int len)
{
    int pos = 0;
    struct memnode *mn;

#define DO_PRN(...) do { \
    if (pos >= len) \
      return -1; \
    pos += snprintf(buf + pos, len - pos, __VA_ARGS__); \
} while (0)
    DO_PRN("Total size: %zi\n", mp->size);
    DO_PRN("Available space: %zi (%zi used)\n", mp->avail,
            mp->size - mp->avail);
    DO_PRN("Largest free area: %zi\n", smget_largest_free_area(mp));
    DO_PRN("Memory pool dump:\n");
    for (mn = &mp->mn; mn; mn = mn->next)
        DO_PRN("\tarea: %zi bytes, %s\n",
                mn->size, mn->used ? "used" : "free");
    return 0;
}

void smdump(struct mempool *mp)
{
    char buf[16384];
    int err = do_dump(mp, buf, sizeof(buf));
    if (!err)
        mp->smerr(0, "%s", buf);
    else
        mp->smerr(3, "dump buffer overflow\n");
}

static FORMAT(printf, 3, 4)
void do_smerror(int prio, struct mempool *mp, const char *fmt, ...)
{
    char buf[16384];
    int err;
    size_t pos;
    va_list al;

    assert(prio != -1);
    va_start(al, fmt);
    pos = vsnprintf(buf, sizeof(buf), fmt, al);
    va_end(al);
    err = -1;
    if (pos < sizeof(buf))
        err = do_dump(mp, buf + pos, sizeof(buf) - pos);
    if (!err)
        mp->smerr(0, "%s", buf);
    else
        mp->smerr(3, "dump buffer overflow\n");
}

static int get_oom_pr(struct mempool *mp, size_t size)
{
    if (size <= smget_largest_free_area(mp))
	return -1;
    if (size > mp->size)
	return 2;
    if (size > mp->avail)
	return 1;
    return 0;
}

static void sm_uncommit(struct mempool *mp, dosaddr_t addr, size_t comb_size)
{
    /* align address up and align down size */
    dosaddr_t a = addr;
    dosaddr_t aa = PAGE_ALIGN(a);
    size_t aligned_size = (comb_size - (aa - a)) & _PAGE_MASK;
    dosaddr_t aligned_addr = aa;
    if (!mp->uncommit || !aligned_size)
      return;
    mp->uncommit(aligned_addr, aligned_size);
}

static int __sm_commit(struct mempool *mp, dosaddr_t addr, size_t size,
	dosaddr_t e_addr, size_t e_size)
{
  if (!mp->commit)
    return 1;
  if (!mp->commit(addr, size)) {
    smerror(mp, "SMALLOC: failed to commit %#x %zi\n", addr, size);
    if (e_size) {
      mp->avail += e_size;
      sm_uncommit(mp, e_addr, e_size);
    }
    return 0;
  }
  return 1;
}

static int sm_commit(struct mempool *mp, dosaddr_t addr, size_t size,
	dosaddr_t e_addr, size_t e_size)
{
    /* align address down and align up size */
    dosaddr_t a = addr;
    dosaddr_t aa = a & _PAGE_MASK;
    size_t aligned_size = PAGE_ALIGN(size + (a - aa));
    dosaddr_t aligned_addr = aa;
    int ok = __sm_commit(mp, aligned_addr, aligned_size, e_addr, e_size);
    if (ok) {
	assert(mp->avail >= size);
	mp->avail -= size;
    }
    return ok;
}

static int sm_commit_simple(struct mempool *mp, dosaddr_t addr, size_t size)
{
  return sm_commit(mp, addr, size, (dosaddr_t)-1, 0);
}

static void mntruncate(struct memnode *pmn, size_t size)
{
  int delta = pmn->size - size;

  if (delta == 0)
    return;
  /* delta can be < 0 */
  if (pmn->next && !pmn->next->used) {
    struct memnode *nmn = pmn->next;

    assert(size > 0 && nmn->size + delta >= 0);

    nmn->size += delta;
    nmn->mem_area -= delta;
    pmn->size -= delta;
    if (nmn->size == 0) {
      pmn->next = nmn->next;
      free(nmn);
      assert(!pmn->next || pmn->next->used);
    }
  } else {
    struct memnode *new_mn;

    assert(size < pmn->size);

    new_mn = (struct memnode *)malloc(sizeof(struct memnode));
    new_mn->next = pmn->next;
    new_mn->size = delta;
    new_mn->used = 0;
    new_mn->mem_area = pmn->mem_area + size;

    pmn->next = new_mn;
    pmn->size = size;
  }
}

static struct memnode *find_mn(struct mempool *mp, dosaddr_t ptr,
    struct memnode **prev)
{
  struct memnode *pmn, *mn;
  if (!POOL_USED(mp)) {
    smerror(mp, "SMALLOC: unused pool passed\n");
    return NULL;
  }
  for (pmn = NULL, mn = &mp->mn; mn; pmn = mn, mn = mn->next) {
    if (mn->mem_area > ptr)
      return NULL;
    if (mn->mem_area == ptr) {
      if (prev)
        *prev = pmn;
      return mn;
    }
  }
  return NULL;
}

static struct memnode *find_mn_at(struct mempool *mp, dosaddr_t ptr)
{
  struct memnode *mn;
  for (mn = &mp->mn; mn; mn = mn->next) {
    if (mn->mem_area > ptr)
      return NULL;
    if (mn->mem_area + mn->size > ptr)
      return mn;
  }
  return NULL;
}

static struct memnode *smfind_free_area(struct mempool *mp, size_t size)
{
  struct memnode *mn;
  for (mn = &mp->mn; mn; mn = mn->next) {
    if (!mn->used && mn->size >= size)
      return mn;
  }
  return NULL;
}

static struct memnode *smfind_free_area_topdown(struct mempool *mp,
    dosaddr_t top, size_t size)
{
  struct memnode *mn;
  struct memnode *mn1 = NULL;
  for (mn = &mp->mn; mn; mn = mn->next) {
    if (top != (dosaddr_t)-1 && mn->mem_area + size > top)
      break;
    if (!mn->used && mn->size >= size)
      mn1 = mn;
  }
  return mn1;
}

static struct memnode *sm_alloc_fixed(struct mempool *mp, dosaddr_t ptr,
    size_t size)
{
  struct memnode *mn;
  ptrdiff_t delta;
  if (!size || !ptr) {
    smerror(mp, "SMALLOC: zero-sized allocation attempted\n");
    return NULL;
  }
  if (!(mn = find_mn_at(mp, ptr))) {
    smerror(mp, "SMALLOC: invalid address %#x on alloc_fixed\n", ptr);
    return NULL;
  }
  if (mn->used) {
    do_smerror(0, mp, "SMALLOC: address %#x already allocated\n", ptr);
    return NULL;
  }
  delta = ptr - mn->mem_area;
  assert(delta >= 0);
  if (size + delta > mn->size) {
    int pr = get_oom_pr(mp, size);
    if (pr < 0)
      pr = 0;
    do_smerror(pr, mp, "SMALLOC: no space %zi at address %#x\n", size, ptr);
    return NULL;
  }
  if (delta) {
    mntruncate(mn, delta);
    mn = mn->next;
    assert(!mn->used && mn->size >= size);
  }
  if (!sm_commit_simple(mp, mn->mem_area, size))
    return NULL;
  mn->used = 1;
  mntruncate(mn, size);
  assert(mn->size == size);
  if (!(mp->flags & SMFLG_NOMEMSET))
    MEMSET_DOS(mn->mem_area, 0, size);
  return mn;
}

static struct memnode *sm_alloc_aligned(struct mempool *mp, size_t align,
    size_t size)
{
  struct memnode *mn;
  int delta;
  dosaddr_t iptr;
  if (!size) {
    smerror(mp, "SMALLOC: zero-sized allocation attempted\n");
    return NULL;
  }
  /* power of 2 align */
  assert(__builtin_popcount(align) == 1);
  align--;
  if (!(mn = smfind_free_area(mp, size + align))) {
    do_smerror(get_oom_pr(mp, size), mp,
	    "SMALLOC: Out Of Memory on alloc, requested=%zu\n", size);
    return NULL;
  }
  /* insert small node to align the start */
  iptr = mn->mem_area;
  delta = ((iptr | align) - iptr + 1) & align;
  if (delta) {
    mntruncate(mn, delta);
    mn = mn->next;
    assert(!mn->used && mn->size >= size);
  }
  if (!sm_commit_simple(mp, mn->mem_area, size))
    return NULL;
  mn->used = 1;
  mntruncate(mn, size);
  assert(mn->size == size);
  if (!(mp->flags & SMFLG_NOMEMSET))
    MEMSET_DOS(mn->mem_area, 0, size);
  return mn;
}

static struct memnode *sm_alloc_mn(struct mempool *mp, size_t size)
{
  return sm_alloc_aligned(mp, 1, size);
}

static struct memnode *sm_alloc_aligned_topdown(struct mempool *mp,
    dosaddr_t top, size_t align, size_t size)
{
  struct memnode *mn;
  int delta;
  dosaddr_t iptr;
  dosaddr_t min_top;
  dosaddr_t iend;
  if (!size) {
    smerror(mp, "SMALLOC: zero-sized allocation attempted\n");
    return NULL;
  }
  /* power of 2 align */
  assert(__builtin_popcount(align) == 1);
  align--;
  if (!(mn = smfind_free_area_topdown(mp, top, size + align))) {
    do_smerror(get_oom_pr(mp, size), mp,
	    "SMALLOC: Out Of Memory on alloc, requested=%zu\n", size);
    return NULL;
  }
  /* use top part of the found area */
  min_top = mn->mem_area + mn->size;
  if (top != (dosaddr_t)-1)
    min_top = _min(min_top, top);
  iptr = (min_top - size) & ~align;
  iend = iptr + size;
  delta = mn->mem_area + mn->size - iend;
  if (delta)
    mntruncate(mn, mn->size - delta);
  assert(iptr >= mn->mem_area);
  delta = iptr - mn->mem_area;
  if (delta) {
    mntruncate(mn, delta);
    mn = mn->next;
    assert(!mn->used && mn->size >= size);
  }
  if (!sm_commit_simple(mp, mn->mem_area, size))
    return NULL;
  mn->used = 1;
  mntruncate(mn, size);
  assert(mn->size == size);
  if (!(mp->flags & SMFLG_NOMEMSET))
    MEMSET_DOS(mn->mem_area, 0, size);
  return mn;
}

static struct memnode *sm_alloc_topdown(struct mempool *mp, size_t size)
{
  return sm_alloc_aligned_topdown(mp, (dosaddr_t)-1, 1, size);
}

dosaddr_t smalloc(struct mempool *mp, size_t size)
{
  struct memnode *mn = sm_alloc_mn(mp, size);
  if (!mn)
    return (dosaddr_t)-1;
  return mn->mem_area;
}

dosaddr_t smalloc_fixed(struct mempool *mp, dosaddr_t ptr, size_t size)
{
  struct memnode *mn = sm_alloc_fixed(mp, ptr, size);
  if (!mn)
    return (dosaddr_t)-1;
  assert(mn->mem_area == ptr);
  return mn->mem_area;
}

dosaddr_t smalloc_aligned(struct mempool *mp, size_t align, size_t size)
{
  struct memnode *mn = sm_alloc_aligned(mp, align, size);
  if (!mn)
    return (dosaddr_t)-1;
  assert((mn->mem_area & (align - 1)) == 0);
  return mn->mem_area;
}

dosaddr_t smalloc_topdown(struct mempool *mp, size_t size)
{
  struct memnode *mn = sm_alloc_topdown(mp, size);
  if (!mn)
    return (dosaddr_t)-1;
  return mn->mem_area;
}

dosaddr_t smalloc_aligned_topdown(struct mempool *mp, dosaddr_t top,
    size_t align, size_t size)
{
  struct memnode *mn = sm_alloc_aligned_topdown(mp, top, align, size);
  if (!mn)
    return (dosaddr_t)-1;
  assert((mn->mem_area & (align - 1)) == 0);
  return mn->mem_area;
}

int smfree(struct mempool *mp, dosaddr_t ptr)
{
  struct memnode *mn, *pmn;
  if (ptr == (dosaddr_t)-1)
    return -1;
  if (!(mn = find_mn(mp, ptr, &pmn))) {
    smerror(mp, "SMALLOC: bad pointer passed to smfree()\n");
    return -1;
  }
  if (!mn->used) {
    smerror(mp, "SMALLOC: attempt to free the not allocated region (double-free)\n");
    return -1;
  }
  assert(mn->size > 0);
  mp->avail += mn->size;
  assert(mp->avail <= mp->size);
  mn->used = 0;
  if (mn->next && !mn->next->used) {
    /* merge with next */
    assert(mn->next->mem_area >= mn->mem_area);
    mntruncate(mn, mn->size + mn->next->size);
  }
  if (pmn && !pmn->used) {
    /* merge with prev */
    assert(pmn->mem_area <= mn->mem_area);
    mntruncate(pmn, pmn->size + mn->size);
    mn = pmn;
  }
  /* uncommit combined area */
  assert(!mn->used);
  sm_uncommit(mp, mn->mem_area, mn->size);
  return 0;
}

/* part of smrealloc() that covers the cases where the
 * extra memnode needs to be allocated for realloc */
static struct memnode *sm_realloc_alloc_mn(struct mempool *mp,
	struct memnode *pmn, struct memnode *mn,
	struct memnode *nmn, size_t size)
{
  struct memnode *new_mn;
  if (pmn && !pmn->used && pmn->size + mn->size +
	(nmn ? (nmn->used ? 0 : nmn->size) : 0) >= size) {
    /* move to prev memnode */
    size_t psize = _min(size, pmn->size);
    if (!sm_commit_simple(mp, pmn->mem_area, psize))
      return NULL;
    if (size > pmn->size + mn->size) {
      if (!sm_commit(mp, nmn->mem_area, size - pmn->size - mn->size,
	    pmn->mem_area, psize))
        return NULL;
    }
    pmn->used = 1;
    MEMMOVE_DOS2DOS(pmn->mem_area, mn->mem_area, mn->size);
    if (!(mp->flags & SMFLG_NOMEMSET))
      MEMSET_DOS(pmn->mem_area + mn->size, 0, size - mn->size);
    mn->used = 0;
    if (size < pmn->size + mn->size) {
      size_t overl = size > pmn->size ? size - pmn->size : 0;
      mp->avail += mn->size - overl;
      assert(mp->avail <= mp->size);
    }
    if (nmn && !nmn->used)	// merge with next
      mntruncate(mn, mn->size + nmn->size);
    mntruncate(pmn, size);
    /* in a very unlikely case mn could be destroyed by resizing */
    mn = pmn->next;
    /* uncommit combined area */
    if (mn && !mn->used)
      sm_uncommit(mp, mn->mem_area, mn->size);
    new_mn = pmn;
  } else {
    /* relocate */
    new_mn = sm_alloc_mn(mp, size);
    if (!new_mn) {
      do_smerror(get_oom_pr(mp, size), mp,
	    "SMALLOC: Out Of Memory on realloc, requested=%zu\n", size);
      return NULL;
    }
    MEMCPY_DOS2DOS(new_mn->mem_area, mn->mem_area, mn->size);
    smfree(mp, mn->mem_area);
  }
  return new_mn;
}

dosaddr_t smrealloc(struct mempool *mp, dosaddr_t ptr, size_t size)
{
  struct memnode *mn, *pmn, *nmn;
  if (!ptr)
    return smalloc(mp, size);
  if (!(mn = find_mn(mp, ptr, &pmn))) {
    smerror(mp, "SMALLOC: bad pointer passed to smrealloc()\n");
    return (dosaddr_t)-1;
  }
  if (!mn->used) {
    smerror(mp, "SMALLOC: attempt to realloc the not allocated region\n");
    return (dosaddr_t)-1;
  }
  if (size == 0) {
    smfree(mp, ptr);
    return (dosaddr_t)-1;
  }
  if (size == mn->size)
    return ptr;
  if (size < mn->size) {
    /* shrink */
    mp->avail += mn->size - size;
    assert(mp->avail <= mp->size);
    mntruncate(mn, size);
    nmn = mn->next;
    assert(nmn && !nmn->used);
    sm_uncommit(mp, nmn->mem_area, nmn->size);
  } else {
    /* grow */
    nmn = mn->next;
    if (nmn && !nmn->used && mn->size + nmn->size >= size) {
      /* expand by shrinking next memnode */
      if (!sm_commit_simple(mp, nmn->mem_area, size - mn->size))
        return (dosaddr_t)-1;
      if (!(mp->flags & SMFLG_NOMEMSET))
        MEMSET_DOS(nmn->mem_area, 0, size - mn->size);
      mntruncate(mn, size);
    } else {
      /* need to allocate new memnode */
      mn = sm_realloc_alloc_mn(mp, pmn, mn, nmn, size);
      if (!mn)
        return (dosaddr_t)-1;
    }
  }
  assert(mn->size == size);
  return mn->mem_area;
}

dosaddr_t smrealloc_aligned(struct mempool *mp, dosaddr_t ptr, int align, size_t size)
{
  struct memnode *mn, *pmn, *nmn;
  assert(__builtin_popcount(align) == 1);
  if (!ptr)
    return smalloc_aligned(mp, align, size);
  if (!(mn = find_mn(mp, ptr, &pmn))) {
    smerror(mp, "SMALLOC: bad pointer passed to smrealloc()\n");
    return (dosaddr_t)-1;
  }
  if (!mn->used) {
    smerror(mp, "SMALLOC: attempt to realloc the not allocated region\n");
    return (dosaddr_t)-1;
  }
  if (size == 0) {
    smfree(mp, ptr);
    return (dosaddr_t)-1;
  }
  if (size == mn->size)
    return ptr;
  if (mn->mem_area & (align - 1)) {
    smerror(mp, "SMALLOC: unaligned pointer passed to smrealloc_aligned()\n");
    return (dosaddr_t)-1;
  }
  if (size < mn->size) {
    /* shrink */
    mp->avail += mn->size - size;
    assert(mp->avail <= mp->size);
    mntruncate(mn, size);
    nmn = mn->next;
    assert(nmn && !nmn->used);
    sm_uncommit(mp, nmn->mem_area, nmn->size);
  } else {
    /* grow */
    nmn = mn->next;
    if (nmn && !nmn->used && mn->size + nmn->size >= size) {
      /* expand by shrinking next memnode */
      if (!sm_commit_simple(mp, nmn->mem_area, size - mn->size))
        return (dosaddr_t)-1;
      if (!(mp->flags & SMFLG_NOMEMSET))
        MEMSET_DOS(nmn->mem_area, 0, size - mn->size);
      mntruncate(mn, size);
    } else {
      /* lazy impl */
      struct memnode *new_mn = sm_alloc_aligned(mp, align, size);
      if (!new_mn)
        return (dosaddr_t)-1;
      MEMCPY_DOS2DOS(new_mn->mem_area, mn->mem_area, mn->size);
      smfree(mp, mn->mem_area);
    }
  }
  assert(mn->size == size);
  return mn->mem_area;
}

static int do_sminit(struct mempool *mp, dosaddr_t start, size_t size, int flags)
{
  mp->size = size;
  mp->mn.size = size;
  mp->mn.used = 0;
  mp->mn.next = NULL;
  mp->mn.mem_area = start;
  mp->avail = size;
  mp->flags = flags;
  mp->commit = NULL;
  mp->uncommit = NULL;
  mp->smerr = smerr;
  return 0;
}

int sminit(struct mempool *mp, dosaddr_t start, size_t size)
{
  return do_sminit(mp, start, size, 0);
}

int sminit_f(struct mempool *mp, dosaddr_t start, size_t size, int flags)
{
  return do_sminit(mp, start, size, flags);
}

static int do_sminit_com(struct mempool *mp, dosaddr_t start, size_t size,
    int (*commit)(dosaddr_t area, size_t size),
    int (*uncommit)(dosaddr_t area, size_t size), int do_uncommit,
    int flags)
{
  do_sminit(mp, start, size, flags);
  mp->commit = commit;
  mp->uncommit = uncommit;
  if (uncommit && do_uncommit)
    uncommit(start, size);
  return 0;
}

int sminit_com(struct mempool *mp, dosaddr_t start, size_t size,
    int (*commit)(dosaddr_t area, size_t size),
    int (*uncommit)(dosaddr_t area, size_t size),
    int flags)
{
  return do_sminit_com(mp, start, size, commit, uncommit, 1, flags);
}

int sminit_comu(struct mempool *mp, dosaddr_t start, size_t size,
    int (*commit)(dosaddr_t area, size_t size),
    int (*uncommit)(dosaddr_t area, size_t size),
    int flags)
{
  return do_sminit_com(mp, start, size, commit, uncommit, 0, flags);
}

void smfree_all(struct mempool *mp)
{
  struct memnode *mn;
  while (POOL_USED(mp)) {
    mn = &mp->mn;
    if (!mn->used)
      mn = mn->next;
    assert(mn && mn->used);
    smfree(mp, mn->mem_area);
  }
  assert(!mp->mn.next);
}

int smdestroy(struct mempool *mp)
{
  unsigned avail = mp->avail;

  smfree_all(mp);
  assert(mp->mn.size >= avail);
  /* return leaked size */
  return mp->mn.size - avail;
}

size_t smget_free_space(struct mempool *mp)
{
  return mp->avail;
}

size_t smget_free_space_upto(struct mempool *mp, dosaddr_t top)
{
  struct memnode *mn;
  int cnt = 0;
  for (mn = &mp->mn; mn; mn = mn->next) {
    if (mn->mem_area + mn->size > top) {
      if (!mn->used && mn->mem_area < top)
        cnt += top - mn->mem_area;
      break;
    }
    if (!mn->used)
      cnt += mn->size;
  }
  return cnt;
}

size_t smget_largest_free_area(struct mempool *mp)
{
  struct memnode *mn;
  size_t size = 0;
  for (mn = &mp->mn; mn; mn = mn->next) {
    if (!mn->used && mn->size > size)
      size = mn->size;
  }
  return size;
}

int smget_area_size(struct mempool *mp, dosaddr_t ptr)
{
  struct memnode *mn;
  if (!(mn = find_mn(mp, ptr, NULL))) {
    smerror(mp, "SMALLOC: bad pointer passed to smget_area_size()\n");
    return -1;
  }
  return mn->size;
}

dosaddr_t smget_base_addr(struct mempool *mp)
{
  return mp->mn.mem_area;
}

void smregister_error_notifier(struct mempool *mp,
	void (*func)(int prio, const char *fmt, ...) FORMAT(printf, 2, 3))
{
  mp->smerr = func;
}

void smregister_default_error_notifier(
	void (*func)(int prio, const char *fmt, ...) FORMAT(printf, 2, 3))
{
  smerr = func;
}
