// SPDX-License-Identifier: Unlicense

#include "xr.h"

#ifdef SHR3D_OPENXR

#include "camera.h"
#include "data.h"
#include "geometry.h"
#include "global.h"
#include "helper.h"
#include "opengl.h"
#include "shader.h"
#include "texture.h"
#include "ui.h"

#ifdef PLATFORM_QUEST_3
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <jni.h>
#endif // PLATFORM_QUEST_3

#include <openxr/openxr.h>
#include <openxr/openxr_platform.h>

void Xr::handleXrControllerInput()
{
  for (i8 i = 0; i < 2; ++i)
  {
    const XrControllerEvent& event = Global::inputXRControllerEvent[i];

    if (bool(event.controllerEventBit & ControllerEventBit::click_thumbstick))
      if (event.click_thumbstick)
        Global::inputQuickRepeater.pressed = true;

    if (bool(event.controllerEventBit & ControllerEventBit::click_a))
      if (event.click_a)
        Global::inputTuner.toggled = !Global::inputTuner.toggled;

    if (bool(event.controllerEventBit & ControllerEventBit::click_b))
      if (event.click_b)
#ifndef PLATFORM_PICO_4
        Global::inputEject.pressed = true;
#else // PLATFORM_PICO_4
        Global::inputHideMenu.toggled = !Global::inputHideMenu.toggled; // on older PICO4 controllers the menu keys can't be assigned
#endif // PLATFORM_PICO_4

    if (bool(event.controllerEventBit & ControllerEventBit::click_x))
      if (event.click_x)
        Global::inputDebugInfo.toggled = !Global::inputDebugInfo.toggled;

    if (bool(event.controllerEventBit & ControllerEventBit::click_y))
      if (event.click_y)
        Settings::metronomeEnabled = !Settings::metronomeEnabled;

    if (bool(event.controllerEventBit & ControllerEventBit::click_menu))
      if (event.click_menu)
        Global::inputHideMenu.toggled = !Global::inputHideMenu.toggled;
  }

#ifdef SHR3D_MUSIC_STRETCHER
  {
    const XrControllerEvent& leftEvent = Global::inputXRControllerEvent[0];
    if (bool(leftEvent.controllerEventBit & ControllerEventBit::value_thumbstick))
    {
      if (Global::inputHideMenu.toggled && leftEvent.thumbstick_y != 0.0f)
        Global::musicStretchRatio = clamp(Global::musicStretchRatio - leftEvent.thumbstick_y * TimeNS_To_Seconds(Global::frameDelta), 1.0f, Const::musicStretcherMaxStretchRatio);
    }
  }
#endif // SHR3D_MUSIC_STRETCHER
  {
    const XrControllerEvent& rightEvent = Global::inputXRControllerEvent[1];
    if (bool(rightEvent.controllerEventBit & ControllerEventBit::value_thumbstick))
    {
      if (Global::inputHideMenu.toggled && rightEvent.thumbstick_x != 0.0f)
        Global::musicPlaybackSeekerTimeNS += TimeNS(f32(30_s) * rightEvent.thumbstick_x * TimeNS_To_Seconds(Global::frameDelta));
    }
  }
}

