// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#include "steamvrrenderer.hpp"
#include "texture.hpp"

using namespace lineag;
static const unsigned int DEFAULT_WIDTH(1280);
static const unsigned int DEFAULT_HEIGHT(720);

void ThreadSleep(unsigned long nMilliseconds) {
#if defined(_WIN32)
  ::Sleep(nMilliseconds);
#elif defined(POSIX)
  usleep(nMilliseconds * 1000);
#endif
}

SteamVRRenderer::SteamVRRenderer()
    : Renderer(),
      hmd(nullptr),
      renderModels(nullptr),
      windowWidth(DEFAULT_WIDTH),
      windowHeight(DEFAULT_HEIGHT),
      openglInitialized(false),
      lensProgramID(0),
      controllerTransformProgramID(0),
      renderModelProgramID(0),
      controllerMatrixLocation(0),
      renderModelMatrixLocation(0),
      renderWidth(0),
      renderHeight(0),
      nearClip(0.f),
      farClip(0.f),
      lensVAO(0),
      glIDVertBuffer(0),
      glIDIndexBuffer(0),
      indexSize(0),
      glControllerVertBuffer(0),
      controllerVAO(0),
      controllerVertcount(0) {}

SteamVRRenderer::~SteamVRRenderer() {
  if (!openglInitialized) {
    return;
  }
}

// -------------- Render Models -------------------------------

CGLRenderModel::CGLRenderModel(const std::string &renderModelName)
    : glIndexBuffer(0), glVertArray(0), glVertBuffer(0), glTexture(0), modelName(renderModelName) {}

CGLRenderModel::~CGLRenderModel() { cleanup(); }

bool CGLRenderModel::init(const vr::RenderModel_t &vrModel, const vr::RenderModel_TextureMap_t &vrDiffuseTexture) {
  initializeOpenGLFunctions();

  // create and bind a VAO to hold state for this model
  glGenVertexArrays(1, &glVertArray);
  glBindVertexArray(glVertArray);

  // Populate a vertex buffer
  glGenBuffers(1, &glVertBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, glVertBuffer);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vr::RenderModel_Vertex_t) * vrModel.unVertexCount, vrModel.rVertexData,
               GL_STATIC_DRAW);

  // Identify the components in the vertex buffer
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vr::RenderModel_Vertex_t),
                        (void *)offsetof(vr::RenderModel_Vertex_t, vPosition));
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vr::RenderModel_Vertex_t),
                        (void *)offsetof(vr::RenderModel_Vertex_t, vNormal));
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vr::RenderModel_Vertex_t),
                        (void *)offsetof(vr::RenderModel_Vertex_t, rfTextureCoord));

  // Create and populate the index buffer
  glGenBuffers(1, &glIndexBuffer);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, glIndexBuffer);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint16_t) * vrModel.unTriangleCount * 3, vrModel.rIndexData,
               GL_STATIC_DRAW);

  glBindVertexArray(0);

  // create and populate the texture
  glGenTextures(1, &glTexture);
  glBindTexture(GL_TEXTURE_2D, glTexture);

  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, vrDiffuseTexture.unWidth, vrDiffuseTexture.unHeight, 0, GL_RGBA,
               GL_UNSIGNED_BYTE, vrDiffuseTexture.rubTextureMapData);

  // If this renders black ask McJohn what's wrong.
  glGenerateMipmap(GL_TEXTURE_2D);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

  GLfloat largest;
  glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &largest);
  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, largest);

  glBindTexture(GL_TEXTURE_2D, 0);

  vertexCount = vrModel.unTriangleCount * 3;

  return true;
}

void CGLRenderModel::cleanup() {
  if (glVertBuffer) {
    glDeleteBuffers(1, &glIndexBuffer);
    glDeleteBuffers(1, &glVertArray);
    glDeleteBuffers(1, &glVertBuffer);
    glIndexBuffer = 0;
    glVertArray = 0;
    glVertBuffer = 0;
  }
}

void CGLRenderModel::draw() {
  glBindVertexArray(glVertArray);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, glTexture);
  glDrawElements(GL_TRIANGLES, vertexCount, GL_UNSIGNED_SHORT, 0);
  glBindVertexArray(0);
}

// -------------- Service initialization ----------------------

bool SteamVRRenderer::initializeSteamVR() {
  // Loading the SteamVR Runtime
  vr::EVRInitError error = vr::VRInitError_None;
  hmd = vr::VR_Init(&error, vr::VRApplication_Scene);

  if (error != vr::VRInitError_None) {
    hmd = nullptr;
    const QString mess = "Unable to init VR runtime: %0";
    emit logMessage(mess.arg(vr::VR_GetVRInitErrorAsEnglishDescription(error)), VideoStitch::LOG_ERROR);
    return false;
  }

  renderModels = (vr::IVRRenderModels *)vr::VR_GetGenericInterface(vr::IVRRenderModels_Version, &error);
  if (!renderModels) {
    hmd = nullptr;
    vr::VR_Shutdown();
    const QString mess = "Unable to get render model interface: %0";
    emit logMessage(mess.arg(vr::VR_GetVRInitErrorAsEnglishDescription(error)), VideoStitch::LOG_ERROR);
    return false;
  }

  if (!vr::VRCompositor()) {
    const QString mess = "Compositor initialization failed. See log file for details";
    emit logMessage(mess, VideoStitch::LOG_ERROR);
    return false;
  }
  return true;
}

