// #TODO: #OS_Win32
// [ ] #Exception handling code in `Win32_Exception_Filter`
// [~] #Thread cleanup: in `thread_deinit` is there any requirement to cleanup child threads?
  // - I think: no? Threads should handle their own lifetimes, and the parent threads should ensure child threads are complete
  // before terminating.
  // Or we can move child threads up to the parent?

constexpr s64 FILETIME_TO_UNIX = 116444736000000000i64;
constexpr u64 Win32_Max_Path_Length = 260;

f64 GetUnixTimestamp () {
    FILETIME fileTime;
    GetSystemTimePreciseAsFileTime(&fileTime);
    s64 ticks = ((s64)fileTime.dwHighDateTime << (s64)32) | (s64)fileTime.dwLowDateTime;
    return (ticks - FILETIME_TO_UNIX) / (10.0 * 1000.0 * 1000.0);
}
s64 GetUnixTimestampNanoseconds () {
    FILETIME fileTime;
    GetSystemTimePreciseAsFileTime(&fileTime);
    
    s64 ticks = ((s64)fileTime.dwHighDateTime << (s64)32) 
                | (s64)fileTime.dwLowDateTime;  // in 100ns ticks
    s64 unix_time = (ticks - FILETIME_TO_UNIX); // in 100ns ticks
    
    s64 unix_time_nanoseconds = unix_time * 100;
    
    return unix_time_nanoseconds;
}

u64 FILETIME_to_ticks (FILETIME fileTime) {
  u64 ticks = ((u64)fileTime.dwHighDateTime << (u64)32) 
                | (u64)fileTime.dwLowDateTime;  // in 100ns ticks
  return ticks;
}

FILETIME u64_to_FILETIME (u64 time_u64) {
  static_assert(sizeof(FILETIME) == sizeof(time_u64));
  FILETIME ft;
  memcpy(&ft, &time_u64, sizeof(FILETIME));
  return ft;
}

string format_time_datetime (FILETIME ft) {
  SYSTEMTIME stUTC, st;
  FileTimeToSystemTime(&ft, &stUTC);
  SystemTimeToTzSpecificLocalTime(nullptr, &stUTC, &st);
  
  return format_string("%04u-%02u-%02u %02u:%02u:%02u.%03u",
             st.wYear,
             st.wMonth,
             st.wDay,
             st.wHour,
             st.wMinute,
             st.wSecond,
             st.wMilliseconds);
}

string format_filetime_as_datetime (FILETIME ft, bool use_24_hour_time=false) {
  char* MONTH_NAMES[12] = { "January", "February", "March", "April", "May", "June",
      "July", "August", "September", "October", "November", "December" };
  SYSTEMTIME stUTC, st;
  bool result = (bool)FileTimeToSystemTime(&ft, &stUTC);
  if (!result) {
    log_error("[format_filetime_as_datetime] FileTimeToSystemTime failed!");
    os_log_error();
    return "";
  }
  result = (bool)SystemTimeToTzSpecificLocalTime(nullptr, &stUTC, &st);
  if (!result) {
    log_error("[format_filetime_as_datetime] SystemTimeToTzSpecificLocalTime failed!");
    os_log_error();
    return "";
  }

  char* month = MONTH_NAMES[st.wMonth - 1];

  if (use_24_hour_time) {
    return format_string("%u %s %u, %02u:%02u:%02u",
        st.wDay,
        month,
        st.wYear,
        st.wHour,
        st.wMinute,
        st.wSecond);
  } else {
    u16 hour12 = st.wHour % 12;
    if (hour12 == 0) hour12 = 12;
  
    char* am_pm = (st.wHour < 12) ? "AM" : "PM";
  
    return format_string("%u %s %u, %02u:%02u:%02u %s",
        st.wDay,
        month,
        st.wYear,
        hour12,
        st.wMinute,
        st.wSecond,
        am_pm);
  }
}

struct OS_System_Info {
  // #cpuid
  s32 logical_processor_count;
  s32 physical_core_count;
  s32 primary_core_count;
  s32 secondary_core_count; // Any weaker or "Efficiency" cores.
  u64 page_size;
  u64 large_page_size;
  u64 allocation_granularity;
  string machine_name;
  
  // #Monitors
  b32 monitors_enumerated;
  Array<Monitor> monitors; // Back with default_allocator
  
  // #Drives
  Arena* drive_arena;
  Table<string, OS_Drive*> drives; // should we just store ptrs to OS_Drive? I think so..
  // That way we can fetch the OS_Drive* and have the pointer be stable. Especially if it's b
  // backed by an arena.
};

enum class Open_Directories_In: s32 {
  Explorer = 0,
  FPilot   = 1
};

struct OS_Process_Info {
  u32 process_id;
  b32 large_pages_allowed;
  string binary_path;
  string working_path;
  string user_program_data_path;
  Array<string> module_load_paths;
  Array<string> environment_paths;
  
  b32 window_class_initialized;
  Open_Directories_In open_directories_in = Open_Directories_In::FPilot;
  Arena* event_arena;
  Array<Window_Info> windows;
};

struct OS_State_Win32 {
  Arena* arena;
  
  OS_System_Info system_info;
  OS_Process_Info process_info;
};

global OS_State_Win32 global_win32_state;
internal b32 global_win32_is_quiet = 0; // No console output (`quiet` flag passed)

// Cached path data: Kinda yuck, but I want these to be globally available.
// #TODO: Should be part of Win32 global state, and we should NOT access without
// a mutex. Also, should be loaded once on startup.
global string PATH;
global ArrayView<string> PATH_split; // views into PATH.

void win32_reload_PATH () {
  push_allocator_label("win32_PATH");
  push_allocator(default_allocator());  // we want to keep PATH and file data around!
  string_free(PATH);
  array_free(PATH_split);
  
  u32 PATH_length = GetEnvironmentVariableA("PATH", nullptr, 0);
  ArrayView<u8> result = ArrayView<u8>(PATH_length);
  GetEnvironmentVariableA("PATH", (LPSTR)result.data, PATH_length);
  
  PATH = to_string(result);
  PATH_split = string_split(PATH, ';');
}

void win32_delete_cached_PATH () {
  push_allocator(default_allocator());  // we want to keep PATH and file data around!
  string_free(PATH);
  array_free(PATH_split);
}

Table<string, OS_Drive*>* get_drive_table () {
  return &global_win32_state.system_info.drives;
}

OS_Drive* get_drive_from_label (string drive_label) {
  // #TODO: Validate the input is in the correct format. We're looking for something like `C:\`
  // not just the drive letter!
  Table<string, OS_Drive*>* drive_table = get_drive_table();
  OS_Drive** drive_ptr = table_find_pointer(drive_table, drive_label);
  Assert(drive_ptr != nullptr);
  return *drive_ptr;
}

ArrayView<OS_Drive*> os_get_available_drives () {
  // #TODO: Maybe have a version of this API that sorts the drives before returning.
  // @Allocates: Recommended to set context allocator to temp().
  auto drive_table = get_drive_table();
  Array<OS_Drive*> drives;
  
  // #hash_table_iterator : instead of writing this everywhere, just use this function
  // to get an Array of drives.
  for (s64 i = 0; i < drive_table->allocated; i += 1) {
    Table_Entry<string, OS_Drive*>* entry = &drive_table->entries[i]; // we should take ptr here if we want to modify?
    
    if (entry->hash > HASH_TABLE_FIRST_VALID_HASH) {
      if (entry->value->label.data == nullptr || !entry->value->is_present) continue; // Some volumes may not be real and therefore have no label.
      
      array_add(drives, entry->value);
    }
  }
  
  return drives;
}

ArrayView<OS_Drive*> os_get_available_drives_sorted () {
  Allocator ctx_allocator = context_allocator();
  push_allocator(temp());
  
  auto drives = os_get_available_drives();
  
  Array<string> drive_letters;
  for_each(d, drives) {
    array_add(drive_letters, copy_string(drives[d]->label));
  }
  
  qsort(drive_letters.data, drive_letters.count, sizeof(string), string_lexicographical_compare);
  
  Array<OS_Drive*> drives_sorted = Array<OS_Drive*>(ctx_allocator);
  auto drive_table = get_drive_table();
  for_each(d, drive_letters) {
    string drive_label = drive_letters[d];
    
    array_add(drives_sorted, get_drive_from_label(drive_label));
  }
  
  return drives_sorted;
}

internal LONG WINAPI Win32_Exception_Filter (EXCEPTION_POINTERS* exception_ptrs) {
  if (global_win32_is_quiet) { ExitProcess(1); }

  local_persist volatile LONG first = 0;
  if(InterlockedCompareExchange(&first, 1, 0) != 0)
  { // prevent failures in other threads to popup same message box
    // this handler just shows first thread that crashes
    // we are terminating afterwards anyway
    for (;;) Sleep(1000);
  }
  
  // #TODO: Runtime assertion failed?
  
  // #Exception handling code (TODO)
#if ENABLE_STACK_TRACE
  if (thread_context()->stack_trace) {
    os_write_string_unsynchronized("\n[Win32_Exception_Filter] Stack Trace\n", true);
    print_stack_trace();
  }
#endif
  
  ExitProcess(1);
  
  return 0;
}

// internal void Main_Entry_Point (int argc, WCHAR **argv);
internal void Win32_Entry_Point (int argc, WCHAR **argv) { stack_trace();
  // #testing printing stack trace (unfinished).
  // os_write_string_unsynchronized("Fatal Error!\n\nStack trace: ", true);
  // print_stack_trace();

  // Timed_Block_Print("Win32_Entry_Point");
  // See: w32_entry_point_caller(); (raddebugger)
  SetUnhandledExceptionFilter(&Win32_Exception_Filter);
  
  SYSTEM_INFO sysinfo = {0};
  GetSystemInfo(&sysinfo);
  
  // Try to allow large pages if we can.
  // b32 large_pages_allowed = 0;
  // {
  //   HANDLE token;
  //   if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token))
  //   {
  //     LUID luid;
  //     if(LookupPrivilegeValue(0, SE_LOCK_MEMORY_NAME, &luid))
  //     {
  //       TOKEN_PRIVILEGES priv;
  //       priv.PrivilegeCount           = 1;
  //       priv.Privileges[0].Luid       = luid;
  //       priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
  //       large_pages_allowed = !!AdjustTokenPrivileges(token, 0, &priv, sizeof(priv), 0, 0);
  //     }
  //     CloseHandle(token);
  //   }
  // }
  
  push_arena(thread_context()->arena);
  
  { OS_System_Info* info = &global_win32_state.system_info;
    info->logical_processor_count = (s32)sysinfo.dwNumberOfProcessors;
    info->page_size               = sysinfo.dwPageSize;
    info->large_page_size         = GetLargePageMinimum();
    info->allocation_granularity  = sysinfo.dwAllocationGranularity;
  }
  { OS_Process_Info* info = &global_win32_state.process_info;
    info->large_pages_allowed = false;
    info->process_id = GetCurrentProcessId();
  }
  // #cpuid
  { OS_System_Info* info = &global_win32_state.system_info;
    u32 length = 0;
    GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, (PDWORD)&length);
    u8* cpu_information_buffer = NewArray<u8>(length);
    GetLogicalProcessorInformationEx(RelationProcessorCore, // *sigh* 
      (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)cpu_information_buffer, (PDWORD)&length);
    
    u32 offset = 0;
    
    u32 all_cpus_count = 0; // all logical cpus
    u32 physical_cpu_count = 0;
    u32 max_performance = 0;
    u32 performance_core_count = 0;
    
    while (offset < length) {
      SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* cpu_information 
        = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)(cpu_information_buffer + offset);
      
      offset += cpu_information->Size;
      
      u32 count_per_group_physical = 1;
      u32 value = (u32)cpu_information->Processor.GroupMask->Mask;
      u32 count_per_group = __popcnt(value); // logical
      if (cpu_information->Relationship != RelationProcessorCore) continue;
      
      if (cpu_information->Processor.EfficiencyClass > max_performance) {
        max_performance = cpu_information->Processor.EfficiencyClass;
        performance_core_count = count_per_group_physical;
      } else if (cpu_information->Processor.EfficiencyClass == max_performance) {
        performance_core_count += count_per_group_physical;
      }
      
      physical_cpu_count += count_per_group_physical;
      all_cpus_count += count_per_group;  
    }
  
    info->physical_core_count = (s32)physical_cpu_count;
    info->primary_core_count  = (s32)performance_core_count;
    info->secondary_core_count = info->physical_core_count - info->primary_core_count;
  } 

  { OS_System_Info* info = &global_win32_state.system_info;
    info->monitors.allocator = default_allocator();
    u8 buffer[MAX_COMPUTERNAME_LENGTH + 1] = {0};
    DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
    if(GetComputerNameA((char*)buffer, &size)) {
      string machine_name_temp = string(size, buffer);
      info->machine_name = copy_string(machine_name_temp);
    }
  }
  
  { OS_Process_Info* info = &global_win32_state.process_info;
    info->windows.allocator = default_allocator();    
    DWORD length = GetCurrentDirectoryW(0, 0);
    // This can be freed later when we call temp_reset();
    u16* memory = NewArray<u16>(temp(), length + 1);
    length = GetCurrentDirectoryW(length + 1, (WCHAR*)memory);
    info->working_path = wide_to_utf8(memory, length);
    Assert(is_valid(info->working_path));
  }
  
  // Setup event arena, allocators for Array<> types.
  if (!global_win32_state.process_info.event_arena) {
    global_win32_state.process_info.event_arena = bootstrap_arena(Arena_Reserve::Size_64K, "global_win32_state.process_info.event_arena");
  }
  
  // [ ] Get Working directory (info->working_path)
  // [ ] GetEnvironmentStringsW
  temp_reset();
}

