// SPDX-License-Identifier: Unlicense

#include "backup.h"
#include "camera.h"
#include "collection.h"
#include "coop.h"
#include "data.h"
#include "file.h"
#include "font.h"
#include "geometry.h"
#include "getopt.h"
#include "global.h"
#include "highway.h"
#include "highway2.h"
#include "hud.h"
#include "hud2.h"
#include "input.h"
#include "midi.h"
#include "milk.h"
#include "opengl.h"
#include "openxr.h"
#include "particle.h"
#include "player.h"
#include "png.h"
#include "recorder.h"
#include "settings.h"
#include "sfx.h"
#include "shader.h"
#include "skybox.h"
#include "sound.h"
#include "stage.h"
#include "stats.h"
#include "stretcher.h"
#include "test.h"
#include "texture.h"
#include "tones.h"
#include "ui.h"
#include "version.h"
#include "window.h"
#include "xr.h"

#include <chrono>

#ifdef SHR3D_SDL
#include <SDL.h>
#endif // SHR3D_SDL
#ifdef SHR3D_WINDOW_SDL
#include <SDL_syswm.h>
#endif // SHR3D_WINDOW_SDL

#ifdef SHR3D_WINDOW_X11
#include <X11/Xlib.h>
#include <GL/gl.h>
#include <GL/glx.h>
#endif // SHR3D_WINDOW_X11

#ifdef __ANDROID__
#include <jni.h>

#ifdef PLATFORM_QUEST_3
#include <unistd.h>
#include <pthread.h>
#include <sys/prctl.h> // for prctl( PR_SET_NAME )
#include <android/log.h>
#include <android/native_window_jni.h> // for native window JNI
#include <android_native_app_glue.h>
#endif // PLATFORM_QUEST_3

#endif // __ANDROID__

#ifdef PLATFORM_ANDROID_SDL
static void updateAndroidRootPath()
{
  ASSERT(Global::rootPath[Global::rootPath.size() - 1] == '/');

  JNIEnv* env = nullptr;
  Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

  jobject activity = (jobject)Global::androidActivity;
  jclass activityClass(env->GetObjectClass(activity));

  jmethodID getApplicationMethod = env->GetMethodID(activityClass, "getApplication", "()Landroid/app/Application;");

  jobject applicationObj = env->CallObjectMethod(activity, getApplicationMethod);

  jclass contextClass = env->FindClass("android/content/Context");

  jmethodID getApplicationContextId = env->GetMethodID(contextClass, "getApplicationContext", "()Landroid/content/Context;");

  jobject contextObj = env->CallObjectMethod(applicationObj, getApplicationContextId);

  jmethodID getFilesDirId = env->GetMethodID(contextClass, "getDataDir", "()Ljava/io/File;");

  jobject dataDirObj = env->CallObjectMethod(contextObj, getFilesDirId);

  jclass fileClass = env->FindClass("java/io/File");

  jmethodID getPathId = env->GetMethodID(fileClass, "getPath", "()Ljava/lang/String;");

  jstring dataDirPathObj = (jstring)env->CallObjectMethod(dataDirObj, getPathId);

  const char* dataDirPathChars = env->GetStringUTFChars(dataDirPathObj, nullptr);
  Global::rootPath = reinterpret_cast<const char*>(dataDirPathChars);
  Global::rootPath += '/';
  env->ReleaseStringUTFChars(dataDirPathObj, dataDirPathChars);

  env->DeleteLocalRef(activityClass);
  env->DeleteLocalRef(applicationObj);
  env->DeleteLocalRef(contextClass);
  env->DeleteLocalRef(contextObj);
  env->DeleteLocalRef(dataDirObj);
  env->DeleteLocalRef(fileClass);
  env->DeleteLocalRef(dataDirPathObj);

  ASSERT(Global::rootPath[Global::rootPath.size() - 1] == '/');
}
#endif // PLATFORM_ANDROID_SDL

static void initX11Window();
void init()
{
  Global::init();

#ifdef __ANDROID__
  File::createPathDirectories();
#endif // __ANDROID__

#ifdef SHR3D_PNG_DECODER
  Png::init();
#endif // SHR3D_PNG_DECODER

#ifdef SHR3D_RECORDER
  Recorder::init();
#endif // SHR3D_RECORDER

#ifdef SHR3D_STARTUP_TESTS
  Test::run();
#endif // SHR3D_STARTUP_TESTS

  Backup::init();

#ifdef SHR3D_OPENXR_PCVR
  if (Settings::xrEnabled)
  {
    OpenXr::init();
  }
#endif // SHR3D_OPENXR_PCVR

#ifdef SHR3D_SDL
  if (SDL_Init(
#ifdef SHR3D_WINDOW_SDL
    SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER |
#endif // SHR3D_WINDOW_SDL
#ifdef SHR3D_AUDIO_SDL
    SDL_INIT_AUDIO |
#endif // SHR3D_AUDIO_SDL
    0
  ))
  {
    SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
    SDL_Quit();
  }
#endif // SHR3D_SDL

#ifdef SHR3D_WINDOW_SDL
#ifdef PLATFORM_ANDROID_SDL
  SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
#endif // PLATFORM_ANDROID_SDL

#if defined PLATFORM_EMSCRIPTEN || defined PLATFORM_ANDROID_SDL || defined SHR3D_OPENGL_ES_FORCE
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#ifdef PLATFORM_WINDOWS
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif // PLATFORM_WINDOWS
#else // PLATFORM_EMSCRIPTEN || PLATFORM_ANDROID_SDL || SHR3D_OPENGL_ES_FORCE
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
  SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
  //SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif // PLATFORM_EMSCRIPTEN || PLATFORM_ANDROID_SDL || SHR3D_OPENGL_ES_FORCE
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
  SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
#ifdef SHR3D_GRAPHICS_MSAA
  if (Settings::graphicsMSAA > 0)
  {
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 1 << Settings::graphicsMSAA);
  }