void SteamVRRenderer::uninitializeSteamVR() { vr::VR_Shutdown(); }

// ----------------- OpenGL rendering configuration ------------

void SteamVRRenderer::configureRendering(int width, int height) {
  Q_UNUSED(width);
  Q_UNUSED(height);

  Renderer::initialize();

  nearClip = 0.1f;
  farClip = 30.0f;

  createAllShaders();
  setupCameras();
  setupStereoRenderTargets();
  setupDistortion();
  setupRenderModels();

  Q_ASSERT(!glGetError());
  openglInitialized = true;
}

void SteamVRRenderer::setupCameras() {
  mat4ProjectionLeft = getHMDMatrixProjectionEye(vr::Eye_Left);
  mat4ProjectionRight = getHMDMatrixProjectionEye(vr::Eye_Right);
  mat4eyePosLeft = getHMDMatrixPoseEye(vr::Eye_Left);
  mat4eyePosRight = getHMDMatrixPoseEye(vr::Eye_Right);
}

Matrix4 SteamVRRenderer::getHMDMatrixProjectionEye(vr::Hmd_Eye eye) {
  if (!hmd) {
    return Matrix4();
  }

  vr::HmdMatrix44_t mat = hmd->GetProjectionMatrix(eye, nearClip, farClip, vr::API_OpenGL);
  return Matrix4(mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1],
                 mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], mat.m[0][3], mat.m[1][3], mat.m[2][3],
                 mat.m[3][3]);
}

Matrix4 SteamVRRenderer::getHMDMatrixPoseEye(vr::Hmd_Eye eye) {
  if (!hmd) {
    return Matrix4();
  }

  vr::HmdMatrix34_t matEyeRight = hmd->GetEyeToHeadTransform(eye);
  Matrix4 matrixObj(matEyeRight.m[0][0], matEyeRight.m[1][0], matEyeRight.m[2][0], 0.0, matEyeRight.m[0][1],
                    matEyeRight.m[1][1], matEyeRight.m[2][1], 0.0, matEyeRight.m[0][2], matEyeRight.m[1][2],
                    matEyeRight.m[2][2], 0.0, matEyeRight.m[0][3], matEyeRight.m[1][3], matEyeRight.m[2][3], 1.0f);

  return matrixObj.invert();
}

bool SteamVRRenderer::setupStereoRenderTargets() {
  if (!hmd) {
    return false;
  }

  hmd->GetRecommendedRenderTargetSize(&renderWidth, &renderHeight);
  createFrameBuffer(renderWidth, renderHeight, leftEyeDesc);
  createFrameBuffer(renderWidth, renderHeight, rightEyeDesc);
  return true;
}

bool SteamVRRenderer::createFrameBuffer(int width, int height, FramebufferDesc &framebufferDesc) {
  glGenFramebuffers(1, &framebufferDesc.renderFramebufferId);
  glBindFramebuffer(GL_FRAMEBUFFER, framebufferDesc.renderFramebufferId);

  glGenRenderbuffers(1, &framebufferDesc.depthBufferId);
  glBindRenderbuffer(GL_RENDERBUFFER, framebufferDesc.depthBufferId);
  glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT, width, height);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, framebufferDesc.depthBufferId);

  glGenTextures(1, &framebufferDesc.renderTextureId);
  glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, framebufferDesc.renderTextureId);
  glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 4, GL_RGBA8, width, height, true);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE,
                         framebufferDesc.renderTextureId, 0);

  glGenFramebuffers(1, &framebufferDesc.resolveFramebufferId);
  glBindFramebuffer(GL_FRAMEBUFFER, framebufferDesc.resolveFramebufferId);

  glGenTextures(1, &framebufferDesc.resolveTextureId);
  glBindTexture(GL_TEXTURE_2D, framebufferDesc.resolveTextureId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
  glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, framebufferDesc.resolveTextureId, 0);

  // check FBO status
  GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
  if (status != GL_FRAMEBUFFER_COMPLETE) {
    return false;
  }

  glBindFramebuffer(GL_FRAMEBUFFER, 0);

  return true;
}