C_LINKAGE DWORD OS_Windows_Thread_Entry_Point (void* parameter) {
  Thread* thread = (Thread*)parameter;

  set_thread_context(thread->context);
  
  DWORD result = (DWORD)thread->proc(thread);
  
  return result;
}

// Individual Thread API
#define thread_task(T)       (T*)thread->data
#define thread_group_task(T) (T*)work

internal bool thread_init (Thread* thread, Thread_Proc proc, string thread_name) {
  Assert(thread != nullptr && proc != nullptr);
  
  DWORD windows_thread_id = 0;

  HANDLE windows_thread = CreateThread(nullptr, 0, OS_Windows_Thread_Entry_Point, 
    thread, CREATE_SUSPENDED, &windows_thread_id);
  
  if (windows_thread == 0 || windows_thread == INVALID_HANDLE_VALUE) {
    return false;
  }
  
  s64 this_thread_index = InterlockedIncrement(&next_thread_index);
  // 2. #NewContext Setup NEW thread local context
  string arena_ex_label;
  string temp_label;
  string error_arena_label;
  string allocator_label;
  string log_builder_label;
  string string_builder_label;
  
  if (is_valid(thread_name)) {
    arena_ex_label       = format_string(temp(), "%s Arena", thread_name.data);
    temp_label           = format_string(temp(), "%s Temp", thread_name.data);
    error_arena_label    = format_string(temp(), "%s Error Arena", thread_name.data);
    allocator_label      = format_string(temp(), "%s Initialization", thread_name.data);
    log_builder_label    = format_string(temp(), "%s Log Builder", thread_name.data);
    string_builder_label = format_string(temp(), "%s String Builder", thread_name.data);
  } else {
    arena_ex_label       = format_string(temp(), "Thread %lld Arena", this_thread_index);
    temp_label           = format_string(temp(), "Thread %lld Temp", this_thread_index);
    error_arena_label    = format_string(temp(), "Thread %lld Error Arena", this_thread_index);
    allocator_label      = format_string(temp(), "Thread %lld Initialization", this_thread_index);
    log_builder_label    = format_string(temp(), "Thread %lld Log Builder", this_thread_index);
    string_builder_label = format_string(temp(), "Thread %lld String Builder", this_thread_index);
  }
  
  push_allocator_label(allocator_label);
  
  ExpandableArena* arena_ex = bootstrap_expandable_arena(Arena_Reserve::Size_64M, arena_ex_label);
  push_arena(arena_ex);
  
  // #NOTE: we don't assign thread_local_context until we hit the #thread_entry_point
  thread->context = New<Thread_Context>();
  thread->context->temp           = bootstrap_expandable_arena(Arena_Reserve::Size_2M, temp_label);
  thread->context->arena          = arena_ex;
  thread->context->allocator      = allocator(arena_ex);
  thread->context->thread_idx     = (s32)this_thread_index;
  // #NOTE: This will disappear once the thread is de-initted. If we want this string, copy it!
  thread->context->thread_name    = copy_string(thread_name);
  thread->context->log_builder    = new_string_builder(Arena_Reserve::Size_64M, log_builder_label);
  thread->context->string_builder = new_string_builder(Arena_Reserve::Size_2M, string_builder_label);
  thread->context->error_arena    = bootstrap_arena(Arena_Reserve::Size_64M, error_arena_label);
  thread->context->logger         = {default_logger_proc, &default_logger};
  
  thread->context->parent_thread_context = thread_context();
  
  thread->os_thread.windows_thread    = windows_thread;
  thread->os_thread.windows_thread_id = windows_thread_id;
  
  thread->proc = proc;
  thread->index = this_thread_index;
  
  thread_context()->child_threads.allocator = allocator(thread_context()->arena);
  array_add(thread_context()->child_threads, thread);
  
  return true;
}

internal void thread_deinit (Thread* thread, bool zero_thread) {
  // Move errors from thread to parent thread
  push_errors_to_parent_thread(thread->context);
  
  if (thread->os_thread.windows_thread) {
    CloseHandle(thread->os_thread.windows_thread);
    thread->os_thread.windows_thread = nullptr;
  }
  
  // remove from thread->context->parent_thread->child_threads
  array_unordered_remove_by_value(thread->context->parent_thread_context->child_threads, thread, 1);
  
  array_reset(*thread->context->log_builder);
  free_string_builder(thread->context->log_builder);
  free_string_builder(thread->context->string_builder);
  arena_delete(thread->context->error_arena);
  arena_delete(thread->context->temp);
  arena_delete(thread->context->arena); // must come last because thread->context is allocated with this arena!
  
  if (zero_thread) memset(thread, 0, sizeof(Thread));
}

internal void thread_start (Thread* thread, void* thread_data) {
  if (thread_data) thread->data = thread_data;
  ResumeThread(thread->os_thread.windows_thread);
}

internal bool thread_is_done (Thread* thread, s32 milliseconds) {
  Assert(milliseconds >= -1);
  
  DWORD result = WaitForSingleObject(thread->os_thread.windows_thread, (DWORD)milliseconds);
  return result != WAIT_TIMEOUT;
}

// #thread_primitives
internal void mutex_init (Mutex* mutex) {
  InitializeCriticalSection(&mutex->csection);
}
internal void mutex_destroy (Mutex* mutex) {
  DeleteCriticalSection(&mutex->csection);
}
internal void lock (Mutex* mutex) {
  EnterCriticalSection(&mutex->csection);
}
internal void unlock (Mutex* mutex) {
  LeaveCriticalSection(&mutex->csection);
}
internal void semaphore_init (Semaphore* sem, s32 initial_value) {
  Assert(initial_value >= 0);
  sem->event = CreateSemaphoreW(nullptr, initial_value, 0x7fffffff, nullptr);
}
internal void semaphore_destroy (Semaphore* sem) {
  CloseHandle(sem->event);
}
internal void signal (Semaphore* sem) {
  ReleaseSemaphore(sem->event, 1, nullptr);
}

internal Wait_For_Result wait_for (Semaphore* sem, s32 milliseconds) {
  DWORD res = 0;
  if (milliseconds < 0) {
    res = WaitForSingleObject(sem->event, INFINITE);
  } else {
    res = WaitForSingleObject(sem->event, (u32)milliseconds);
  }
  
  Assert(res != WAIT_FAILED);
  
  if (res == WAIT_OBJECT_0) return Wait_For_Result::SUCCESS;
  if (res == WAIT_TIMEOUT)  return Wait_For_Result::TIMEOUT;
  
  return Wait_For_Result::ERROR;
}

internal void condition_variable_init (Condition_Variable* cv) {
  InitializeConditionVariable(&cv->condition_variable);
}
internal void condition_variable_destroy (Condition_Variable* cv) {
  // No action required.
}
internal void wait (Condition_Variable* cv, Mutex* mutex, s32 wait_time_ms) {
  SleepConditionVariableCS(&cv->condition_variable, &mutex->csection, (DWORD)wait_time_ms);
}
internal void wake (Condition_Variable* cv) {
  WakeConditionVariable(&cv->condition_variable);
}
internal void wake_all (Condition_Variable* cv) {
  WakeAllConditionVariable(&cv->condition_variable);
}

internal string get_error_string (OS_Error_Code error_code) {
  u16* lpMsgBuf;
  bool success = (bool)FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
                    nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
                    (LPWSTR)&lpMsgBuf, 0, nullptr);
  if (!success) { return ""; }
  
  push_allocator(temp());
  
  string result = wide_to_utf8(lpMsgBuf);
  
  LocalFree(lpMsgBuf);
  
  return trim_right(result, "\r\t\n");
}

internal void os_log_error () {
  OS_Error_Code error_code = GetLastError();
  log_error(" > GetLastError code: %d, %s", error_code, get_error_string(error_code).data);
}

internal bool file_is_valid (File file) {
  if (file.handle == INVALID_HANDLE_VALUE) return false;
  if (file.handle == 0) return false;
  
  return true;
}

internal File file_open (string file_path, bool for_writing, bool keep_existing_content, bool log_errors) {
  HANDLE handle;
  
  push_allocator(temp()); // for utf8 -> wide conversions:
  
  if (for_writing) {
    u32 creation = (keep_existing_content) ? OPEN_ALWAYS : CREATE_ALWAYS;
    handle = CreateFileW(
      (LPCWSTR)utf8_to_wide(file_path).data, 
      FILE_GENERIC_READ | FILE_GENERIC_WRITE,
      FILE_SHARE_READ, 
      nullptr, creation, 0, nullptr);
  } else {
    u32 creation = OPEN_EXISTING;
    handle = CreateFileW(
      (LPCWSTR)utf8_to_wide(file_path).data,
      FILE_GENERIC_READ,
      FILE_SHARE_READ, 
      nullptr, creation, 0, nullptr);
  }
  
  if (handle == INVALID_HANDLE_VALUE && log_errors) {
    // OS_Error_Code error_code = GetLastError();
    log_error("Could not open file `%s`", copy_string(temp(), file_path).data);
    os_log_error();
  }
  
  File file;
  file.handle = handle;
  
  return file;
}

internal void file_close (File* file) {
  CloseHandle(file->handle);
}

internal bool file_read (File file, u8* data, s64 bytes_to_read_count, s64* bytes_read_count) {
  // ignore bytes_read_count if null.
  if (data == nullptr) {
    log_error("file_read called with null destination pointer.");
    os_log_error();
    if (bytes_read_count) (*bytes_read_count) = 0;
    return false;
  }
  if (bytes_to_read_count <= 0) {
    if (bytes_read_count) (*bytes_read_count) = 0;
    return false;
  }
  
  bool read_success = false;
  s64 total_read = 0;
  
  // loop to read more data than can be specified by the DWORD param ReadFile takes:
  while (total_read < bytes_to_read_count) {
    s64 remaining = bytes_to_read_count - total_read;
    DWORD to_read;
    if (remaining <= 0x7FFFFFFF) {
      to_read = (DWORD)remaining;
    } else {
      to_read = 0x7FFFFFFF; // 2147483647 bytes ~2GB
    }
    
    DWORD single_read_length = 0;
    read_success = (bool)ReadFile(file.handle, data + total_read, to_read, &single_read_length, nullptr);
    
    total_read += single_read_length;
    if (!read_success || single_read_length == 0) {
      break;
    }
  }
  
  if (bytes_read_count) (*bytes_read_count) = total_read;
  return read_success;
}

internal bool file_length (File file, s64* length) {
  if (length == nullptr) {
    log_error("Calling file_length with null `length` param!");
    return false;
  }
  if (!file_is_valid(file)) { return false; }
  s64 size;
  bool success = (bool)GetFileSizeEx(file.handle, (PLARGE_INTEGER)&size);
  
  (*length) = size;
  
  return true;
}

internal bool file_length (string file_path, s64* length) {
  if (length == nullptr) {
    log_error("Calling file_length with null `length` param!");
    return false;
  }
  File f = file_open(file_path);
  
  if (!file_is_valid(f)) { return false; }
  bool success = file_length(f, length);
  
  file_close(&f);
  
  return success;
}

internal s64 file_current_position (File file) {
  constexpr s64 invalid_file_position = -1;
  if (!file_is_valid(file)) { return invalid_file_position; }
  s64 offset = 0;
  LARGE_INTEGER liDistanceToMove;
  bool result = (bool)SetFilePointerEx(file.handle, liDistanceToMove, (PLARGE_INTEGER)&offset, FILE_CURRENT);
  if (!result) { return invalid_file_position; }
  return (s64)offset;
}

internal bool file_set_position (File file, s64 position) {
  if (!file_is_valid(file)) { return false; }
  if (position < 0) { Assert(false); return false; }
  
  LARGE_INTEGER position_li; position_li.QuadPart = position;
  return (bool)SetFilePointerEx(file.handle, position_li, nullptr, FILE_BEGIN);
}