#endif // SHR3D_GRAPHICS_MSAA
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

  i32 fullScreenFlag = SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL;
  if (Settings::graphicsFullscreen == FullscreenMode::borderless)
    fullScreenFlag |= SDL_WINDOW_FULLSCREEN_DESKTOP;

  Global::window = SDL_CreateWindow("Shr3D", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
    Settings::graphicsWindowWidth, Settings::graphicsWindowHeight, fullScreenFlag);
  ASSERT(Global::window != nullptr);

  {
    const SDL_Surface* surface = SDL_GetWindowSurface(Global::window);
    Global::resolutionWidth = surface->w;
    Global::resolutionHeight = surface->h;
  }

#ifdef SHR3D_CUSTOM_CURSOR
  if (Settings::uiCursorCustom)
  {
    SDL_Surface* cursorSurface = SDL_CreateRGBSurfaceFrom((void*)Data::Texture::cursor, 128, 128, 32, 128 * 4, 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000);

    if (Settings::uiCursorSize == 128)
    {
      Global::defaultCursor = SDL_CreateColorCursor(cursorSurface, 0, 0);
      SDL_FreeSurface(cursorSurface);
    }
    else
    {
      SDL_Surface* scaledCursorSurface = SDL_CreateRGBSurface(
        0,
        Settings::uiCursorSize,
        Settings::uiCursorSize,
        32,
        0x000000FF,
        0x0000FF00,
        0x00FF0000,
        0xFF000000
      );

      SDL_BlitScaled(cursorSurface, nullptr, scaledCursorSurface, nullptr);
      SDL_FreeSurface(cursorSurface);
      Global::defaultCursor = SDL_CreateColorCursor(scaledCursorSurface, 0, 0);
      SDL_FreeSurface(scaledCursorSurface);
    }
    SDL_SetCursor(Global::defaultCursor);
  }
  else
#endif // SHR3D_CUSTOM_CURSOR
    Global::defaultCursor = SDL_GetCursor();


  Global::glContext = SDL_GL_CreateContext(Global::window);
  ASSERT(Global::glContext != nullptr);
#endif // SHR3D_WINDOW_SDL

#if not defined(PLATFORM_OPENXR_ANDROID) && not defined(PLATFORM_EMSCRIPTEN) && not defined(PLATFORM_ANDROID_SDL)
  OpenGl::init();
#endif // !PLATFORM_OPENXR_ANDROID && !PLATFORM_EMSCRIPTEN && !SHR3D_WINDOW_ANDROID

  Shader::init();

  Font1::init();

#ifdef SHR3D_ENVIRONMENT_SKYBOX
  Skybox::init();
#endif // SHR3D_ENVIRONMENT_SKYBOX

#ifdef SHR3D_ENVIRONMENT_STAGE
  Stage::init();
#endif // SHR3D_ENVIRONMENT_STAGE

#ifdef SHR3D_MUSIC_STRETCHER
  Stretcher::init();
#endif // SHR3D_MUSIC_STRETCHER

#ifdef SHR3D_WINDOW_SDL
#ifndef PLATFORM_EMSCRIPTEN
  SDL_GL_SetSwapInterval(to_underlying_(Settings::graphicsVSync));
#endif // PLATFORM_EMSCRIPTEN
#endif // SHR3D_WINDOW_SDL
#ifdef SHR3D_WINDOW_WIN32
  wglSwapIntervalEXT(to_underlying_(Settings::graphicsVSync));
#endif // SHR3D_WINDOW_WIN32

  GL(glEnable(GL_CULL_FACE));
  GL(glEnable(GL_DEPTH_TEST));
  GL(glDepthFunc(GL_LEQUAL));
  GL(glEnable(GL_BLEND));
  GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

  GL(glGenVertexArrays(1, &Global::dynamicDrawVao));
  GL(glGenBuffers(1, &Global::vbo));
  GL(glGenBuffers(1, &Global::ebo));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBindBuffer(GL_ARRAY_BUFFER, Global::vbo));
  GL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, Global::ebo));

  Global::texture =
#ifdef __ANDROID__
    Texture::openGlLoadTextureAstc(Data::Texture::texture_astc, sizeof(Data::Texture::texture_astc));
#else // __ANDROID__
    Texture::openGlLoadTextureDds(Data::Texture::texture_dds, sizeof(Data::Texture::texture_dds));
#endif // __ANDROID__

#ifdef SHR3D_OPENXR_PCVR
  OpenXr::init2();
#endif // SHR3D_OPENXR_PCVR

#ifdef SHR3D_WINDOW_SDL
  if (Global::gameController == nullptr && SDL_NumJoysticks() >= 1 && SDL_IsGameController(0))
    Global::gameController = SDL_GameControllerOpen(0);
#endif // SHR3D_WINDOW_SDL

  Geometry::init();

  Sound::init();
#ifdef SHR3D_SFX
  Sfx::init();
#endif // SHR3D_SFX
#ifdef SHR3D_PSARC
  Tones::init();
