////////////////////////////////
// NOTE(allen): Static Configuration

#if !defined(MEM_DEFAULT_RESERVE_SIZE)
# define MEM_DEFAULT_RESERVE_SIZE GB(1)
#endif
#if !defined(MEM_COMMIT_BLOCK_SIZE)
# define MEM_COMMIT_BLOCK_SIZE MB(64)
#endif
#if !defined(MEM_MAX_ALIGN)
# define MEM_MAX_ALIGN 64
#endif
#if !defined(MEM_SCRATCH_POOL_COUNT)
# define MEM_SCRATCH_POOL_COUNT 2
#endif

////////////////////////////////
// NOTE(allen): Symbolic Constant Functions

link_function OperatingSystem
operating_system_from_context(void){
  OperatingSystem result = OperatingSystem_Null;
#if OS_WINDOWS
  result = OperatingSystem_Windows;
#elif OS_LINUX
  result = OperatingSystem_Linux;
#elif OS_MAC
  result = OperatingSystem_Mac;
#endif
  return(result);
}

link_function Architecture
architecture_from_context(void){
  Architecture result = Architecture_Null;
#if ARCH_X64
  result = Architecture_X64;
#elif ARCH_X86
  result = Architecture_X86;
#elif ARCH_ARM
  result = Architecture_Arm;
#elif ARCH_ARM64
  result = Architecture_Arm64;
#endif
  return(result);
}

link_function char*
string_from_operating_system(OperatingSystem os){
  char *result = "(null)";
  switch (os){
    case OperatingSystem_Windows:
    {
      result = "windows";
    }break;
    case OperatingSystem_Linux:
    {
      result = "linux";
    }break;
    case OperatingSystem_Mac:
    {
      result = "mac";
    }break;
  }
  return(result);
}

link_function char*
string_from_architecture(Architecture arch){
  char *result = "(null)";
  switch (arch){
    case Architecture_X64:
    {
      result = "x64";
    }break;
    case Architecture_X86:
    {
      result = "x86";
    }break;
    case Architecture_Arm:
    {
      result = "arm";
    }break;
    case Architecture_Arm64:
    {
      result = "armm64";
    }break;
  }
  return(result);
}

link_function char*
string_from_month(Month month){
  char *result = "(null)";
  switch (month){
    case Month_Jan:
    {
      result = "jan";
    }break;
    case Month_Feb:
    {
      result = "feb";
    }break;
    case Month_Mar:
    {
      result = "mar";
    }break;
    case Month_Apr:
    {
      result = "apr";
    }break;
    case Month_May:
    {
      result = "may";
    }break;
    case Month_Jun:
    {
      result = "jun";
    }break;
    case Month_Jul:
    {
      result = "jul";
    }break;
    case Month_Aug:
    {
      result = "aug";
    }break;
    case Month_Sep:
    {
      result = "sep";
    }break;
    case Month_Oct:
    {
      result = "oct";
    }break;
    case Month_Nov:
    {
      result = "nov";
    }break;
    case Month_Dec:
    {
      result = "dec";
    }break;
  }
  return(result);
}

link_function char*
string_from_day_of_week(DayOfWeek day_of_week){
  char *result = "(null)";
  switch (day_of_week){
    case DayOfWeek_Sunday:
    {
      result = "sunday";
    }break;
    case DayOfWeek_Monday:
    {
      result = "monday";
    }break;
    case DayOfWeek_Tuesday:
    {
      result = "tuesday";
    }break;
    case DayOfWeek_Wednesday:
    {
      result = "wednesday";
    }break;
    case DayOfWeek_Thursday:
    {
      result = "thursday";
    }break;
    case DayOfWeek_Friday:
    {
      result = "friday";
    }break;
    case DayOfWeek_Saturday:
    {
      result = "saturday";
    }break;
  }
  return(result);
}

////////////////////////////////
// NOTE(allen): Time Functions

link_function DenseTime
dense_time_from_date_time(DateTime *in){
  U32 year_encoded = (U32)((S32)in->year + 0x8000);
  DenseTime result = 0;
  result += year_encoded;
  result *= 12;
  result += (in->mon - 1);
  result *= 31;
  result += (in->day - 1);
  result *= 24;
  result += in->hour;
  result *= 60;
  result += in->min;
  result *= 61;
  result += in->sec;
  result *= 1000;
  result += in->msec;
  return(result);
}

link_function DateTime
date_time_from_dense_time(DenseTime in){
  DateTime result = {0};
  result.msec = in%1000;
  in /= 1000;
  result.sec = in%61;
  in /= 61;
  result.min = in%60;
  in /= 60;
  result.hour = in%24;
  in /= 24;
  result.day = (in%31) + 1;
  in /= 31;
  result.mon = (in%12) + 1;
  in /= 12;
  S32 year_encoded = (S32)in;
  result.year = (year_encoded - 0x8000);
  return(result);
}

////////////////////////////////
// NOTE(allen): Memory Functions

#define MEM_INITIAL_COMMIT KB(4)
#define MEM_INTERNAL_MIN_SIZE AlignUpPow2(sizeof(Arena), MEM_MAX_ALIGN)