ArrayView<u8> read_entire_file (File file, bool add_null_terminator) {
  ArrayView<u8> file_data;

  bool result = file_length(file, &file_data.count);
  if (!result) return {};
  result = file_set_position(file, 0);
  if (!result) return {};
  
  s64 total_file_size = file_data.count;
  
  if (add_null_terminator) {
    total_file_size += 1;
  }
  
  file_data.data = NewArray<u8>(total_file_size, false);
  if (file_data.data == nullptr) return {};  
  
  s64 bytes_read = 0;
  result = file_read(file, file_data.data, file_data.count, &bytes_read);
  if (!result) {
    array_free(file_data);
    return {};
  }
  
  if (add_null_terminator) {
    file_data[total_file_size-1] = 0;
  }
  
  Assert(bytes_read == file_data.count);
  file_data.count = bytes_read;
  
  return file_data;
}

internal ArrayView<u8> read_entire_file (string file_path, bool add_null_terminator, bool log_errors) {
  File f = file_open(file_path, false, false, log_errors);
  if (!file_is_valid(f)) return {};
  
  ArrayView<u8> file_data = read_entire_file(f, add_null_terminator);
  
  file_close(&f);
  
  return file_data;
}

internal bool file_write (File* file, void* data, s64 length) {
  // @incomplete - deal with inputs > 32 bits (>2GB)
  u32 length_u32 = (u32)length;
  Assert(length == length_u32);
  
  u32 bytes_written;
  bool result = (bool)WriteFile(file->handle, data, length_u32, (LPDWORD)&bytes_written, nullptr);
  
  return result;
}

force_inline bool file_write (File* file, ArrayView<u8> view) {
  return file_write(file, view.data, view.count);
}

internal bool write_entire_file (string file_path, void* file_data, s64 count) {
  File f = file_open(file_path, true, false);
  if (!file_is_valid(f)) return false;
  
  bool result = file_write(&f, file_data, count);
  
  file_close(&f);
  return result;
}

internal bool write_entire_file (string file_path, ArrayView<u8> file_data) {
  return write_entire_file(file_path, file_data.data, file_data.count);
}

internal LRESULT // see os_w32_wnd_proc from raddebugger.
win32_wnd_proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  LRESULT result = 0;
  bool good = true;
  
  Arena* event_arena = global_win32_state.process_info.event_arena;
  Assert(event_arena != nullptr);
  
  switch (uMsg) {
    default: {
      result = DefWindowProcW(hwnd, uMsg, wParam, lParam);
    } break;
    case WM_CLOSE: {
      ExitProcess(0); // #temp.
    } break;
  }
  
  return result;
}

internal bool file_exists (string file_path) {
  push_allocator(temp());
  
  DWORD result = GetFileAttributesW((LPCWSTR)utf8_to_wide(file_path).data);
  
  return (result != INVALID_FILE_ATTRIBUTES);
}

