#ifndef SCRATCH_H
#define SCRATCH_H

#include "arena.h"
#include "sys.h"
#include "thread_local.h"

#define SCRATCH_ARENAS_PER_THREAD 2
#define SCRATCH_ARENA_RESERVE (GIGABYTE(64))

/* ========================== *
 * Thread local state
 * ========================== */

struct scratch_ctx {
    struct arena arenas[SCRATCH_ARENAS_PER_THREAD];

#if RTC
    u64 next_scratch_id;
    u64 scratch_id_stack[16384];
    u64 scratch_id_stack_count;
#endif
};

THREAD_LOCAL_VAR_DECL_EXTERN(tl_scratch_ctx, struct scratch_ctx);

/* ========================== *
 * Scratch begin
 * ========================== */

INLINE void scratch_dbg_push(struct scratch_ctx *ctx, struct temp_arena *temp)
{
#if RTC
    if (ctx->scratch_id_stack_count >= ARRAY_COUNT(ctx->scratch_id_stack)) {
        sys_panic(STR("Max debug scratch depth reached"));
    }
    temp->scratch_id = ctx->next_scratch_id++;
    ctx->scratch_id_stack[ctx->scratch_id_stack_count++] = temp->scratch_id;
#else
    (UNUSED)ctx;
    (UNUSED)temp;
#endif
}

/* Any arena parameters in the calling function's context should be passed into this
 * function as a potential `conflict`. This is to prevent conflicts when the
 * context's arena is itself a scratch arena (since parameterized arenas are
 * often used to allocate persistent results for the caller).
 *
 * Call `scratch_begin_no_conflict` instead if there is no arena in the current
 * context that could potentially be a scratch arena. */
#define scratch_begin(c) _scratch_begin(c)

INLINE struct temp_arena _scratch_begin(struct arena *potential_conflict)
{
    /* This function is currently hard-coded to support 2 scratch arenas */
    CT_ASSERT(SCRATCH_ARENAS_PER_THREAD == 2);

    /* Use `scratch_begin_no_conflict` if no conflicts are present */
    ASSERT(potential_conflict != NULL);

    struct scratch_ctx *ctx = (struct scratch_ctx *)thread_local_var_eval(&tl_scratch_ctx);
    struct arena *scratch_arena = &ctx->arenas[0];
    if (potential_conflict && scratch_arena->base == potential_conflict->base) {
        scratch_arena = &ctx->arenas[1];
    }
    struct temp_arena temp = arena_temp_begin(scratch_arena);
    scratch_dbg_push(ctx, &temp);
    return temp;
}

/* This macro declares an unused "arena" variable that will error if an existing "arena"
 * variable is present (due to shadowing). This is for catching obvious cases of
 * `scratch_begin_no_conflict` getting called when an `arena` variable already
 * exists in the caller's context (`scratch_begin(arena)` should be called
 * instead). */
#define scratch_begin_no_conflict() \
    _scratch_begin_no_conflict(); \
    do { \
        struct arena *arena = NULL; \
        (UNUSED)arena; \
    } while (0)

INLINE struct temp_arena _scratch_begin_no_conflict(void)
{
    struct scratch_ctx *ctx = (struct scratch_ctx *)thread_local_var_eval(&tl_scratch_ctx);
    struct arena *scratch_arena = &ctx->arenas[0];
    struct temp_arena temp = arena_temp_begin(scratch_arena);
    scratch_dbg_push(ctx, &temp);
    return temp;
}

/* ========================== *
 * Scratch end
 * ========================== */

INLINE void scratch_end(struct temp_arena scratch_temp)
{
#if RTC
    struct scratch_ctx *ctx = (struct scratch_ctx *)thread_local_var_eval(&tl_scratch_ctx);
    if (ctx->scratch_id_stack_count > 0) {
        u64 scratch_id = scratch_temp.scratch_id;
        u64 expected_id = ctx->scratch_id_stack[--ctx->scratch_id_stack_count];
        /* This assertion exists to catch cases where a scratch_end was forgotten.
         * It will fail if a scratch arena is reset out of order.
         * E.g. there is a missing scratch_end somewhere on a different scratch
         * arena (one that was created between the scratch_begin & the
         * scratch_end of the arena being reset here). */
        ASSERT(scratch_id == expected_id);
    }
#endif
    arena_temp_end(scratch_temp);
}

#endif