mat4 Xr::mat4_CreateProjection(const float tanAngleLeft, const float tanAngleRight, const float tanAngleUp, const float tanAngleDown, const float nearZ, const float farZ)
{
  const float tanAngleWidth = tanAngleRight - tanAngleLeft;

  // Set to tanAngleDown - tanAngleUp for a clip space with positive Y down (Vulkan).
  // Set to tanAngleUp - tanAngleDown for a clip space with positive Y up (OpenGL / D3D / Metal).
  const float tanAngleHeight = tanAngleUp - tanAngleDown;

  // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES).
  // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).
  const float offsetZ = nearZ;

  if (farZ <= nearZ) {
    // place the far plane at infinity
    const mat4 result{
      .m00 = 2.0f / tanAngleWidth,
      .m10 = 0.0f,
      .m20 = 0.0f,
      .m30 = 0.0f,

      .m01 = 0.0f,
      .m11 = 2.0f / tanAngleHeight,
      .m21 = 0.0f,
      .m31 = 0.0f,

      .m02 = (tanAngleRight + tanAngleLeft) / tanAngleWidth,
      .m12 = (tanAngleUp + tanAngleDown) / tanAngleHeight,
      .m22 = -1.0f,
      .m32 = -1.0f,

      .m03 = 0.0f,
      .m13 = 0.0f,
      .m23 = -(nearZ + offsetZ),
      .m33 = 0.0f
    };

    return result;
  }
  else {
    // normal projection
    const mat4 result{
      .m00 = 2.0f / tanAngleWidth,
      .m10 = 0.0f,
      .m20 = 0.0f,
      .m30 = 0.0f,

      .m01 = 0.0f,
      .m11 = 2.0f / tanAngleHeight,
      .m21 = 0.0f,
      .m31 = 0.0f,

      .m02 = (tanAngleRight + tanAngleLeft) / tanAngleWidth,
      .m12 = (tanAngleUp + tanAngleDown) / tanAngleHeight,
      .m22 = -(farZ + offsetZ) / (farZ - nearZ),
      .m32 = -1.0f,

      .m03 = 0.0f,
      .m13 = 0.0f,
      .m23 = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ),
      .m33 = 0.0f
    };

    return result;
  }
}

mat4 Xr::mat4_CreateProjectionFov(const XrFovf& fov, const float nearZ, const float farZ)
{
  const float tanLeft = tanf(fov.angleLeft);
  const float tanRight = tanf(fov.angleRight);

  const float tanDown = tanf(fov.angleDown);
  const float tanUp = tanf(fov.angleUp);

  return mat4_CreateProjection(tanLeft, tanRight, tanUp, tanDown, nearZ, farZ);
}

mat4 Xr::mat4_InvertRigidBody(const mat4& src)
{
  const mat4 result
  {
    .m00 = src.m00,
    .m10 = src.m01,
    .m20 = src.m02,
    .m30 = 0.0f,
    .m01 = src.m10,
    .m11 = src.m11,
    .m21 = src.m12,
    .m31 = 0.0f,
    .m02 = src.m20,
    .m12 = src.m21,
    .m22 = src.m22,
    .m32 = 0.0f,
    .m03 = -(src.m00 * src.m03 + src.m10 * src.m13 + src.m20 * src.m23),
    .m13 = -(src.m01 * src.m03 + src.m11 * src.m13 + src.m21 * src.m23),
    .m23 = -(src.m02 * src.m03 + src.m12 * src.m13 + src.m22 * src.m23),
    .m33 = 1.0f
  };

  return result;
}


mat4 Xr::mat4_CreateFromQuaternion(const XrQuaternionf& quat)
{
  const f32 x2 = quat.x + quat.x;
  const f32 y2 = quat.y + quat.y;
  const f32 z2 = quat.z + quat.z;

  const f32 xx2 = quat.x * x2;
  const f32 yy2 = quat.y * y2;
  const f32 zz2 = quat.z * z2;

  const f32 yz2 = quat.y * z2;
  const f32 wx2 = quat.w * x2;
  const f32 xy2 = quat.x * y2;
  const f32 wz2 = quat.w * z2;
  const f32 xz2 = quat.x * z2;
  const f32 wy2 = quat.w * y2;

  const mat4 result
  {
    .m00 = 1.0f - yy2 - zz2,
    .m10 = xy2 + wz2,
    .m20 = xz2 - wy2,
    .m30 = 0.0f,

    .m01 = xy2 - wz2,
    .m11 = 1.0f - xx2 - zz2,
    .m21 = yz2 + wx2,
    .m31 = 0.0f,

    .m02 = xz2 + wy2,
    .m12 = yz2 - wx2,
    .m22 = 1.0f - xx2 - yy2,
    .m32 = 0.0f,

    .m03 = 0.0f,
    .m13 = 0.0f,
    .m23 = 0.0f,
    .m33 = 1.0f
  };

  return result;
}

mat4 Xr::mat4_CreateTranslationRotation(const XrVector3f& translation, const XrQuaternionf& rotation)
{
  const mat4 rotationMatrix = mat4_CreateFromQuaternion(rotation);

  const mat4 translationMatrix
  {
    .m03 = translation.x,
    .m13 = translation.y,
    .m23 = translation.z
  };

  return vec::multiply(translationMatrix, rotationMatrix);
}