void SteamVRRenderer::setupDistortion() {
  if (!hmd) {
    return;
  }

  GLushort lensGridSegmentCountH = 43;
  GLushort lensGridSegmentCountV = 43;

  float w = (float)(1.0 / float(lensGridSegmentCountH - 1));
  float h = (float)(1.0 / float(lensGridSegmentCountV - 1));
  float u, v = 0;

  std::vector<VertexDataLens> vertices;
  VertexDataLens vert;

  // left eye distortion verts
  float Xoffset = -1;
  for (int y = 0; y < lensGridSegmentCountV; ++y) {
    for (int x = 0; x < lensGridSegmentCountH; ++x) {
      u = x * w;
      v = 1 - y * h;
      vert.position = Vector2(Xoffset + u, -1 + 2 * y * h);
      vr::DistortionCoordinates_t dc0 = hmd->ComputeDistortion(vr::Eye_Left, u, v);
      vert.texCoordRed = Vector2(dc0.rfRed[0], 1 - dc0.rfRed[1]);
      vert.texCoordGreen = Vector2(dc0.rfGreen[0], 1 - dc0.rfGreen[1]);
      vert.texCoordBlue = Vector2(dc0.rfBlue[0], 1 - dc0.rfBlue[1]);
      vertices.push_back(vert);
    }
  }

  // right eye distortion verts
  Xoffset = 0;
  for (int y = 0; y < lensGridSegmentCountV; ++y) {
    for (int x = 0; x < lensGridSegmentCountH; ++x) {
      u = x * w;
      v = 1 - y * h;
      vert.position = Vector2(Xoffset + u, -1 + 2 * y * h);
      vr::DistortionCoordinates_t dc0 = hmd->ComputeDistortion(vr::Eye_Right, u, v);
      vert.texCoordRed = Vector2(dc0.rfRed[0], 1 - dc0.rfRed[1]);
      vert.texCoordGreen = Vector2(dc0.rfGreen[0], 1 - dc0.rfGreen[1]);
      vert.texCoordBlue = Vector2(dc0.rfBlue[0], 1 - dc0.rfBlue[1]);
      vertices.push_back(vert);
    }
  }

  std::vector<GLushort> indices;
  GLushort a, b, c, d;

  GLushort offset = 0;
  for (GLushort y = 0; y < lensGridSegmentCountV - 1; ++y) {
    for (GLushort x = 0; x < lensGridSegmentCountH - 1; ++x) {
      a = lensGridSegmentCountH * y + x + offset;
      b = lensGridSegmentCountH * y + x + 1 + offset;
      c = (y + 1) * lensGridSegmentCountH + x + 1 + offset;
      d = (y + 1) * lensGridSegmentCountH + x + offset;
      indices.push_back(a);
      indices.push_back(b);
      indices.push_back(c);
      indices.push_back(a);
      indices.push_back(c);
      indices.push_back(d);
    }
  }

  offset = lensGridSegmentCountH * lensGridSegmentCountV;
  for (GLushort y = 0; y < lensGridSegmentCountV - 1; ++y) {
    for (GLushort x = 0; x < lensGridSegmentCountH - 1; ++x) {
      a = lensGridSegmentCountH * y + x + offset;
      b = lensGridSegmentCountH * y + x + 1 + offset;
      c = (y + 1) * lensGridSegmentCountH + x + 1 + offset;
      d = (y + 1) * lensGridSegmentCountH + x + offset;
      indices.push_back(a);
      indices.push_back(b);
      indices.push_back(c);
      indices.push_back(a);
      indices.push_back(c);
      indices.push_back(d);
    }
  }
  indexSize = (unsigned int)indices.size();

  glGenVertexArrays(1, &lensVAO);
  glBindVertexArray(lensVAO);

  glGenBuffers(1, &glIDVertBuffer);
  glBindBuffer(GL_ARRAY_BUFFER, glIDVertBuffer);
  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexDataLens), &vertices[0], GL_STATIC_DRAW);

  glGenBuffers(1, &glIDIndexBuffer);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, glIDIndexBuffer);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLushort), &indices[0], GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(VertexDataLens), (void *)offsetof(VertexDataLens, position));

  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(VertexDataLens),
                        (void *)offsetof(VertexDataLens, texCoordRed));

  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexDataLens),
                        (void *)offsetof(VertexDataLens, texCoordGreen));

  glEnableVertexAttribArray(3);
  glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE, sizeof(VertexDataLens),
                        (void *)offsetof(VertexDataLens, texCoordBlue));

  glBindVertexArray(0);

  glDisableVertexAttribArray(0);
  glDisableVertexAttribArray(1);
  glDisableVertexAttribArray(2);
  glDisableVertexAttribArray(3);

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

void SteamVRRenderer::setupRenderModels() {
  memset(trackedDeviceToRenderModel, 0, sizeof(trackedDeviceToRenderModel));
  if (!hmd) {
    return;
  }

  for (uint32_t trackedDevice = vr::k_unTrackedDeviceIndex_Hmd + 1; trackedDevice < vr::k_unMaxTrackedDeviceCount;
       ++trackedDevice) {
    if (!hmd->IsTrackedDeviceConnected(trackedDevice)) {
      continue;
    }
    setupRenderModelForTrackedDevice(trackedDevice);
  }
}

std::string getTrackedDeviceString(vr::IVRSystem *hmd, vr::TrackedDeviceIndex_t device, vr::TrackedDeviceProperty prop,
                                   vr::TrackedPropertyError *error = nullptr) {
  uint32_t requiredBufferLen = hmd->GetStringTrackedDeviceProperty(device, prop, nullptr, 0, error);
  if (requiredBufferLen == 0) {
    return std::string();
  }

  char *buffer = new char[requiredBufferLen];
  requiredBufferLen = hmd->GetStringTrackedDeviceProperty(device, prop, buffer, requiredBufferLen, error);
  std::string result = buffer;
  delete[] buffer;
  return result;
}