#endif // SHR3D_PSARC
  Stats::init();
  Collection::init();
#ifdef SHR3D_MIDI
  Midi::init();
#endif // SHR3D_MIDI

  Ui::init();

  // all non 3rd party opengl is using the same VBO setup.
  // milk and ui (3rd party) has to restore the opengl state after rendering
//#ifdef PLATFORM_QUEST_3
//#else // PLATFORM_QUEST_3
  GL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), nullptr)); // vertex coords
  GL(glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)))); // uv coords
  GL(glEnableVertexAttribArray(0));
  GL(glEnableVertexAttribArray(1));
  //#endif // PLATFORM_QUEST_3
}

static void tickTime()
{
  static std::chrono::high_resolution_clock::time_point last_frame = std::chrono::high_resolution_clock::now();
  static std::chrono::high_resolution_clock::time_point current_frame;
  current_frame = std::chrono::high_resolution_clock::now();
  Global::frameDelta = (current_frame - last_frame).count();
  Global::time_ += Global::frameDelta;
  last_frame = current_frame;
}

void tick()
{
  tickTime();

#ifdef SHR3D_OPENXR_PCVR
  if (Settings::xrEnabled)
    OpenXr::tick();
#endif // SHR3D_OPENXR_PCVR

  Player::tick();

#ifdef SHR3D_ENVIRONMENT_SKYBOX
  Skybox::tick();
#endif // SHR3D_ENVIRONMENT_SKYBOX

#ifdef SHR3D_ENVIRONMENT_STAGE
  Stage::tick();
#endif // SHR3D_ENVIRONMENT_STAGE

#ifdef PLATFORM_ANDROID_SDL
#ifdef SHR3D_MIDI
  Midi::tick();
#endif // SHR3D_MIDI
#endif // PLATFORM_ANDROID_SDL

#ifdef SHR3D_OPENXR
#ifdef SHR3D_OPENXR_PCVR
  if (Global::xrInitialized)
#endif // SHR3D_OPENXR_PCVR
  {
    Hud::tick(Global::hudCtx, Global::selectedArrangementIndex, Global::xrResolutionWidth, Global::xrResolutionHeight);
#ifndef PLATFORM_PICO_4
    Ui::tick(Global::xrResolutionWidth, Global::xrResolutionHeight);
#endif // PLATFORM_PICO_4
  }
#ifdef SHR3D_OPENXR_PCVR
  else
#endif // SHR3D_OPENXR_PCVR
#endif // SHR3D_OPENXR
#ifndef PLATFORM_OPENXR_ANDROID
  {
    Hud::tick(Global::hudCtx, Global::selectedArrangementIndex, Global::resolutionWidth, Global::resolutionHeight);
    Ui::tick(Global::resolutionWidth, Global::resolutionHeight);
  }
#endif // PLATFORM_OPENXR_ANDROID

  Highway::tick(Global::highwayCtx, Global::selectedArrangementIndex);

#ifdef SHR3D_COOP
  Coop::tick();
#endif // SHR3D_COOP

  // Workaround for opengl errors when using VST plugins that also use openGL for example GTune. No idea why it works or why it is needed.
#ifdef PLATFORM_WINDOWS
#ifdef SHR3D_WINDOW_SDL
  SDL_GL_MakeCurrent(Global::window, nullptr);
  SDL_GL_MakeCurrent(Global::window, Global::glContext);
#else // SHR3D_WINDOW_SDL
  wglMakeCurrent(Global::hdc, Global::pluginGlContext);
  wglMakeCurrent(Global::hdc, Global::glContext);
#endif // SHR3D_WINDOW_SDL
#endif // PLATFORM_WINDOWS

#ifdef SHR3D_ENVIRONMENT_MILK
  Milk::tick();
#endif // SHR3D_ENVIRONMENT_MILK

#ifdef SHR3D_BENCHMARK_FPS
  ++Global::benchmarkScore;
#endif // SHR3D_BENCHMARK_FPS
}

void render(const mat4& viewProjectionMat, const mat4& highwayViewProjectionMat, const mat4& stageViewProjectionMat)
{
#if defined(PLATFORM_WINDOWS) || defined(PLATFORM_LINUX)
  if (Global::inputWireframe.toggled)
    GL(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE));
  else
    GL(glPolygonMode(GL_FRONT_AND_BACK, GL_FILL));
#endif // PLATFORM_WINDOWS || PLATFORM_LINUX

#ifdef SHR3D_ENVIRONMENT_MILK
  Milk::render();
#endif // SHR3D_ENVIRONMENT_MILK

#ifdef SHR3D_ENVIRONMENT_SKYBOX
  Skybox::render(viewProjectionMat);
#endif // SHR3D_ENVIRONMENT_SKYBOX

#ifdef SHR3D_ENVIRONMENT_STAGE
  if (!Settings::environmentStage.empty())
    Stage::render(stageViewProjectionMat);
#endif // SHR3D_ENVIRONMENT_STAGE

  GL(glBindVertexArray(Global::staticDrawVao));

#ifdef SHR3D_RENDERER_DEVELOPMENT
  switch (Settings::highwayRenderer)
  {
  case Renderer::production:
#endif // SHR3D_RENDERER_DEVELOPMENT
    Highway::render(Global::highwayCtx, Global::selectedArrangementIndex, highwayViewProjectionMat);
#ifdef SHR3D_RENDERER_DEVELOPMENT
    break;
  case Renderer::development:
    Highway2::render(Global::highwayCtx, Global::selectedArrangementIndex, highwayViewProjectionMat);
    break;
  }