StaticAssert(sizeof(Arena) <= MEM_INITIAL_COMMIT,
             mem_check_arena_size);

StaticAssert(IsPow2OrZero(MEM_COMMIT_BLOCK_SIZE) &&
             MEM_COMMIT_BLOCK_SIZE != 0,
             mem_check_commit_block_size);

StaticAssert(IsPow2OrZero(MEM_MAX_ALIGN) && MEM_MAX_ALIGN != 0,
             mem_check_max_align);

link_function Arena*
arena_alloc_reserve(U64 reserve_size, B32 growing){
  ProfBeginFunc();
  
  Arena *result = 0;
  if (reserve_size >= MEM_INITIAL_COMMIT){
    void *memory = mem_reserve(reserve_size);
    if (mem_commit(memory, MEM_INITIAL_COMMIT)){
      AsanPoison(memory, reserve_size);
      AsanUnpoison(memory, MEM_INTERNAL_MIN_SIZE);
      result = (Arena*)memory;
      result->current = result;
      result->prev = 0;
      result->alignment = sizeof(void*);
      result->growing = growing;
      result->base_pos = 0;
      result->chunk_cap = reserve_size;
      result->chunk_pos = MEM_INTERNAL_MIN_SIZE;
      result->chunk_commit_pos = MEM_INITIAL_COMMIT;
    }
  }
  Assert(result != 0);
  
  ProfEndFunc();
  return(result);
}

link_function Arena*
arena_alloc(void){
  Arena *result = arena_alloc_reserve(MEM_DEFAULT_RESERVE_SIZE, 1);
  return(result);
}

link_function void
arena_release(Arena *arena){
  ProfBeginFunc();
  
  Arena *ptr = arena->current;
  for (;ptr != 0;){
    Arena *prev = ptr->prev;
    AsanPoison(ptr, ptr->chunk_cap);
    mem_release(ptr, ptr->chunk_cap);
    ptr = prev;
  }
  
  ProfEndFunc();
}

link_function void*
arena_push_no_zero(Arena *arena, U64 size){
  ProfBeginFunc();
  
  void *result = 0;
  
  Arena *current = arena->current;
  
  // allocate new chunk if necessary
  if (arena->growing){
    U64 next_chunk_pos = AlignUpPow2(current->chunk_pos, arena->alignment);
    next_chunk_pos += size;
    if (next_chunk_pos > current->chunk_cap){
      U64 new_reserve_size = MEM_DEFAULT_RESERVE_SIZE;
      U64 enough_to_fit = size + MEM_INTERNAL_MIN_SIZE;
      if (new_reserve_size < enough_to_fit){
        new_reserve_size = AlignUpPow2(enough_to_fit, KB(4));
      }
      
      void *memory = mem_reserve(new_reserve_size);
      if (mem_commit(memory, MEM_INITIAL_COMMIT)){
        AsanPoison(memory, new_reserve_size);
        AsanUnpoison(memory, MEM_INTERNAL_MIN_SIZE);
        Arena *new_chunk = (Arena*)memory;
        new_chunk->prev = current;
        new_chunk->base_pos = current->base_pos + current->chunk_cap;
        new_chunk->chunk_cap = new_reserve_size;
        new_chunk->chunk_pos = MEM_INTERNAL_MIN_SIZE;
        new_chunk->chunk_commit_pos = MEM_INITIAL_COMMIT;
        current = arena->current = new_chunk;
      }
    }
  }
  
  {
    // if there is room in this chunk's reserve ...
    U64 result_pos = AlignUpPow2(current->chunk_pos, arena->alignment);
    U64 next_chunk_pos = result_pos + size;
    if (next_chunk_pos <= current->chunk_cap){
      
      // commit more memory if necessary
      if (next_chunk_pos > current->chunk_commit_pos){
        U64 next_commit_pos_aligned =
          AlignUpPow2(next_chunk_pos, MEM_COMMIT_BLOCK_SIZE);
        U64 next_commit_pos =
          ClampTop(next_commit_pos_aligned, current->chunk_cap);
        U64 commit_size = next_commit_pos - current->chunk_commit_pos;
        if (mem_commit((U8*)current + current->chunk_commit_pos, commit_size)){
          arena->chunk_commit_pos = next_commit_pos;
        }
      }
      
      // if there is room in the commit range, return memory & advance pos
      if (next_chunk_pos <= current->chunk_commit_pos){
        AsanUnpoison((U8*)current + current->chunk_pos,
                     next_chunk_pos - current->chunk_pos);
        result = (U8*)current + result_pos;
        current->chunk_pos = next_chunk_pos;
      }
    }
  }
  
  ProfEndFunc();
  return(result);
}