void SteamVRRenderer::setupRenderModelForTrackedDevice(vr::TrackedDeviceIndex_t trackedDeviceIndex) {
  if (trackedDeviceIndex >= vr::k_unMaxTrackedDeviceCount) {
    return;
  }

  // try to find a model we've already set up
  std::string renderModelName = getTrackedDeviceString(hmd, trackedDeviceIndex, vr::Prop_RenderModelName_String);
  CGLRenderModel *renderModel = findOrLoadRenderModel(renderModelName.c_str());
  if (!renderModel) {
    std::string trackingSystemName =
        getTrackedDeviceString(hmd, trackedDeviceIndex, vr::Prop_TrackingSystemName_String);
    const QString mess = "Unable to load render model for tracked device %0 %1 %2";
    emit logMessage(mess.arg(trackedDeviceIndex).arg(trackingSystemName.c_str()).arg(renderModelName.c_str()),
                    VideoStitch::LOG_ERROR);
  } else {
    trackedDeviceToRenderModel[trackedDeviceIndex] = renderModel;
    showTrackedDevice[trackedDeviceIndex] = true;
  }
}

CGLRenderModel *SteamVRRenderer::findOrLoadRenderModel(const char *renderModelName) {
  CGLRenderModel *renderModel = nullptr;
  for (std::vector<CGLRenderModel *>::iterator i = vecRenderModels.begin(); i != vecRenderModels.end(); ++i) {
    if (!stricmp((*i)->getName().c_str(), renderModelName)) {
      renderModel = *i;
      break;
    }
  }

  // load the model if we didn't find one
  if (!renderModel) {
    vr::RenderModel_t *model;
    vr::EVRRenderModelError error;
    while (1) {
      error = vr::VRRenderModels()->LoadRenderModel_Async(renderModelName, &model);
      if (error != vr::VRRenderModelError_Loading) break;
      ThreadSleep(1);
    }

    if (error != vr::VRRenderModelError_None) {
      QString mess;
      mess.append("Unable to load render model ")
          .append(renderModelName)
          .append(" - ")
          .append(vr::VRRenderModels()->GetRenderModelErrorNameFromEnum(error))
          .append("\n");
      emit logMessage(mess, VideoStitch::LOG_ERROR);
      return nullptr;  // move on to the next tracked device
    }

    vr::RenderModel_TextureMap_t *texture;
    while (1) {
      error = vr::VRRenderModels()->LoadTexture_Async(model->diffuseTextureId, &texture);
      if (error != vr::VRRenderModelError_Loading) break;
      ThreadSleep(1);
    }

    if (error != vr::VRRenderModelError_None) {
      QString mess;
      mess.append("Unable to load render texture id:")
          .append(model->diffuseTextureId)
          .append(" for render model ")
          .append(renderModelName)
          .append("\n");
      emit logMessage(mess, VideoStitch::LOG_ERROR);
      vr::VRRenderModels()->FreeRenderModel(model);
      return nullptr;  // move on to the next tracked device
    }

    renderModel = new CGLRenderModel(renderModelName);
    if (!renderModel->init(*model, *texture)) {
      QString mess;
      mess.append("Unable to create GL model from render model ").append(renderModelName).append("\n");
      emit logMessage(mess, VideoStitch::LOG_ERROR);
      delete renderModel;
      renderModel = nullptr;
    } else {
      vecRenderModels.push_back(renderModel);
    }
    vr::VRRenderModels()->FreeRenderModel(model);
    vr::VRRenderModels()->FreeTexture(texture);
  }
  return renderModel;
}

GLuint SteamVRRenderer::compileGLShader(const char *shaderName, const char *vertexShaderSource,
                                        const char *fragmentShaderSource) {
  const GLuint programID = glCreateProgram();
  const GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
  glCompileShader(vertexShader);

  GLint vertexShaderCompiled = GL_FALSE;
  glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompiled);
  if (vertexShaderCompiled != GL_TRUE) {
    QString mess;
    mess.append(shaderName).append(" - Unable to compile vertex shader ").append(vertexShader).append("!\n");
    emit logMessage(mess, VideoStitch::LOG_ERROR);
    glDeleteProgram(programID);
    glDeleteShader(vertexShader);
    return 0;
  }
  glAttachShader(programID, vertexShader);
  glDeleteShader(vertexShader);  // the program hangs onto this once it's attached

  GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
  glCompileShader(fragmentShader);

  GLint fragmentShaderCompiled = GL_FALSE;
  glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
  if (fragmentShaderCompiled != GL_TRUE) {
    QString mess;
    mess.append(shaderName).append(" - Unable to compile fragment shader ").append(fragmentShader).append("!\n");
    emit logMessage(mess, VideoStitch::LOG_ERROR);
    glDeleteProgram(programID);
    glDeleteShader(fragmentShader);
    return 0;
  }

  glAttachShader(programID, fragmentShader);
  glDeleteShader(fragmentShader);  // the program hangs onto this once it's attached
  glLinkProgram(programID);

  GLint programSuccess = GL_TRUE;
  glGetProgramiv(programID, GL_LINK_STATUS, &programSuccess);
  if (programSuccess != GL_TRUE) {
    const QString mess = "%0 - Error linking program %1";
    emit logMessage(mess.arg(shaderName).arg(programID), VideoStitch::LOG_ERROR);
    glDeleteProgram(programID);
    return 0;
  }

  glUseProgram(programID);
  glUseProgram(0);

  return programID;
}