f32 Xr::dotProduct(const vec3& v0, const vec3& v1)
{
  return v0.x * v1.x + v0.y * v1.y + v0.z * v1.z;
}

vec3 Xr::quaternionToDirection(const vec4& quat)
{
  // Normalize the quaternion
  const f32 length = std::sqrt(quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w);
  const f32 invLength = 1.0f / length;

  const f32 x = quat.x * invLength;
  const f32 y = quat.y * invLength;
  const f32 z = quat.z * invLength;

  return vec3{ .x = x, .y = y, .z = z };
}

bool Xr::isIntersectUiPlaneWithLine(const vec3& linePoint, const vec3& lineDirection, const f32 planePosZ, f32& intersectX, f32& intersectY)
{
  const vec3 planePoint{
    .z = planePosZ
  };
  const vec3 planeNormal{
    .z = 1.0f
  };

  const f32 vpt = lineDirection.x * planeNormal.x + lineDirection.y * planeNormal.y + lineDirection.z * planeNormal.z;
  if (vpt == 0)
    return false; //The direction of the line is parallel to the direction of the plane, there is no intersection point

  const f32 t = ((planePoint.x - linePoint.x) * planeNormal.x + (planePoint.y - linePoint.y) * planeNormal.y + (planePoint.z - linePoint.z) * planeNormal.z) / vpt;
  intersectX = linePoint.x + lineDirection.x * t;
  intersectY = linePoint.y + lineDirection.y * t;

  return true;
}

void Xr::calulateControllerUiIntersection(const XrPosef& aimPose, f32& intersectX, f32& intersectY)
{
  const mat4 m = Xr::mat4_CreateTranslationRotation(aimPose.position, aimPose.orientation);
  const vec4 p1 = vec::multiply(m, { .w = 1.0f });
  const vec4 p2 = vec::multiply(m, { .z = -1.0f, .w = 1.0f });
  const vec3 diff = {
    .x = p2.x - p1.x,
    .y = p2.y - p1.y,
    .z = p2.z - p1.z
  };
  const vec3& linePoint = *reinterpret_cast<const vec3*>(&aimPose.position);
  const vec3 lineDirection = vec::norm(diff);
  Xr::isIntersectUiPlaneWithLine(linePoint, lineDirection, Settings::uiXrZ, intersectX, intersectY);
}

//static XrQuaternionf ToQuaternion(const f32 roll, const f32 pitch, const f32 yaw) // roll (x), pitch (y), yaw (z), angles are in radians
//{
//  // Abbreviations for the various angular functions
//
//  const f32 cr = cos(roll * 0.5f);
//  const f32 sr = sin(roll * 0.5f);
//  const f32 cp = cos(pitch * 0.5f);
//  const f32 sp = sin(pitch * 0.5f);
//  const f32 cy = cos(yaw * 0.5f);
//  const f32 sy = sin(yaw * 0.5f);
//
//  XrQuaternionf q;
//  q.w = cr * cp * cy + sr * sp * sy;
//  q.x = sr * cp * cy - cr * sp * sy;
//  q.y = cr * sp * cy + sr * cp * sy;
//  q.z = cr * cp * sy - sr * sp * cy;
//
//  return q;
//}

