// #TODO: #OS_Win32
// [ ] #Thread cleanup: in `thread_deinit` is there any requirement to cleanup child threads?
// [ ] #Exception handling code in `Win32_Exception_Filter`
// [ ] #cpuid - enumerate CPUs and Thread count (current implementation doesn't work)

#if OS_WINDOWS
constexpr s64 FILETIME_TO_UNIX = 116444736000000000i64;
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;
}
#endif

struct OS_System_Info {
  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;
};

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;
};

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

global OS_State_Win32 os_state_w32;
internal b32 win32_g_is_quiet = 0; // No console output

internal LONG WINAPI Win32_Exception_Filter (EXCEPTION_POINTERS* exception_ptrs) {
  if (win32_g_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);
  }
  
  // #Exception handling code (TODO)
  
  return 0;
}

// internal void Main_Entry_Point (int argc, WCHAR **argv);
internal void Win32_Entry_Point (int argc, WCHAR **argv) {
  // 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 = &os_state_w32.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 = &os_state_w32.process_info;
    info->large_pages_allowed = false;
    info->process_id = GetCurrentProcessId();
  }
  // #cpuid
  /*{ OS_System_Info* info = &os_state_w32.system_info;
    // [ ] Extract input args 
    u32 length;
    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;
    u32 max_performance = 0;
    u32 performance_core_count = 0;
    // u32 efficient_core_count;
    
    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;
      }
      
      all_cpus_count += count_per_group;  
    }
  
    info->physical_core_count = (s32)all_cpus_count;
    info->primary_core_count  = (s32)performance_core_count;
  } 
  // info->secondary_core_count = ;
  */
  { OS_System_Info* info = &os_state_w32.system_info;
    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 = &os_state_w32.process_info;
    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));
  }
  
  // [ ] Get Working directory (info->working_path)
  // [ ] GetEnvironmentStringsW
  temp_reset();
  printf("Hello there!\n\n");
}

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
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);
  // #NewContext
  ExpandableArena* arena_ex = expandable_arena_new(Arena_Reserve::Size_64M, 16);
  
  thread->context = New<Thread_Context>(allocator(arena_ex));
  thread->context->temp        = expandable_arena_new(Arena_Reserve::Size_2M, 16);
  thread->context->arena       = arena_ex;
  thread->context->allocator   = allocator(arena_ex);
  thread->context->thread_idx  = (s32)this_thread_index;
  thread->context->thread_name = copy_string(thread_name);
  thread->context->log_builder = new_string_builder(Arena_Reserve::Size_64M);
  
  thread->os_thread.windows_thread    = windows_thread;
  thread->os_thread.windows_thread_id = windows_thread_id;
  
  thread->proc = proc;
  thread->index = this_thread_index;
  
  return true;
}

internal void thread_deinit (Thread* thread) {
  if (thread->os_thread.windows_thread) {
    CloseHandle(thread->os_thread.windows_thread);
  }
  thread->os_thread.windows_thread = nullptr;
  
  arena_delete(thread->context->temp);
  arena_delete(thread->context->arena);
  free_string_builder(thread->context->log_builder);
  
  // memset(thread, 0xCD, sizeof(Thread);
}

internal void thread_start (Thread* thread) {
  ResumeThread(thread->os_thread.windows_thread);
}

internal bool thread_is_done (Thread* thread, s32 milliseconds=0) {
  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 result; // trim_right(result, "\r\n");
}

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`, code: %d, %s", file_path, error_code, get_error_string(error_code).data);
  }
  
  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.\n");
    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);
  
  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);
}

internal ArrayView<u8> read_entire_file (File file) {
  ArrayView<u8> file_data;

  bool result = file_length(file, &file_data.count);
  if (!result) return {};
  result = file_set_position(file, 0);
  if (!result) return {};
  
  file_data.data = NewArray<u8>(file_data.count, 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 {};
  }
  
  Assert(bytes_read == file_data.count);
  file_data.count = bytes_read;
  
  return file_data;
}

internal ArrayView<u8> read_entire_file (string file_path, 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);
  
  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;
}

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);
}

// #window_creation -> put API in OS_Win32.h
// void initialize_window_class(float background_color[3]) {}
Window_Type create_window (string new_window_name, bool center_window) {
  return 0;
}

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

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