link_function void
arena_pop_to(Arena *arena, U64 pos){
  ProfBeginFunc();
  
  Arena *current = arena->current;
  U64 total_pos = current->base_pos + current->chunk_pos;
  if (pos < total_pos){
    // release all chunks that begin after this pos
    U64 clamped_total_pos = ClampBot(pos, MEM_INTERNAL_MIN_SIZE);
    for (; clamped_total_pos < current->base_pos; ){
      Arena *prev = current->prev;
      AsanPoison(current, current->chunk_cap);
      mem_release(current, current->chunk_cap);
      current = prev;
    }
    
    // update arena's current
    {
      arena->current = current;
    }
    
    // update the chunk position of the chunk in
    //  the chain that contains this position.
    {
      U64 chunk_pos = clamped_total_pos - current->base_pos;
      U64 clamped_chunk_pos = ClampBot(chunk_pos, MEM_INTERNAL_MIN_SIZE);
      AsanPoison((U8*)current + clamped_chunk_pos,
                 current->chunk_pos - clamped_chunk_pos);
      current->chunk_pos = clamped_chunk_pos;
    }
  }
  
  ProfEndFunc();
}

link_function U64
arena_current_pos(Arena *arena){
  Arena *current = arena->current;
  U64 result = current->base_pos + current->chunk_pos;
  return(result);
}

link_function void*
arena_push(Arena *arena, U64 size){
  void *result = arena_push_no_zero(arena, size);
  MemoryZero(result, size);
  return(result);
}

link_function void
arena_align(Arena *arena, U64 pow2_align){
  Assert(IsPow2OrZero(pow2_align) && pow2_align != 0 &&
         pow2_align <= MEM_MAX_ALIGN);
  Arena *current = arena->current;
  U64 p = current->chunk_pos;
  U64 p_aligned = AlignUpPow2(p, pow2_align);
  U64 z = p_aligned - p;
  if (z > 0){
    arena_push(arena, z);
  }
}

link_function void
arena_pop_amount(Arena *arena, U64 amount){
  Arena *current = arena->current;
  U64 total_pos = current->base_pos + current->chunk_pos;
  if (amount <= total_pos){
    U64 new_pos = total_pos - amount;
    arena_pop_to(arena, new_pos);
  }
}

////////////////////////////////
// NOTE(allen): Temp Helper Functions

link_function ArenaTemp
arena_begin_temp(Arena *arena){
  U64 pos = arena_current_pos(arena);
  ArenaTemp temp = {arena, pos};
  return(temp);
}

link_function void
arena_end_temp(ArenaTemp *temp){
  arena_pop_to(temp->arena, temp->pos);
}

////////////////////////////////
// NOTE(allen): Scratch

threadvar Arena *m__scratch_pool[MEM_SCRATCH_POOL_COUNT] = {0};

link_function ArenaTemp
arena_get_scratch(Arena **conflict_array, U32 count){
  ProfBeginFunc();
  
  // init on first time
  if (m__scratch_pool[0] == 0){
    Arena **scratch_slot = m__scratch_pool;
    for (U64 i = 0;
         i < MEM_SCRATCH_POOL_COUNT;
         i += 1, scratch_slot += 1){
      *scratch_slot = arena_alloc();
    }
  }
  
  // get non-conflicting arena
  ArenaTemp result = {0};
  Arena **scratch_slot = m__scratch_pool;
  for (U64 i = 0;
       i < MEM_SCRATCH_POOL_COUNT;
       i += 1, scratch_slot += 1){
    B32 is_non_conflict = 1;
    Arena **conflict_ptr = conflict_array;
    for (U32 j = 0; j < count; j += 1, conflict_ptr += 1){
      if (*scratch_slot == *conflict_ptr){
        is_non_conflict = 0;
        break;
      }
    }
    if (is_non_conflict){
      result = arena_begin_temp(*scratch_slot);
      break;
    }
  }
  
  ProfEndFunc();
  return(result);
}

////////////////////////////////
// NOTE(allen): String Compound Constructor Functions

link_function void
str8_list_push_explicit(String8List *list, String8 string,
                        String8Node *node_memory){
  node_memory->string = string;
  SLLQueuePush(list->first, list->last, node_memory);
  list->node_count += 1;
  list->total_size += string.size;
}

link_function void
str8_list_push(Arena *arena, String8List *list, String8 string){
  String8Node *node = push_array(arena, String8Node, 1);
  str8_list_push_explicit(list, string, node);
}

link_function String8
str8_join(Arena *arena, String8List *list,
          StringJoin *join_optional){
  ProfBeginFunc();
  
  // setup join parameters
  local StringJoin dummy_join = {0};
  StringJoin *join = join_optional;
  if (join == 0){
    join = &dummy_join;
  }
  
  // compute total size
  U64 size = (join->pre.size +
              join->post.size +
              join->mid.size*(list->node_count - 1) +
              list->total_size);
  
  // begin string build
  U8 *str = push_array(arena, U8, size + 1);
  U8 *ptr = str;
  
  // write pre
  MemoryCopy(ptr, join->pre.str, join->pre.size);
  ptr += join->pre.size;
  
  B32 is_mid = 0;
  for (String8Node *node = list->first;
       node != 0;
       node = node->next){
    // write mid
    if (is_mid){
      MemoryCopy(ptr, join->mid.str, join->mid.size);
      ptr += join->mid.size;
    }
    
    // write node string
    MemoryCopy(ptr, node->string.str, node->string.size);
    ptr += node->string.size;
    
    is_mid = 1;
  }
  
  // write post
  MemoryCopy(ptr, join->post.str, join->post.size);
  ptr += join->post.size;
  
  // write null
  *ptr = 0;
  
  String8 result = str8(str, size);
  ProfEndFunc();
  return(result);
}