internal bool path_is_directory (string file_path, bool* success=nullptr) {
  push_allocator(temp());
  
  DWORD result = GetFileAttributesW((LPCWSTR)utf8_to_wide(file_path).data);
  
  if (result == INVALID_FILE_ATTRIBUTES) {
    if (success) (*success) = false;
    return false;
  }
  
  if (success) (*success) = true;
  return (result & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

internal bool win32_create_directory_internal (string path, SECURITY_ATTRIBUTES* attr=nullptr) {
  const u64 Max_Directory_Path = Win32_Max_Path_Length - 12;
  const string Long_Path_Prefix = "\\\\?\\";

  push_allocator(temp()); // nothing we create here needs to stick around
  
  wstring path_wide = utf8_to_wide(path);
  
  // CreateDirectory does not support paths longer than Max_Directory_Path unless they start with the Long_Path_Prefix
  if (!begins_with(path, Long_Path_Prefix)) {
    u32 full_path_size = GetFullPathNameW((LPCWSTR)path_wide.data, 0, nullptr, nullptr);
    if (full_path_size == 0) {
      log_error("[win32_create_directory_internal] GetFullPathNameW failed with input %s", copy_string(path).data);
      os_log_error();
      return false;
    }
    
    if (full_path_size > Max_Directory_Path) {
      s64 prefixed_full_path_size = full_path_size + Long_Path_Prefix.count; // size in wchar_t with the prefix
      // This is a bit of a silly way to do this...
      u16* full_path_wide = NewArray<u16>(2 * prefixed_full_path_size);
      wstring long_path_prefix_wide = utf8_to_wide(Long_Path_Prefix);
      memcpy(full_path_wide, long_path_prefix_wide.data, Long_Path_Prefix.count * 2);
      u32 real_full_path_size = GetFullPathNameW((LPCWSTR)path_wide.data, full_path_size, (LPWSTR)full_path_wide + Long_Path_Prefix.count, nullptr);
      if (!real_full_path_size) {
        log_error("[win32_create_directory_internal] GetFullPathNameW failed on long-path inputs");
        os_log_error();
        return false;
      }
      Assert(real_full_path_size <= full_path_size - 1); // -1 because on success, it does NOT include the null character, but on failure it does. This used to be ==, but in some cases we get a shorter path... I guess the return value of GetFullPathNameW is inexact in some cases!
      
      path_wide = { real_full_path_size, full_path_wide };
    }
  }
  
  bool success = CreateDirectoryW((LPCWSTR)path_wide.data, attr);
  if (success) return true;
  
  u32 error = GetLastError();
  if (error == ERROR_ALREADY_EXISTS) return true;
  
  log_error("[win32_create_directory_internal] Failed to create directory.");
  os_log_error();
  
  return false;
}

// I've always hated "mkdir, makedir" and any variations of it.
internal bool os_create_directory (string name, bool recursive=false) {
  if (recursive) {
    s64 start_index = 0;
#if OS_WINDOWS
    // Windows doesn't allow creating a root directory (e.g. "X:" or "X:\\")
    if (name.count > 3 && (name.data[1] == ':')) {
      start_index = 3;
    }
#endif
    if (name.count > 0 && name.data[0] == '/' || name.data[0] == '\\') {
      start_index = 1;
    }
    // #TODO: Find next index:
    s64 index = find_index_from_left(name, '/', start_index);
#if OS_WINDOWS
    if (index == -1) {
      index = find_index_from_left(name, '\\', start_index);
    }
#endif
    while (index != -1) {
      bool success = win32_create_directory_internal(string_view(name, 0, index));
      if (!success) { return false; }
      
      // Look for the next slash. If there are multiple slahes in a row, use the last one:
      while (true) {
        start_index = index + 1;
        index = find_index_from_left(name, '/', start_index);
#if OS_WINDOWS
        if (index == -1) {
          index = find_index_from_left(name, '\\', start_index);
        }
#endif
  
        if (index != start_index) break;
      }
    }
    
    if (start_index == name.count-1) {
      // we already created innermost directory
      return true;
    }
  } // if (recursive)
  
  return win32_create_directory_internal(name);
}

internal BOOL
monitor_enum_proc (HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM data) {
  Monitor monitor = {};
  monitor.left   = rect->left;
  monitor.top    = rect->top;
  monitor.right  = rect->right;
  monitor.bottom = rect->bottom;
  
  monitor.monitor_info.cbSize = sizeof(MONITORINFO);
  
  GetMonitorInfoW(hMonitor, &monitor.monitor_info);
  monitor.primary = !!(monitor.monitor_info.dwFlags & 0x1);
  monitor.present = true;
  
  push_allocator_label("global_win32_state.system_info.monitors");
  array_add(global_win32_state.system_info.monitors, monitor);
  
  return true;
}

// #TODO(Low Priority): how do I setup a callback if monitors configuration is changed?
// we handle WM_DISPLAYCHANGE or WM_DEVICECHANGE and call EnumDisplayMonitors again.
internal void os_enumerate_monitors () {
  if (!global_win32_state.system_info.monitors_enumerated) {
    // should reset array?
    if (!EnumDisplayMonitors(nullptr, nullptr, monitor_enum_proc, 0)) {
      log_fatal_error("Failed to enumerate monitors\n");
      Assert(false); // Failed to enumerate monitors
      ExitProcess(1);
    }
    
    global_win32_state.system_info.monitors_enumerated = true;
  }
}

Window_Dimensions platform_get_centered_window_dimensions (bool open_on_largest_monitor=false) {
  os_enumerate_monitors();
  
  Assert(global_win32_state.system_info.monitors.count > 0); // must have at least 1 monitor!
  
  Array<Monitor> monitors = global_win32_state.system_info.monitors;
  Monitor monitor = monitors[0];
  
  if (open_on_largest_monitor) {
    s64 max_area = 0;
    for (s64 i = 0; i < monitors.count; i += 1) {
      s64 width  = monitors[i].right  - monitors[i].left;
      s64 height = monitors[i].bottom - monitors[i].top;
      s64 area   = width * height;
      
      if (max_area < area) {
        monitor = monitors[i];
        max_area = area;
      } else if (max_area == area && monitors[i].primary) {
        // if monitors are the same dimension, then just use primary monitor
        monitor = monitors[i];
      }
    }
  } else { // Opens on whatever monitor is marked as "primary."
    for (s64 i = 0; i < monitors.count; i += 1) {
      if (monitors[i].primary) {
        monitor = monitors[i];
        break;
      }
    }
  }
  
  s64 monitor_width  = monitor.right  - monitor.left;
  s64 monitor_height = monitor.bottom - monitor.top;
  
  s64 window_width  = (s64)((f64)monitor_width / 1.125);
  s64 window_height = (s64)((f64)monitor_height / 1.25);
  s64 window_x  = (monitor.left + (monitor_width  / 2) - (window_width / 2));
  s64 window_y  = (monitor.top  + (monitor_height / 2) - (window_height / 2));
  
  Window_Dimensions dimensions = {
    (s32)window_x,
    (s32)window_y,
    (s32)window_width,
    (s32)window_height,
  };
  
  return dimensions;
}

bool Win32_Set_Main_Icon () {
  Window_Info* info = get_main_window_pointer();
  if (info->icon) {
    HICON old_icon = (HICON)SendMessage(info->window, WM_SETICON, ICON_BIG, (LPARAM)info->icon);
    if (old_icon) DestroyIcon(old_icon);
    return true;
  }
  
  return false;
}

#define ICON_CONTEXT_MENU_ITEM_ID 5011
#define MAIN_WINDOW_TRAY_ICON_ID 5001
#define WM_TRAYICON WM_USER + 1 // our own value to identify when receiving a message.

bool Win32_Set_Tray_Icon (string tooltip_text) {
  Window_Info* info = get_main_window_pointer();
  if (info->icon_minimized) {
    push_allocator(temp());
    wstring tooltip_text_wide = utf8_to_wide(tooltip_text);
    Assert(tooltip_text_wide.count < 128);
    
    info->nid.cbSize = sizeof(NOTIFYICONDATAW);
    info->nid.hWnd = info->window;
    info->nid.uID  = MAIN_WINDOW_TRAY_ICON_ID;
    info->nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    info->nid.uCallbackMessage = WM_TRAYICON; // Custom message when interacting with the tray icon
    info->nid.hIcon = info->icon_minimized;
    memcpy(info->nid.szTip, tooltip_text_wide.data, tooltip_text_wide.count);
    
    bool success = Shell_NotifyIconW(NIM_ADD, &info->nid);
    if (success) {
      info->tray_icon_added = true;
      return true;
    }
  }
  
  return false;
}

bool Win32_Show_Tray_Icon () {
  Window_Info* info = get_main_window_pointer();
  if (info && info->tray_icon_added) {
    return Shell_NotifyIconW(NIM_ADD, &info->nid);
  }
  return false;
}

bool Win32_Hide_Tray_Icon () {
  Window_Info* info = get_main_window_pointer();
  if (info && info->tray_icon_added) {
    return Shell_NotifyIconW(NIM_DELETE, &info->nid);
  }
  return false;
}

bool Win32_Hide_Window_Titlebar () {
  Window_Info* info = get_main_window_pointer(); 
  if (info && info->window) {
    LONG style = GetWindowLongW(info->window, GWL_STYLE);
    style &= (LONG)(~(WS_CAPTION | WS_SYSMENU));
    SetWindowLongW(info->window, GWL_STYLE, style);
    SetWindowPos(info->window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
    return true;
  }
  return false;
}

bool Win32_Show_Window_Titlebar () {
  Window_Info* info = get_main_window_pointer(); 
  if (info && info->window) {
    LONG style = GetWindowLongW(info->window, GWL_STYLE);
    style |= (LONG)(WS_CAPTION | WS_SYSMENU);
    SetWindowLongW(info->window, GWL_STYLE, style);
    SetWindowPos(info->window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);
    return true;
  }
  return false;
}

void Win32_Minimize_Window_To_Tray (Window_Info* info) {
  if (info && info->window) {
    ShowWindow(info->window, SW_HIDE);
    Win32_Show_Tray_Icon();
    info->minimized_to_tray = true;
  }
}

void Win32_Restore_Window_From_Tray (Window_Info* info) {
  if (info && info->window && info->minimized_to_tray) {
    ShowWindow(info->window, SW_RESTORE);
    Win32_Hide_Tray_Icon();
    info->minimized_to_tray = false;
  }
}

// Win32_Restore_Window_From_Tray();
void Win32_Bring_Window_To_Foreground (Window_Info* info) {
  Win32_Restore_Window_From_Tray(info);
  if (info && info->window) {
    if (os_window_is_minimized(info->window)) {
      ShowWindow(info->window, SW_RESTORE);
    }
    SetForegroundWindow(info->window);
  }
}

bool Win32_Load_Main_Window_Icon_Minimized (string icon_path) {
  HICON result = (HICON)LoadImageW(nullptr, (LPCWSTR)utf8_to_wide(icon_path).data, IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
  Window_Info* info = get_main_window_pointer();
  info->icon_minimized = result;
  
  return (result != nullptr);
}

bool Win32_Load_Main_Window_Icon (string icon_path) {
  HICON result = (HICON)LoadImageW(nullptr, (LPCWSTR)utf8_to_wide(icon_path).data, IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
  Window_Info* info = get_main_window_pointer();
  info->icon = result;

  return (result != nullptr);
}

// #window_creation -> put API in OS_Win32.h
// Instead of returning WindowType, return the handle + other information.
bool os_create_window (string new_window_name, Window_Type parent, bool center_window, bool open_on_largest_monitor, bool display_window, void* wnd_proc_override) {
  local_persist string class_name = "Win32_Window_Class";
    
  WNDCLASSEXW wc = {};
  if (!global_win32_state.process_info.window_class_initialized) {
    global_win32_state.process_info.window_class_initialized = true;
    
    wc.cbSize = sizeof(WNDCLASSEXW);
    wc.style  = CS_HREDRAW | CS_VREDRAW;
    if (!wnd_proc_override) {
      wc.lpfnWndProc   = win32_wnd_proc;
    } else {
      wc.lpfnWndProc   = (WNDPROC)wnd_proc_override;
    }
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = nullptr;
    wc.hIcon         = get_main_window().icon;
    wc.hCursor       = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW);
    wc.hbrBackground = nullptr;
    wc.lpszMenuName  = nullptr;
    wc.lpszClassName = (LPCWSTR)utf8_to_wide(class_name).data;
    
    if (RegisterClassExW(&wc) == 0) {
      log_error("RegisterClassExW Failed.");
      return false;
    }
  }
  
  Window_Dimensions wd = { 100, 100, 640, 480 };
  
  if (center_window) {
    wd = platform_get_centered_window_dimensions(open_on_largest_monitor);
    log("[Window_Dimensions] location: (%d, %d); size: %dx%d px", 
      wd.window_x, wd.window_y, wd.window_width, wd.window_height);
  }
  
  HWND hwnd = CreateWindowExW(
    WS_EX_APPWINDOW,
    wc.lpszClassName,
    (LPCWSTR)utf8_to_wide(new_window_name).data,
    WS_OVERLAPPEDWINDOW,
    wd.window_x, wd.window_y, wd.window_width, wd.window_height,
    nullptr, nullptr, nullptr, nullptr
  );
  
  if (!hwnd) {
    Assert(false);
    DestroyWindow(hwnd);
    return false;
  }
  
  // Display the window:
  if (display_window) {
    UpdateWindow(hwnd);
    ShowWindow(hwnd, SW_SHOW);
  }
  
  // Save window to stack
  Window_Info info = {};
  info.window               = hwnd;
  info.initial_dimensions   = wd;
  info.is_main_window       = (parent == nullptr);
  
  // Should we mutex this? Seems very unlikely we'll ever need to call this
  // from multiple threads at the same time. 
  push_allocator_label("global_win32_state.process_info.windows");
  array_add(global_win32_state.process_info.windows, info);
  
  return true;
}

Window_Info get_main_window () {
  Array<Window_Info> windows = global_win32_state.process_info.windows;
  if (windows.count <= 0) {
    return {};
  }
  
  for (s64 i = 0; i < windows.count; i += 1) {
    if (windows[i].is_main_window) {
      return windows[i];
    }
  }
  
  return {};
}

Window_Info* get_main_window_pointer () {
  s64 window_count = global_win32_state.process_info.windows.count;
  
  for (s64 i = 0; i < window_count; i += 1) {
    if (global_win32_state.process_info.windows[i].is_main_window) {
      return &global_win32_state.process_info.windows[i];
    }
  }
  
  return nullptr;
}

Window_Info* get_window_info (Window_Type window) {
  s64 window_count = global_win32_state.process_info.windows.count;
  
  for (s64 i = 0; i < window_count; i += 1) {
    if (global_win32_state.process_info.windows[i].window == window) {
      return &global_win32_state.process_info.windows[i];
    }
  }
  
  return nullptr;
}

bool get_window_dimensions(Window_Info* info, s32* width, s32* height) {
  if (info == nullptr || width == nullptr || height == nullptr) return false;
  Assert(width && height);
  
  RECT c = {};
  bool success = GetClientRect(info->window, &c);
  
  if (!success) {
    (*width)  = 0;
    (*height) = 0;
    return false;
  }
  
  (*width)  = (s32)(c.right  - c.left);
  (*height) = (s32)(c.bottom - c.top);
  
  return true;
}

bool os_window_is_minimized (Window_Type window) {
  return (bool)IsIconic(window);
}

bool os_main_window_is_minimized () {
  return os_window_is_minimized(get_main_window().window);
}

s32 os_cpu_logical_core_count () {
  OS_System_Info* info = &global_win32_state.system_info;
  return info->logical_processor_count;
}

s32 os_cpu_physical_core_count () {
  OS_System_Info* info = &global_win32_state.system_info;
  return info->physical_core_count;
}

s32 os_cpu_primary_core_count () {
  OS_System_Info* info = &global_win32_state.system_info;
  return info->primary_core_count;
}

s32 os_cpu_secondary_core_count () {
  OS_System_Info* info = &global_win32_state.system_info;
  return info->secondary_core_count;
}

// #ProgramData
string os_program_data_path () {
  ArrayView<u16> path = ArrayView<u16>(temp(), Win32_Max_Path_Length + 1);
  HRESULT r = SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, 0, (LPWSTR)path.data);
  if (r != S_OK) {
    log_error("[os_program_data_path] Failed to get program data path.");
    os_log_error();
  }
  
  return wide_to_utf8(path.data);
}

// #Drives
internal Win32_Drive* copy_win32_drive (Win32_Drive* drive) {
  Win32_Drive* result = New<Win32_Drive>();
  
  result->label        = copy_string(drive->label);
  result->volume_name  = copy_string(drive->volume_name);
  result->type                 = drive->type;
  result->file_system          = drive->file_system;
  result->full_size            = drive->full_size;
  result->free_space           = drive->free_space;
  result->serial_number        = drive->serial_number;
  result->max_component_length = drive->max_component_length;
  result->file_system_flags    = drive->file_system_flags;
  result->is_present           = drive->is_present;
  
  return result;
}

bool Win32_Discover_Drives () {
  push_allocator_label("Win32_Discover_Drives");
  global_win32_state.system_info.drive_arena = bootstrap_arena(Arena_Reserve::Size_2M, "global_win32_state.system_info.drive_arena");
  push_arena(global_win32_state.system_info.drive_arena);
  // Initialize drive_table if necessary.
  Table<string, OS_Drive*>* drive_table = get_drive_table();
  if (!drive_table->allocated) {
    drive_table->allocator        = default_allocator();
    // #TODO(Low priority): #hash_table need a macro for initializing with string keys!
    drive_table->hash_function    = string_hash_function_fnv1a;
    drive_table->compare_function = string_keys_match;
    s64 slots_to_allocate = 64;
    table_init(drive_table, slots_to_allocate);
  }
  
  u16 lpBuf[1024];
  u32 result_length = GetLogicalDriveStringsW(1024, (LPWSTR)lpBuf);
  if (!result_length) {
    log_error("GetLogicalDriveStringsW failed!");
    os_log_error();
    return false;
  }
  
  bool completed = false;
  s32 current_index = 0;
  
  while (true) {
    completed = completed || (current_index >= 1024) || lpBuf[current_index] == 0;
    if (completed) break;
    
    u16* logical_drive = lpBuf + current_index;
    
    string drive_label = wide_to_utf8(logical_drive);
    
    u64 total_number_of_bytes;u64 total_number_of_free_bytes;
    bool result = GetDiskFreeSpaceExW((LPCWSTR)logical_drive, nullptr, 
          (PULARGE_INTEGER)&total_number_of_bytes, (PULARGE_INTEGER)&total_number_of_free_bytes);
    
    Win32_Drive_Type drive_type = (Win32_Drive_Type)GetDriveTypeW((LPCWSTR)logical_drive);
    
    log("Found %s drive, type %s", drive_label.data, to_string(drive_type).data);
    
    current_index += (s32)(drive_label.count + 1);
    
    bool just_added = false;
    OS_Drive** drive_ptr = table_find_or_add(drive_table, drive_label, &just_added);
    OS_Drive* drive = New<OS_Drive>();
    (*drive_ptr) = drive; // fill table slot with data.
    
    if (!just_added) { // delete old strings before updating
      // This is silly, but there's a small chance the volume has been renamed so...
      string_free(drive->label); // this is actually just stupid.
      string_free(drive->volume_name);
    }
    
    u16 volume_name[Win32_Max_Path_Length] = {};
    u16 file_system_name[Win32_Max_Path_Length] = {};
    DWORD serial_number = 0; DWORD max_comp_len = 0; DWORD file_system_flags = 0;
    if (GetVolumeInformationW((LPCWSTR)logical_drive, (LPWSTR)volume_name, 
        Win32_Max_Path_Length, &serial_number, &max_comp_len, &file_system_flags, 
        (LPWSTR)file_system_name, Win32_Max_Path_Length)) {
      drive->label = drive_label;
      if (volume_name[0] == 0) {
        drive->volume_name = copy_string("Local Disk");
      } else {
        drive->volume_name = wide_to_utf8(volume_name);
      }
      if (drive->volume_name == "") { drive->volume_name = copy_string("Local Disk"); } // Probably redundant??
      
      drive->type = (Win32_Drive_Type)drive_type;
      { push_allocator(temp());
        drive->file_system = Win32_filesystem_from_string(wide_to_utf8(file_system_name));
      }
      drive->serial_number = serial_number;
      drive->max_component_length = max_comp_len;
      drive->file_system_flags = file_system_flags;
      drive->is_present = true;
      push_allocator(temp());
      log(" - volume name: %s", drive->volume_name.data);
      log(" - file_system: %s", wide_to_utf8(file_system_name).data);
    } else {
      log_error("GetVolumeInformationW failed! (drive label: %s)", drive_label.data);
      os_log_error();
      drive->is_present = false;
    }
  }
  
  return true;
}

// Drive label includes `:\`
bool Win32_Drive_Exists (string drive_label) {
  push_allocator(temp());
  LPCWSTR drive_label_wide = (LPCWSTR)utf8_to_wide(drive_label).data;
  UINT type = GetDriveTypeW(drive_label_wide);
  return (type != DRIVE_UNKNOWN && type != DRIVE_NO_ROOT_DIR);
  // Alternative method:
  // return (bool)GetVolumeInformationW(drive_label_wide, nullptr, 0, nullptr, nullptr, nullptr, nullptr, 0);
}

string Win32_drive_letter (string any_path) {
  // #TODO: remove leading `\\.\` if present, assert if drive letter is invalid.
  // we copy so it is null-terminated, and can be used as %s in format_string.
  return copy_string({1, any_path.data});
}

string os_get_machine_name () {
  constexpr u8 WIN32_MAX_COMPUTER_LENGTH_NAME = 31;
  u16 buffer[WIN32_MAX_COMPUTER_LENGTH_NAME + 1];
  
  u32 count = WIN32_MAX_COMPUTER_LENGTH_NAME + 1;
  if (GetComputerNameW((LPWSTR)buffer, (LPDWORD)&count)) {
    return wide_to_utf8(buffer);
  }
  
  return "";
}

// #TODO: #window_creation #window_manipulation
// [ ] resize_window
// [ ] position_window
// [ ] toggle_fullscreen
// [ ] get_dimensions

// #TODO: #window_interaction (mouse/keyboard)
// [ ] get_mouse_pointer_position
// [ ] ... What APIs do I need for Keyboard

// #FileSort #FileSearch


// #GlobalHotkeys
struct Win32_Global_Hotkey {
  s32 hotkey_id;
  u32 modifiers;
  u32 virtual_key_code;
  b32 registered;
};

void win32_unregister_global_hotkey (Win32_Global_Hotkey* gh) {
  if (gh->registered) {
    bool success = (bool)UnregisterHotKey(nullptr, gh->hotkey_id);
    if (!success) {
      log_error("Failed to register global hotkey");
      os_log_error();
    }
    Assert(success);
  }
  
  gh->registered = false;
}

void win32_register_global_hotkey (Win32_Global_Hotkey* gh) {
  bool success = (bool)RegisterHotKey(nullptr, gh->hotkey_id, gh->modifiers, gh->virtual_key_code);
  if (!success) {
    log_error("Failed to register global hotkey"); // maybe print gh data?
    os_log_error();
    gh->registered = false;
  }
  Assert(success);
  
  gh->registered = true;
}

u32 hex_to_int (u8 c) {
  if (c >= 'a') return (c - 'a' + 0xA);
  if (c >= 'A') return (c - 'A' + 0xA);
  if (c >= '0') return (c - '0');
  return 1;
}

GUID guid_from_string (string str) {
  Assert(str.count == 36);
  GUID id; // #NoInit
  // Expecting format: "00000000-0000-0000-C000-000000000046"
  id.Data1 = (hex_to_int(str[0]) << 28) | (hex_to_int(str[1]) << 24) | (hex_to_int(str[2]) << 20) | (hex_to_int(str[3]) << 16) |
             (hex_to_int(str[4]) << 12) | (hex_to_int(str[5]) <<  8) | (hex_to_int(str[6]) <<  4) | (hex_to_int(str[7]) <<  0); 
  Assert(str[8] == '-');
  
  id.Data2 = ((hex_to_int(str[9]) << 12) | (hex_to_int(str[10]) << 8) | (hex_to_int(str[11]) << 4) | (hex_to_int(str[12]) << 0));
  Assert(str[13] == '-');
  id.Data3 = ((hex_to_int(str[14]) << 12) | (hex_to_int(str[15]) << 8) | (hex_to_int(str[16]) << 4) | (hex_to_int(str[17]) << 0));
  Assert(str[18] == '-');
  id.Data4[0] = ((hex_to_int(str[19]) << 4) | (hex_to_int(str[20]) << 0));
  Assert(str[23] == '-');
  id.Data4[1] = ((hex_to_int(str[21]) << 4) | (hex_to_int(str[22]) << 0));
  id.Data4[2] = ((hex_to_int(str[24]) << 4) | (hex_to_int(str[25]) << 0));
  id.Data4[3] = ((hex_to_int(str[26]) << 4) | (hex_to_int(str[27]) << 0));
  id.Data4[4] = ((hex_to_int(str[28]) << 4) | (hex_to_int(str[29]) << 0));
  id.Data4[5] = ((hex_to_int(str[30]) << 4) | (hex_to_int(str[31]) << 0));
  id.Data4[6] = ((hex_to_int(str[32]) << 4) | (hex_to_int(str[33]) << 0));
  id.Data4[7] = ((hex_to_int(str[34]) << 4) | (hex_to_int(str[35]) << 0));
  
  return id;
}

internal HICON get_hicon_used_for_file (string path, bool big_icon=false) {
  SHFILEINFOW sfi = {};
  
  push_allocator(temp());
  wstring wide_path = utf8_to_wide(path);
  bool is_directory = path_is_directory(path);

  u32 dwFileAttributes = (is_directory) ? FILE_ATTRIBUTE_DIRECTORY : FILE_ATTRIBUTE_NORMAL;
  
  if (!big_icon) {
    u32* result = (u32*)SHGetFileInfoW((LPCWSTR)wide_path.data, (DWORD)dwFileAttributes, &sfi, 
            sizeof(SHFILEINFOW), (u32)(SHGFI_ICON | SHGFI_USEFILEATTRIBUTES | SHGFI_LARGEICON));
    
    if (result != 0 && sfi.hIcon) {
      return sfi.hIcon;
    } else {
      log_error("Failed to load hicon for path %s", copy_string(path));
      os_log_error();
      return nullptr;
    }
  }
  
  if (big_icon) {
    u32* result = (u32*)SHGetFileInfoW((LPCWSTR)wide_path.data, (DWORD)dwFileAttributes, &sfi, 
            sizeof(SHFILEINFOW), (u32)(SHGFI_SYSICONINDEX));
    // log("[SHGetFileInfoW] result: 0x%p", result);
    GUID guid = guid_from_string("46EB5926-582E-4017-9FDF-E8998DAA0950");
    HIMAGELIST il = {};
    HRESULT r = SHGetImageList(SHIL_JUMBO, (const IID &)guid, (void**)&il);
    if (FAILED(r) || !il) { 
      log_error("[get_hicon_used_for_file big_icons=true] SHGetImageList failed!");
      os_log_error();
      return nullptr;
    }
    HICON hIcon = ImageList_GetIcon(il, sfi.iIcon, 0);
    
    ((IUnknown*)il)->Release();
    
    return hIcon;
  }
  
  return {};
}

force_inline internal void internal_icon_copy_row (ArrayView<u32> src, ArrayView<u32> dst) {
  Assert(src.count == dst.count);
  memcpy(dst.data, src.data, dst.count * sizeof(u32));
}

internal void flip_icon_vertical_in_place (Icon* icon) {
  push_allocator(temp());
  ArrayView<u32> row_copy = ArrayView<u32>(icon->width);
  
  for (s64 i = 0; i < icon->height/2; i += 1) {
    u32* start_ptr = (u32*)icon->bitmap.data;
    u32* end_ptr   = start_ptr + (icon->width * icon->height);
    ArrayView<u32> top_row    = { icon->width, start_ptr + (i * icon->height) };
    ArrayView<u32> bottom_row = { icon->width, end_ptr - ((i+1) * icon->height) };
    
    // copy top row to bottom row
    internal_icon_copy_row(top_row,    row_copy);
    internal_icon_copy_row(bottom_row, top_row);
    internal_icon_copy_row(row_copy,   bottom_row);
  }
}

force_inline Icon* win32_load_large_icon_internal (string full_path) { 
  if (!file_exists(full_path)) return nullptr;
  
  HICON hIcon = get_hicon_used_for_file(full_path, true);
  if (!hIcon) {
    log_error("[get_hicon_used_for_file] failed for path: %s", copy_string(full_path).data);
    os_log_error();
    return nullptr;
  }
  
  ICONINFOEXW icon_info = {};
  icon_info.cbSize = sizeof(ICONINFOEXW);
  bool get_icon_success = (bool)GetIconInfoExW(hIcon, &icon_info);
  if (!get_icon_success) {
    log_error("[GetIconInfoExW] failed for path: %s", copy_string(full_path).data); 
    os_log_error();
    return nullptr;
  }
  
  BITMAPINFO bitmap_info = {};
  bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  
  HDC hdc = get_main_window_pointer()->hdc;
  
  s32 result = GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr, 
          &bitmap_info, DIB_RGB_COLORS);
  if (result == 0 || result == ERROR_INVALID_PARAMETER) {
    log_error("[1] GetDIBits failed!");
    os_log_error();
    return nullptr;
  }
  
  // Force a known-safe format:
  bitmap_info.bmiHeader.biCompression = BI_RGB;
  bitmap_info.bmiHeader.biBitCount    = 32;
  bitmap_info.bmiHeader.biPlanes      = 1;
  
  Icon* icon = New<Icon>();
  icon->path = copy_string(full_path);
  icon->width = bitmap_info.bmiHeader.biWidth;
  icon->height = bitmap_info.bmiHeader.biHeight;
  icon->bitmap = ArrayView<u8>(bitmap_info.bmiHeader.biSizeImage);
  
  result = GetDIBits(hdc, icon_info.hbmColor, 0, bitmap_info.bmiHeader.biHeight, icon->bitmap.data, 
          &bitmap_info, DIB_RGB_COLORS);
  if (result == 0 || result == ERROR_INVALID_PARAMETER) {
    log_error("[2] GetDIBits failed!");
    os_log_error();
    return nullptr;
  }
  
  flip_icon_vertical_in_place(icon);
  
  DeleteObject(icon_info.hbmColor);
  DeleteObject(icon_info.hbmMask);
  DestroyIcon(hIcon);

  return icon;
}

void delete_icon (Icon* icon) {
  string_free(icon->path);
  array_free(icon->bitmap);
  internal_free(icon);
}

Icon* load_large_icon (string full_path) { // @allocates
  return win32_load_large_icon_internal(full_path);
}

bool win32_open_file_unattached_no_args_v2 (string full_path) {
  push_allocator(temp());
  wstring full_path_w = utf8_to_wide(full_path);
  
  // 1. Create the job object
  HANDLE job = CreateJobObjectW(nullptr, nullptr);
  if (!job) {
    log("CreateJobObject failed: %lu\n", GetLastError());
    return false;
  }

  // 2. Enable SILENT breakaway
  JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = {};
  info.BasicLimitInformation.LimitFlags =
      JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;

  if (!SetInformationJobObject(
          job,
          JobObjectExtendedLimitInformation,
          &info,
          sizeof(info))) {
      log_error("SetInformationJobObject failed");
      os_log_error();
      return false;
  }

  // 3. Assign *yourself* to the job
  if (!AssignProcessToJobObject(job, GetCurrentProcess())) {
      log_error("AssignProcessToJobObject failed");
      os_log_error();
      return false;
  }

  STARTUPINFOW si = {};
  si.cb = sizeof(si);

  PROCESS_INFORMATION pi = {};
  
  bool success = CreateProcessW((LPCWSTR)full_path_w.data, nullptr, 
    nullptr, nullptr, 
    FALSE, 0,
    nullptr, nullptr, 
    &si, &pi);
    
  if (!success) {
    log_error("[win32_open_file_unattached_no_args] CreateProcessW(%s) failed", wide_to_utf8(full_path_w.data).data);
    os_log_error();
    return false;
  }
  
  CloseHandle(pi.hThread);
  CloseHandle(pi.hProcess);
  CloseHandle(job);
  
  return true;
}

bool win32_select_item_in_explorer (string path) {
  push_allocator(temp());
  string quoted_path = format_string("/select,\"%s\"", copy_string(path).data);
  HINSTANCE result = ShellExecuteW(nullptr, L"open", 
      L"explorer.exe", (LPCWSTR)utf8_to_wide(quoted_path).data, 
      nullptr, SW_SHOWNORMAL);
  if ((s64)result <= 32) {
    log_error("[win32_select_item_in_explorer] ShellExecuteW failed with args `open explorer.exe %s`", 
      quoted_path);
    os_log_error();
    return false;
  }
  
  return true;
}

bool win32_open_directory_in_explorer (string path) {
  push_allocator(temp());
  string quoted_path = format_string("\"%s\"", copy_string(path).data);
  HINSTANCE result = ShellExecuteW(nullptr, L"open", 
      L"explorer.exe", (LPCWSTR)utf8_to_wide(quoted_path).data, 
      nullptr, SW_SHOWNORMAL);
  if ((s64)result <= 32) {
    log_error("[win32_open_directory_in_explorer] ShellExecuteW failed with args `open explorer.exe %s`", 
      quoted_path);
    os_log_error();
    return false;
  }
  
  return true;
}

bool win32_open_in_file_pilot (string path) {
  push_allocator(temp());
  string quoted_path = format_string("\"%s\"", copy_string(path).data);
  HINSTANCE result = ShellExecuteW(nullptr, nullptr, 
      L"FPilot.exe", (LPCWSTR)utf8_to_wide(quoted_path).data, 
      nullptr, SW_SHOWNORMAL);
  if ((s64)result <= 32) {
    log_error("[win32_open_in_file_pilot] ShellExecuteW failed with args `null FPilot.exe %s`", 
      quoted_path);
    os_log_error();
    return false;
  }
  
  return false;
}

force_inline bool win32_select_item_in_file_explorer (string path) {
  OS_Process_Info* info = &global_win32_state.process_info;
  switch (info->open_directories_in) {
    case Open_Directories_In::Explorer: {
      return win32_select_item_in_explorer(path);
    }
    case Open_Directories_In::FPilot: {
      return win32_open_in_file_pilot(path);
    }
    default:
      log_error("[win32_open_directory], global_win32_state.process_info.open_directories_in is not a recognized value: %d", info->open_directories_in);
      Assert(false); // invalid state!
      return false;
  }
}

force_inline bool win32_open_directory (string path) {
  // decide to open in explorer or FPilot:
  OS_Process_Info* info = &global_win32_state.process_info;
  switch (info->open_directories_in) {
    case Open_Directories_In::Explorer: {
      return win32_open_directory_in_explorer(path);
    }
    case Open_Directories_In::FPilot: {
      return win32_open_in_file_pilot(path);
    }
    default:
      log_error("[win32_open_directory], global_win32_state.process_info.open_directories_in is not a recognized value: %d", info->open_directories_in);
      Assert(false); // invalid state!
      return false;
  }
}

bool win32_open_file_in_default_program (string path, string working_directory) {
  if (path_is_directory(path)) {
    return win32_open_directory(path);
  }
  
  push_allocator(temp());

  // We use ShellExecuteExW so we can properly detatch from new launched process.
  wstring full_path_w = utf8_to_wide(path);
  wstring working_dir_w = utf8_to_wide(working_directory);

  SHELLEXECUTEINFOW sei = {};
  sei.cbSize      = sizeof(SHELLEXECUTEINFOW);
  sei.fMask       = SEE_MASK_NOCLOSEPROCESS; // |SEE_MASK_NO_CONSOLE
  sei.lpFile      = (LPCWSTR)full_path_w.data;
  sei.lpDirectory = (LPCWSTR)working_dir_w.data;
  sei.nShow       = SW_SHOWNORMAL;

  bool success = (bool)ShellExecuteExW(&sei);
  if (!success) {
    log_error("[win32_open_file_in_default_program] ShellExecuteExW failed with args path: `%s`, working_directory: `%s`", 
      wide_to_utf8(full_path_w).data, wide_to_utf8(working_dir_w).data);
    os_log_error();
  }

  return success;
}

bool win32_open_file_unattached_no_args (string full_path) {
  /////// VERSION 1 -- DOESN'T DETACH
  // SHELLEXECUTEINFOW sei = {};
  // sei.cbSize = sizeof(SHELLEXECUTEINFOW);
  // sei.fMask  = SEE_MASK_NOASYNC | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS;
  // sei.lpVerb = L"open";
  // // sei.lpVerb = L"runas"; // launch as administrator
  // sei.lpFile = (LPCWSTR)full_path_w.data;
  // sei.nShow  = SW_SHOWNORMAL;
  
  // if (!ShellExecuteExW(&sei)) {
  //   log_error("[win32_open_file_unattached_no_args] ShellExecuteExW(%s) failed", wide_to_utf8(full_path_w.data).data);
  //   os_log_error();
  //   return false;
  // }
  
  /////// VERSION 2 -- TESTING...
  push_allocator(temp());
  wstring full_path_w = utf8_to_wide(full_path);
  
  STARTUPINFOW si = {};
  si.cb = sizeof(si);

  PROCESS_INFORMATION pi = {};
  
  bool success = CreateProcessW((LPCWSTR)full_path_w.data, nullptr, 
    nullptr, nullptr, 
    FALSE, CREATE_BREAKAWAY_FROM_JOB,
    nullptr, nullptr, 
    &si, &pi);
    
  if (!success) {
    log_error("[win32_open_file_unattached_no_args] CreateProcessW(%s) failed", wide_to_utf8(full_path_w.data).data);
    os_log_error();
    return false;
  }
  
  // If we launch applications in our job object, they will quit when 
  // this application quits, so we have to do this chicanery to get the
  // launched exes to be independent:
  // JOBOBJECT_EXTENDED_LIMIT_INFORMATION info = {};
  // info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_BREAKAWAY_OK;
  
  // bool success = (bool)SetInformationJobObject(
  //     job,
  //     JobObjectExtendedLimitInformation,
  //     &info,
  //     sizeof(info)
  // );
  // if (!success) {
  //   log_error("SetInformationJobObject failed!");
  //   os_log_error();
  // }
  // sei.hProcess should get registered?
  
  return true;
}

void open_exe_unattached_old (string full_path) {
  // Old version: `Open_Exe_Unattached`
  push_allocator(temp());
  wstring full_path_w = utf8_to_wide(full_path);
  
  // Dirty no-good low-down hack to get the new process to remain separate from this process.
  // Everything else I tried did NOT work. This is basically just changing the parent of our 
  // job process to another process, so if we exit, it stays alive. 
  // In this case explorer is the one that launches the program.
  // This is also WAY slower than just calling CreateProcessW
  bool success = (bool)ShellExecuteW(nullptr, L"open", L"explorer.exe",
              (LPCWSTR)full_path_w.data, nullptr, SW_SHOWNORMAL);
  if (!success) {
    log_error("ShellExecuteW(open explorer.exe %s) failed", wide_to_utf8(full_path_w).data);
    os_log_error();
  }
}

force_inline Icon* win32_load_small_icon_internal (string full_path) {
  if (!file_exists(full_path)) return nullptr;
  
  HICON hIcon = get_hicon_used_for_file(full_path, false);
  if (!hIcon) {
    log_error("[get_hicon_used_for_file] failed for path: %s", copy_string(full_path).data);
    os_log_error();
    return nullptr;
  }
  
  ICONINFOEXW icon_info = {};
  icon_info.cbSize = sizeof(ICONINFOEXW);
  bool get_icon_success = (bool)GetIconInfoExW(hIcon, &icon_info);
  if (!get_icon_success) {
    log_error("[GetIconInfoExW] failed for path: %s", copy_string(full_path).data); 
    os_log_error();
    return nullptr;
  }
  
  BITMAPINFO bitmap_info = {};
  bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  
  HDC hdc = get_main_window_pointer()->hdc;
  
  s32 result = GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr, 
          &bitmap_info, DIB_RGB_COLORS);
  if (result == 0 || result == ERROR_INVALID_PARAMETER) {
    log_error("[win32_load_small_icon_internal][1] GetDIBits failed!");
    os_log_error();
    return nullptr;
  }
  
  // Force a known-safe format:
  bitmap_info.bmiHeader.biCompression = BI_RGB;
  bitmap_info.bmiHeader.biBitCount    = 32;
  bitmap_info.bmiHeader.biPlanes      = 1;
  
  Icon* icon = New<Icon>();
  icon->path = copy_string(full_path);
  icon->width = bitmap_info.bmiHeader.biWidth;
  icon->height = bitmap_info.bmiHeader.biHeight;
  icon->bitmap = ArrayView<u8>(bitmap_info.bmiHeader.biSizeImage);
  
  result = GetDIBits(hdc, icon_info.hbmColor, 0, bitmap_info.bmiHeader.biHeight, icon->bitmap.data, 
          &bitmap_info, DIB_RGB_COLORS);
  if (result == 0 || result == ERROR_INVALID_PARAMETER) {
    log_error("[2] GetDIBits failed!");
    os_log_error();
    DeleteObject(icon_info.hbmColor);
    DeleteObject(icon_info.hbmMask);
    return nullptr;
  }
  
  flip_icon_vertical_in_place(icon);
  
  DeleteObject(icon_info.hbmColor);
  DeleteObject(icon_info.hbmMask);
  DestroyIcon(hIcon);
  
  return icon;
}

void os_clipboard_set_text (string s) {
  push_allocator(temp());
  
  Window_Info* info = get_main_window_pointer();
  
  if (!OpenClipboard(info->window)) return;
  
  EmptyClipboard();
  
  wstring ws = utf8_to_wide(s);
  s64 ws_bytes = ws.count * 2 + 2;
  
  HGLOBAL clipbuffer = GlobalAlloc(0, (u64)ws_bytes);
  u8* buffer = (u8*)GlobalLock(clipbuffer);
  memcpy(buffer, ws.data, ws_bytes);
  GlobalUnlock(clipbuffer);

  SetClipboardData(CF_UNICODETEXT, clipbuffer);
  
  CloseClipboard();
}

string os_clipboard_get_text () { // #allocates
  Window_Info* info = get_main_window_pointer();
  
  if (OpenClipboard(info->window)) {
    HANDLE hData = GetClipboardData(CF_UNICODETEXT);
    u16* buffer = (u16*)GlobalLock(hData);
    string s = wide_to_utf8(buffer);
    GlobalUnlock(hData);
    CloseClipboard();
    return s;
  }
  
  return "";
}

void os_clipboard_clear () {
  Window_Info* info = get_main_window_pointer();
  
  if (OpenClipboard(info->window)) {
    EmptyClipboard();
    CloseClipboard();
  }
}

bool os_execute_program_with_arguments (string exe_path, string args) {
  push_allocator(temp());
  bool success = (bool)ShellExecuteW(
    nullptr, 
    L"open", 
    (LPCWSTR)utf8_to_wide(exe_path).data, 
    (LPCWSTR)utf8_to_wide(args).data, 
    nullptr,
    SW_SHOWNORMAL);
  
  if (!success) {
    log_error("[os_execute_program_with_arguments] ShellExecuteW failed with arguments: open `%s %s`",
      copy_string(temp(), exe_path), copy_string(temp(), args));
    os_log_error();
  }
  
  return success;
}

string powershell_path () {
  push_allocator(temp());

  u16 sysDir[Win32_Max_Path_Length];
  u32 chars_copied = GetSystemDirectoryW((LPWSTR)sysDir, Win32_Max_Path_Length);
  if (chars_copied == 0) {
    log_error("[GetSystemDirectoryW] failed!");
    os_log_error();
    return "";
  }
  
  wcscat((LPWSTR)sysDir, L"\\WindowsPowerShell\\v1.0\\powershell.exe");
  // Any better way to get this?
  // "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
  return copy_string(temp(), trim(wide_to_utf8(sysDir)));
}

bool win32_run_powershell_script (string script) {
  // calls os_execute_program_with_arguments
  bool success = os_execute_program_with_arguments(powershell_path(), script);
  if (!success) {
    log_error("[win32_run_powershell_script] failed to execute with script: `%s`",
      copy_string(temp(), script).data);
    os_log_error();
  }
  
  return success;
}

bool os_run_shell_script (string script_path, string working_directory) {
  // should have some global setting for preferred terminal to open
  // For now, default to powershell.
  push_allocator(temp());
  string powershell_script = format_string("-NoExit -Command \"Set-Location '%s'\"; & '%s'", 
    copy_string(working_directory).data, copy_string(script_path).data);
    
  return win32_run_powershell_script(powershell_script);
}

bool os_safe_delete_file (string path) {
  push_allocator(temp());
  u16 from[Win32_Max_Path_Length + 2] = {};
  // #TODO: must be double null terminated:
  wstring path_w = utf8_to_wide(path);
  Assert(path_w.count < Win32_Max_Path_Length);
  memcpy(from, path_w.data, path_w.count * sizeof(u16));
  
  SHFILEOPSTRUCTW op = {};
  op.wFunc  = FO_DELETE;
  op.pFrom  = (PCZZWSTR)from;
  op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
  
  // SHFileOperation fails on any path prefixed with "\?".
  return SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted;
}

bool os_delete_directory (string path) {
  push_allocator(temp());
  auto_release_temp();
  
  wstring path_w = utf8_to_wide(path);
  
  u32 full_path_size = GetFullPathNameW((LPCWSTR)path_w.data, 0, nullptr, nullptr);
  if (!full_path_size) {
    log_error("[os_delete_directory] GetFullPathNameW failed with input `%s`, 0, null, null", 
      copy_string(path).data);
    os_log_error();
    return false;
  }
  
  wstring absolute_path_wide = wstring((full_path_size + 1));
  u32 final_length = GetFullPathNameW((LPCWSTR)path_w.data, full_path_size, (LPWSTR)absolute_path_wide.data, nullptr);
  if (!final_length) {
    log_error("[os_delete_directory] GetFullPathNameW failed with input `%s`, `%d`, `absolute_path_wide`, null", 
      copy_string(path).data, full_path_size);
    os_log_error();
    return false;
  }
  
  Assert(final_length <= full_path_size-1); // -1 because on success, it does NOT include the null character, but on failure it does. This used to be ==, but in some cases we get a shorter path... I guess the return value of GetFullPathNameW is inexact in some cases!
  absolute_path_wide[final_length + 1] = (u16)0; // extra null character after the string-terminating null character.
  
  SHFILEOPSTRUCTW op = {};
  op.wFunc  = FO_DELETE;
  op.pFrom  = (PCZZWSTR)absolute_path_wide.data;
  // op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
  
  s32 result = SHFileOperationW(&op);
  if (result != 0) {
    log_error("[os_delete_directory] SHFileOperationW failed! See error code:");
    os_log_error();
    return false;
  } else if (op.fAnyOperationsAborted) {
    log_error("[os_delete_directory] SHFileOperationW was not able to delete anything (op.fAnyOperationsAborted = true).");
    os_log_error();
    return false;
  } else {
    return true;
  }
}

bool os_delete_file_or_directory (string path) {
  if (path_is_directory(path)) {
    return os_delete_directory(path);
  }
  
  push_allocator(temp());
  wstring path_w = utf8_to_wide(path);
  return DeleteFileW((LPCWSTR)path_w.data) != 0;
}

void os_clear_unused_pages () {
  SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1);
}

