#include "Python.h"

/* A simple arena block structure.

   Measurements with standard library modules suggest the average
   allocation is about 20 bytes and that most compiles use a single
   block.

   TODO(jhylton): Think about a realloc API, maybe just for the last
   allocation?
*/

#define DEFAULT_BLOCK_SIZE 8192
#define ALIGNMENT               8

typedef struct _block {
    /* Total number of bytes owned by this block available to pass out.
     * Read-only after initialization.  The first such byte starts at
     * ab_mem.
     */
    size_t ab_size;

    /* Total number of bytes already passed out.  The next byte available
     * to pass out starts at ab_mem + ab_offset.
     */
    size_t ab_offset;

    /* An arena maintains a singly-linked, NULL-terminated list of
     * all blocks owned by the arena.  These are linked via the
     * ab_next member.
     */
    struct _block *ab_next;

    /* Pointer to the first allocatable byte owned by this block.  Read-
     * only after initialization.
     */
    void *ab_mem;
} block;

/* The arena manages two kinds of memory, blocks of raw memory
   and a list of PyObject* pointers.  PyObjects are decrefed
   when the arena is freed.
*/

struct _arena {
    /* Pointer to the first block allocated for the arena, never NULL.
       It is used only to find the first block when the arena is
       being freed.
     */
    block *a_head;

    /* Pointer to the block currently used for allocation.  It's
       ab_next field should be NULL.  If it is not-null after a
       call to block_alloc(), it means a new block has been allocated
       and a_cur should be reset to point it.
     */
    block *a_cur;

    /* A Python list object containing references to all the PyObject
       pointers associated with this area.  They will be DECREFed
       when the arena is freed.
    */
    PyObject *a_objects;

#if defined(Py_DEBUG)
    /* Debug output */
    size_t total_allocs;
    size_t total_size;
    size_t total_blocks;
    size_t total_block_size;
    size_t total_big_blocks;
#endif
};

static block *
block_new(size_t size)
{
    /* Allocate header and block as one unit.
       ab_mem points just past header. */
    block *b = (block *)PyMem_Malloc(sizeof(block) + size);
    if (!b)
        return NULL;
    b->ab_size = size;
    b->ab_mem = (void *)(b + 1);
    b->ab_next = NULL;
    b->ab_offset = (char *)_Py_ALIGN_UP(b->ab_mem, ALIGNMENT) -
            (char *)(b->ab_mem);
    return b;
}

static void
block_free(block *b) {
    while (b) {
        block *next = b->ab_next;
        PyMem_Free(b);
        b = next;
    }
}

static void *
block_alloc(block *b, size_t size)
{
    void *p;
    assert(b);
    size = _Py_SIZE_ROUND_UP(size, ALIGNMENT);
    if (b->ab_offset + size > b->ab_size) {
        /* If we need to allocate more memory than will fit in
           the default block, allocate a one-off block that is
           exactly the right size. */
        /* TODO(jhylton): Think about space waste at end of block */
        block *newbl = block_new(
                        size < DEFAULT_BLOCK_SIZE ?
                        DEFAULT_BLOCK_SIZE : size);
        if (!newbl)
            return NULL;
        assert(!b->ab_next);
        b->ab_next = newbl;
        b = newbl;
    }

    assert(b->ab_offset + size <= b->ab_size);
    p = (void *)(((char *)b->ab_mem) + b->ab_offset);
    b->ab_offset += size;
    return p;
}

PyArena *
PyArena_New()
{
    PyArena* arena = (PyArena *)PyMem_Malloc(sizeof(PyArena));
    if (!arena)
        return (PyArena*)PyErr_NoMemory();

    arena->a_head = block_new(DEFAULT_BLOCK_SIZE);
    arena->a_cur = arena->a_head;
    if (!arena->a_head) {
        PyMem_Free((void *)arena);
        return (PyArena*)PyErr_NoMemory();
    }
    arena->a_objects = PyList_New(0);
    if (!arena->a_objects) {
        block_free(arena->a_head);
        PyMem_Free((void *)arena);
        return (PyArena*)PyErr_NoMemory();
    }
#if defined(Py_DEBUG)
    arena->total_allocs = 0;
    arena->total_size = 0;
    arena->total_blocks = 1;
    arena->total_block_size = DEFAULT_BLOCK_SIZE;
    arena->total_big_blocks = 0;
#endif
    return arena;
}

void
PyArena_Free(PyArena *arena)
{
    assert(arena);
#if defined(Py_DEBUG)
    /*
    fprintf(stderr,
        "alloc=%zu size=%zu blocks=%zu block_size=%zu big=%zu objects=%zu\n",
        arena->total_allocs, arena->total_size, arena->total_blocks,
        arena->total_block_size, arena->total_big_blocks,
        PyList_Size(arena->a_objects));
    */
#endif
    block_free(arena->a_head);
    /* This property normally holds, except when the code being compiled
       is sys.getobjects(0), in which case there will be two references.
    assert(arena->a_objects->ob_refcnt == 1);
    */

    Py_DECREF(arena->a_objects);
    PyMem_Free(arena);
}

void *
PyArena_Malloc(PyArena *arena, size_t size)
{
    void *p = block_alloc(arena->a_cur, size);
    if (!p)
        return PyErr_NoMemory();
#if defined(Py_DEBUG)
    arena->total_allocs++;
    arena->total_size += size;
#endif
    /* Reset cur if we allocated a new block. */
    if (arena->a_cur->ab_next) {
        arena->a_cur = arena->a_cur->ab_next;
#if defined(Py_DEBUG)
        arena->total_blocks++;
        arena->total_block_size += arena->a_cur->ab_size;
        if (arena->a_cur->ab_size > DEFAULT_BLOCK_SIZE)
            ++arena->total_big_blocks;
#endif
    }
    return p;
}

int
PyArena_AddPyObject(PyArena *arena, PyObject *obj)
{
    int r = PyList_Append(arena->a_objects, obj);
    if (r >= 0) {
        Py_DECREF(obj);
    }
    return r;
}