link_function String8List
str8_split(Arena *arena, String8 string, U8 *splits, U32 count){
  ProfBeginFunc();
  
  String8List result = {0};
  
  U8 *ptr = string.str;
  U8 *word_first = ptr;
  U8 *opl = string.str + string.size;
  for (;ptr < opl; ptr += 1){
    // is this a split
    U8 byte = *ptr;
    B32 is_split_byte = 0;
    for (U32 i = 0; i < count; i += 1){
      if (byte == splits[i]){
        is_split_byte = 1;
        break;
      }
    }
    
    if (is_split_byte){
      // try to emit word, advance word first pointer
      if (word_first < ptr){
        str8_list_push(arena, &result, str8_range(word_first, ptr));
      }
      word_first = ptr + 1;
    }
  }
  
  // try to emit final word
  if (word_first < ptr){
    str8_list_push(arena, &result, str8_range(word_first, ptr));
  }
  
  ProfEndFunc();
  return(result);
}

// TODO(allen): where do I want this to live??
#include <stdio.h>

link_function String8
str8_pushfv(Arena *arena, char *fmt, va_list args){
  ProfBeginFunc();
  
  // in case we need to try a second time
  va_list args2;
  va_copy(args2, args);
  
  // try to build the string in 1024 bytes
  U64 buffer_size = 1024;
  U8 *buffer = push_array(arena, U8, buffer_size);
  U64 actual_size = vsnprintf((char*)buffer, buffer_size, fmt, args);
  
  String8 result = {0};
  if (actual_size < buffer_size){
    // if first try worked, put back what we didn't use and finish
    arena_pop_amount(arena, buffer_size - actual_size - 1);
    result = str8(buffer, actual_size);
  }
  else{
    // if first try failed, reset and try again with correct size
    arena_pop_amount(arena, buffer_size);
    U8 *fixed_buffer = push_array(arena, U8, actual_size + 1);
    U64 final_size = vsnprintf((char*)fixed_buffer, actual_size + 1, fmt, args2);
    result = str8(fixed_buffer, final_size);
  }
  
  // end args2
  va_end(args2);
  
  ProfEndFunc();
  return(result);
}

link_function String8
str8_pushf(Arena *arena, char *fmt, ...){
  va_list args;
  va_start(args, fmt);
  String8 result = str8_pushfv(arena, fmt, args);
  va_end(args);
  return(result);
}

link_function void
str8_list_pushf(Arena *arena, String8List *list, char *fmt, ...){
  va_list args;
  va_start(args, fmt);
  String8 string = str8_pushfv(arena, fmt, args);
  va_end(args);
  str8_list_push(arena, list, string);
}

link_function String8
str8_push_copy(Arena *arena, String8 string){
  String8 result = {0};
  result.str = push_array(arena, U8, string.size + 1);
  result.size = string.size;
  MemoryCopy(result.str, string.str, string.size);
  result.str[result.size] = 0;
  return(result);
}

////////////////////////////////
// NOTE(allen): String Comparison Functions

link_function B32
str8_match(String8 a, String8 b, StringMatchFlags flags){
  ProfBeginFunc();
  
  B32 result = 0;
  if (a.size == b.size){
    result = 1;
    B32 no_case = ((flags & StringMatchFlag_NoCase) != 0);
    for (U64 i = 0; i < a.size; i += 1){
      U8 ac = a.str[i];
      U8 bc = b.str[i];
      if (no_case){
        ac = str8_char_uppercase(ac);
        bc = str8_char_uppercase(bc);
      }
      if (ac != bc){
        result = 0;
        break;
      }
    }
  }
  
  ProfEndFunc();
  return(result);
}

////////////////////////////////
// NOTE(allen): Unicode Functions

link_function StringDecode
str_decode_utf8(U8 *str, U32 cap){
  ProfBeginFunc();
  
  local U8 length[] = {
    1, 1, 1, 1, // 000xx
    1, 1, 1, 1,
    1, 1, 1, 1,
    1, 1, 1, 1,
    0, 0, 0, 0, // 100xx
    0, 0, 0, 0,
    2, 2, 2, 2, // 110xx
    3, 3,       // 1110x
    4,          // 11110
    0,          // 11111
  };
  local U8 first_byte_mask[] = { 0, 0x7F, 0x1F, 0x0F, 0x07 };
  local U8 final_shift[] = { 0, 18, 12, 6, 0 };
  
  StringDecode result = {0};
  if (cap > 0){
    result.codepoint = '#';
    result.size = 1;
    
    U8 byte = str[0];
    U8 l = length[byte >> 3];
    if (0 < l && l <= cap){
      U32 cp = (byte & first_byte_mask[l]) << 18;
      switch (l){
        case 4: cp |= ((str[3] & 0x3F) << 0);
        case 3: cp |= ((str[2] & 0x3F) << 6);
        case 2: cp |= ((str[1] & 0x3F) << 12);
        default: break;
      }
      cp >>= final_shift[l];
      
      result.codepoint = cp;
      result.size = l;
    }
  }
  
  ProfEndFunc();
  return(result);
}