// #NTFS#MFT 
constexpr u64 MFT_FILE_REFERENCE_ID_ROOT = U64_MAX;

struct File_Enumeration_Results {
  Serializer strings; // Serializer?
  ArenaArray<u32> offsets;
  ArenaArray<s16> lengths;
  ArenaArray<u64> sizes;
  ArenaArray<u64> modtimes; 
  // Index of the parent directory
  ArenaArray<u64> parent_indices;
  
  // For back-linking (remove and put into NTFS_mft_read_raw) (Temporary)
  ArenaArray<u64> reference_ids; // FRN
  ArenaArray<u64> parent_ids;    // Link to parent FRN.
};


s64 array_bytes (File_Enumeration_Results* results) {
  if (!results) return 0;
  s64 total_bytes = 0;
  total_bytes += (results->offsets.count        * sizeof(u32));
  total_bytes += (results->lengths.count        * sizeof(s16));
  total_bytes += (results->sizes.count          * sizeof(u64));
  total_bytes += (results->modtimes.count       * sizeof(u64));
  total_bytes += (results->parent_indices.count * sizeof(u64));
  
  total_bytes += (results->reference_ids.count  * sizeof(u64));
  total_bytes += (results->parent_ids.count     * sizeof(u64));
  
  return total_bytes;
}