bool SteamVRRenderer::createAllShaders() {
  controllerTransformProgramID = compileGLShader("Controller",

                                                 // vertex shader
                                                 "#version 410\n"
                                                 "uniform mat4 matrix;\n"
                                                 "layout(location = 0) in vec4 position;\n"
                                                 "layout(location = 1) in vec3 v3ColorIn;\n"
                                                 "out vec4 v4Color;\n"
                                                 "void main()\n"
                                                 "{\n"
                                                 "	v4Color.xyz = v3ColorIn; v4Color.a = 1.0;\n"
                                                 "	gl_Position = matrix * position;\n"
                                                 "}\n",

                                                 // fragment shader
                                                 "#version 410\n"
                                                 "in vec4 v4Color;\n"
                                                 "out vec4 outputColor;\n"
                                                 "void main()\n"
                                                 "{\n"
                                                 "   outputColor = v4Color;\n"
                                                 "}\n");
  controllerMatrixLocation = glGetUniformLocation(controllerTransformProgramID, "matrix");
  if (controllerMatrixLocation == -1) {
    const QString mess = "Unable to find matrix uniform in controller shader";
    emit logMessage(mess, VideoStitch::LOG_ERROR);
    return false;
  }

  renderModelProgramID = compileGLShader("render model",

                                         // vertex shader
                                         "#version 410\n"
                                         "uniform mat4 matrix;\n"
                                         "layout(location = 0) in vec4 position;\n"
                                         "layout(location = 1) in vec3 v3NormalIn;\n"
                                         "layout(location = 2) in vec2 v2TexCoordsIn;\n"
                                         "out vec2 v2TexCoord;\n"
                                         "void main()\n"
                                         "{\n"
                                         "	v2TexCoord = v2TexCoordsIn;\n"
                                         "	gl_Position = matrix * vec4(position.xyz, 1);\n"
                                         "}\n",

                                         // fragment shader
                                         "#version 410 core\n"
                                         "uniform sampler2D diffuse;\n"
                                         "in vec2 v2TexCoord;\n"
                                         "out vec4 outputColor;\n"
                                         "void main()\n"
                                         "{\n"
                                         "   outputColor = texture( diffuse, v2TexCoord);\n"
                                         "}\n"

  );
  renderModelMatrixLocation = glGetUniformLocation(renderModelProgramID, "matrix");
  if (renderModelMatrixLocation == -1) {
    QString mess;
    mess.append("Unable to find matrix uniform in render model shader\n");
    emit logMessage(mess, VideoStitch::LOG_ERROR);
    return false;
  }

  lensProgramID =
      compileGLShader("Distortion",

                      // vertex shader
                      "#version 410 core\n"
                      "layout(location = 0) in vec4 position;\n"
                      "layout(location = 1) in vec2 v2UVredIn;\n"
                      "layout(location = 2) in vec2 v2UVGreenIn;\n"
                      "layout(location = 3) in vec2 v2UVblueIn;\n"
                      "noperspective  out vec2 v2UVred;\n"
                      "noperspective  out vec2 v2UVgreen;\n"
                      "noperspective  out vec2 v2UVblue;\n"
                      "void main()\n"
                      "{\n"
                      "	v2UVred = v2UVredIn;\n"
                      "	v2UVgreen = v2UVGreenIn;\n"
                      "	v2UVblue = v2UVblueIn;\n"
                      "	gl_Position = position;\n"
                      "}\n",

                      // fragment shader
                      "#version 410 core\n"
                      "uniform sampler2D mytexture;\n"

                      "noperspective  in vec2 v2UVred;\n"
                      "noperspective  in vec2 v2UVgreen;\n"
                      "noperspective  in vec2 v2UVblue;\n"

                      "out vec4 outputColor;\n"

                      "void main()\n"
                      "{\n"
                      "	float fBoundsCheck = ( (dot( vec2( lessThan( v2UVgreen.xy, vec2(0.05, 0.05)) ), vec2(1.0, "
                      "1.0))+dot( vec2( greaterThan( v2UVgreen.xy, vec2( 0.95, 0.95)) ), vec2(1.0, 1.0))) );\n"
                      "	if( fBoundsCheck > 1.0 )\n"
                      "	{ outputColor = vec4( 0, 0, 0, 1.0 ); }\n"
                      "	else\n"
                      "	{\n"
                      "		float red = texture(mytexture, v2UVred).x;\n"
                      "		float green = texture(mytexture, v2UVgreen).y;\n"
                      "		float blue = texture(mytexture, v2UVblue).z;\n"
                      "		outputColor = vec4( red, green, blue, 1.0  ); }\n"
                      "}\n");

  return controllerTransformProgramID != 0 && renderModelProgramID != 0 && lensProgramID != 0;
}

