#pragma once

#define ALLOCATOR_DEBUG_MODE 1
#define ALLOCATOR_POISON_MEMORY_ON_ALLOCATION \
                  (BUILD_DEBUG && ALLOCATOR_DEBUG_MODE)
                  
#if ALLOCATOR_POISON_MEMORY_ON_ALLOCATION 
  #define ALLOCATOR_INIT_VALUE 0xCD
#else
  #define ALLOCATOR_INIT_VALUE 0
#endif

enum class Allocator_Mode: s32 {
    ALLOCATE   = 0,
    RESIZE     = 1,
    DEALLOCATE = 2,
    // IS_THIS_YOURS = 3,
    DETAILS    = 4, // #who_am_i
};

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

struct Allocator {
    Allocator_Proc proc;
    void* data;
    
    bool operator ! () {
      return (proc == nullptr);
    }
    
    bool operator==(const Allocator& other) const {
      return proc == other.proc && data == other.data;
    }

    bool operator!=(const Allocator& other) const {
      return !(*this == other);
    }
};

// Public Allocator API:
// Note that alignment is handled on a per-allocator basis.
void* internal_alloc    (s64 size);
void  internal_free     (void* memory);
void* internal_realloc  (void* memory, s64 size, s64 old_size);

template <typename T> T* New (Allocator allocator, bool initialize=true) {
  auto memory = (T*)allocator.proc(Allocator_Mode::ALLOCATE, sizeof(T), 0, nullptr, allocator.data);
  
  if (initialize) {
    (*memory) = T();
  }
  
  return memory;
}

template <typename T> T* New (bool initialize=true) {
  auto memory = (T*)internal_alloc(sizeof(T));
  
  if (initialize) {
    (*memory) = T();
  }
  
  return memory;
}

// For raw-pointer arrays.
template <typename T> T* NewArray (Allocator allocator, s64 count, bool initialize=true) {
  auto memory = (T*)allocator.proc(Allocator_Mode::ALLOCATE, count * sizeof(T), 0, nullptr, allocator.data);
  
  if (initialize) {
    for (s64 i = 0; i < count; i += 1) {
      memory[i] = T();
    }
  }
  
  return memory;
}

template <typename T> T* NewArray (s64 count, bool initialize=true) {
  auto memory = (T*)internal_alloc(count * sizeof(T));

  if (initialize) {
    for (s64 i = 0; i < count; i += 1) {
      memory[i] = T();
    }
  }

  return memory;
}

// Likely will rarely be used, if ever. See: internal_realloc
template <typename T> force_inline T* Resize (Allocator allocator, void* memory, s64 size, s64 old_size, bool initialize=true) {
  void* result = allocator.proc(Allocator_Mode::RESIZE, size, old_size, memory, allocator.data);
  return result;
}

// There's not really any reason for this to be a template
force_inline void Delete (Allocator allocator, void* memory) {
  allocator.proc(Allocator_Mode::DEALLOCATE, 0, 0, memory, allocator.data);
}

// We use internal functions when we assume the user just wants to use the
// current allocator on the context.
// For Resizes and Deletes, use internal_realloc and internal_free.

// #NOTE: Initialize<T> and reset_struct<T> are exactly the same!
template <typename T> force_inline void Initialize (T* memory) { (*memory) = T(); }
template <typename T> force_inline void reset_struct (T* src)  { (*src)    = T(); }

template <typename T> void zero_struct (T* src) {
  memset(src, 0, sizeof(T));
}

template <typename T> void poison_struct (T* src) {
  memset(src, 0xCD, sizeof(T));
}

template <typename T> T* copy_struct (T* src) {
  T* dst = New<T>(false);
  memcpy(dst, src, sizeof(T));
}

// #TODO: Turn this into a macro that also provides the 
// file, line number to the print.
void print_context_allocator ();