#endif // SHR3D_RENDERER_DEVELOPMENT

#ifdef SHR3D_PARTICLE
  if (Settings::highwayParticlePlayedNotes || Settings::highwayParticleCollisionNotes)
    Particle::render(highwayViewProjectionMat);
#endif // SHR3D_PARTICLE

  GL(glBindVertexArray(Global::dynamicDrawVao));

#ifdef SHR3D_HUD_DEVELOPMENT
  switch (Settings::hudRenderer)
  {
  case Renderer::production:
#endif // SHR3D_HUD_DEVELOPMENT
#ifdef PLATFORM_OPENXR_ANDROID
    Hud::render(Global::hudCtx, Global::selectedArrangementIndex, viewProjectionMat);
#else // PLATFORM_OPENXR_ANDROID
#ifdef SHR3D_OPENXR
    if (Global::xrInitialized)
      Hud::render(Global::hudCtx, Global::selectedArrangementIndex, viewProjectionMat);
    else
#endif // SHR3D_OPENXR
      Hud::render(Global::hudCtx, Global::selectedArrangementIndex, Global::resolutionWidth, Global::resolutionHeight);
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_HUD_DEVELOPMENT
    break;
  case Renderer::development:
    Hud2::render(Global::hudCtx, Global::selectedArrangementIndex, Global::resolutionWidth, Global::resolutionHeight);
    break;
  }
#endif // SHR3D_HUD_DEVELOPMENT

#ifdef SHR3D_COOP
  Coop::render();
#endif // SHR3D_COOP
}

void fini()
{
#ifdef SHR3D_AUDIO_SUPERPOWERED
  Sound::fini();
#endif // SHR3D_AUDIO_SUPERPOWERED

#ifdef SHR3D_MIDI
  Midi::fini();
#endif // SHR3D_MIDI
#ifdef SHR3D_PSARC
  Tones::fini();
#endif // SHR3D_PSARC
  Stats::fini();
  Settings::fini();
#ifndef PLATFORM_EMSCRIPTEN

#ifdef SHR3D_OPENXR_PCVR
  if (Global::xrInitialized)
    OpenXr::fini();
#endif // SHR3D_OPENXR_PCVR
  quick_exit(0);
#endif // PLATFORM_EMSCRIPTEN
}

#ifdef SHR3D_WINDOW_SDL
static void handleInput()
{
  Input::handleInputBegin();
  Ui::handleInputBegin();
  SDL_Event event;
  while (SDL_PollEvent(&event) != 0)
  {
    Window_::handleInput(event);
    Ui::handleInput(&event);
  }
  Ui::handleInputEnd();

  Input::handleInputEnd();
  Input::proccessInputEvents();
}
#endif // SHR3D_WINDOW_SDL

#ifndef PLATFORM_OPENXR_ANDROID
static void mainloop()
{
#ifdef SHR3D_WINDOW_SDL
  handleInput();
#endif // SHR3D_WINDOW_SDL

  tick();

#ifdef SHR3D_OPENXR_PCVR
  if (Settings::xrEnabled && Global::xrInitialized)
    OpenXr::render();
  else
#endif // SHR3D_OPENXR_PCVR
  {
#ifndef PLATFORM_OPENXR_ANDROID
    GL(glClearColor(Settings::environmentClearColor.r, Settings::environmentClearColor.g, Settings::environmentClearColor.b, 1.0f));
    GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
#endif // PLATFORM_OPENXR_ANDROID

    {
      static vec3 cameraTargetPosition;
      static vec3 cameraCurrentPosition;
      const mat4 viewMat = Camera::calculateViewMat(Settings::cameraMode, Global::selectedArrangementIndex, cameraTargetPosition, cameraCurrentPosition);
      const mat4 viewProjectionMat = Camera::calculateProjectionViewMat(Global::resolutionWidth, Global::resolutionHeight, viewMat);
      static vec3 highwayTargetPosition;
      static vec3 highwayCurrentPosition;
      const mat4 highwayViewProjectionMat = Camera::calculateHighwayProjectionViewMat(Settings::cameraMode, viewProjectionMat, highwayTargetPosition, highwayCurrentPosition);
      render(viewProjectionMat, highwayViewProjectionMat, viewProjectionMat);
    }

    {
      i32 width;
      i32 height;
#ifdef SHR3D_WINDOW_SDL
      SDL_GetWindowSize(Global::window, &width, &height);
#endif // SHR3D_WINDOW_SDL
#ifdef SHR3D_WINDOW_WIN32
      {
        RECT rect;
        GetClientRect(Global::window, &rect);
        width = rect.right - rect.left;
        height = rect.bottom - rect.top;
      }
#endif // SHR3D_WINDOW_WIN32

#ifndef PLATFORM_OPENXR_ANDROID
      Ui::render(width, height);
      Ui::renderClear();
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_WINDOW_SDL
      SDL_GL_SwapWindow(Global::window);
#endif // SHR3D_WINDOW_SDL
#ifdef SHR3D_WINDOW_WIN32
      SwapBuffers(Global::hdc);
#endif // SHR3D_WINDOW_WIN32
    }
  }
}
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_WINDOW_X11

typedef GLXContext(*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);