link_function U32
str_encode_utf8(U8 *dst, U32 codepoint){
  ProfBeginFunc();
  
  U32 size = 0;
  if (codepoint < (1 << 8)){
    dst[0] = codepoint;
    size = 1;
  }
  else if (codepoint < (1 << 11)){
    dst[0] = 0xC0 | (codepoint >> 6);
    dst[1] = 0x80 | (codepoint & 0x3F);
    size = 2;
  }
  else if (codepoint < (1 << 16)){
    dst[0] = 0xE0 | (codepoint >> 12);
    dst[1] = 0x80 | ((codepoint >> 6) & 0x3F);
    dst[2] = 0x80 | (codepoint & 0x3F);
    size = 3;
  }
  else if (codepoint < (1 << 21)){
    dst[0] = 0xF0 | (codepoint >> 18);
    dst[1] = 0x80 | ((codepoint >> 12) & 0x3F);
    dst[2] = 0x80 | ((codepoint >> 6) & 0x3F);
    dst[3] = 0x80 | (codepoint & 0x3F);
    size = 4;
  }
  else{
    dst[0] = '#';
    size = 1;
  }
  
  ProfEndFunc();
  return(size);
}

link_function StringDecode
str_decode_utf16(U16 *str, U32 cap){
  ProfBeginFunc();
  
  StringDecode result = {'#', 1};
  U16 x = str[0];
  if (x < 0xD800 || 0xDFFF < x){
    result.codepoint = x;
  }
  else if (cap >= 2){
    U16 y = str[1];
    if (0xD800 <= x && x < 0xDC00 &&
        0xDC00 <= y && y < 0xE000){
      U16 xj = x - 0xD800;
      U16 yj = y - 0xDc00;
      U32 xy = (xj << 10) | yj;
      result.codepoint = xy + 0x10000;
      result.size = 2;
    }
  }
  
  ProfEndFunc();
  return(result);
}

link_function U32
str_encode_utf16(U16 *dst, U32 codepoint){
  ProfBeginFunc();
  
  U32 size = 0;
  if (codepoint < 0x10000){
    dst[0] = codepoint;
    size = 1;
  }
  else{
    U32 cpj = codepoint - 0x10000;
    dst[0] = (cpj >> 10) + 0xD800;
    dst[1] = (cpj & 0x3FF) + 0xDC00;
    size = 2;
  }
  
  ProfEndFunc();
  return(size);
}

link_function String32
str32_from_str8(Arena *arena, String8 string){
  ProfBeginFunc();
  
  U32 *memory = push_array(arena, U32, string.size + 1);
  
  U32 *dptr = memory;
  U8 *ptr = string.str;
  U8 *opl = string.str + string.size;
  for (; ptr < opl;){
    StringDecode decode = str_decode_utf8(ptr, (U64)(opl - ptr));
    *dptr = decode.codepoint;
    ptr += decode.size;
    dptr += 1;
  }
  
  *dptr = 0;
  
  U64 alloc_count = string.size + 1;
  U64 string_count = (U64)(dptr - memory);
  U64 unused_count = alloc_count - string_count - 1;
  arena_pop_amount(arena, unused_count*sizeof(*memory));
  
  String32 result = {memory, string_count};
  ProfEndFunc();
  return(result);
}

link_function String8
str8_from_str32(Arena *arena, String32 string){
  ProfBeginFunc();
  
  U8 *memory = push_array(arena, U8, string.size*4 + 1);
  
  U8 *dptr = memory;
  U32 *ptr = string.str;
  U32 *opl = string.str + string.size;
  for (; ptr < opl;){
    U32 size = str_encode_utf8(dptr, *ptr);
    ptr += 1;
    dptr += size;
  }
  
  *dptr = 0;
  
  U64 alloc_count = string.size*4 + 1;
  U64 string_count = (U64)(dptr - memory);
  U64 unused_count = alloc_count - string_count - 1;
  arena_pop_amount(arena, unused_count*sizeof(*memory));
  
  String8 result = {memory, string_count};
  ProfEndFunc();
  return(result);
}

link_function String16
str16_from_str8(Arena *arena, String8 string){
  ProfBeginFunc();
  U16 *memory = push_array(arena, U16, string.size*2 + 1);
  
  U16 *dptr = memory;
  U8 *ptr = string.str;
  U8 *opl = string.str + string.size;
  for (; ptr < opl;){
    StringDecode decode = str_decode_utf8(ptr, (U64)(opl - ptr));
    U32 enc_size = str_encode_utf16(dptr, decode.codepoint);
    ptr += decode.size;
    dptr += enc_size;
  }
  
  *dptr = 0;
  
  U64 alloc_count = string.size*2 + 1;
  U64 string_count = (U64)(dptr - memory);
  U64 unused_count = alloc_count - string_count - 1;
  arena_pop_amount(arena, unused_count*sizeof(*memory));
  
  String16 result = {memory, string_count};
  ProfEndFunc();
  return(result);
}