void file_enumeration_results_init (File_Enumeration_Results* results) {
  arena_array_init(&results->strings, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: strings");
  arena_array_init(&results->offsets, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: offsets");
  arena_array_init(&results->lengths, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: lengths");
  arena_array_init(&results->sizes,   Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: sizes");
  arena_array_init(&results->modtimes, Arena_Reserve::Size_2G, 0,"File_Enumeration_Results: modtimes");
  
  arena_array_init(&results->reference_ids, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: reference_ids");
  arena_array_init(&results->parent_ids,    Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: parent_ids");
  
  arena_array_init(&results->parent_indices, Arena_Reserve::Size_2G, 0, "File_Enumeration_Results: parent_indices");
}

void file_enumeration_results_free (File_Enumeration_Results* results) {
  arena_array_free(results->strings);
  arena_array_free(results->offsets);
  arena_array_free(results->lengths);
  arena_array_free(results->sizes);
  arena_array_free(results->modtimes);
  
  // arena_array_free(results->reference_ids);
  // arena_array_free(results->parent_ids);
  
  arena_array_free(results->parent_indices);
}

struct NTFS_MFT_Enumeration_Info {
  bool thread_started;
  f64 start_time;
  f64 end_time;
};

struct NTFS_MFT_Data {
  Win32_Drive* drive; // copy! so we can verify data!
  // data: see Win32_File_Enumeration_Drive, minus the bit tables
  File_Enumeration_Results dirs;
  File_Enumeration_Results files;
  
  // #SortIndices (RadixSort, Qsort_r)
  Arena* arena; // for backing below indices
  // #TODO: allocate RadixSort with temp(), then array_copy the results with arena.
  ArrayView<u32> dirs_sorted_modtime;
  ArrayView<u32> files_sorted_modtime;
  ArrayView<u32> dirs_sorted_size;
  ArrayView<u32> files_sorted_size;
  ArrayView<u32> dirs_sorted_name;
  ArrayView<u32> files_sorted_name;
  ArrayView<u32> dirs_sorted_path;
  ArrayView<u32> files_sorted_path;
  
  f64 start_time;
  f64 end_time;
};

struct MFT_File_Info {
  string name;
  u64 parent_index;
};

MFT_File_Info file_info (NTFS_MFT_Data* mft_data, s64 src_index, bool is_directory) {
  File_Enumeration_Results* f = 
      (is_directory) ? &mft_data->dirs : &mft_data->files;
      
  s64 name_count = (f->lengths)[src_index];
  u32 offset     = (f->offsets)[src_index];
  u8* string_ptr = &f->strings.data[offset];
  
  MFT_File_Info info = {};
  info.name = {name_count, string_ptr};
  info.parent_index = f->parent_indices[src_index];
  
  return info;
}

s64 file_count (NTFS_MFT_Data* mft_data) {
  File_Enumeration_Results* f = &mft_data->files;
  return f->offsets.count; 
}

s64 directory_count (NTFS_MFT_Data* mft_data) {
  File_Enumeration_Results* f = &mft_data->dirs;
  return f->offsets.count;
}

string mft_drive_label (NTFS_MFT_Data* mft_data) {
  push_allocator(temp());
  return copy_string(mft_data->drive->label);
}

string full_path (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
  File_Enumeration_Results* f = 
      (is_directory) ? &mft_data->dirs : &mft_data->files;
  
  Array<string> paths_reverse = {};
  paths_reverse.allocator = temp();
  
  // Root
  MFT_File_Info info = file_info(mft_data, index, is_directory);
  string directory_name = info.name; // last item in
  
  while (info.parent_index != MFT_FILE_REFERENCE_ID_ROOT && info.parent_index != 0) {
    info = file_info(mft_data, info.parent_index, true);
    array_add(paths_reverse, info.name);
  }
  
  String_Builder* sb = context_builder();
  reset_string_builder(sb, true);
  
  string drive_label = mft_drive_label(mft_data);
  append(sb, drive_label);
  
  for_each_reverse(i, paths_reverse) {
    append(sb, paths_reverse[i]);
    append(sb, "\\");
  }
  
  append(sb, directory_name);
  
  return builder_to_string(sb);
}

FILETIME file_modtime (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
  File_Enumeration_Results* f = 
      (is_directory) ? &mft_data->dirs : &mft_data->files;
  
  FILETIME ft;
  memcpy(&ft, &(f->modtimes)[index], sizeof(u64));
  
  return ft;
}

s64 file_size_bytes (NTFS_MFT_Data* mft_data, s64 index, bool is_directory) {
  File_Enumeration_Results* f = 
      (is_directory) ? &mft_data->dirs : &mft_data->files;
  
  return (s64)(f->sizes[index]);
}

struct NTFS_MFT_Enumeration {
  Arena*                     arena;
  ArrayView<OS_Drive*>       drives;
  ArrayView<NTFS_MFT_Data>   results;
};

// Singleton data because we only want one set of threads enumerating disks at one time.
global Arena*        ntfs_mft_setup_arena;
global Thread*       ntfs_mft_master_thread; // This thread isn't really necessary, it's just used so we can get the most accurate timing.
global Thread_Group* ntfs_mft_thread_group;

global NTFS_MFT_Enumeration        ntfs_mft_data;
global NTFS_MFT_Enumeration_Info   ntfs_mft_enum_info;

// force_inline // should this be inlined??
void NTFS_MFT_add_record (NTFS_MFT_Data* mft_data, NTFS_File* file) {
  wstring file_name_w = {file->name_count, file->name_data};
  string file_name = wide_to_utf8(file_name_w);
  File_Enumeration_Results* r;
  if (file->is_directory) {
    r = &mft_data->dirs;
  } else {
    r = &mft_data->files;
  }
  
  u32 offset = AddString_NoCount(&r->strings, file_name.data, (s16)file_name.count);
  array_add((r->offsets),  offset);
  array_add((r->lengths),  (s16)file_name.count);
  // Use the existing file reference ids to link files and parent directories:
  array_add((r->modtimes), file->file_modtime);
  array_add((r->sizes),    file->file_size);
  
  array_add((r->reference_ids), file->record_id);
  array_add((r->parent_ids),    file->parent_id);
}

// Replace add_record and follow pattern in `win32_file_enum_thread_proc`
bool NTFS_read_internal (NTFS_MFT_Internal* mft, void* buffer, u64 from, u64 count) {
  s32 high = (s32)(from >> 32);
  DWORD result = SetFilePointer(mft->handle, (s32)(from & 0xFFFFFFFF), (PLONG)&high, FILE_BEGIN);
  if (result == INVALID_SET_FILE_POINTER) {
    log_error("SetFilePointer to %p on drive %s failed!", from, copy_string(mft->drive_label).data);
    os_log_error();
    return false;
  }
  u32 bytes_accessed_internal;
  BOOL success = ReadFile(mft->handle, buffer, (DWORD)count, (LPDWORD)&bytes_accessed_internal, nullptr);
  if (!success) {
    log_error("ReadFile @ %p on drive %s failed!", from, copy_string(mft->drive_label).data);
    os_log_error();
    return false;
  }
  mft->bytes_accessed += bytes_accessed_internal;
  
  return bytes_accessed_internal == count;
}

bool NTFS_MFT_read_raw (NTFS_MFT_Data* mft_data) {
  Assert(mft_data != nullptr);
  if (mft_data == nullptr) { return false; }

  auto_release_temp();
  push_allocator(temp());
  
  string drive_path = copy_string(mft_data->drive->label);
  if (mft_data->drive->file_system != File_System::NTFS) {
    log_warning("[NTFS_MFT_read_raw] Failed to enumerate %s as it is not an NTFS drive!", drive_path.data);
  }
  
  string drive_letter = Win32_drive_letter(drive_path); // copies.
  
  string create_file_target = format_string("\\\\.\\%s:", drive_letter.data);
  HANDLE file_handle = CreateFileA((LPCSTR)create_file_target.data, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
  
  if (file_handle == INVALID_HANDLE_VALUE) {
    log_error("CreateFileA failed on target %s. Most likely you do not have admin permissions.", create_file_target.data);
    os_log_error();
    return false;
  }
  
  NTFS_MFT_Internal* mft = new_ntfs_mft_internal();
  mft->handle = file_handle;
  mft->drive_label = drive_path;
  
  bool success;
  NTFS_BootSector boot_sector;
  success = NTFS_read_internal(mft, &boot_sector, 0, 512);
  if (!success) {
    log_error("[NTFS_MFT_read_raw] Failed to read boot sector for drive %s!", drive_path.data);
    os_log_error();
    return false;
  }
  
  u64 bytes_per_cluster = (boot_sector.bytesPerSector * boot_sector.sectorsPerCluster);
  success = NTFS_read_internal(mft, mft->mft_file.data, boot_sector.mftStart * bytes_per_cluster, NTFS_MFT_File_Record_Size);
  if (!success) {
    log_error("[NTFS_MFT_read_raw] Failed to read MFT for drive %s! This drive may be corrupted.", drive_path.data);
    os_log_error();
    return false;
  }
  
  NTFS_FileRecordHeader* file_record_start = (NTFS_FileRecordHeader*)mft->mft_file.data;
  if (file_record_start->magic != 0x454C4946) {
    log_error("[NTFS_read_drive_raw] Magic number check failed! This drive is not NTFS or is corrupted!");
    return false;
  }
  
  NTFS_AttributeHeader* attribute = (NTFS_AttributeHeader*)(mft->mft_file.data + file_record_start->firstAttributeOffset);
  NTFS_NonResidentAttributeHeader* data_attribute = nullptr;
  u64 approximate_record_count = 0;
  
  while (true) {
    if (attribute->attributeType == 0x80) {
      data_attribute = (NTFS_NonResidentAttributeHeader*)attribute;
    } else if (attribute->attributeType == 0xB0) {
      approximate_record_count = ((NTFS_NonResidentAttributeHeader*)attribute)->attributeSize * 8;
    } else if (attribute->attributeType == 0xFFFFFFFF) {
        break;
    }
    
    attribute = (NTFS_AttributeHeader*) ((u8*) attribute + attribute->length);
  } // while (true)
  
  Assert(data_attribute != nullptr);
  
  NTFS_RunHeader* dataRun = (NTFS_RunHeader*)((u8*)data_attribute + data_attribute->dataRunsOffset);
  u64 cluster_number = 0, records_processed = 0;
  
  // outer loop
  while (((u8*)dataRun - (u8*)data_attribute) < data_attribute->length && dataRun->lengthFieldBytes) {
    u64 length = 0, offset = 0;
    
    for (u8 i = 0; i < dataRun->lengthFieldBytes; i += 1) {
      length |= (u64)(((u8*)dataRun)[1 + i]) << (i * 8);
    }
    
    for (u8 i = 0; i < dataRun->offsetFieldBytes; i += 1) {
      offset |= (u64)(((u8*)dataRun)[1 + dataRun->lengthFieldBytes + i]) << (i * 8);
    }
    
    if (offset & ((u64) 1 << (dataRun->offsetFieldBytes * 8 - 1))) {
      for (s64 i = dataRun->offsetFieldBytes; i < 8; i += 1) {
        offset |= ((u64)0xFF << (u64)(i * 8));
      }
    }
    
    cluster_number += offset;
    dataRun = (NTFS_RunHeader*)((u8*)dataRun + 1 + dataRun->lengthFieldBytes + dataRun->offsetFieldBytes);
    
    u64 files_remaining = length * bytes_per_cluster / NTFS_MFT_File_Record_Size;
    u64 position_in_block = 0;
    
    while (files_remaining) { // enumerate files in chunks of 65536
      u64 files_to_load = NTFS_MFT_Files_Per_Buffer;
      if (files_remaining < NTFS_MFT_Files_Per_Buffer) {
        files_to_load = files_remaining;
      }
      
      success = NTFS_read_internal(mft, mft->mft_buffer.data, cluster_number * bytes_per_cluster + position_in_block, files_to_load * NTFS_MFT_File_Record_Size);
      if (!success) {
        log_error("[NTFS_MFT_read_raw] Failed to read MFT for drive %s! This drive may be corrupted.", drive_path.data);
        os_log_error();
        return false;
      }
      position_in_block += files_to_load * NTFS_MFT_File_Record_Size;
      files_remaining   -= files_to_load;
      
      for (s64 i = 0; i < (s64)files_to_load; i += 1) { // load 
        // Even on an M.2 SSD, processing the file records takes only a fraction of the time to read the data, so there's not much point in multithreading this:
        NTFS_FileRecordHeader* fileRecord = (NTFS_FileRecordHeader*)(mft->mft_buffer.data + NTFS_MFT_File_Record_Size * i);
        records_processed += 1;
        u64 record_id = records_processed - 1;
        
        NTFS_File file = {};
        bool file_is_directory = (bool)fileRecord->isDirectory;
        
        // A file record may be blank or unused; just skip it. 
        if (!fileRecord->inUse) continue;
        
        NTFS_AttributeHeader* attribute = (NTFS_AttributeHeader*)((u8*)fileRecord + fileRecord->firstAttributeOffset);
        Assert(fileRecord->magic == 0x454C4946);
        if (fileRecord->magic != 0x454C4946) {
          log_error("[NTFS_read_drive_raw] Magic number check failed! This drive is likely corrupted!");
          return false;
        }
        
        // inner loop
        while ((u8*)attribute - (u8*)fileRecord < NTFS_MFT_File_Record_Size) {
          bool is_named = (attribute->nameOffset != 0);
          bool is_nonresident = false;
          // #NOTE: The attribute list is of variable length and terminated with 0xFFFFFFFF. For 1K MFT records, the attribute list starts at offset 0x30.
          // source: https://flatcap.github.io/linux-ntfs/ntfs/concepts/file_record.html
          // 0x10: $STANDARD_INFORMATION - contains file altered time, etc. 
          if (attribute->attributeType == 0x10) { // $STANDARD_INFORMATION
            NTFS_FileStandardInformationHeader* fileInfoAttribute = (NTFS_FileStandardInformationHeader*)attribute;
            
            file.file_modtime = fileInfoAttribute->modificationTime;
            // DOS file permissions
            add_file_permissions(&file, fileInfoAttribute->filePermissions);
          }
          // 0x30: $FILE_NAME - stores the name of the file attribute (always resident)
          if (attribute->attributeType == 0x30) { // $FILE_NAME
            // #NOTE [VERY IMPORTANT]: All fields, except the parent directory, are only updated when the filename is changed. Until then, they just become out of date. $STANDARD_INFORMATION Attribute, however, will always be kept up-to-date.
            NTFS_FileNameAttributeHeader* fileNameAttribute = (NTFS_FileNameAttributeHeader*)attribute;
            // #namespace: https://flatcap.github.io/linux-ntfs/ntfs/concepts/filename_namespace.html
            // 2 means dos-friendly, but I'm not even sure we need to check this!
            is_nonresident = fileNameAttribute->nonResident;
            
            if (fileNameAttribute->namespaceType != 2 && !is_nonresident) {
              // We need both the sequenceNumber and parentRecordNumber here
              u64 sequence_number = ((u64)fileNameAttribute->sequenceNumber) << (u64)48;
              file.parent_id    = sequence_number | (u64)((u64)fileNameAttribute->parentRecordNumber & 0xFFFFFFFFFFULL);
              Assert(record_id == (u64)fileRecord->recordNumber);
              file.record_id    = (((u64)fileRecord->sequenceNumber) << 48ULL) | (u64)((u64)record_id & 0xFFFFFFFFFFULL);
              file.name_count   = fileNameAttribute->fileNameLength;
              file.name_data    = (u16*)fileNameAttribute->fileName;
              file.is_directory = file_is_directory;
            }
          }
          // 0x50 = $SECURITY_DESCRIPTOR source: https://flatcap.github.io/linux-ntfs/ntfs/attributes/security_descriptor.html
          // 0x80: $DATA - This Attribute contains the file's data. A file's size is the size of its unnamed Data Stream.
            // https://flatcap.github.io/linux-ntfs/ntfs/concepts/file.html#data
          // #TODO: should skip if file_name is null.
          if (attribute->attributeType == 0x80 && !is_named) { // $DATA
            // #NOTE: The size of the attribute depends on two things. Does it have a name? Is it resident?
            // #TODO: Check if file is compressed then access compressedSize
            // source: https://flatcap.github.io/linux-ntfs/ntfs/concepts/attribute_header.html#flags

            if (attribute->nonResident) {
              add_flags(&file, attribute->flags); // compressed, encrypted, sparse
              
              NTFS_NonResidentAttributeHeader* nonresident_attribute = (NTFS_NonResidentAttributeHeader*)attribute;
              // dataRunsOffset should be 0x40 if attribute is Non-Resident, No Name
              // Assert(nonresident_attribute->dataRunsOffset == 0x40);
              
              // This size should be correct even if compressed.
              // #NOTE: VERY IMPORTANT: The top 16 bits are garbage and should be masked out!
              // they do not have any meaning or encode any useful information!
              // s64 attribute_size = nonresident_attribute->attributeSize  & 0xFFFFFFFFFFFFULL;
              file.file_size = nonresident_attribute->streamDataSize & 0xFFFFFFFFFFFFULL;
              if (file.is_directory) { file.file_size = 0; }
            }  
          }

          if (attribute->attributeType == 0xFFFFFFFF) {
            // add_record(drive->data, &file);
            // See Dense_FS drive->data
            mft->object_count += 1;
            NTFS_MFT_add_record(mft_data, &file);
            
            break;
          }
          
          attribute = (NTFS_AttributeHeader*)((u8*)attribute + attribute->length);
        } // while: inner loop
      } // for i: 0..files_to_load-1
    } // while: files_remaining
  } // while: outer loop
  
  CloseHandle(file_handle);
  string timer_label = format_string(temp(), "Hash Table Part %s", copy_string(drive_path).data);
  Timed_Block_Print(timer_label);
  
  // The Table is temporary so it can live on the stack:
  // This doesn't really need to be an ArenaTable, since we know the number of slots we need..
  auto table = ArenaTable<u64, s32>(); // mapping FRN to Indices
  table_init(&table, directory_count(mft_data), Arena_Reserve::Size_2G, "Directory ID ArenaTable");
  for (s64 i = 0; i < directory_count(mft_data); i += 1) {
    table_set(&table, mft_data->dirs.reference_ids[i], (s32)i);
  }
  
  // Link directories
  s32 parent_failed_dir = 0;
  array_resize(mft_data->dirs.parent_indices, directory_count(mft_data), false);
  for (s64 i = 0; i < directory_count(mft_data); i += 1) {
    u64 parent_id = mft_data->dirs.parent_ids[i];
    s32 result_index = 0;
    bool success = table_find(&table, parent_id, &result_index);
    // Assert(success);
    if (success) {
      mft_data->dirs.parent_indices[i] = result_index;
    } else {
      mft_data->dirs.parent_indices[i] = MFT_FILE_REFERENCE_ID_ROOT;
      parent_failed_dir += 1;
    }
  }
  
  // Link files
  s32 parent_failed_file = 0;
  array_resize(mft_data->files.parent_indices, file_count(mft_data), false);
  for (s64 i = 0; i < file_count(mft_data); i += 1) {
    u64 parent_id = mft_data->files.parent_ids[i];
    s32 result_index = 0;
    bool success = table_find(&table, parent_id, &result_index);
    // Assert(success);
    if (success) {
      mft_data->files.parent_indices[i] = result_index;
    } else {
      mft_data->files.parent_indices[i] = MFT_FILE_REFERENCE_ID_ROOT;
      parent_failed_file += 1;
    }
  }
  
  log("[%s] Failed to find parent for %lld dirs and %lld files", copy_string(drive_path).data, parent_failed_dir, parent_failed_file);
  
  // we should be able to purge both reference_id and parent_id
  arena_array_free(mft_data->dirs.reference_ids);
  arena_array_free(mft_data->dirs.parent_ids);
  arena_array_free(mft_data->dirs.sizes); // unused
  arena_array_free(mft_data->files.reference_ids);
  arena_array_free(mft_data->files.parent_ids);
  table_release(&table);
  
  // post-processing
  mft_data->end_time = GetUnixTimestamp();
  f64 elapsed_time = mft_data->end_time - mft_data->start_time;
  log("Finished enumeration for drive %s in %s. Found %s records.", copy_string(drive_path).data, format_time_seconds(elapsed_time).data, format_int_with_commas(records_processed).data);
  
  return true;
}

Thread_Continue_Status ntfs_mft_enumeration_thread_group_proc (Thread_Group* group, Thread* thread, void* work) {
  auto mft_data = thread_group_task(NTFS_MFT_Data);
  
  mft_data->start_time = GetUnixTimestamp();
  
  bool success = NTFS_MFT_read_raw(mft_data);
  // Assert(success);
  
  return Thread_Continue_Status::CONTINUE;
}

s64 ntfs_mft_enumerate_drives_thread_proc (Thread* thread) {
  auto task = thread_task(NTFS_MFT_Enumeration);
  
  s64 work_added = 0;
  s64 work_completed = 0;
  
  for_each(d, task->drives) {
    NTFS_MFT_Data* result = &task->results[d];
    
    add_work(ntfs_mft_thread_group, result);
    work_added += 1;
  }

  while (work_completed < work_added) {
    ArrayView<void*> cw = get_completed_work(ntfs_mft_thread_group);
    
    work_completed += cw.count;
    Sleep(1);
  }
  
  ntfs_mft_enum_info.end_time = GetUnixTimestamp();
  f64 elapsed_time = ntfs_mft_enum_info.end_time - ntfs_mft_enum_info.start_time;
  log("[ntfs_mft_enumerate_drives_thread_proc] Completed %d tasks in %s.", 
    work_completed, format_time_seconds(elapsed_time).data);
  
  return 0;
}

bool ntfs_mft_initialize () {
  Timed_Block_Print("ntfs_mft_initialize");
  // This arena contains temporary stuff that's required only to bootstrap enumeration, so it can be discarded at once
  ntfs_mft_setup_arena   = bootstrap_arena(Arena_Reserve::Size_64M, "ntfs_mft_setup_arena"); // can probably be a fixed arena
  push_arena(ntfs_mft_setup_arena);
  ntfs_mft_master_thread = New<Thread>();
  string thread_name     = "NTFS MFT Enumeration - Master Thread";
  bool success = thread_init(ntfs_mft_master_thread, ntfs_mft_enumerate_drives_thread_proc, thread_name);
  if (!success) {
    log_error("[ntfs_mft_initialize] Failed to initialize thread!");
    os_log_error(); Assert(false);
    return false;
  }
  
  ntfs_mft_thread_group = New<Thread_Group>();
  ntfs_mft_thread_group->allocator = allocator(ntfs_mft_setup_arena);
  
  // s32 thread_count = 1;
  s32 thread_count = os_cpu_physical_core_count();
  
  thread_group_init(ntfs_mft_thread_group, thread_count, ntfs_mft_enumeration_thread_group_proc, "ntfs_mft_enumeration", true);
  
  return true;
}

void ntfs_mft_begin_enumeration () { Timed_Block_Print("ntfs_mft_begin_enumeration");
  ntfs_mft_enum_info.thread_started = true;
  ntfs_mft_enum_info.start_time     = GetUnixTimestamp();
  
  ntfs_mft_data.arena  = bootstrap_arena(Arena_Reserve::Size_64M, "ntfs_mft_data.arena");
  push_arena(ntfs_mft_data.arena);
  ntfs_mft_data.drives  = os_get_available_drives(); // copies
  ntfs_mft_data.results = ArrayView<NTFS_MFT_Data>(ntfs_mft_data.drives.count);
  // Make copy of Win32_Drive to result using copy_win32_drive
  for_each(d, ntfs_mft_data.drives) {
    Win32_Drive* drive = ntfs_mft_data.drives[d];
    NTFS_MFT_Data* mft_data = &ntfs_mft_data.results[d];
    
    file_enumeration_results_init(&mft_data->dirs);
    file_enumeration_results_init(&mft_data->files);
    
    mft_data->drive = copy_win32_drive(drive);
  }
  
  thread_start(ntfs_mft_master_thread, &ntfs_mft_data);
  thread_group_start(ntfs_mft_thread_group);
}

bool ntfs_mft_enumeration_started () {
  return ntfs_mft_enum_info.thread_started;
}

bool ntfs_mft_enumeration_is_done () {
  if (ntfs_mft_master_thread == nullptr) return true;
  
  if (thread_is_done(ntfs_mft_master_thread)) {
    thread_deinit(ntfs_mft_master_thread, true);
    bool success = thread_group_shutdown(ntfs_mft_thread_group);
    Assert(success);
    arena_delete(ntfs_mft_setup_arena);
    ntfs_mft_master_thread = nullptr;
    ntfs_mft_setup_arena   = nullptr;
    ntfs_mft_thread_group  = nullptr;
    return true;
  }
  
  return false;
}