static void initX11Window()
{
  Display* display = XOpenDisplay(nullptr);
  ASSERT(display != nullptr);

  const int screen = DefaultScreen(display);
  Window root = RootWindow(display, screen);

  GLint majorGLX, minorGLX = 0;
  glXQueryVersion(display, &majorGLX, &minorGLX);
  if (majorGLX <= 1 && minorGLX < 2) {
  }

  const GLint glxAttribs[] = {
    GLX_X_RENDERABLE    , True,
    GLX_DRAWABLE_TYPE   , GLX_WINDOW_BIT,
    GLX_RENDER_TYPE     , GLX_RGBA_BIT,
    GLX_X_VISUAL_TYPE   , GLX_TRUE_COLOR,
    GLX_RED_SIZE        , 8,
    GLX_GREEN_SIZE      , 8,
    GLX_BLUE_SIZE       , 8,
    GLX_ALPHA_SIZE      , 8,
    GLX_DEPTH_SIZE      , 24,
    GLX_STENCIL_SIZE    , 8,
    GLX_DOUBLEBUFFER    , True,
    None
  };

  int fbcount;
  GLXFBConfig* fbc = glXChooseFBConfig(display, screen, glxAttribs, &fbcount);
  ASSERT(fbc != nullptr);

  int best_fbc = -1, worst_fbc = -1, best_num_samp = -1, worst_num_samp = 999;
  for (int i = 0; i < fbcount; ++i) {
    XVisualInfo* vi = glXGetVisualFromFBConfig(display, fbc[i]);
    if (vi != 0) {
      int samp_buf, samples;
      glXGetFBConfigAttrib(display, fbc[i], GLX_SAMPLE_BUFFERS, &samp_buf);
      glXGetFBConfigAttrib(display, fbc[i], GLX_SAMPLES, &samples);

      if (best_fbc < 0 || (samp_buf && samples > best_num_samp)) {
        best_fbc = i;
        best_num_samp = samples;
      }
      if (worst_fbc < 0 || !samp_buf || samples < worst_num_samp)
        worst_fbc = i;
      worst_num_samp = samples;
    }
    XFree(vi);
  }
  GLXFBConfig bestFbc = fbc[best_fbc];
  XFree(fbc); // Make sure to free this!

  XVisualInfo* visual = glXGetVisualFromFBConfig(display, bestFbc);

  XSetWindowAttributes windowAttribs;
  windowAttribs.border_pixel = BlackPixel(display, screen);
  windowAttribs.background_pixel = BlackPixel(display, screen);
  windowAttribs.override_redirect = True;
  windowAttribs.colormap = XCreateColormap(display, root, visual->visual, AllocNone);
  windowAttribs.event_mask = ExposureMask;

  Window window = XCreateWindow(display, root, 0, 0, Settings::graphicsWindowWidth, Settings::graphicsWindowHeight, 0, visual->depth, InputOutput, visual->visual, CWBackPixel | CWColormap | CWBorderPixel | CWEventMask, &windowAttribs);


  //XSelectInput(display, window, swa.event_mask);
  XStoreName(display, window, "Shr3D");

  // Set up OpenGL 3.3 context attributes
  static int context_attribs[] = {
      GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
      GLX_CONTEXT_MINOR_VERSION_ARB, 3,
      GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
      None
  };

  glXCreateContextAttribsARBProc glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddressARB((const GLubyte*)"glXCreateContextAttribsARB");
  GLXContext context = glXCreateContextAttribsARB(display, bestFbc, nullptr, True, context_attribs);
  ASSERT(context);

  glXMakeCurrent(display, window, context);

  GL(glViewport(0, 0, Global::resolutionWidth, Global::resolutionHeight));

  XMapWindow(display, window);

  init();

  while (!Global::appQuit)
  {
    if (XPending(display))
    {
      XEvent event;
      XNextEvent(display, &event);

      Input::handleInputBegin();
      //Ui::handleInputBegin();

      switch (event.type)
      {
      case Expose:
        XWindowAttributes attribs;
        XGetWindowAttributes(display, window, &attribs);
        Global::resolutionWidth = attribs.width;
        Global::resolutionHeight = attribs.height;
        GL(glViewport(0, 0, Global::resolutionWidth, Global::resolutionHeight));
        //glXSwapBuffers(display, window);
        break;
      case KeyPress:
      case KeyRelease:
      case ButtonPress:
      case ButtonRelease:
        //Window_::handleInput(event);
        //Ui::handleInput(&event);
        break;
      }

      //Ui::handleInputEnd();

      Input::handleInputEnd();
      Input::proccessInputEvents();

    }

    mainloop();

    //drawTriangle();
    glXSwapBuffers(display, window);
  }

  fini();
  unreachable();
}
#endif // SHR3D_WINDOW_X11

#ifdef __ANDROID__
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
  Global::g_JVM = vm;

  return JNI_VERSION_1_4;
}

//JNIEnv* GetJniEnv() {
//  JNIEnv* jni_env = 0;
//  Global::g_JVM->AttachCurrentThread(&jni_env, 0);
//  return jni_env;
//}

#endif // __ANDROID__

#ifndef SHR3D_WINDOW_WIN32

