#pragma once

#if BUILD_DEBUG
constexpr s64 ARRAY_ARENA_START_OFFSET = 2 * CPU_CACHE_LINE_SIZE; // sizeof(Arena)+sizeof(array)
#else
constexpr s64 ARRAY_ARENA_START_OFFSET = 1 * CPU_CACHE_LINE_SIZE; // double check this lol
#endif

template <typename T>
struct ArenaArray { // #downcasts to an ArrayView.
  using ValueType = T;
  s64 count;
  T* data;
  s64 allocated;
  Arena* arena;
  
  ArenaArray() {
    memset(this, 0, sizeof(*this));
  }
  
  T& operator[] (s64 index) {
#if ARRAY_ENABLE_BOUNDS_CHECKING
    if (index < 0 || index >= count) { debug_break(); } // index out of bounds
#endif

    return static_cast<T*>(data)[index];
  }
};

template <typename T> s64 arena_array_usage_bytes (ArenaArray<T>* array) {
  return (ARRAY_ARENA_START_OFFSET + array->count * sizeof(T));
}

static_assert(ARRAY_ARENA_START_OFFSET >= sizeof(Arena) + sizeof(ArenaArray<u8>));

// #NOTE: I am not defining arena_array_init (ArenaArray<T>*), because I do not want to
// encourage it's usage!
// This may not be correct - check!
template <typename T> void arena_array_init (ArenaArray<T>* array, Arena_Reserve reserve_size, s64 preallocate_count, string label="") {
  string arena_label = format_string(temp(), "ArenaArray:init (%s)", label.data);
  array->arena = bootstrap_arena(reserve_size, arena_label);
  push_arena(array->arena);
  push_alignment(array->arena, 1);
  
  s64 commit_size_bytes = preallocate_count * sizeof(T);
  if (arena_commit_first_pages(array->arena, commit_size_bytes, ARRAY_ARENA_START_OFFSET)) {
    array->allocated = preallocate_count;
  }
  
  array->count = 0;
  
  array->data  = array_start<T>(*array);
}

// Use arena_array_free to delete, see: array_reset and reset_keeping_memory for resetting.
template <typename T>
ArenaArray<T>* arena_array_new (s64 preallocate_count, Arena_Reserve reserve_size, string label="") {
  string arena_label = format_string(temp(), "ArenaArray:new (%s)", label.data);
  Arena* arena = bootstrap_arena(reserve_size, arena_label);
  push_arena(arena);
  push_alignment(arena, 1);
  ArenaArray<T>* array = New<ArenaArray<T>>(true);
  array->arena = arena;
  
  s64 commit_size_bytes = preallocate_count * sizeof(T);
  if (arena_commit_first_pages(array->arena, commit_size_bytes, ARRAY_ARENA_START_OFFSET)) {
    array->allocated = preallocate_count;
  }
  
  array->count = 0;
  array->arena = arena;

  array->data  = array_start<T>(*array);
  
  return array;
}

template <typename T> T* array_start (ArenaArray<T>& array) {
  T* memory = (T*)(array.arena->memory_base + ARRAY_ARENA_START_OFFSET);
  Assert((u8*)memory >= (u8*)array.arena->current_point);
  return memory;
}

s64 max_array_size (ArenaArray<u8>& array) {
  u8* address_limit = array.arena->memory_base + reserve_size(array.arena);
  u8* address_start = array_start(array);
  return (s64)(address_limit - address_start);
}

template <typename T> bool is_valid (ArenaArray<T>* array) {
  if (array == nullptr) return false;
  if (array->arena == nullptr) return false;
  return is_valid(array->arena);
}

template <typename T> bool is_empty (ArenaArray<T>& array) {
  return ((array.count == 0) || !is_valid(array.arena));
}

template <typename T> s64 memory_usage (ArenaArray<T>& array) {
  if (array == nullptr) return 0;
  return arena_usage_committed_bytes(array.arena);
}

template <typename T> void arena_array_free (ArenaArray<T>& array) {
  arena_delete(array.arena);
}

template <typename T> ArrayView<T> array_view (ArenaArray<T> array) {
  ArrayView<T> av;
  av.count = array.count;
  av.data = array.data;
  return av;
}

template <typename T> ArrayView<T> to_view (ArenaArray<T>& array) {
  ArrayView<T> av;
  av.count = array.count;
  av.data = array.data;
  return av;
}

template <typename T> ArrayView<T> to_view (ArenaArray<T>& array, s64 start_offset, s64 count) {
  Assert(start_offset >= 0); Assert(count >= 0);
  
  ArrayView<T> av = { 0, nullptr };
  
  if (start_offset >= array.count) {
    return av; // empty
  }

  av.count = count;
  av.data = array.data + start_offset;
  
  if (start_offset + count > array.count) {
    av.count = array.count - offset;
  }
  
  return av;
}

template <typename T> void copy_from_view (ArenaArray<T>* array, ArrayView<T> view) {
  T* start = array->data + array->count;
  s64 new_count = array->count + view.count;
  array_resize(*array, new_count, false);
  memcpy(start, view.data, view.count * sizeof(T));
}

template <typename T> void array_add (ArenaArray<T>& array, ArrayView<T> items) {
  T* current_point = &array.data[array.count];
  s64 final_count = array.count + items.count;
  
  if (array.allocated < final_count) {
    array_reserve(array, final_count);
  }
  
  memcpy(current_point, items.data, items.count * sizeof(T));
  
  array.count += items.count;
}

template <typename T> void array_add (ArenaArray<T>& array, T item) {
  maybe_grow(array);
  array.data[array.count] = item;
  array.count += 1;
}