// ------------------ Main rendering loop -----------------------

void SteamVRRenderer::render() {
  // for now as fast as possible
  if (hmd) {
    drawControllers();
    renderStereoTargets();
    renderDistortion();

    vr::Texture_t leftEyeTexture = {(void *)leftEyeDesc.resolveTextureId, vr::API_OpenGL, vr::ColorSpace_Gamma};
    vr::VRCompositor()->Submit(vr::Eye_Left, &leftEyeTexture);
    vr::Texture_t rightEyeTexture = {(void *)rightEyeDesc.resolveTextureId, vr::API_OpenGL, vr::ColorSpace_Gamma};
    vr::VRCompositor()->Submit(vr::Eye_Right, &rightEyeTexture);
  }

  // Spew out the controller and pose count whenever they change.
  if (trackedControllerCount != trackedControllerCount_Last || validPoseCount != validPoseCount_Last) {
    validPoseCount_Last = validPoseCount;
    trackedControllerCount_Last = trackedControllerCount;
    QString mess;
    mess.append("PoseCount:")
        .append(validPoseCount)
        .append("(")
        .append(poseClasses.c_str())
        .append(") Controllers:")
        .append(trackedControllerCount)
        .append("\n");
    emit logMessage(mess, VideoStitch::LOG_NOTICE);
  }

  updateHMDMatrixPose();
}

void SteamVRRenderer::renderStereoTargets() {
  glClearColor(0.15f, 0.15f, 0.18f, 1.0f);  // nice background color, but not black
  glEnable(GL_MULTISAMPLE);

  // Left Eye
  glBindFramebuffer(GL_FRAMEBUFFER, leftEyeDesc.renderFramebufferId);
  glViewport(0, 0, renderWidth, renderHeight);
  renderVideoFrame(vr::Eye_Left);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);

  glDisable(GL_MULTISAMPLE);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, leftEyeDesc.renderFramebufferId);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, leftEyeDesc.resolveFramebufferId);

  glBlitFramebuffer(0, 0, renderWidth, renderHeight, 0, 0, renderWidth, renderHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

  glEnable(GL_MULTISAMPLE);

  // Right Eye
  glBindFramebuffer(GL_FRAMEBUFFER, rightEyeDesc.renderFramebufferId);
  glViewport(0, 0, renderWidth, renderHeight);
  renderVideoFrame(vr::Eye_Right);
  glBindFramebuffer(GL_FRAMEBUFFER, 0);

  glDisable(GL_MULTISAMPLE);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, rightEyeDesc.renderFramebufferId);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, rightEyeDesc.resolveFramebufferId);

  glBlitFramebuffer(0, 0, renderWidth, renderHeight, 0, 0, renderWidth, renderHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);

  glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}