#ifndef PLATFORM_OPENXR_ANDROID
//#ifndef PLATFORM_ANDROID_SDL
int main(int argc, char* argv[])
//#else // PLATFORM_ANDROID_SDL
//extern "C" void android_main(struct android_app* androidApp)
//#endif // PLATFORM_ANDROID_SDL
{
  // untested
  //#ifdef PLATFORM_LINUX
  //{
  //  const int ret = mlockall(MCL_FUTURE); // never swap out any memory to disk (for audio buffers)
  //  ASSERT (ret == 0); // Failed to lock memory
  //}
  //#endif // PLATFORM_LINUX

#ifdef PLATFORM_ANDROID_SDL
#ifdef SHR3D_WINDOW_SDL
  Global::androidActivity = SDL_AndroidGetActivity();
#endif // SHR3D_WINDOW_SDL
  updateAndroidRootPath();
#endif // PLATFORM_ANDROID_SDL

  if (File::exists(Global::pathSettingsIni.c_str()))
    Global::installMode = InstallMode::installed;

  Settings::init(
#ifdef SHR3D_GETOPT
    argc, argv
#endif // SHR3D_GETOPT
  );

#ifdef SHR3D_WINDOW_X11
  initX11Window();
#else // SHR3D_WINDOW_X11

  init();

#ifdef PLATFORM_EMSCRIPTEN
  emscripten_set_main_loop(mainloop, -1, 1);
#else
  while (!Global::appQuit)
    mainloop();

  fini();
#endif // PLATFORM_EMSCRIPTEN
#endif // SHR3D_WINDOW_X11

#ifndef PLATFORM_OPENXR_ANDROID
  //return 0;
#endif // PLATFORM_OPENXR_ANDROID
}
#endif // SHR3D_WINDOW_WIN32
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_WINDOW_WIN32
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  LRESULT ret = 0;

  switch (msg)
  {
  case WM_INPUT:
  {
    HRAWINPUT hRawInput = (HRAWINPUT)lParam;

    UINT size;
    if (GetRawInputData(hRawInput, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)) != 0)
    {
      ASSERT(false);
      break;
    }

    ASSERT(size < 100);
    BYTE buffer[100];

    ASSERT(buffer != nullptr);

    if (GetRawInputData(hRawInput, RID_INPUT, buffer, &size, sizeof(RAWINPUTHEADER)) != size)
    {
      ASSERT(false);
      break;
    }

    RAWINPUT* raw = (RAWINPUT*)buffer;
    if (raw->header.dwType == RIM_TYPEKEYBOARD)
    {
      if (GetForegroundWindow() == hwnd)
      {
        RAWKEYBOARD& keyboard = raw->data.keyboard;
        if (keyboard.Flags == RI_KEY_MAKE || keyboard.Flags == RI_KEY_E0 || keyboard.Flags == RI_KEY_E1)
        {
          Input::keyStateChange(keyboard.VKey, true);

          if (keyboard.VKey == VK_RETURN && GetKeyState(VK_MENU) & (1 << 15)) // alt return
          {
            Window_::toggleFullscreen(Global::window, Global::resolutionWidth, Global::resolutionHeight, Settings::graphicsFullscreen, Global::lastMainWindowPlacement);
          }
        }
        else if (keyboard.Flags & RI_KEY_BREAK)
        {
          Input::keyStateChange(keyboard.VKey, false);
        }
      }
    }
    if (raw->header.dwType == RIM_TYPEMOUSE)
    {
      if (GetForegroundWindow() == hwnd)
      {
        // Handle mouse input
        RAWMOUSE& mouse = raw->data.mouse;
#ifdef SHR3D_OPENXR_PCVR
        Global::inputCursorPosXrX += mouse.lLastX;
        Global::inputCursorPosXrY += mouse.lLastY;
#endif // SHR3D_OPENXR_PCVR

        if (mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN)
        {
          Input::keyStateChange(VK_LBUTTON, true);
        }
        else if (mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP)
        {
          Input::keyStateChange(VK_LBUTTON, false);
        }
        else if (mouse.usButtonFlags & RI_MOUSE_WHEEL)
        {
          Global::inputWheelDelta += i16(mouse.usButtonData);
        }
      }
    }
    break;
  }
  break;
  case WM_MOUSEMOVE:
  {
    Global::inputCursorPosX = LOWORD(lParam);
    Global::inputCursorPosY = HIWORD(lParam);
    break;
  }
  break;
#ifdef SHR3D_OPENXR_PCVR
  case WM_SETFOCUS:
    if (Global::xrInitialized) // clip cursor. In XR the user should not be able to click outside the window.
    {
      RECT clientRect;
      GetClientRect(hwnd, &clientRect);
      POINT topLeft = { clientRect.left, clientRect.top };
      POINT bottomRight = { clientRect.right, clientRect.bottom };
      MapWindowPoints(hwnd, NULL, &topLeft, 1);
      MapWindowPoints(hwnd, NULL, &bottomRight, 1);
      RECT screenRect = { topLeft.x, topLeft.y, bottomRight.x, bottomRight.y };
      ClipCursor(&screenRect);
      Global::inputCursorPosXrX = Global::xrResolutionWidth / 2;
      Global::inputCursorPosXrY = Global::xrResolutionHeight / 2;
    }
    break;
#endif // SHR3D_OPENXR_PCVR
  case WM_SYSCOMMAND:
    if (wParam == SC_KEYMENU && (lParam >> 16) <= 0) // disable alt key. It causes beeps because it tries with shortcurts from the non exsisting menu bar.
      return 0;
    return DefWindowProcA(hwnd, msg, wParam, lParam);
  case WM_SIZE:
    Global::resolutionWidth = LOWORD(lParam);
    Global::resolutionHeight = HIWORD(lParam);

    if (Settings::graphicsFullscreen == FullscreenMode::windowed)
    {
      Settings::graphicsWindowWidth = Global::resolutionWidth;
      Settings::graphicsWindowHeight = Global::resolutionHeight;
    }

    glViewport(0, 0, Global::resolutionWidth, Global::resolutionHeight);
    break;
  case WM_CLOSE:
  case WM_DESTROY:
    PostQuitMessage(0);
    break;
  default:
    ret = DefWindowProcA(hwnd, msg, wParam, lParam);
    break;
  }

  Ui::handleInput(hwnd, msg, wParam, lParam);

  return ret;
}

