#include "arena.h"
#include "sys.h"
#include "memory.h"
#include "string.h"

/* Arbitrary block size */
#define ARENA_BLOCK_SIZE 4096

/* NOTE: Application will exit if arena fails to reserve or commit initial
 * memory. */
struct arena arena_alloc(u64 reserve)
{
    __prof;
    struct arena arena = { 0 };

    /* Round up to nearest block size */
    u64 block_remainder = reserve % ARENA_BLOCK_SIZE;
    if (block_remainder > 0) {
        reserve += ARENA_BLOCK_SIZE - block_remainder;
    }

    arena.base = sys_memory_reserve(reserve);
    if (!arena.base) {
        /* Hard fail on memory reserve failure for now */
        sys_panic(STR("Failed to reserve memory"));
    }
    arena.reserved = reserve;

    /* Commit one block to start with */
    arena.base = sys_memory_commit(arena.base, ARENA_BLOCK_SIZE);
    __profalloc(arena.base, ARENA_BLOCK_SIZE);
    ASAN_POISON(arena.base, ARENA_BLOCK_SIZE);
    if (!arena.base) {
        /* Hard fail on commit failure */
        sys_panic(STR("Failed to commit initial memory block: System may be out of memory"));
    }
    arena.committed = ARENA_BLOCK_SIZE;

    /* Arena should be 64k aligned */
    ASSERT(((u64)arena.base & 0xFFFF) == 0);

    return arena;
}

void arena_release(struct arena *arena)
{
    __prof;
    __proffree(arena->base);
    sys_memory_release(arena->base);
}

/* NOTE: Application will exit if arena fails to commit memory */
void *_arena_push_bytes(struct arena *arena, u64 size, u64 align)
{
    ASSERT(align > 0);
    ASSERT(!arena->readonly);

    void *start = NULL;

    /* TODO: Remove this check? (Only here to avoid aligning when size = 0) */
    if (size > 0) {
        u64 aligned_start_pos = (arena->pos + (align - 1));
        aligned_start_pos -= aligned_start_pos % align;

        u64 new_pos = aligned_start_pos + size;
        if (new_pos > arena->committed) {
            __profscope(_arena_push_bytes_COMMIT);
            /* Commit new block(s) */
            u64 blocks_needed = (new_pos - arena->committed + ARENA_BLOCK_SIZE - 1) / ARENA_BLOCK_SIZE;
            u64 commit_bytes = blocks_needed * ARENA_BLOCK_SIZE;
            u64 new_capacity = arena->committed + commit_bytes;
            if (new_capacity > arena->reserved) {
                /* Hard fail if we overflow reserved memory for now */
                sys_panic(STR("Failed to commit new memory block: Overflow of reserved memory"));
            }
            void *commit_address = arena->base + arena->committed;
            if (!sys_memory_commit(commit_address, commit_bytes)) {
                /* Hard fail on memory allocation failure for now */
                sys_panic(STR("Failed to commit new memory block: System may be out of memory"));
            }
            __proffree(arena->base);
            __profalloc(arena->base, arena->committed + commit_bytes);
            ASAN_POISON(commit_address, commit_bytes);
            arena->committed += commit_bytes;
        }
        start = arena->base + aligned_start_pos;
        arena->pos = new_pos;
        ASAN_UNPOISON(start, (arena->base + arena->pos) - (u8 *)start);
    } else {
        start = arena->base + arena->pos;
    }

    return start;
}

/* Copies the memory from the source arena into the destination arena,
 * replacing old contents. Destination arena will be expanded if necessary. */
void arena_copy_replace(struct arena *dest, struct arena *src)
{
    arena_reset(dest);
    u64 data_size = src->pos;
    u8 *data_src = src->base;
    u8 *data_dest = _arena_push_bytes(dest, data_size, 1);
    MEMCPY(data_dest, data_src, data_size);
}

void arena_decommit_unused_blocks(struct arena *arena)
{
    ASSERT(!arena->readonly);
    u64 next_block_pos = ARENA_BLOCK_SIZE * ((arena->pos + (ARENA_BLOCK_SIZE - 1)) / ARENA_BLOCK_SIZE);
    if (arena->committed > next_block_pos) {
        u8 *decommit_start = arena->base + next_block_pos;
        u64 decommit_size = (arena->base + arena->committed) - decommit_start;
        sys_memory_decommit(decommit_start, decommit_size);
        arena->committed = next_block_pos;
    }
}

void arena_set_readonly(struct arena *arena)
{
    sys_memory_set_committed_readonly(arena->base, arena->committed);
#if RTC
    arena->readonly = true;
#endif
}