link_function String8
str8_from_str16(Arena *arena, String16 string){
  ProfBeginFunc();
  U8 *memory = push_array(arena, U8, string.size*3 + 1);
  
  U8 *dptr = memory;
  U16 *ptr = string.str;
  U16 *opl = string.str + string.size;
  for (; ptr < opl;){
    StringDecode decode = str_decode_utf16(ptr, (U64)(opl - ptr));
    U16 enc_size = str_encode_utf8(dptr, decode.codepoint);
    ptr += decode.size;
    dptr += enc_size;
  }
  
  *dptr = 0;
  
  U64 alloc_count = string.size*3 + 1;
  U64 string_count = (U64)(dptr - memory);
  U64 unused_count = alloc_count - string_count - 1;
  arena_pop_amount(arena, unused_count*sizeof(*memory));
  
  String8 result = {memory, string_count};
  ProfEndFunc();
  return(result);
}

////////////////////////////////
// NOTE(allen): Log

threadvar LOG_ThreadVars *log_vars = 0;

link_function void
log_accum_begin(void){
  LOG_ThreadVars *vars = log_vars;
  if (vars == 0){
    Arena *arena = arena_alloc();
    vars = log_vars = push_array(arena, LOG_ThreadVars, 1);
    vars->arena = arena;
  }
  
  U64 pos = arena_current_pos(vars->arena);
  LOG_Node *node = push_array(vars->arena, LOG_Node, 1);
  if (node == 0){
    vars->over_stack += 1;
  }
  else{
    node->pos = pos;
    SLLStackPush(vars->stack, node);
  }
}

link_function void
log_emit(String8 message){
  LOG_ThreadVars *vars = log_vars;
  if (vars != 0 &&
      vars->over_stack == 0){
    LOG_Node *node = vars->stack;
    if (node != 0){
      String8 msg_copy = str8_push_copy(vars->arena, message);
      str8_list_push(vars->arena, &node->log, msg_copy);
    }
  }
}

link_function void
log_emitf(char *fmt, ...){
  LOG_ThreadVars *vars = log_vars;
  if (vars != 0 &&
      vars->over_stack == 0){
    LOG_Node *node = vars->stack;
    if (node != 0){
      va_list args;
      va_start(args, fmt);
      String8 string = str8_pushfv(vars->arena, fmt, args);
      va_end(args);
      str8_list_push(vars->arena, &node->log, string);
    }
  }
}

link_function String8
log_accum_end(Arena *arena){
  String8 result = {0};
  LOG_ThreadVars *vars = log_vars;
  if (vars != 0){
    if (vars->over_stack == 0){
      LOG_Node *node = vars->stack;
      if (node != 0){
        result = str8_join(arena, &node->log, 0);
        SLLStackPop(vars->stack);
        arena_pop_to(vars->arena, node->pos);
      }
    }
    else{
      vars->over_stack -= 1;
    }
  }
  return(result);
}

////////////////////////////////
// NOTE(allen): Errors

// IMPORTANT: It is important that strings get pushed
// *on top* of the node that holds them within the arena.
// basically: Assert((U8*)node < (U8*)node->error.str);
// except that stops working if the arena is chained...

threadvar ER_ThreadVars *er_vars = 0;

link_function void
er_accum_begin(void){
  ER_ThreadVars *vars = er_vars;
  if (vars == 0){
    Arena *arena = arena_alloc_reserve(KB(64), 0);
    vars = er_vars = push_array(arena, ER_ThreadVars, 1);
    vars->arena = arena;
  }
  
  U64 pos = arena_current_pos(vars->arena);
  ER_Node *node = push_array(vars->arena, ER_Node, 1);
  if (node == 0){
    vars->over_stack += 1;
  }
  else{
    node->pos = pos;
    SLLStackPush(vars->stack, node);
  }
}

link_function void
er_emit(String8 error){
  ER_ThreadVars *vars = er_vars;
  if (vars != 0 &&
      vars->over_stack == 0){
    ER_Node *node = vars->stack;
    if (node != 0 && node->error.size == 0){
      node->error = str8_push_copy(vars->arena, error);
    }
  }
}

link_function void
er_emitf(char *fmt, ...){
  ER_ThreadVars *vars = er_vars;
  if (vars != 0 &&
      vars->over_stack == 0){
    ER_Node *node = vars->stack;
    if (node != 0 && node->error.size == 0){
      va_list args;
      va_start(args, fmt);
      String8 string = str8_pushfv(vars->arena, fmt, args);
      va_end(args);
      node->error = string;
    }
  }
}