void SteamVRRenderer::renderVideoFrame(vr::Hmd_Eye eye) {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glEnable(GL_DEPTH_TEST);

  // render the spherical frame
  {
    const Matrix4 mvp = getCurrentViewProjectionMatrix(eye);
    QMatrix4x4 qt_mvp;
    qt_mvp.setRow(0, QVector4D(mvp.get()[0], mvp.get()[4], mvp.get()[8], mvp.get()[12]));
    qt_mvp.setRow(1, QVector4D(mvp.get()[1], mvp.get()[5], mvp.get()[9], mvp.get()[13]));
    qt_mvp.setRow(2, QVector4D(mvp.get()[2], mvp.get()[6], mvp.get()[10], mvp.get()[14]));
    qt_mvp.setRow(3, QVector4D(mvp.get()[3], mvp.get()[7], mvp.get()[11], mvp.get()[15]));

    // our world does not use the same coordinate system as OpenGL
    qt_mvp.rotate(90.0, 1.0, 0.0, 0.0);
    // rotate to have the center of our rendered panorama in the window be at the center in VR
    qt_mvp.rotate(90.0, 0.0, 0.0, 1.0);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    switch (Texture::getLeft().getType()) {
      case Texture::Type::PANORAMIC:
        Texture::getLeft().latePanoramaDef();

        sphereProgram.bind();
        if (eye == vr::Eye_Right && (Texture::getLeft().id != Texture::getRight().id) &&
            Texture::getRight().id != Texture::ID_NONE) {
          Texture::getRight().latePanoramaDef();
          std::lock_guard<std::mutex> lk(*Texture::getRight().lock.get());
          glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Texture::getRight().pixelBuffer);
          glBindTexture(GL_TEXTURE_2D, Texture::getRight().id);
          glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, Texture::getRight().getWidth(), Texture::getRight().getHeight(),
                          GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

          sphereProgram.setUniformValue("texture", 0);
          sphereProgram.setUniformValue("mvp_matrix", qt_mvp);
          sphereProgram.release();

          Renderer::renderSphere();
        } else if (Texture::getLeft().id != Texture::ID_NONE) {
          std::lock_guard<std::mutex> lk(*Texture::getLeft().lock.get());
          glBindBuffer(GL_PIXEL_UNPACK_BUFFER, Texture::getLeft().pixelBuffer);
          glBindTexture(GL_TEXTURE_2D, Texture::getLeft().id);
          glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, Texture::getLeft().getWidth(), Texture::getLeft().getHeight(),
                          GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

          sphereProgram.setUniformValue("texture", 0);
          sphereProgram.setUniformValue("mvp_matrix", qt_mvp);
          sphereProgram.release();

          Renderer::renderSphere();
        } else {
          placeHolderTex.bind();
        }
        break;
      case Texture::Type::CUBEMAP:
      case Texture::Type::EQUIANGULAR_CUBEMAP:
        Q_ASSERT(false);
        // XXX TODO FIXME
        break;
    }
  }

  bool isInputCapturedByAnotherProcess = hmd->IsInputFocusCapturedByAnotherProcess();

  if (!isInputCapturedByAnotherProcess) {
    // draw the controller axis lines
    glUseProgram(controllerTransformProgramID);
    glUniformMatrix4fv(controllerMatrixLocation, 1, GL_FALSE, getCurrentViewProjectionMatrix(eye).get());
    glBindVertexArray(controllerVAO);
    glDrawArrays(GL_LINES, 0, controllerVertcount);
    glBindVertexArray(0);
  }

  // ----- Render Model rendering -----
  glUseProgram(renderModelProgramID);

  for (uint32_t trackedDevice = 0; trackedDevice < vr::k_unMaxTrackedDeviceCount; ++trackedDevice) {
    if (!trackedDeviceToRenderModel[trackedDevice] || !showTrackedDevice[trackedDevice]) {
      continue;
    }

    const vr::TrackedDevicePose_t &pose = trackedDevicePose[trackedDevice];
    if (!pose.bPoseIsValid) {
      continue;
    }

    if (isInputCapturedByAnotherProcess &&
        hmd->GetTrackedDeviceClass(trackedDevice) == vr::TrackedDeviceClass_Controller) {
      continue;
    }

    const Matrix4 &deviceToTracking = devicePose[trackedDevice];
    const Matrix4 mvp = getCurrentViewProjectionMatrix(eye) * deviceToTracking;
    glUniformMatrix4fv(renderModelMatrixLocation, 1, GL_FALSE, mvp.get());

    trackedDeviceToRenderModel[trackedDevice]->draw();
  }

  glUseProgram(0);
}

Matrix4 SteamVRRenderer::getCurrentViewProjectionMatrix(vr::Hmd_Eye eye) {
  Matrix4 mvp;
  if (eye == vr::Eye_Left) {
    mvp = mat4ProjectionLeft * mat4eyePosLeft * mat4HMDPose;
  } else if (eye == vr::Eye_Right) {
    mvp = mat4ProjectionRight * mat4eyePosRight * mat4HMDPose;
  }
  return mvp;
}

