/**
 * @file collision_arena.h
 * @brief The bump allocator libccd allocates from, so that libakgl does not.
 *
 * libakgl does not call `malloc` at runtime. Every object comes from a fixed
 * array declared in `akgl/heap.h`, and that is a promise the manual makes in
 * seven places and `AGENTS.md` makes as a rule. libccd's narrowphase does not
 * know that: its EPA path builds an expanding polytope out of `realloc` and
 * `free`, and `ccdGJKPenetration` documents a `-2` return for the case where
 * that fails.
 *
 * Rather than compile a hand-picked subset of libccd's sources -- which puts a
 * file list in this repository that really lives upstream, and rots silently on
 * the next bump -- the whole library is compiled and pointed at this arena.
 * `CMakeLists.txt` redirects `__CCD_ALLOC_MEMORY`, and `src/ccd_arena_shim.h`
 * redirects `free`, on libccd's translation units only.
 *
 * @section arena_lifetime The lifetime is one narrowphase call
 *
 * `akgl_collision_test` resets the arena on entry, so every allocation lives
 * exactly as long as the query that made it. libccd frees its polytope before
 * returning in any case, so nothing needs to survive, and that is what lets
 * `akgl_ccd_arena_free` be a no-op and the allocator be a pointer bump. There
 * is no free list, no coalescing and no fragmentation, because there is no
 * reuse within a call.
 *
 * @section arena_exhaustion Exhaustion is reported, not fatal
 *
 * A refused allocation returns `NULL`, which libccd already handles -- it
 * unwinds and returns `-2`, which `akgl_collision_test` turns into
 * #AKGL_ERR_COLLISION naming the high-water mark. A too-small arena is a loud
 * failure with a number in it, not a crash and not a silently missed collision.
 *
 * @warning **Not thread safe.** There is one arena and it has no lock. libakgl
 *          serialises its frame behind `akgl_game.statelock`; a caller running
 *          narrowphase queries from two threads needs a different design here,
 *          not a mutex bolted on.
 *
 * @note These are exported so the test suite can check that libccd really is
 *       allocating from here, and so a benchmark can record the high-water
 *       mark. A game has no reason to call them.
 */

#ifndef _AKGL_COLLISION_ARENA_H_
#define _AKGL_COLLISION_ARENA_H_

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include <akerror.h>

#include <akgl/types.h>

/**
 * @brief Bytes of static storage the arena hands out.
 *
 * Sized by measurement rather than derivation: the benchmark records the
 * high-water mark and this is set above the worst observed case. It bounds
 * memory the same way `ccd_t::max_iterations` bounds time, and both must be
 * bounded -- libccd's own default for the latter is `(unsigned long)-1`.
 */
#ifndef AKGL_CCD_ARENA_BYTES
#define AKGL_CCD_ARENA_BYTES    (64 * 1024)
#endif

/**
 * @brief Allocate or grow a block, in the shape `realloc(3)` has.
 *
 * This is what `__CCD_ALLOC_MEMORY` expands to inside libccd. A `NULL` @p ptr
 * allocates; a non-`NULL` one allocates a new block and copies the old contents
 * across, because a bump allocator cannot grow in place unless the block is the
 * most recent one, and it is not worth a special case for a path that runs at
 * most a few times per query.
 *
 * @param ptr  Block to grow, or `NULL` to allocate a new one.
 * @param size Bytes wanted. A @p size of 0 releases nothing and returns `NULL`,
 *             matching what libccd does with the result.
 * @return A pointer into the arena, or `NULL` when the arena is full.
 */
void *akgl_ccd_arena_realloc(void *ptr, size_t size);

/**
 * @brief Release a block. A no-op, because the whole arena is released at once.
 *
 * @param ptr Block to release. `NULL` is accepted, as `free(3)` accepts it.
 */
void akgl_ccd_arena_free(void *ptr);

/** @brief Drop everything. Called at the top of each narrowphase query. */
void akgl_ccd_arena_reset(void);

/**
 * @brief Bytes currently handed out.
 *
 * Zero after `akgl_ccd_arena_reset`, and non-zero after a narrowphase query
 * that allocated -- releasing a block does not lower it, because
 * akgl_ccd_arena_free is a no-op and the whole arena is released at once.
 *
 * Non-zero after a query is what proves libccd is allocating from here rather
 * than from the C library, which is worth checking rather than assuming: the
 * redirect lived in CMake's COMPILE_DEFINITIONS at first, where a function-like
 * macro cannot survive, and it was dropped without a diagnostic.
 */
size_t akgl_ccd_arena_used(void);

/** @brief The most bytes ever handed out at once, since the process started. */
size_t akgl_ccd_arena_highwater(void);

/** @brief How many allocations have been refused for want of room. */
uint32_t akgl_ccd_arena_exhausted(void);

/*
 * The following is part of the internal API. It is exposed so the test suite
 * can reach it and is not meant to be called by a game.
 */

/**
 * @brief Run one narrowphase query through libccd's allocating path.
 *
 * Two boxes of half-extent 1 are placed @p separation apart on x and tested
 * with `ccdGJKPenetration` -- the EPA path, chosen because EPA is the half of
 * libccd that allocates and therefore the half that exercises the arena. A
 * narrowphase on the frame path prefers MPR, which allocates nothing.
 *
 * This exists so a test can reach two things nothing else can. That a real
 * libccd query allocates from the arena rather than the C library, which
 * akgl_ccd_arena_used() shows. And that no `free` inside libccd escapes to the
 * C library, which is shown by the process surviving: the real `free(3)` given
 * a pointer it never issued is undefined behaviour, and glibc aborts on it.
 *
 * @param separation Distance between the two box centres on x. Below 2.0 they
 *                   overlap; above it they do not.
 * @param hit        Receives whether the boxes intersect. Required.
 * @param depth      Receives the penetration depth, or 0 when they do not
 *                   intersect. Required.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKERR_NULLPOINTER If @p hit or @p depth is `NULL`.
 * @throws AKGL_ERR_COLLISION If the arena ran out of room. The message carries
 *         the high-water mark and the current ceiling.
 */
akerr_ErrorContext AKERR_NOIGNORE *akgl_collision_arena_selftest(float32_t separation, bool *hit, float32_t *depth);

/**
 * @brief Narrow the arena, so a test can drive a real query into exhaustion.
 *
 * The interesting failure is not that akgl_ccd_arena_realloc returns `NULL` --
 * that is one comparison. It is what happens afterwards: libccd has to notice,
 * unwind its half-built polytope, and return `-2`, and the caller has to turn
 * that into #AKGL_ERR_COLLISION rather than reporting a missed collision or
 * dereferencing the `NULL`. Reaching that path any other way means rebuilding
 * the library with a smaller #AKGL_CCD_ARENA_BYTES, which CI cannot do and a
 * reader cannot repeat.
 *
 * @param limit Usable bytes. 0, or anything above #AKGL_CCD_ARENA_BYTES,
 *              restores the full arena.
 */
void akgl_ccd_arena_set_limit(size_t limit);

#endif