void Xr::drawXrControllers(const mat4& viewProjectionMat, const XrActionStatePose controllerActionStatePose[2], const mat4 controllerModel[2], const mat4 aimModel[2])
{
  ASSERT(controllerActionStatePose[0].isActive || controllerActionStatePose[1].isActive);

  GL(glUseProgram(Shader::model));

#ifdef SHR3D_OPENXR_CONTROLLER_PICO4
#ifdef SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
  if (Settings::xrController == XrController::pico4)
#endif // SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
  {
    static GLuint texture = Texture::openGlLoadTexture(
#ifdef __ANDROID__
      Data::Texture::xrController_Pico4_astc, sizeof(Data::Texture::xrController_Pico4_astc)
#else // __ANDROID__
      Data::Texture::xrController_Pico4_dds, sizeof(Data::Texture::xrController_Pico4_dds)
#endif // __ANDROID__
    );
    GL(glBindTexture(GL_TEXTURE_2D, texture));
  }
#endif // SHR3D_OPENXR_CONTROLLER_PICO4

  if (controllerActionStatePose[0].isActive)
  {
    const mat4 mvp = vec::multiply(viewProjectionMat, controllerModel[0]);
    GL(glUniformMatrix4fv(Shader::modelUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    //GL(glBindVertexArray(Global::dynamicDrawVao));
    //static Debug::ObjFileHotReloadContext ctx;
    //Debug::drawObjFileHotReload(ctx, "res/pico4Left.obj");
    //GL(glBindVertexArray(Global::staticDrawVao));

#ifdef SHR3D_OPENXR_CONTROLLER_PICO4
#ifdef SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    if (Settings::xrController == XrController::pico4)
#endif // SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    {
      GL(glDrawElements_(xrController_Pico4Left));
    }
#endif // SHR3D_OPENXR_CONTROLLER_PICO4
#ifdef SHR3D_OPENXR_CONTROLLER_QUEST3
#ifdef SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    if (Settings::xrController == XrController::quest3)
#endif // SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    {
      static GLuint texture = Texture::openGlLoadTexture(
#ifdef __ANDROID__
        Data::Texture::xrController_Quest3_Left_astc, sizeof(Data::Texture::xrController_Quest3_Left_astc)
#else // __ANDROID__
        Data::Texture::xrController_Quest3_Left_dds, sizeof(Data::Texture::xrController_Quest3_Left_dds)
#endif // __ANDROID__
      );
      GL(glBindTexture(GL_TEXTURE_2D, texture));
      GL(glDrawElements_(xrController_Quest3Left));
    }
#endif // SHR3D_OPENXR_CONTROLLER_QUEST3

    if (Settings::xrController == XrController::quest3Neon)
    {
      static GLuint texture = Texture::openGlLoadTexture(
#ifdef __ANDROID__
        Data::Texture::xrController_Quest3_Neon_Left_astc, sizeof(Data::Texture::xrController_Quest3_Neon_Left_astc)
#else // __ANDROID__
        Data::Texture::xrController_Quest3_Neon_Left_dds, sizeof(Data::Texture::xrController_Quest3_Neon_Left_dds)
#endif // __ANDROID__
      );
      GL(glBindTexture(GL_TEXTURE_2D, texture));
      GL(glDrawElements_(xrController_Quest3NeonLeft));
    }

  }
  if (controllerActionStatePose[1].isActive)
  {
    const mat4 mvp = vec::multiply(viewProjectionMat, controllerModel[1]);
    GL(glUniformMatrix4fv(Shader::modelUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    //GL(glBindVertexArray(Global::dynamicDrawVao));
    //static Debug::ObjFileHotReloadContext ctx;
    //Debug::drawObjFileHotReload(ctx, "res/pico4Right.obj");
    //GL(glBindVertexArray(Global::staticDrawVao));

#ifdef SHR3D_OPENXR_CONTROLLER_PICO4
#ifdef SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    if (Settings::xrController == XrController::pico4)
#endif // SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    {
      GL(glDrawElements_(xrController_Pico4Right));
    }
#endif // SHR3D_OPENXR_CONTROLLER_PICO4
#ifdef SHR3D_OPENXR_CONTROLLER_QUEST3
#ifdef SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    else if (Settings::xrController == XrController::quest3)
#endif // SHR3D_OPENXR_CONTROLLER_PICO4_AND_QUEST3
    {
      static GLuint texture = Texture::openGlLoadTexture(
#ifdef __ANDROID__
        Data::Texture::xrController_Quest3_Right_astc, sizeof(Data::Texture::xrController_Quest3_Right_astc)
#else // __ANDROID__
        Data::Texture::xrController_Quest3_Right_dds, sizeof(Data::Texture::xrController_Quest3_Right_dds)
#endif // __ANDROID__
      );
      GL(glBindTexture(GL_TEXTURE_2D, texture));
      GL(glDrawElements_(xrController_Quest3Right));
    }
#endif // SHR3D_OPENXR_CONTROLLER_QUEST3

    if (Settings::xrController == XrController::quest3Neon)
    {
      static GLuint texture = Texture::openGlLoadTexture(
#ifdef __ANDROID__
        Data::Texture::xrController_Quest3_Neon_Right_astc, sizeof(Data::Texture::xrController_Quest3_Neon_Right_astc)
#else // __ANDROID__
        Data::Texture::xrController_Quest3_Neon_Right_dds, sizeof(Data::Texture::xrController_Quest3_Neon_Right_dds)
#endif // __ANDROID__
      );
      GL(glBindTexture(GL_TEXTURE_2D, texture));
      GL(glDrawElements_(xrController_Quest3NeonRight));
    }
  }

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));

  GL(glUseProgram(Shader::xrPointer));

  GL(glUniform4f(Shader::xrPointerUniformColor, Settings::xrPointerColor.r, Settings::xrPointerColor.g, Settings::xrPointerColor.b, Settings::xrPointerColor.a));

  if (controllerActionStatePose[0].isActive)
  {
    const mat4 mvp = vec::multiply(viewProjectionMat, aimModel[0]);
    GL(glUniformMatrix4fv(Shader::xrPointerUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    GL(glDrawElements_(xrPointer));
  }
  if (controllerActionStatePose[1].isActive)
  {
    const mat4 mvp = vec::multiply(viewProjectionMat, aimModel[1]);
    GL(glUniformMatrix4fv(Shader::xrPointerUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    GL(glDrawElements_(xrPointer));
  }
}

void Xr::drawXrCursor(const mat4& viewProjectionMat, const f32 inputXRControllerPosX, const f32 inputXRControllerPosY, const f32 inputXRControllerPosZ)
{
  {
    const f32 size = 0.006f;

    const mat4 model = {
      .m00 = size,
      .m11 = size,
      .m03 = inputXRControllerPosX,
      .m13 = inputXRControllerPosY,
      .m23 = inputXRControllerPosZ
    };
    const mat4 mvp = vec::multiply(viewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::dotInlayUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(planeZ));
}

#ifdef PLATFORM_OPENXR_ANDROID
void Xr::renderHandTracking(const XrHandJointLocationEXT m_jointLocations[2][XR_HAND_JOINT_COUNT_EXT], const mat4& viewProjectionMat)
{
  std::vector<mat4> models;
  for (auto hand = 0; hand < 2; hand++)
  {
    for (int i = 0; i < XR_HAND_JOINT_COUNT_EXT; i++)
    {
      const XrHandJointLocationEXT& jointLocation = m_jointLocations[hand][i];
      if (jointLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT && jointLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT)
      {
        mat4 model = mat4_CreateTranslationRotation(jointLocation.pose.position, jointLocation.pose.orientation);
        models.push_back(model);
      }
    }
  }

  {
    GL(glUseProgram(Shader::model));
    for (const mat4& model : models)
    {
      const mat4 mvp = vec::multiply(viewProjectionMat, model);
      GL(glUniformMatrix4fv(Shader::modelUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

      //GL(glBufferData(GL_ARRAY_BUFFER, sizeof(Data::Geometry::xrDebugAxis), Data::Geometry::xrDebugAxis, GL_STATIC_DRAW));
      //GL(glDrawArrays(GL_TRIANGLES, 0, sizeof(Data::Geometry::xrDebugAxis) / (sizeof(f32) * 5)));
    }
  }
}
#endif // PLATFORM_OPENXR_ANDROID

GLuint Xr::GetDepthTexture(GLuint colorTexture)
{
  // If a depth-stencil view has already been created for this back-buffer, use it.
  static std::map<GLuint, GLuint> m_colorToDepthMap;
  auto depthBufferIt = m_colorToDepthMap.find(colorTexture);
  if (depthBufferIt != m_colorToDepthMap.end()) {
    return depthBufferIt->second;
  }

  // This back-buffer has no corresponding depth-stencil texture, so create one with matching dimensions.
  GLint width;
  GLint height;
  GL(glBindTexture(GL_TEXTURE_2D, colorTexture));
  GL(glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width));
  GL(glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height));

  GLuint depthTexture;
  GL(glGenTextures(1, &depthTexture));
  GL(glBindTexture(GL_TEXTURE_2D, depthTexture));
  GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
  GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST));
  GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
  GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
  GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr));

  m_colorToDepthMap.insert(std::make_pair(colorTexture, depthTexture));

  return depthTexture;
}