void SteamVRRenderer::renderDistortion() {
  glDisable(GL_DEPTH_TEST);
  glViewport(0, 0, windowWidth, windowHeight);

  glBindVertexArray(lensVAO);
  glUseProgram(lensProgramID);

  // render left lens (first half of index array )
  glBindTexture(GL_TEXTURE_2D, leftEyeDesc.resolveTextureId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  glDrawElements(GL_TRIANGLES, indexSize / 2, GL_UNSIGNED_SHORT, 0);

  // render right lens (second half of index array )
  glBindTexture(GL_TEXTURE_2D, rightEyeDesc.resolveTextureId);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
  glDrawElements(GL_TRIANGLES, indexSize / 2, GL_UNSIGNED_SHORT, (const void *)(indexSize));

  glBindVertexArray(0);
  glUseProgram(0);
}

void SteamVRRenderer::drawControllers() {
  // don't draw controllers if somebody else has input focus
  if (hmd->IsInputFocusCapturedByAnotherProcess()) {
    return;
  }

  std::vector<float> vertdataarray;

  controllerVertcount = 0;
  trackedControllerCount = 0;

  for (vr::TrackedDeviceIndex_t trackedDevice = vr::k_unTrackedDeviceIndex_Hmd + 1;
       trackedDevice < vr::k_unMaxTrackedDeviceCount; ++trackedDevice) {
    if (!hmd->IsTrackedDeviceConnected(trackedDevice)) {
      continue;
    }

    if (hmd->GetTrackedDeviceClass(trackedDevice) != vr::TrackedDeviceClass_Controller) {
      continue;
    }

    trackedControllerCount += 1;

    if (!trackedDevicePose[trackedDevice].bPoseIsValid) {
      continue;
    }

    const Matrix4 &mat = devicePose[trackedDevice];
    const Vector4 center = mat * Vector4(0, 0, 0, 1);

    for (int i = 0; i < 3; ++i) {
      Vector3 color(0, 0, 0);
      Vector4 point(0, 0, 0, 1);
      point[i] += 0.05f;  // offset in X, Y, Z
      color[i] = 1.0;     // R, G, B
      point = mat * point;
      vertdataarray.push_back(center.x);
      vertdataarray.push_back(center.y);
      vertdataarray.push_back(center.z);
      vertdataarray.push_back(color.x);
      vertdataarray.push_back(color.y);
      vertdataarray.push_back(color.z);
      vertdataarray.push_back(point.x);
      vertdataarray.push_back(point.y);
      vertdataarray.push_back(point.z);
      vertdataarray.push_back(color.x);
      vertdataarray.push_back(color.y);
      vertdataarray.push_back(color.z);

      controllerVertcount += 2;
    }

    Vector4 start = mat * Vector4(0, 0, -0.02f, 1);
    Vector4 end = mat * Vector4(0, 0, -39.f, 1);
    Vector3 color(.92f, .92f, .71f);
    vertdataarray.push_back(start.x);
    vertdataarray.push_back(start.y);
    vertdataarray.push_back(start.z);
    vertdataarray.push_back(color.x);
    vertdataarray.push_back(color.y);
    vertdataarray.push_back(color.z);
    vertdataarray.push_back(end.x);
    vertdataarray.push_back(end.y);
    vertdataarray.push_back(end.z);
    vertdataarray.push_back(color.x);
    vertdataarray.push_back(color.y);
    vertdataarray.push_back(color.z);

    controllerVertcount += 2;
  }

  // Setup the VAO the first time through.
  if (controllerVAO == 0) {
    glGenVertexArrays(1, &controllerVAO);
    glBindVertexArray(controllerVAO);

    glGenBuffers(1, &glControllerVertBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, glControllerVertBuffer);

    const GLuint stride = 2 * 3 * sizeof(float);
    GLuint offset = 0;

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (const void *)offset);

    offset += sizeof(Vector3);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (const void *)offset);

    glBindVertexArray(0);
  }

  glBindBuffer(GL_ARRAY_BUFFER, glControllerVertBuffer);

  // set vertex data if we have some
  if (vertdataarray.size() > 0) {
    //$ TODO: Use glBufferSubData for this...
    glBufferData(GL_ARRAY_BUFFER, sizeof(float) * vertdataarray.size(), &vertdataarray[0], GL_STREAM_DRAW);
  }
}

// ------------------------- Tracking ----------------------

void SteamVRRenderer::updateHMDMatrixPose() {
  if (!hmd) {
    return;
  }

  vr::VRCompositor()->WaitGetPoses(trackedDevicePose, vr::k_unMaxTrackedDeviceCount, nullptr, 0);

  // We just got done with the glFinish - the seconds since last vsync should be 0.
  const float secondsSinceLastVsync = 0.0f;
  const float frameDuration =
      1.0f / hmd->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float);
  const float secondsUntilPhotons =
      frameDuration - secondsSinceLastVsync +
      hmd->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float);
  hmd->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, secondsUntilPhotons, trackedDevicePose,
                                       vr::k_unMaxTrackedDeviceCount);

  validPoseCount = 0;
  poseClasses.clear();
  for (int device = 0; device < vr::k_unMaxTrackedDeviceCount; ++device) {
    if (trackedDevicePose[device].bPoseIsValid) {
      validPoseCount++;
      devicePose[device] = convertSteamVRMatrixToMatrix4(trackedDevicePose[device].mDeviceToAbsoluteTracking);
      if (devClassChar[device] == 0) {
        switch (hmd->GetTrackedDeviceClass(device)) {
          case vr::TrackedDeviceClass_Controller:
            devClassChar[device] = 'C';
            break;
          case vr::TrackedDeviceClass_HMD:
            devClassChar[device] = 'H';
            break;
          case vr::TrackedDeviceClass_Invalid:
            devClassChar[device] = 'I';
            break;
          case vr::TrackedDeviceClass_Other:
            devClassChar[device] = 'O';
            break;
          case vr::TrackedDeviceClass_TrackingReference:
            devClassChar[device] = 'T';
            break;
          default:
            devClassChar[device] = '?';
            break;
        }
      }
      poseClasses += devClassChar[device];
    }
  }

  if (trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) {
    mat4HMDPose = devicePose[vr::k_unTrackedDeviceIndex_Hmd].invert();
  }
}

Matrix4 SteamVRRenderer::convertSteamVRMatrixToMatrix4(const vr::HmdMatrix34_t &matPose) {
  Matrix4 matrixObj(matPose.m[0][0], matPose.m[1][0], matPose.m[2][0], 0.0, matPose.m[0][1], matPose.m[1][1],
                    matPose.m[2][1], 0.0, matPose.m[0][2], matPose.m[1][2], matPose.m[2][2], 0.0, matPose.m[0][3],
                    matPose.m[1][3], matPose.m[2][3], 1.0f);
  return matrixObj;
}
