make_crash_allocator :: () -> Allocator {
   return .{ proc = crash_allocator_proc };
}

crash_allocator_proc :: (mode: Allocator_Mode, size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
   message: string;

   if mode == {
      case .ALLOCATE;
         message = basic.tprint("Attempt to allocate % byte(s) using the crash allocator!", size);
      case .RESIZE;
         message = basic.tprint("Attempt to resize (from % to % byte(s)) using the crash allocator!", old_size, size);
      case .FREE;
         message = basic.tprint("Attempt to free % byte(s) using the crash allocator!", size);
   }

   loc := meta.get_stack_trace_caller_location();
   basic.assert(false, message, loc = loc);
   debug_break();
   return null;
}


Arena :: struct {
   memory:      *void;
   memory_size: u64;
   offset:      u64;
}

init_arena :: (a: *Arena, memory: *void, size: u64) {
   a.memory      = memory;
   a.memory_size = size;
   a.offset      = 0;
}

make_arena_allocator :: (arena: *Arena) -> Allocator {
   return .{
      data = arena,
      proc = xx arena_allocator_proc,
   };
}

Extended_Allocator_Mode :: enum {
   using Allocator_Mode;

   request_memory     :: Allocator_Mode.ALLOCATE;
   resize_memory      :: Allocator_Mode.RESIZE;
   release_memory     :: Allocator_Mode.FREE;
   first_time_used    :: Allocator_Mode.STARTUP;
   release_everything :: Allocator_Mode.SHUTDOWN;

   reset_state;
   save_point;         // should return *s64
   restore_save_point; // 'old_size' will be the dereferenced return value of 'save_point'
}

arena_allocator_proc :: (mode: Extended_Allocator_Mode, size: s64, old_size: s64, old_memory: *void, allocator_data: *void) -> *void {
   arena := allocator_data.(*Arena);
   if mode == {
      case .request_memory;
         return arena_alloc(arena, size);

      case .resize_memory;
         if old_memory == null {
            return arena_alloc(arena, size);
         }

         if size == 0 {
            return null;
         }

         if size == old_size {
            return old_memory;
         }

         new_memory := arena_alloc(arena, size);
         memcpy(new_memory, old_memory, old_size);
         return new_memory;

      case .save_point;
         return *arena.offset;

      case .restore_save_point;
         arena.offset = old_size.(u64);
   }

   return null;
}

arena_alloc :: (a: *Arena, count: int, alignment := Default_Align, loc := #caller_location) -> *void {
   basic.assert(a.memory != null, "arena: not initialized", loc = loc);
   basic.assert(power_of_two(alignment));

   end := a.memory.(*u8) + a.offset;
   ptr := align_to(end.(int), alignment);
   total_size := (count + ptr.(*u8) - end.(*u8)).(u64);

   basic.assert(a.offset + total_size <= a.memory_size, "arena: out of memory", loc = loc);
   a.offset += total_size;

   return ptr.(*void);
}


#scope_file;

basic :: #import "Basic"; // @future
meta :: #import "jc/meta";


// ----------------------------------------------------------
// TESTS
// ----------------------------------------------------------

#if RUN_TESTS {
   test :: #import "jc/test";

   #run {
      test.run("arena:basic", t => {
         memory := request_memory(1 * Kilobyte);
         defer     release_memory(memory);

         arena: Arena;
         init_arena(*arena, memory, 1 * Kilobyte);

         context.allocator = make_arena_allocator(*arena);
         save_point := allocator_save();

         i := request_memory(int);
         basic.assert(i != null);
         basic.assert(arena.offset == size_of(int));
         allocator_restore(save_point);
      });
   }
}
