reload_main :: () {
   system_allocator := context.allocator;

   // Ensure we're not allocating anywhere unexpected
   context.allocator = mem.make_crash_allocator();

   basic.set_working_directory(strings.path_strip_filename(system.get_path_of_running_executable()));

   lib_api, ok := load_library(0);
   basic.assert(ok, "host: failed to load library");

   lib_versions: [..]Library;
   lib_versions.allocator = system_allocator;

   // H always points to the currently loaded dynamic library.
   H := lib_api;
   next_lib_version := 1;

   defer {
      for * lib_versions {
         cleanup_library(it);
      }

      cleanup_library(*H);
      basic.free(lib_versions.data,, allocator = system_allocator);
   }

   basic.print("host: allocating % bytes memory (% for state)\n", H.max_memory, H.state_size);

   lib_memory := basic.alloc(xx H.max_memory,, allocator = system_allocator);
   assert(lib_memory != null, "host: failed to allocate memory!");
   defer free(lib_memory,, allocator = system_allocator);

   lib_arena: mem.Arena;
   mem.init_arena(*lib_arena, lib_memory, H.max_memory);

   lib_allocator := mem.make_arena_allocator(*lib_arena);
   lib_state := basic.alloc(xx H.state_size,, allocator = lib_allocator);

   H.init(lib_state, lib_allocator, true);
   H.setup();

   while true {
      defer reset_temporary_storage();

      status := H.frame();
      if status == .quit break;

      reload := status == .soft_reload || status == .hard_reload;
      if !reload {
         path := basic.tprint(Default_Lib_Path, Lib_Extension);
         mod  := futils.file_modtime_and_size(path);
         if mod != H.mod_time {
            reload = true;
         }
      }

      if !reload {
         continue;
      }

      new_api, ok := load_library(next_lib_version);
      if !ok {
         basic.print("host: failed to load version % of the library\n", next_lib_version);
         continue;
      }

      old_ssize := H.state_size;
      old_msize := H.max_memory;
      new_ssize := new_api.state_size;
      new_msize := new_api.max_memory;
      if old_ssize != new_ssize || old_msize != new_msize {
         status = .hard_reload;
      }

      // hard reloads are when full initialization has to happen again,
      // as if we restarted the program. this is mostly due to memory/state
      // size changing between reloads. however, programs can explicitly request this.
      if status == .hard_reload {
         basic.print("host: performing hard reload...\n");

         // the currently allocated memory block is only safe to reuse
         // if the memory size hasn't changed between reloads.
         if old_msize <= new_msize {
            lib_arena.offset = 0;
         }
         // otherwise we need to reallocate the program memory
         // and reinitialize.
         else {
            free(lib_memory,, allocator = system_allocator);

            lib_memory = basic.alloc(xx new_msize,, allocator = system_allocator);
            basic.assert(lib_memory != null, "host: failed to allocator new memory");
            mem.init_arena(*lib_arena, lib_memory, new_msize);
         }

         // in either case, we want ensure lib_state is at the start of our program's memory.
         lib_state = basic.alloc(xx new_ssize,, allocator = lib_allocator);
      }
      else {
         // we don't need to do anything special for soft reloads, only transfer
         // the current state pointer and allocator to the library.
         basic.print("host: performing soft reload...\n");
         basic.array_add(*lib_versions, H);
      }

      H = new_api;
      next_lib_version += 1;
      H.init(lib_state, lib_allocator, status == .hard_reload);
   }

   H.teardown();
}


#scope_file;

Library :: struct {
   max_memory: u64;
   state_size: u64;

   init:      Init_Proc;
   setup:     Setup_Proc;
   teardown:  Teardown_Proc;
   frame:     Frame_Proc;

   version:  int;
   handle:   *void;
   mod_time: basic.Apollo_Time;
}

Default_Lib_Path :: #run tprint("%.\%", lib_name);
Temp_Lib_Path    :: #run tprint("__temp_%_\%.\%", lib_name);

#if OS == {
   case .WINDOWS;
      Lib_Extension :: "dll";
   case .MACOS;
      Lib_Extension :: "dylib";
   case .LINUX;
      Lib_Extension :: "so";
}

load_library :: (version: int) -> Library, bool {
   default_path := basic.tprint(Default_Lib_Path, Lib_Extension);

   mod, size, exists := futils.file_modtime_and_size(default_path);
   if !exists {
      basic.print("host: % did not exist\n", default_path);
      return .{}, false;
   }

   new_path := basic.tprint(Temp_Lib_Path, version, Lib_Extension);
   if !futils.copy_file(default_path, new_path,, allocator = basic.temp) {
      basic.print("host: could not copy % to %\n", default_path, new_path);
      return .{}, false;
   }

   handle := os_load_library(new_path);
   if !handle {
      basic.print("host: failed to load library at %\n", new_path);
      return .{}, false;
   }

   return .{
      max_memory = os_find_library_symbol(handle, "max_memory").(*u64).*,
      state_size = os_find_library_symbol(handle, "state_size").(*u64).*,
      init       = os_find_library_symbol(handle, "init"),
      setup      = os_find_library_symbol(handle, "setup"),
      teardown   = os_find_library_symbol(handle, "teardown"),
      frame      = os_find_library_symbol(handle, "frame"),

      version  = version,
      handle   = handle,
      mod_time = mod,
   }, true;
}

cleanup_library :: (library: *Library) {
   path := basic.tprint(Temp_Lib_Path, library.version, Lib_Extension);
   if library.handle && !os_unload_library(library.handle) {
      basic.print("host: failed to unload temporary library: %\n", path);
   }

   if !file.file_delete(path) {
      basic.print("host: failed to delete temporary library: %\n", path);
   }
}

#if OS == .WINDOWS {
   #import "Windows";
   // @note(judah): Haven't tested this on my windows machine,
   // but I remember things working like this.

   os_load_library :: (path: string) -> *void {
      handle := LoadLibraryA(temp_c_string(path));
      return handle;
   }

   os_unload_library :: (handle: *void) -> bool {
      return FreeLibrary(handle);
   }

   os_find_library_symbol :: (handle: *void, symbol: string) -> *void {
      return GetProcAddress(handle, temp_c_string(symbol));
   }
}
else #if OS == .MACOS || OS == .LINUX {
   #import "POSIX";

   os_load_library :: (path: string) -> *void {
      handle := dlopen(basic.temp_c_string(path), RTLD_NOW);
      return handle;
   }

   os_unload_library :: (handle: *void) -> bool {
      return dlclose(handle) == 0;
   }

   os_find_library_symbol :: (handle: *void, symbol: string) -> *void {
      return dlsym(handle, basic.temp_c_string(symbol));
   }
}
else {
   #assert(false, "only windows, mac, and linux are supported for now");
}

mem :: #import "jc/memory";

basic   :: #import "Basic"; // @future
system  :: #import "System"; // @future
file    :: #import "File"; // @future
futils  :: #import "File_Utilities"; // @future
strings :: #import "String"; // @future