#ifdef SHR3D_OPENXR_PCVR
static void drawMouseCursor(const i32 xrResolutionWidth, const i32 xrResolutionHeight, const mat4& viewProjectionMat)
{
  static i32 lastInputCursorPosX;
  static i32 lastInputCursorPosY;
  static f32 lastCursorMovement;

  if (Global::inputHideMenu.toggled && lastInputCursorPosX == Global::inputCursorPosXrX && lastInputCursorPosY == Global::inputCursorPosXrY)
  {
    lastCursorMovement += TimeNS_To_Seconds(Global::frameDelta);
    if (lastCursorMovement > Const::cursorHideCursorTimeout)
      return;
  }
  else
  {
    lastCursorMovement = 0.0f;
    lastInputCursorPosX = Global::inputCursorPosXrX;
    lastInputCursorPosY = Global::inputCursorPosXrY;
  }

  {
    GL(glUseProgram(Shader::model));

    static GLuint texture = Texture::openGlLoadTextureRaw(Data::Texture::cursor, 128, 128, GL_RGBA);
    GL(glBindTexture(GL_TEXTURE_2D, texture));

    const f32 left = -1.0f + 2.0f * f32(Global::inputCursorPosXrX) / f32(xrResolutionWidth);
    const f32 right = left + 0.00078125f * f32(Settings::uiCursorSize);
    const f32 top = 1.0f - 2.0f * f32(Global::inputCursorPosXrY) / f32(xrResolutionHeight);
    const f32 bottom = top - 0.00078125f * f32(Settings::uiCursorSize);

    const mat4 model{
      .m00 = 0.000390625f * f32(Settings::uiCursorSize),
      .m11 = 0.000390625f * f32(Settings::uiCursorSize),
      .m03 = left + 0.5f * (right - left),
      .m13 = top - 0.5f * (top - bottom),
      .m23 = Settings::uiXrZ
    };
    const mat4 mvp = vec::multiply(viewProjectionMat, model);

    GL(glUniformMatrix4fv(Shader::modelUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    GL(glDrawElements_(planeZ));

    GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
  }
}
#endif // SHR3D_OPENXR_PCVR

extern void render(const mat4& viewProjectionMat, const mat4& highwayViewProjectionMat, const mat4& stageViewProjectionMat);
void Xr::RenderView(const XrCompositionLayerProjectionView& layerView, const GLuint colorTexture, const GLuint depthTexture, const XrActionStatePose controllerActionStatePose[2], const XrPosef aimPose[2], const mat4 controllerModel[2], const mat4 aimModel[2], const mat4& stageModel
#ifdef PLATFORM_OPENXR_ANDROID
  , const XrHandJointLocationEXT jointLocations[2][XR_HAND_JOINT_COUNT_EXT]
#endif // PLATFORM_OPENXR_ANDROID
)
{
  GL(glViewport(layerView.subImage.imageRect.offset.x, layerView.subImage.imageRect.offset.y, layerView.subImage.imageRect.extent.width, layerView.subImage.imageRect.extent.height));

  GL(glCullFace(GL_BACK));
  GL(glEnable(GL_CULL_FACE));
  GL(glEnable(GL_DEPTH_TEST));

#if defined(PLATFORM_PICO_4) && defined(SHR3D_GRAPHICS_MSAA)
  if (static const GLsizei msaa = 1 << Settings::graphicsMSAA; msaa >= 2)
  {
    GL(glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0, msaa));
    GL(glFramebufferTexture2DMultisampleEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0, msaa));
  }
  else
#endif // PLATFORM_OPENXR_ANDROID && SHR3D_GRAPHICS_MSAA
  {
    GL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0));
    GL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0));
  }

  ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);

  GL(glClearColor(Settings::environmentClearColor.r, Settings::environmentClearColor.g, Settings::environmentClearColor.b, Settings::environmentClearColor.a));
  GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT));

  const auto & pose = layerView.pose;
  const mat4 projection = Xr::mat4_CreateProjectionFov(layerView.fov, 0.05f, 100.0f);
  const mat4 toView = Xr::mat4_CreateTranslationRotation(pose.position, pose.orientation);
  const mat4 view = Xr::mat4_InvertRigidBody(toView);

  const mat4 viewProjectionMatWithoutCamera = vec::multiply(projection, view);

  {
    static vec3 cameraTargetPosition;
    static vec3 cameraCurrentPosition;
    const mat4 viewMat = Camera::calculateViewMat(
#ifdef SHR3D_OPENXR_PCVR
      Settings::cameraPcVrMode
#else // SHR3D_OPENXR_PCVR
      Settings::cameraMode
#endif // SHR3D_OPENXR_PCVR
      , Global::selectedArrangementIndex, cameraTargetPosition, cameraCurrentPosition);
    const mat4 viewProjectionMatWithCamera = vec::multiply(viewProjectionMatWithoutCamera, viewMat);
    static vec3 highwayTargetPosition;
    static vec3 highwayCurrentPosition;
    const mat4 highwayViewProjectionMat = Camera::calculateHighwayProjectionViewMat(
#ifdef SHR3D_OPENXR_PCVR
      Settings::cameraPcVrMode
#else // SHR3D_OPENXR_PCVR
      Settings::cameraMode
#endif // SHR3D_OPENXR_PCVR
      , viewProjectionMatWithCamera, highwayTargetPosition, highwayCurrentPosition);

    const mat4 stageProjectionMatWithCamera = vec::multiply(viewProjectionMatWithCamera, stageModel);

    render(viewProjectionMatWithCamera, highwayViewProjectionMat, stageProjectionMatWithCamera);
  }

  Ui::render(Global::xrResolutionWidth, Global::xrResolutionHeight, &viewProjectionMatWithoutCamera);

  GL(glBindVertexArray(Global::staticDrawVao));

  if (controllerActionStatePose[0].isActive || controllerActionStatePose[1].isActive)
  {
    if (controllerActionStatePose[0].isActive)
      Xr::calulateControllerUiIntersection(aimPose[0], Global::inputXrControllerLeftPosX, Global::inputXrControllerLeftPosY);
    if (controllerActionStatePose[1].isActive)
      Xr::calulateControllerUiIntersection(aimPose[1], Global::inputXrControllerRightPosX, Global::inputXrControllerRightPosY);

    {
      GL(glUseProgram(Shader::dotInlay));

      GL(glUniform4f(Shader::dotInlayUniformColor, Settings::xrCursorColor[0].r, Settings::xrCursorColor[0].g, Settings::xrCursorColor[0].b, Settings::xrCursorColor[0].a));
      GL(glUniform4f(Shader::dotInlayUniformColor2, Settings::xrCursorColor[1].r, Settings::xrCursorColor[1].g, Settings::xrCursorColor[1].b, Settings::xrCursorColor[1].a));

      GL(glBindTexture(GL_TEXTURE_2D, Global::texture));

      if (controllerActionStatePose[0].isActive)
        Xr::drawXrCursor(viewProjectionMatWithoutCamera, Global::inputXrControllerLeftPosX, Global::inputXrControllerLeftPosY, Settings::uiXrZ);
      if (controllerActionStatePose[1].isActive)
        Xr::drawXrCursor(viewProjectionMatWithoutCamera, Global::inputXrControllerRightPosX, Global::inputXrControllerRightPosY, Settings::uiXrZ);
    }

    {
      Xr::drawXrControllers(viewProjectionMatWithoutCamera, controllerActionStatePose, controllerModel, aimModel);
    }
  }

#ifdef PLATFORM_OPENXR_ANDROID
  Xr::renderHandTracking(jointLocations, viewProjectionMatWithoutCamera);
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_OPENXR_PCVR
  drawMouseCursor(layerView.subImage.imageRect.extent.width, layerView.subImage.imageRect.extent.height, viewProjectionMatWithoutCamera);
#endif // SHR3D_OPENXR_PCVR

  GL(glBindVertexArray(Global::dynamicDrawVao));
}

#endif // SHR3D_OPENXR