#ifdef SHR3D_CUSTOM_CURSOR
static HCURSOR customCursor(const u8* cursorPixels, const u64 cursorPixelsSize, const i32 cursorSize)
{
  ASSERT(cursorPixelsSize == 65536); // cursorPixels is supposed to be a 128x128 BGRA image.

  HDC hdc_mem = CreateCompatibleDC(NULL);
  BITMAPINFO bmp_info = { 0 };
  bmp_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
  bmp_info.bmiHeader.biWidth = 128; // icon width
  bmp_info.bmiHeader.biHeight = 128; // icon height
  bmp_info.bmiHeader.biPlanes = 1;
  bmp_info.bmiHeader.biBitCount = 32;
  bmp_info.bmiHeader.biCompression = BI_RGB;

  void* pBits;
  HBITMAP hbm_mem = CreateDIBSection(hdc_mem, &bmp_info, DIB_RGB_COLORS, &pBits, NULL, 0);

  memcpy(pBits, cursorPixels, 65536);

  ICONINFO iconinfo
  {
    .fIcon = FALSE,
    .xHotspot = 0,
    .yHotspot = 0,
    .hbmMask = hbm_mem,
    .hbmColor = hbm_mem
  };

  const HICON hIcon = CreateIconIndirect(&iconinfo);

  DeleteObject(hbm_mem);
  DeleteDC(hdc_mem);

  if (cursorSize == 128)
    return (HCURSOR)hIcon;

  const HICON hIconResized = (HICON)CopyImage(hIcon, IMAGE_CURSOR, cursorSize, cursorSize, LR_DEFAULTCOLOR);
  DestroyIcon(hIcon);

  return (HCURSOR)hIconResized;
}
#endif // SHR3D_CUSTOM_CURSOR

static void initWgl()
{
  const HWND dummy = CreateWindowA(
    "STATIC",
    "DummyWindow",
    WS_OVERLAPPED,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    0,
    0,
    0,
    0
  );
  ASSERT(dummy != 0);
  const HDC hdc = GetDC(dummy);
  ASSERT(hdc != 0);
  PIXELFORMATDESCRIPTOR desc =
  {
      .nSize = sizeof(desc),
      .nVersion = 1,
      .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
      .iPixelType = PFD_TYPE_RGBA,
      .cColorBits = 32,
  };
  const int format = ChoosePixelFormat(hdc, &desc);
  ASSERT(format != 0);
  DescribePixelFormat(hdc, format, sizeof(desc), &desc);
  SetPixelFormat(hdc, format, &desc);
  const HGLRC hglrc = wglCreateContext(hdc);
  ASSERT(hglrc != 0);
  wglMakeCurrent(hdc, hglrc);
  OpenGl::initWin32();
  wglMakeCurrent(NULL, NULL);
  wglDeleteContext(hglrc);
  ReleaseDC(dummy, hdc);
  DestroyWindow(dummy);
}

int WINAPI WinMain(
  _In_ HINSTANCE hInstance,
  _In_opt_ HINSTANCE /*hPrevInstance*/,
  _In_ LPSTR /*lpCmdLine*/,
  _In_ int nShowCmd)
{
  if (File::exists(Global::pathSettingsIni.c_str()))
    Global::installMode = InstallMode::installed;

  Settings::init(__argc, __argv);

  initWgl();

  WNDCLASSA wc = {
    .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
    .lpfnWndProc = WndProc,
    .hInstance = hInstance,
    .hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1)),
    .hCursor =
#ifdef SHR3D_CUSTOM_CURSOR
      Settings::uiCursorCustom ? customCursor(Data::Texture::cursorWin32, sizeof(Data::Texture::cursorWin32), Settings::uiCursorSize) :