template <typename T> T* array_add (ArenaArray<T>& array) {
  maybe_grow(array);
  
  T* result = &array.data[array.count];
  
  (*result) = T();
  
  array.count += 1;
  return result;
}

template <typename T> force_inline void maybe_grow (ArenaArray<T>& array) {
  if (array.count >= array.allocated) {
    s64 reserve = 2 * array.allocated;
    if (reserve < 8) 
      reserve = 8; // so we don't try to reserve 0 bytes.
    reserve_internal((ArenaArray<u8>&)array, reserve, sizeof(T));
  }
}

template <typename T> force_inline void array_reserve (ArenaArray<T>& array, s64 desired_item_count) {
  reserve_internal((ArenaArray<u8>&)array, desired_item_count, sizeof(T));
}

template <typename T> void array_resize (ArenaArray<T>& array, s64 desired_item_count, bool initialize=true) {
  s64 old_count = array.count;
  reserve_internal((ArenaArray<u8>&)array, desired_item_count, sizeof(T));
  array.count = desired_item_count;
  if (initialize) {
    init_range(array.data, old_count, desired_item_count);
  }
}

void array_arena_realloc (ArenaArray<u8>& array, s64 new_size, s64 old_size) {
  Assert(new_size <= max_array_size(array));
  
  u8* array_begin = (u8*)array.data;
  void* result_end = (void*)(array_begin + new_size);
  
  // Check if we need more pages:
  if (result_end > array.arena->first_uncommitted_page) {
    // Critical error if we run out of address space!
    if (result_end > arena_address_limit(array.arena)) {
      printf("[Error] Failed to allocate because Arena is full and cannot expand!\n");
      Assert(false); // Failed to allocate because Arena is full and cannot expand
      return;
    }
    
    extend_committed_pages(array.arena, (u8*)result_end);
  }
}

void reserve_internal (ArenaArray<u8>& array, s64 desired_item_count, s64 element_size) {
  if (desired_item_count <= array.allocated) return;
  
  array_arena_realloc(array, desired_item_count * element_size, array.allocated * element_size);
  
  array.allocated = desired_item_count;
}

template <typename T> void init_range (T* ptr, s64 start_offset, s64 end_offset) {
  for (s64 i = start_offset; i < end_offset; i += 1) {
    T* current_item = ptr + i;
    (*current_item) = T(); // is this correct in-place init?
  }
}

template <typename T> void array_poison_range (ArenaArray<T>& array, s64 start, s64 count) {
#if BUILD_DEBUG
  if (count == 0) return;
  Assert(start >= 0 && start < array.count);
  Assert(start + count <= array.count);
  // Check that these ranges make sense
  T* start_address = &array[start];
  memset(start_address, 0xCD, count * sizeof(T));
#endif
}

template <typename T> force_inline void array_reset (ArenaArray<T>& array) {
  // reset backing arena:
  arena_reset(array.arena);
  array.count = 0;
  // should be # of bytes committed / sizeof(T):
  s64 committed_bytes = (s64)(array.arena->first_uncommitted_page - array.data);
  array.allocated = committed_bytes / sizeof(T);
}

template <typename T> force_inline void reset_keeping_memory (ArenaArray<T>& array) {
  array.count = 0;
}

template <typename T> force_inline void ordered_remove_by_index (ArenaArray<T>& array, s64 index) {
  Assert(index >= 0);  Assert(index < array.count);
  
  for (s64 i = index; i < array.count-1; i += 1) {
    array.data[i] = array.data[i - 1];
  }
  
  array.count -= 1;
}

template <typename T> force_inline void unordered_remove_by_index (ArenaArray<T>& array, s64 index) {
  Assert(index >= 0);  Assert(index < array.count);
  
  s64 last_index = array.count - 1;
  if index != last_index { array.data[index] = array.data[last_index]; }
  
  array.count -= 1;
}

template <typename T> void insert_at(ArenaArray<T>& array, s64 offset, ArenaArray<T>& src_array) {
  Assert(offset >= 0 && offset <= array.count);
  if (!is_valid(src_array)) return;
  
  s64 new_count = array.count + src_array.count;
  array_reserve(array, new_count);
  
  T* src = array.data + offset;
  T* dst = src + src_array.count;
  memcpy(dst, src, (array.count - offset) * sizeof(T));
  
  memcpy(array.data + offset, src_array.data, src_array.count * sizeof(T));
}

template <typename T> T pop (ArenaArray<T>& array) {
  T result = array.data[array.count-1];
  array.count -= 1;
  return result;
}

template <typename T> T peek (ArenaArray<T>& array) {
  return array.data[array.count-1];
}

template <typename T> T* peek_pointer (ArenaArray<T>& array) {
  return &(array.data[array.count-1]);
}

template <typename T> void delete_range (ArenaArray<T>& array, s64 start_offset, s64 count) {
  Assert(start_offset >= 0 && count >= 0 && start_offset + count <= array.count);
  memcpy(array.data + start_offset, array.data + start_offset + count, (array.count - start_offset - count) * sizeof(T));
  array.count -= count;
}

template <typename T> ArenaArray<T>& array_copy (ArenaArray<T>& array) {
  auto new_array = arena_array_new<T>(array.arena.reserve_size);
  array_reserve(new_array, array.count);
  memcpy(new_array.data, array.data, array.count * sizeof(T));
  new_array.count = array.count;
  return new_array;
}

template <typename T> ArrayView<T> array_copy_as_view (ArenaArray<T>& array) {
  ArrayView<T> view = { array.count, array.data };
  return view;
}

