internal
b8 init_imgui(GLFWwindow* window) {
  IMGUI_CHECKVERSION();
  ImGui::CreateContext();
  ImGuiIO& io = ImGui::GetIO(); (void) io;
  io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
  io.IniFilename = nullptr;
  io.LogFilename = nullptr;

  ImGui::StyleColorsDark();

  ImGui_ImplGlfw_InitForOpenGL(window, true);
  ImGui_ImplOpenGL3_Init("#version 330");

  return true;
}

internal 
void glfw_error_callback(i32 error, const char *description) {
  fprintf(stderr, "GLFW error %d: %s\n", error, description);
}

internal
void glfw_key_callback(GLFWwindow *window, i32 key, i32 /*scancode*/, i32 action, i32 /*mods*/)
{
  App_State *app = (App_State *)glfwGetWindowUserPointer(window);
  Input_Key ikey = app->viewer.glfw_key_mapping[key];
  if (ikey == KEY_INVALID)
    return;

  u8 &state = app->user_input.key_state[ikey];
  switch (action) {
  case GLFW_PRESS:
    if (!(state & KEY_STATE_IS_DOWN)) 
      state |= KEY_STATE_JUST_PRESSED;
    state |= KEY_STATE_IS_DOWN;
    break;
    
  case GLFW_RELEASE:
    if (state & KEY_STATE_IS_DOWN) 
      state |= KEY_STATE_JUST_RELEASED;
    state &= ~KEY_STATE_IS_DOWN;
    break;

  case GLFW_REPEAT:
    state |= KEY_STATE_JUST_PRESSED;
    break;

  default:;
  }
}

internal
void glfw_init_key_mapping(Viewer &viewer)
{
  viewer.glfw_key_mapping[GLFW_KEY_Q]           = KEY_Q;
  viewer.glfw_key_mapping[GLFW_KEY_UP]          = KEY_UP;
  viewer.glfw_key_mapping[GLFW_KEY_DOWN]        = KEY_DOWN;
  viewer.glfw_key_mapping[GLFW_KEY_LEFT]        = KEY_LEFT;
  viewer.glfw_key_mapping[GLFW_KEY_RIGHT]       = KEY_RIGHT;
  viewer.glfw_key_mapping[GLFW_KEY_LEFT_ALT]    = KEY_ALT;
  viewer.glfw_key_mapping[GLFW_KEY_TAB]         = KEY_TAB;
  viewer.glfw_key_mapping[GLFW_KEY_LEFT_SHIFT]  = KEY_SHIFT;
  viewer.glfw_key_mapping[GLFW_KEY_KP_ADD]      = KEY_PLUS;
  viewer.glfw_key_mapping[GLFW_KEY_KP_SUBTRACT] = KEY_MINUS;
  viewer.glfw_key_mapping[GLFW_KEY_ESCAPE]      = KEY_ESC;
}

internal 
GLFWwindow *init_glfw(App_State &app, i32 desired_win_width, i32 desired_win_height)
{
  glfw_init_key_mapping(app.viewer);
  
  glfwSetErrorCallback(glfw_error_callback);
  glfwInit();
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  glfwWindowHint(GLFW_DOUBLEBUFFER, GLFW_TRUE);
  glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
  glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);
  glfwWindowHint(GLFW_DEPTH_BITS, 32);
#ifndef NDEBUG
  glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, true);
#endif

  GLFWwindow *window = glfwCreateWindow(
      desired_win_width, desired_win_height,
      "RNTuple Viewer " V_MAJOR "." V_MINOR,
      nullptr,
      nullptr
      );

  glfwSetWindowUserPointer(window, &app);
  glfwSetKeyCallback(window, glfw_key_callback);
  glfwMakeContextCurrent(window);

  return window;
}

internal
void monitor_mouse_btn(GLFWwindow *window, u8 *mouse_btn_state, i32 glfw_btn, Mouse_Button btn)
{
  u8 *state = &mouse_btn_state[btn];
  if (glfwGetMouseButton(window, glfw_btn) == GLFW_PRESS) {
    if (!(*state & MOUSE_BTN_STATE_IS_DOWN)) 
      *state |= MOUSE_BTN_STATE_JUST_PRESSED;
    *state |= MOUSE_BTN_STATE_IS_DOWN;
  } else if (glfwGetMouseButton(window, glfw_btn) == GLFW_RELEASE) {
    if (*state & MOUSE_BTN_STATE_IS_DOWN) 
      *state |= MOUSE_BTN_STATE_JUST_RELEASED;
    *state &= ~MOUSE_BTN_STATE_IS_DOWN;
  }
}

internal 
void reset_user_input(User_Input &input)
{
  for (u32 i = 0; i < KEY_COUNT; ++i)
    input.key_state[i] &= ~(KEY_STATE_JUST_PRESSED|KEY_STATE_JUST_RELEASED);

  for (u32 i = 0; i < MOUSE_BTN_COUNT; ++i)
    input.mouse_btn_state[i] &= ~(MOUSE_BTN_STATE_JUST_PRESSED|MOUSE_BTN_STATE_JUST_RELEASED);
}

internal 
void run_main_loop(GLFWwindow *window, Arena *arena, App_State &app)
{
  f32 delta_time_ms;
  chr::time_point last_saved_time = chr::high_resolution_clock::now();

  app.delta_time_accum.max = 100;
  app.delta_time_accum.base = arena_push_array<f32>(arena, app.delta_time_accum.max);
  
  while (!app.should_quit) {
    chr::time_point frame_start = chr::high_resolution_clock::now();
    u64 time_since_prev_frame_us = chr::duration_cast<chr::microseconds>(frame_start - last_saved_time).count();
    delta_time_ms = time_since_prev_frame_us * 0.001f;
    last_saved_time = frame_start;

    reset_user_input(app.user_input);

    glfwPollEvents();

    // Update window size
    {
      Window_Data &wdata = app.win_data;

      i32 prev_width = wdata.width;
      i32 prev_height = wdata.height;

      glfwGetWindowSize(window, &wdata.width, &wdata.height);

      wdata.size_just_changed = prev_width != wdata.width || prev_height != wdata.height;
    }

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    // Check if the inspected file changed
    {
      // TODO: this should re-run get_tfile_data and get_rntuple_data!
      char buf[sizeof(inotify_event) + NAME_MAX + 1];
      ssize_t nbytes = read(app.inspected_file.inot, buf, sizeof(buf));
      if (nbytes)
        app.inspected_file.size = file_size(app.inspected_file.stream);    
    }

    if ((app.user_input.key_state[KEY_ESC] & KEY_STATE_IS_DOWN) || glfwWindowShouldClose(window)) {
      app.should_quit = true;
      break;
    }

    b32 focused = glfwGetWindowAttrib(window, GLFW_FOCUSED);
    if (focused) {
      // NOTE: keyboard is updated via the callback because that's the only way to intercept GLFW_REPEAT events
      
      // Update mouse
      f64 mposx, mposy;
      glfwGetCursorPos(window, &mposx, &mposy);

      u8 *mouse_btn_state = app.user_input.mouse_btn_state;
      monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_LEFT,  MOUSE_BTN_LEFT);
      monitor_mouse_btn(window, mouse_btn_state, GLFW_MOUSE_BUTTON_RIGHT, MOUSE_BTN_RIGHT);

      app.user_input.mouse_pos.x = static_cast<i32>(mposx);
      app.user_input.mouse_pos.y = static_cast<i32>(mposy);
    }

    update_and_render(arena, app, delta_time_ms);

    // ImGui::ShowDemoWindow();

    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    glfwSwapBuffers(window);
  }  
}