/**
 * @file collision_arena.c
 * @brief The bump allocator libccd allocates from. See akgl/collision_arena.h.
 */

#include <string.h>

#include <akgl/collision_arena.h>

/**
 * @brief Alignment every block starts on.
 *
 * `max_align_t` is what C11 offers and is 16 on x86-64. libccd allocates
 * structures of doubles and pointers, so 16 covers everything it asks for
 * without needing to know what it asked for.
 */
#define ARENA_ALIGN     16

/**
 * @brief Bookkeeping stored immediately before each block.
 *
 * Only the size is kept, and only because `realloc` has to know how much to
 * copy when it grows a block. It is padded to #ARENA_ALIGN so the payload that
 * follows is aligned too.
 */
typedef struct {
    size_t size;                                /**< Payload bytes, not counting this header. */
    uint8_t pad[ARENA_ALIGN - sizeof(size_t)];  /**< Keeps the payload on an ARENA_ALIGN boundary. */
} arena_Header;

/** @brief The arena. Static storage, so this file allocates nothing, ever. */
static uint8_t arena_bytes[AKGL_CCD_ARENA_BYTES];
/** @brief Offset of the next free byte. */
static size_t arena_used;
/** @brief Largest arena_used ever reached. Diagnostic; never reset. */
static size_t arena_highwater;
/** @brief Allocations refused for want of room. Diagnostic; never reset. */
static uint32_t arena_exhausted;
/**
 * @brief Usable bytes. #AKGL_CCD_ARENA_BYTES unless a test has narrowed it.
 *
 * Exists so the exhaustion path can be driven end to end through libccd. The
 * mapping from a refused allocation to #AKGL_ERR_COLLISION runs through libccd's
 * own unwinding and its `-2` return, and the only other way to reach it is to
 * rebuild the library with a smaller ceiling -- which is not something CI can do
 * and not something a reader can repeat.
 */
static size_t arena_limit = AKGL_CCD_ARENA_BYTES;

/** @brief Round @p n up to the next #ARENA_ALIGN boundary. */
static size_t arena_align(size_t n)
{
    return ((n + (ARENA_ALIGN - 1)) & ~((size_t)(ARENA_ALIGN - 1)));
}

void *akgl_ccd_arena_realloc(void *ptr, size_t size)
{
    arena_Header *header = NULL;
    size_t need = 0;
    void *out = NULL;

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

    need = sizeof(arena_Header) + arena_align(size);
    if ( (arena_used + need) > arena_limit ) {
	/*
	 * Refuse rather than grow. libccd checks its allocations and unwinds to
	 * a -2 return, which akgl_collision_test reports as AKGL_ERR_COLLISION
	 * with the high-water mark in the message, so the operator finds out
	 * what to raise AKGL_CCD_ARENA_BYTES to.
	 */
	arena_exhausted += 1;
	return NULL;
    }

    header = (arena_Header *)&arena_bytes[arena_used];
    header->size = size;
    out = (void *)(&arena_bytes[arena_used + sizeof(arena_Header)]);
    arena_used += need;
    if ( arena_used > arena_highwater ) {
	arena_highwater = arena_used;
    }

    /*
     * Growing means allocating and copying: a bump allocator can only extend
     * the most recent block in place, and libccd's reallocs are a handful per
     * query against an arena that is reset per query. The old block is left
     * where it is and reclaimed by the reset with everything else.
     */
    if ( ptr != NULL ) {
	header = (arena_Header *)((uint8_t *)ptr - sizeof(arena_Header));
	if ( header->size < size ) {
	    memcpy(out, ptr, header->size);
	} else {
	    memcpy(out, ptr, size);
	}
    }
    return out;
}

void akgl_ccd_arena_free(void *ptr)
{
    /*
     * Nothing to do. Every block dies with the arena at the top of the next
     * query, which is what makes this allocator a pointer bump rather than a
     * heap. The function exists so that libccd's free() calls -- redirected
     * here by src/ccd_arena_shim.h -- do not reach the C library holding a
     * pointer it never issued.
     */
    (void)ptr;
}

void akgl_ccd_arena_reset(void)
{
    arena_used = 0;
}

size_t akgl_ccd_arena_used(void)
{
    return arena_used;
}

size_t akgl_ccd_arena_highwater(void)
{
    return arena_highwater;
}

uint32_t akgl_ccd_arena_exhausted(void)
{
    return arena_exhausted;
}

void akgl_ccd_arena_set_limit(size_t limit)
{
    if ( (limit == 0) || (limit > AKGL_CCD_ARENA_BYTES) ) {
	arena_limit = AKGL_CCD_ARENA_BYTES;
    } else {
	arena_limit = limit;
    }
}