link_function String8
er_accum_end(Arena *arena){
  String8 result = {0};
  ER_ThreadVars *vars = er_vars;
  if (vars != 0){
    if (vars->over_stack == 0){
      ER_Node *node = vars->stack;
      if (node != 0){
        result = str8_push_copy(arena, node->error);
        SLLStackPop(vars->stack);
        arena_pop_to(vars->arena, node->pos);
      }
    }
    else{
      vars->over_stack -= 1;
    }
  }
  return(result);
}

////////////////////////////////
// NOTE(allen): Interleaving Functions

link_function String8
bop_interleave(Arena *arena, void **in,
               U64 lane_count, U64 el_size, U64 el_count){
  // TODO(allen): look at disassembly for real speed work
  
  // setup buffer
  String8 result = {0};
  result.size = lane_count*el_size*el_count;
  result.str = push_array(arena, U8, result.size);
  
  // fill loop
  U8 *out_ptr = result.str;
  U64 in_off = 0;
  for (U64 i = 0; i < el_count; i += 1, in_off += el_size){
    U8 **in_base_ptr = (U8**)in;
    for (U64 j = 0; j < lane_count; j += 1, in_base_ptr += 1){
      MemoryCopy(out_ptr, *in_base_ptr + in_off, el_size);
      out_ptr += el_size;
    }
  }
  
  return(result);
}

link_function String8*
bop_uninterleave(Arena *arena, void *in,
                 U64 lane_count, U64 el_size, U64 el_count){
  // TODO(allen): look at disassembly for real speed work
  
  // compute sizes
  U64 bytes_per_lane = el_size*el_count;
  U64 total_size = lane_count*bytes_per_lane;
  
  // allocate outs
  String8 *result = push_array(arena, String8, lane_count);
  for (U64 i = 0; i < lane_count; i += 1){
    result[i].str = push_array(arena, U8, bytes_per_lane);
    result[i].size = bytes_per_lane;
  }
  
  // fill loop
  U8 *in_ptr = (U8*)in;
  U64 out_off = 0;
  for (U64 i = 0; i < el_count; i += 1, out_off += el_size){
    String8 *out_buffer = result;
    for (U64 j = 0; j < lane_count; j += 1, out_buffer += 1){
      MemoryCopy(out_buffer->str + out_off, in_ptr, el_size);
      in_ptr += el_size;
    }
  }
  
  return(result);
}

////////////////////////////////
// NOTE(allen): Conversions

link_function String8
bop_f32_from_s24(Arena *arena, String8 in){
  // TODO(allen): look at disassembly for real speed work
  
  // round size
  U64 el_count = in.size/3;
  
  // allocate out
  F32 *out = push_array(arena, F32, el_count);
  
  // fill loop
  U8 *in_ptr = in.str;
  F32 *out_ptr = out;
  F32 *opl_ptr = out_ptr + el_count;
  for (; out_ptr < opl_ptr; out_ptr += 1){
    // read s24
    S32 x = (*in_ptr);
    in_ptr += 1;
    x |= (*in_ptr) << 8;
    in_ptr += 1;
    x |= (*in_ptr) << 16;
    in_ptr += 1;
    if ((x & (1 << 23)) != 0){
      x |= (0xFF << 24);
    }
    
    // divide to float
    F32 fx = 0.f;
    if (x >= 0){
      fx = ((F32)x)/(8388607.f);
    }
    else{
      fx = ((F32)x)/(8388608.f);
    }
    
    // write f32
    *out_ptr = fx;
  }
  
  // package output buffer
  String8 result = {0};
  result.str = (U8*)out;
  result.size = el_count*sizeof(F32);
  return(result);
}

link_function String8
bop_s24_from_f32(Arena *arena, String8 in){
  // TODO(allen): look at disassembly for real speed work
  
  // round size
  U64 el_count = in.size/4;
  
  // allocate out
  U8 *out = push_array(arena, U8, el_count*3);
  
  // fill loop
  F32 *in_ptr = (F32*)in.str;
  U8 *out_ptr = out;
  U8 *opl_ptr = out_ptr + el_count*3;
  for (; out_ptr < opl_ptr; out_ptr += 3, in_ptr += 1){
    // read f32
    F32 x = (*in_ptr);
    F32 x_clamped = Clamp(-1.f, x, 1.f);
    
    // multiply to s24 (inside a s32)
    S32 fx = 0;
    if (x >= 0){
      fx = 0x7FFFFF*x_clamped;
    }
    else{
      fx = 0x800000*x_clamped;
    }
    
    // write s24
    out_ptr[0] = fx&0xFF;
    out_ptr[1] = (fx >> 8)&0xFF;
    out_ptr[2] = (fx >> 16)&0xFF;
  }
  
  // package output buffer
  String8 result = {0};
  result.str = (U8*)out;
  result.size = el_count*3;
  return(result);
}