#endif // SHR3D_CUSTOM_CURSOR
      LoadCursor(NULL, IDC_ARROW),
    .lpszClassName = "Shr3DWindowClass",
  };

  if (!RegisterClassA(&wc))
    ASSERT(false); // Failed to register window.

  RECT rect = { 0, 0, Settings::graphicsWindowWidth, Settings::graphicsWindowHeight };
  AdjustWindowRectEx(&rect, Window_::windowedStyle, FALSE, 0);

  const i32 windowWidth = rect.right - rect.left;
  const i32 windowHeight = rect.bottom - rect.top;

  Global::window = CreateWindowA(
    wc.lpszClassName,
    "Shr3D v" VERSION_STR,
    Window_::windowedStyle,
    (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2, // center the window
    (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2,
    windowWidth,
    windowHeight,
    NULL,
    NULL,
    wc.hInstance,
    NULL
  );

  ASSERT(Global::window != nullptr); // Failed to create window

  if (Settings::graphicsFullscreen == FullscreenMode::borderless)
  {
    // I have not found a way to set borderless fullscreen mode on CreateWindowEx
    SetWindowLong(Global::window, GWL_STYLE, Window_::borderlessStyle);
    SetWindowPos(Global::window, HWND_TOP, 0, 0, 2560, 1440, SWP_FRAMECHANGED);
  }
#ifndef NDEBUG
  else
  {
    // check that the size is correct
    RECT clientRect;
    GetClientRect(Global::window, &clientRect);
    ASSERT(clientRect.right == Settings::graphicsWindowWidth);
    ASSERT(clientRect.bottom == Settings::graphicsWindowHeight);
  }
#endif // NDEBUG

  {
    RAWINPUTDEVICE rid[2] = {
      { // Keyboard
        .usUsagePage = 0x01, // HID_USAGE_PAGE_GENERIC
        .usUsage = 0x06,     // HID_USAGE_GENERIC_KEYBOARD
        .dwFlags = 0,
        .hwndTarget = /*Global::window*/ 0
      },
      { // Mouse
        .usUsagePage = 0x01, // HID_USAGE_PAGE_GENERIC
        .usUsage = 0x02,     // HID_USAGE_GENERIC_MOUSE
        .dwFlags = 0,
        .hwndTarget = /*Global::window*/ 0
      }
    };

    if (!RegisterRawInputDevices(rid, 2, sizeof(RAWINPUTDEVICE)))
    {
      ASSERT(false); // Failed to register raw input devices
    }
  }

  Global::hdc = GetDC(Global::window);
  ASSERT(Global::hdc != 0);

  {
    int attrib[] =
    {
        WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
        WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
        WGL_DOUBLE_BUFFER_ARB,  GL_TRUE,
        WGL_PIXEL_TYPE_ARB,     WGL_TYPE_RGBA_ARB,
        WGL_COLOR_BITS_ARB,     32,
        WGL_DEPTH_BITS_ARB,     24,
        WGL_STENCIL_BITS_ARB,   8,
#ifdef SHR3D_GRAPHICS_MSAA
        WGL_SAMPLE_BUFFERS_ARB, 1,
        WGL_SAMPLES_ARB,        1 << Settings::graphicsMSAA,
#endif // SHR3D_GRAPHICS_MSAA
        0
    };
#ifdef SHR3D_GRAPHICS_MSAA
    if (Settings::graphicsMSAA == 0)
      attrib[14] = 0;
#endif // SHR3D_GRAPHICS_MSAA

    UINT formats;
    wglChoosePixelFormatARB(Global::hdc, attrib, NULL, 1, &Global::pixelFormat, &formats);

    Global::pixelFormatDescriptor.nSize = sizeof(Global::pixelFormatDescriptor);
    DescribePixelFormat(Global::hdc, Global::pixelFormat, sizeof(Global::pixelFormatDescriptor), &Global::pixelFormatDescriptor);

    SetPixelFormat(Global::hdc, Global::pixelFormat, &Global::pixelFormatDescriptor);
  }

  // Specify that we want to create an OpenGL 3.3 core profile context
  const int gl33_attribs[] = {
      WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
      WGL_CONTEXT_MINOR_VERSION_ARB, 3,
      WGL_CONTEXT_PROFILE_MASK_ARB,  WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
      0
  };

  Global::glContext = wglCreateContextAttribsARB(Global::hdc, NULL, gl33_attribs);
  ASSERT(Global::glContext != 0);
  Global::pluginGlContext = wglCreateContextAttribsARB(Global::hdc, NULL, gl33_attribs);
  ASSERT(Global::pluginGlContext != 0);

  wglMakeCurrent(Global::hdc, Global::glContext);

  init();

  ShowWindow(Global::window, nShowCmd);

  while (!Global::appQuit) {

    Input::handleInputBegin();
    Ui::handleInputBegin();

    MSG msg;
    while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
      if (msg.message == WM_QUIT) {
        Global::appQuit = true;
      }
      else {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
      }
    }

#ifdef SHR3D_OPENXR
    if (Global::inputXRActiveController != XrActiveController::mouseKeyboard)
      Ui::handleInputXr();
#endif // SHR3D_OPENXR

    Ui::handleInputEnd();
    Input::handleInputEnd();
    Input::proccessInputEvents();

    mainloop();
  }

  fini();
  unreachable();
}
#endif // SHR3D_WINDOW_WIN32


#ifdef SHR3D_WINDOW_ANDROID

extern "C" JNIEXPORT void JNICALL Java_app_shr3d_MainActivity_passActivity(JNIEnv* env, jclass clazz, jobject activity) {
  Global::androidActivity = env->NewGlobalRef(activity);
}

extern "C" JNIEXPORT void JNICALL Java_app_shr3d_MainActivity_init(JNIEnv* env, jclass clazz) {
  updateAndroidRootPath();
  if (File::exists(Global::pathSettingsIni.c_str()))
    Global::installMode = InstallMode::installed;
  Settings::init();
  init();
}

extern "C" JNIEXPORT void JNICALL Java_app_shr3d_MainActivity_resize(JNIEnv* env, jclass clazz, jint width, jint height) {
  Global::resolutionWidth = width;
  Global::resolutionHeight = height;
}

extern "C" JNIEXPORT void JNICALL Java_app_shr3d_MainActivity_tickAndRender(JNIEnv* env, jclass clazz) {
  //Ui::handleInputBegin();
  tick();
  render(Global::viewProjectionMat);
  Ui::render(Global::resolutionWidth, Global::resolutionHeight);
  Ui::renderClear();
  Ui::handleInputEnd();
}

extern "C" JNIEXPORT void JNICALL Java_app_shr3d_MainActivity_touchEvent(JNIEnv* env, jclass clazz, jint action, jfloat x, jfloat y) {
  Ui::handleInput(action, x, y);
}

#endif // SHR3D_WINDOW_ANDROID

