#include <stdlib.h>
#include <string.h>

#if !defined(m_reserve)
#error missing definition for 'm_reserve' type: (U64)->void*
#endif
#if !defined(m_commit)
#error missing definition for 'm_commit' type: (void*, U64)->void
#endif
#if !defined(m_decommit)
#error missing definition for 'm_decommit' type: (void*, U64)->void
#endif
#if !defined(m_release)
#error missing definition for 'm_release' type: (void*, U64)->void
#endif

static Arena *g_scratch_arena = 0;

root_function void
m_change_memory_noop(void *ptr, U64 size) {}

// Malloc implementation of the M_Base_memory
root_function void*
m_malloc_reserve(U64 size) {
  return malloc(size);
}

root_function void
m_malloc_release(void *ptr, U64 size) {
  free(ptr);
}

//~ 64-bit memory arena

root_function Arena
*m_make_arena_reserve(U64 reserve_size) {
  Arena *result = 0;
  U64 initial_commit_size = ARENA_COMMIT_GRANULARITY;
  if (reserve_size >= initial_commit_size) {
    void *memory = m_reserve(reserve_size);
    // Since we use "header" space we must ensure the initial commit can fit the Arena struct.
    Assert(initial_commit_size >= sizeof(Arena));
    m_commit(memory, ARENA_COMMIT_GRANULARITY);
    result = (Arena*)memory;//(Arena*)result; <- this has to be mistake in Allen's video.. ?
    // After we have pointed to our newly reserved and commited memory,
    // we fill in the "header" parts, which are just the members of the arena type.
    result->capacity = reserve_size;
    result->commit_pos = initial_commit_size;
    result->align = 8; // 8-bytes alignment?
    result->pos = sizeof(Arena); // Here we point the position to after the Arena "header" section.
  }
  return result;
}

root_function Arena*
m_make_arena() {
  Arena* result = m_make_arena_reserve(M_DEFAULT_RESERVE_SIZE);
  return result;
}

root_function void*
m_arena_push(Arena *arena, U64 size) {
  void *result = 0;
  if (arena->pos + size <= arena->capacity) {
    /*U8 *base = (U8 *)arena; // Get memory base pointer.
        // Adjust by any alignment if necessary.
        // Doing modulo ensures we get a number in the 0-align-1 range.
        U64 post_align_pos = (arena->pos + (arena->align-1)):
        post_align_pos = post_align_pos % arena->align;
        // What's happening here? Almost certainly the align will overflow here?
        // Are we filling the allocated space backwards or what's up?
        // TODO(anton): UNDERSTAND
        U64 align = post_align_pos - arena->pos;
        result = base + arena->pos + align;
        arena->pos += size + align;*/
    // Do Allen4th version until I understand the above.
    result = ((U8*) arena) + arena->pos;
    arena->pos += size; // increment pos by what we want to push
        
    U64 p = arena->pos;
    U64 commit_p = arena->commit_pos;
    if (p > commit_p) {
      U64 p_aligned = AlignUpToPow2(p, M_COMMIT_BLOCK_SIZE);
      U64 next_commit_p = ClampTop(p_aligned, arena->capacity); // Make sure new commit_p won't overshoot capacity
      U64 commit_size = next_commit_p - commit_p;
      m_commit((U8 *)arena + commit_p, commit_size);
      arena->commit_pos = next_commit_p;
    }
  } else {
    // NOTE(anton): Should implement some fallback but now we fail.
  }
  return result;
}

root_function void
m_arena_pop_to(Arena *arena, U64 pos) {
  if (pos < arena->pos) {
    arena->pos = pos;
        
    U64 p = arena->pos;
    U64 p_aligned = AlignUpToPow2(p, M_COMMIT_BLOCK_SIZE);
    U64 next_commit_p = ClampTop(p_aligned, arena->capacity);
        
    U64 commit_p = arena->commit_pos;
    if (next_commit_p < commit_p) {
      U64 decommit_size = commit_p - next_commit_p;
      m_decommit((U8 *)arena + next_commit_p, decommit_size);
      arena->commit_pos = next_commit_p;
    }
  }
}

root_function void m_arena_pop(Arena* arena, U64 size) {
  U64 min_pos = sizeof(Arena);
  U64 size_to_pop = Min(size, arena->pos);
  U64 new_pos = arena->pos - size_to_pop;
  new_pos = Max(new_pos, min_pos);
  m_arena_pop_to(arena, new_pos);
}

/** Push size and set the memory to zero. */
root_function void*
m_arena_push_zero(Arena *arena, U64 size) {
  void *result = m_arena_push(arena, size);
  MemoryZero(result, size);
  return result;
}

root_function void 
m_arena_clear(Arena *arena)
{
  // We clear the input arena by popping off everything
  // after the actual Arena information.
  m_arena_pop_to(arena, sizeof(Arena));
}

root_function void
m_arena_align(Arena *arena, U64 pow2_alignment) {
  U64 p = arena->pos;
  U64 p_aligned = AlignUpToPow2(p, pow2_alignment);
  U64 z = p_aligned - p;
  if (z > 0) {
    m_arena_push(arena, z);
  }
}

root_function void
m_arena_release(Arena* arena) {
  m_release(arena, arena->capacity);
}

root_function ArenaTemp
m_arena_temp_begin(Arena *arena) {
  ArenaTemp temp = { 0 };
  temp.arena = arena;
  temp.pos = arena->pos;
  return temp;
}

root_function void
m_arena_temp_end(ArenaTemp temp) {
  m_arena_pop_to(temp.arena, temp.pos);
}