link_function String8
bop_s16_from_f32(Arena *arena, String8 in){
  // TODO(allen): look at disassembly for real speed work
  
  // round size
  U64 el_count = in.size/4;
  
  // allocate out
  S16 *out = push_array(arena, S16, el_count);
  
  // fill loop
  F32 *in_ptr = (F32*)in.str;
  S16 *out_ptr = out;
  S16 *opl_ptr = out_ptr + el_count;
  for (; out_ptr < opl_ptr; out_ptr += 1, in_ptr += 1){
    // read f32
    F32 x = (*in_ptr);
    F32 x_clamped = Clamp(-1.f, x, 1.f);
    
    // multiply to s16 (inside a s32)
    S32 fx = 0;
    if (x >= 0){
      fx = 0x7FFF*x_clamped;
    }
    else{
      fx = 0x8000*x_clamped;
    }
    
    // write s16
    *out_ptr = fx;
  }
  
  // package output buffer
  String8 result = {0};
  result.str = (U8*)out;
  result.size = el_count*sizeof(S16);
  return(result);
}


////////////////////////////////
// NOTE(allen): PRNG Functions

#include "pcg/pcg_basic.c"

link_function PRNG
prng_make_from_seed(PRNG_Seed *seed){
  PRNG prng = {0};
  
  U64 st0  = *(U64*)(seed->v + 0);
  U64 seq0 = *(U64*)(seed->v + 8);
  U64 st1  = *(U64*)(seed->v + 16);
  U64 seq1 = *(U64*)(seed->v + 24);
  
  if ((seq0|1) == (seq1|1)){
    seq1 = ~seq1;
  }
  
  pcg32_srandom_r(&prng.gen[0], st0, seq0);
  pcg32_srandom_r(&prng.gen[1], st1, seq1);
  
  return(prng);
}

link_function U32
prng_next_u32(PRNG *prng){
  U32 result = pcg32_random_r(&prng->gen[0]);
  return(result);
}

link_function U64
prng_next_u64(PRNG *prng){
  U32 hi = pcg32_random_r(&prng->gen[0]);
  U32 lo = pcg32_random_r(&prng->gen[1]);
  U32 result = (((U64)hi) << 32) | lo;
  return(result);
}

link_function U32
prng_next_bounded(PRNG *prng, U32 bound){
  U32 threshold = (-bound)%bound;
  U32 r = pcg32_random_r(&prng->gen[0]);
  for (;r < threshold;){
    r = pcg32_random_r(&prng->gen[0]);
  }
  U32 result = r%bound;
  return(result);
}

// [0,1]
link_function F32
prng_next_unital_f32(PRNG *prng){
  U32 x = prng_next_u32(prng);
  U32 x_masked = (x & 0x1FFFFF); // 21-bit mask
  F32 result = (F32)x_masked/2097151.f;
  return(result);
}

// [-1,1]
link_function F32
prng_next_biunital_f32(PRNG *prng){
  U32 x = prng_next_u32(prng);
  U32 x_masked = (x & 0x3FFFFF); // 22-bit mask
  F32 result = ((F32)x_masked/2097151.f) - 1.f;
  return(result);
}

link_function B32
prng_next_pcoin(PRNG *prng, F32 p){
  F32 x = prng_next_unital_f32(prng);
  B32 result = (x <= p);
  return(result);
}

////////////////////////////////
// NOTE(allen): Math Functions

link_function F32
math_gaussian(F32 sigma, F32 x){
  // 1/(sqrt(2*pi)*sigma) * e^(-x*x/(2*sigma*sigma))
  
  F32 sqrt_2pi = sqrt_F32(tau_F32);
  F32 inv_sqrt_2pi_sigma = 1.f/(sqrt_2pi*sigma);
  F32 sigma_p2 = sigma*sigma;
  
  F32 exponent = -x*x/(2*sigma_p2);
  F32 power = pow_F32(e_F32, exponent);
  
  F32 result = inv_sqrt_2pi_sigma*power;
  return(result);
}

link_function Array_F32
math_gaussian_kernel(Arena *arena, F32 sigma, U32 extra_reach){
  Assert(sigma > 0.f);
  
  // allocate
  U32 reach = (U32)ceil_F32(sigma) + extra_reach;
  U64 count = reach*2 + 1;
  F32 *vals = push_array(arena, F32, count);
  
  // calculate guassian samples & sum
  F32 sum = 0;
  {
    F32 sqrt_2pi = sqrt_F32(tau_F32);
    F32 inv_sqrt_2pi_sigma = 1.f/(sqrt_2pi*sigma);
    F32 sigma_p2 = sigma*sigma;
    
    F32 mid = (F32)reach;
    F32 *val_ptr = vals;
    for (U64 i = 0; i < count; i += 1, val_ptr += 1){
      F32 x = (F32)i - mid;
      F32 exponent = -x*x/(2*sigma_p2);
      F32 power = pow_F32(e_F32, exponent);
      F32 g = inv_sqrt_2pi_sigma*power;
      
      sum += g;
      *val_ptr = g;
    }
  }
  
  // normalise kernel
  {
    F32 *val_ptr = vals;
    for (U64 i = 0; i < count; i += 1, val_ptr += 1){
      *val_ptr = *val_ptr/sum;
    }
  }
  
  // return result
  Array_F32 result = {0};
  result.vals = vals;
  result.count = count;
  return(result);
}
