#pragma once

struct ExpandableArena; // fwd declare #temp

#if OS_WINDOWS
  const u32 ARENA_DEFAULT_COMMIT_PAGE_COUNT = 16;    // 16 * 4k page = 64kB
  const s64 ARENA_DEFAULT_COMMIT_SIZE_BYTES = 65536;
#endif

constexpr u16 ARENA_DEFAULT_ALIGNMENT = CPU_REGISTER_WIDTH_BYTES;

#define ARENA_DEBUG BUILD_DEBUG

constexpr s32 Arena_Reserve_Count = 7;
enum class Arena_Reserve: u8 {
  Size_64K = 0, // these are used as indices in Arena_Free_List!
  Size_2M  = 1,
  Size_64M = 2,
  Size_2G  = 3,
  Size_64G = 4,
  Size_2T  = 5,
  Size_64T = 6,
};

constexpr s64 Arena_Sizes[Arena_Reserve_Count] = {
    64LL * 1024,
     2LL * 1024 * 1024,
    64LL * 1024 * 1024,
     2LL * 1024 * 1024 * 1024,
    64LL * 1024 * 1024 * 1024,
     2LL * 1024 * 1024 * 1024 * 1024,
    64LL * 1024 * 1024 * 1024 * 1024,
};

enum class Arena_Flags: u8 { 
  None = 0, 
  Chained = 0x01, 
  Is_Bootstrapped = 0x02,
  Large_Pages = 0x40, 
  // Secure_Arena = 0xF0 // #NOTE: Secure Arenas are not implemented yet!
};

force_inline Arena_Flags operator | (Arena_Flags a, Arena_Flags b) {
  return (Arena_Flags)(((u8)a) | ((u8)b));
}

force_inline Arena_Flags operator & (Arena_Flags a, Arena_Flags b) {
  return (Arena_Flags)(((u8)a) & ((u8)b));
}

force_inline Arena_Flags& operator |= (Arena_Flags& a, Arena_Flags b) {
    a = a | b;
    return a;
}

force_inline Arena_Flags operator ~ (Arena_Flags a) {
  return (Arena_Flags)(~((u8)a));
}

struct Arena {
  u8* current_point               = nullptr;
  u8* memory_base                 = nullptr;
  u8* first_uncommitted_page      = nullptr;
  u16 alignment                   = CPU_REGISTER_WIDTH_BYTES;
  Arena_Reserve reserve_size      = Arena_Reserve::Size_64K;
  Arena_Flags flags               = Arena_Flags::None;
  u32 initial_commit_page_count   = ARENA_DEFAULT_COMMIT_PAGE_COUNT;
#if BUILD_DEBUG 
  string file_path;
  string function_name;
  s32 line_number;
#endif
};

typedef void* (*Memory_Wipe_Function)(void* memory, u64 byte_count);

void* arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_size, void* old_memory, void* allocator_data);

// Main API
#if BUILD_DEBUG
#define bootstrap_arena(_reserve_) \
  bootstrap_arena_internal((_reserve_), (s32)ARENA_DEFAULT_COMMIT_PAGE_COUNT, __FILE__, __FUNCTION__, __LINE__)
#else
#define bootstrap_arena(_reserve_) \
  bootstrap_arena_internal((_reserve_), (s32)ARENA_DEFAULT_COMMIT_PAGE_COUNT)
#endif

Arena* bootstrap_arena_internal (Arena_Reserve new_reserve, s32 commit_page_count=ARENA_DEFAULT_COMMIT_PAGE_COUNT,
        string file_path="", string function_name="", s32 line_number=0);

void arena_init (Arena* arena, Arena_Reserve new_reserve, s32 commit_page_count=16); // For when we're *not* bootstrapping arenas: (I'm debating if we should keep this..)
bool arena_commit_first_pages (Arena* arena, s64 commit_size, s64 start_offset=0); // This is useful for initializing arenas (arena_init), and for starting Arena-backed arrays.

void arena_clear_flags (Arena* arena);
void arena_set_bootstrap_flag (Arena* arena);
void arena_set_secure_flag (Arena* arena);

bool arena_is_bootstrapped (Arena* arena);

void arena_reset_keeping_memory (Arena* arena); // just sets current point to arena_start
void arena_reset (Arena* arena); // frees excess pages
void arena_reset_overwriting_memory (Arena* arena, Memory_Wipe_Function wipe_function);

// Internal API (should not be called directly by program)
void* arena_alloc (Arena* arena, s64 byte_count);

// Utilties
u8* arena_start (Arena* arena);
u8* arena_address_limit (Arena* arena);
s64 arena_usage_bytes (Arena* arena);
s64 arena_usage_committed_bytes (Arena* arena);
// arena_details (requires print)
s64 reserve_size (Arena* arena);
s64 reserve_size (Arena_Reserve ar);
bool is_valid (Arena* arena);
Allocator allocator (Arena* arena);

// Platform-Specific Implementations (forward-declared)
void platform_init (Arena* arena, s64 new_reserve);
void extend_committed_pages (Arena* arena, u8* end);
void free_pages_down_to (Arena* arena, s64 pages_to_keep);
void arena_delete (Arena* arena);

Arena_Reserve next_reserve_size (s64 size);

struct Push_Alignment { // #rename to Arena_Push_Alignment?
  Arena* arena;
  u16 original_alignment;
  
  Push_Alignment(Arena* arena, u16 alignment) {
    Assert(is_valid(arena));
    this->arena = arena;
    this->original_alignment = arena->alignment;
    this->arena->alignment = alignment;
  }
  
  Push_Alignment(ExpandableArena* arena_ex, u16 alignment) {
    Push_Alignment((Arena*)arena_ex, alignment);
  }
  
  ~Push_Alignment() {
    arena->alignment = original_alignment;
  }
};


// Do this later:
// arena_lock_pages :: (using arena: *Arena, start_address: *u8, byte_count: s64)
// arena_unlock_pages :: (using arena: *Arena, start_address: *u8, byte_count: s64)

// #FixedArena is a super simple arena where you allocate a fixed block up front (fully committed), 
// and use it as-is. 
// #NOTE: we can save space be always backing with a known allocator (e.g. default_allocator()).
struct FixedArena {
  ArrayView<u8> memory;
  s64 cursor;
  Allocator allocator;
};

void* fixed_arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_size, void* old_memory, void* allocator_data);

// #FixedArena API
FixedArena* bootstrap_fixed_arena (s64 size, Allocator backing_allocator = { default_allocator_proc, nullptr });
force_inline void destroy_arena (FixedArena* arena);
Allocator allocator (FixedArena* arena);

s64 bytes_in_use (ArrayView<Arena*> arenas) {
  // does not include overhead from committed pages!
  s64 sum = 0;
  
  for (s64 i = 0; i < arenas.count; i += 1) {
    sum += arena_usage_bytes(arenas[i]);
  }
  
  return sum;
}

s64 committed_bytes (ArrayView<Arena*> arenas) {
  s64 sum = 0;
  
  for (s64 i = 0; i < arenas.count; i += 1) {
    sum += arena_usage_committed_bytes(arenas[i]);
  }
  
  return sum;
}

#if BUILD_DEBUG
  global Mutex arenas_in_use_mutex;
  global Array<Arena*> arenas_in_use;
  
  force_inline void initialize_arenas_in_use_list ();
  force_inline void add_arena_to_in_use_list(Arena* arena);
  force_inline void remove_arena_from_in_use_list (Arena* arena);
#endif
