//
// Copyright (c) 2017 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This sample shows basic usage of the GL_OVR_multiview2 extension.

#include "SampleApplication.h"

#include "util/geometry_utils.h"
#include "util/shader_utils.h"

#include <iostream>

namespace
{

void FillTranslationMatrix(float xOffset, float yOffset, float zOffset, float *matrix)
{
    matrix[0] = 1.0f;
    matrix[1] = 0.0f;
    matrix[2] = 0.0f;
    matrix[3] = xOffset;

    matrix[4] = 0.0f;
    matrix[5] = 1.0f;
    matrix[6] = 0.0f;
    matrix[7] = yOffset;

    matrix[8]  = 0.0f;
    matrix[9]  = 0.0f;
    matrix[10] = 1.0f;
    matrix[11] = zOffset;

    matrix[12] = 0.0f;
    matrix[13] = 0.0f;
    matrix[14] = 0.0f;
    matrix[15] = 1.0f;
}

}  // namespace

class MultiviewSample : public SampleApplication
{
  public:
    MultiviewSample(int argc, char **argv)
        : SampleApplication("Multiview", argc, argv, 3, 0),
          mMultiviewProgram(0),
          mMultiviewPersperiveUniformLoc(-1),
          mMultiviewLeftEyeCameraUniformLoc(-1),
          mMultiviewRightEyeCameraUniformLoc(-1),
          mMultiviewTranslationUniformLoc(-1),
          mMultiviewFBO(0),
          mColorTexture(0),
          mDepthTexture(0),
          mQuadVAO(0),
          mQuadVBO(0),
          mCubeVAO(0),
          mCubePosVBO(0),
          mCubeNormalVBO(0),
          mCubeIBO(0),
          mCombineProgram(0)
    {}

    bool initialize() override
    {
        // Check whether the GL_OVR_multiview2 extension is supported. If not, abort
        // initialization.
        const char *allExtensions = reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS));
        const std::string paddedExtensions = std::string(" ") + allExtensions + std::string(" ");
        if (paddedExtensions.find(std::string(" GL_OVR_multiview2 ")) == std::string::npos)
        {
            std::cout << "GL_OVR_multiview2 is not available." << std::endl;
            return false;
        }

        // A view covers horizontally half of the screen.
        int viewWidth  = getWindow()->getWidth() / 2;
        int viewHeight = getWindow()->getHeight();

        // Create color and depth texture arrays with two layers to which we render each view.
        glGenTextures(1, &mColorTexture);
        glBindTexture(GL_TEXTURE_2D_ARRAY, mColorTexture);
        glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, viewWidth, viewHeight, 2, 0, GL_RGBA,
                     GL_UNSIGNED_BYTE, nullptr);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        glGenTextures(1, &mDepthTexture);
        glBindTexture(GL_TEXTURE_2D_ARRAY, mDepthTexture);
        glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_DEPTH_COMPONENT32F, viewWidth, viewHeight, 2, 0,
                     GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);

        // Generate multiview framebuffer for layered rendering.
        glGenFramebuffers(1, &mMultiviewFBO);
        glBindFramebuffer(GL_FRAMEBUFFER, mMultiviewFBO);
        glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, mColorTexture, 0, 0,
                                         2);
        glFramebufferTextureMultiviewOVR(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, mDepthTexture, 0, 0,
                                         2);
        GLenum drawBuffer = GL_COLOR_ATTACHMENT0;
        glDrawBuffers(1, &drawBuffer);

        // Check that the framebuffer is complete. Abort initialization otherwise.
        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        {
            return false;
        }

        // Create multiview program and query the uniform locations.
        // The program has two code paths based on the gl_ViewID_OVR attribute which tells us which
        // view is currently being rendered to. Based on it we decide which eye's camera matrix to
        // use.
        constexpr char kMultiviewVS[] =
            "#version 300 es\n"
            "#extension GL_OVR_multiview2 : require\n"
            "layout(num_views = 2) in;\n"
            "layout(location=0) in vec3 posIn;\n"
            "layout(location=1) in vec3 normalIn;\n"
            "uniform mat4 uPerspective;\n"
            "uniform mat4 uCameraLeftEye;\n"
            "uniform mat4 uCameraRightEye;\n"
            "uniform mat4 uTranslation;\n"
            "out vec3 oNormal;\n"
            "void main()\n"
            "{\n"
            "   vec4 p = uTranslation * vec4(posIn,1.);\n"
            "   if (gl_ViewID_OVR == 0u) {\n"
            "       p = uCameraLeftEye * p;\n"
            "   } else {\n"
            "       p = uCameraRightEye * p;\n"
            "   }\n"
            "   oNormal = normalIn;\n"
            "   gl_Position = uPerspective * p;\n"
            "}\n";

        constexpr char kMultiviewFS[] =
            "#version 300 es\n"
            "#extension GL_OVR_multiview2 : require\n"
            "precision mediump float;\n"
            "out vec4 color;\n"
            "in vec3 oNormal;\n"
            "void main()\n"
            "{\n"
            "   vec3 col = 0.5 * oNormal + vec3(0.5);\n"
            "   color = vec4(col, 1.);\n"
            "}\n";

        mMultiviewProgram = CompileProgram(kMultiviewVS, kMultiviewFS);
        if (!mMultiviewProgram)
        {
            return false;
        }
        mMultiviewPersperiveUniformLoc = glGetUniformLocation(mMultiviewProgram, "uPerspective");
        mMultiviewLeftEyeCameraUniformLoc =
            glGetUniformLocation(mMultiviewProgram, "uCameraLeftEye");
        mMultiviewRightEyeCameraUniformLoc =
            glGetUniformLocation(mMultiviewProgram, "uCameraRightEye");
        mMultiviewTranslationUniformLoc = glGetUniformLocation(mMultiviewProgram, "uTranslation");

        // Create a normal program to combine both layers of the color array texture.
        constexpr char kCombineVS[] =
            "#version 300 es\n"
            "in vec2 vIn;\n"
            "out vec2 uv;\n"
            "void main()\n"
            "{\n"
            "   gl_Position = vec4(vIn, 0., 1.);\n"
            "   uv = vIn * .5 + vec2(.5);\n"
            "}\n";

        constexpr char kCombineFS[] =
            "#version 300 es\n"
            "precision mediump float;\n"
            "precision mediump sampler2DArray;\n"
            "uniform sampler2DArray uMultiviewTex;\n"
            "in vec2 uv;\n"
            "out vec4 color;\n"
            "void main()\n"
            "{\n"
            "   float scaledX = 2.0 * uv.x;\n"
            "   float layer = floor(scaledX);\n"
            "   vec2 adjustedUV = vec2(fract(scaledX), uv.y);\n"
            "   vec3 texColor = texture(uMultiviewTex, vec3(adjustedUV, layer)).rgb;\n"
            "   color = vec4(texColor, 1.);\n"
            "}\n";

        mCombineProgram = CompileProgram(kCombineVS, kCombineFS);
        if (!mCombineProgram)
        {
            return false;
        }

        // Generate a quad which covers the whole screen.
        glGenVertexArrays(1, &mQuadVAO);
        glBindVertexArray(mQuadVAO);

        glGenBuffers(1, &mQuadVBO);
        glBindBuffer(GL_ARRAY_BUFFER, mQuadVBO);
        const float kQuadPositionData[] = {1.f, -1.f, 1.f, 1.f, -1.f, -1.f, -1.f, 1.f};
        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 8, kQuadPositionData, GL_STATIC_DRAW);
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(0);
        glBindVertexArray(0);

        // Generate a cube.
        GenerateCubeGeometry(1.0f, &mCube);
        glGenVertexArrays(1, &mCubeVAO);
        glBindVertexArray(mCubeVAO);

        glGenBuffers(1, &mCubePosVBO);
        glBindBuffer(GL_ARRAY_BUFFER, mCubePosVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(angle::Vector3) * mCube.positions.size(),
                     mCube.positions.data(), GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(0);

        glGenBuffers(1, &mCubeNormalVBO);
        glBindBuffer(GL_ARRAY_BUFFER, mCubeNormalVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(angle::Vector3) * mCube.normals.size(),
                     mCube.normals.data(), GL_STATIC_DRAW);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(1);

        glGenBuffers(1, &mCubeIBO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mCubeIBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * mCube.indices.size(),
                     mCube.indices.data(), GL_STATIC_DRAW);

        glBindVertexArray(0);

        glEnable(GL_DEPTH_TEST);
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

        return true;
    }

    void destroy() override
    {
        glDeleteProgram(mMultiviewProgram);
        glDeleteFramebuffers(1, &mMultiviewFBO);
        glDeleteTextures(1, &mColorTexture);
        glDeleteTextures(1, &mDepthTexture);
        glDeleteVertexArrays(1, &mQuadVAO);
        glDeleteBuffers(1, &mQuadVBO);
        glDeleteVertexArrays(1, &mCubeVAO);
        glDeleteBuffers(1, &mQuadVBO);
        glDeleteBuffers(1, &mCubePosVBO);
        glDeleteBuffers(1, &mCubeNormalVBO);
        glDeleteBuffers(1, &mCubeIBO);
        glDeleteProgram(mCombineProgram);
    }

    void draw() override
    {
        // Draw to multiview fbo.
        {
            // Generate the perspective projection matrix.
            const int viewWidth          = getWindow()->getWidth() / 2;
            const int viewHeight         = getWindow()->getHeight();
            const float kFOV             = 90.f;
            const float kNear            = 1.0f;
            const float kFar             = 100.0f;
            const float kPlaneDifference = kFar - kNear;
            const float kXYScale         = 1.f / (tanf(kFOV / 2.0f));
            const float kAspectRatio     = static_cast<float>(viewWidth) / viewHeight;
            float kPerspectiveProjectionMatrix[16];
            kPerspectiveProjectionMatrix[0] = kXYScale / kAspectRatio;
            kPerspectiveProjectionMatrix[1] = .0f;
            kPerspectiveProjectionMatrix[2] = .0f;
            kPerspectiveProjectionMatrix[3] = .0f;

            kPerspectiveProjectionMatrix[4] = .0f;
            kPerspectiveProjectionMatrix[5] = kXYScale;
            kPerspectiveProjectionMatrix[6] = .0f;
            kPerspectiveProjectionMatrix[7] = .0f;

            kPerspectiveProjectionMatrix[8]  = .0f;
            kPerspectiveProjectionMatrix[9]  = .0;
            kPerspectiveProjectionMatrix[10] = -kFar / kPlaneDifference;
            kPerspectiveProjectionMatrix[11] = -1.f;

            kPerspectiveProjectionMatrix[12] = .0f;
            kPerspectiveProjectionMatrix[13] = .0;
            kPerspectiveProjectionMatrix[14] = -kFar * kNear / kPlaneDifference;
            kPerspectiveProjectionMatrix[15] = .0;

            // Generate the camera matrices for the left and right eye.
            const float kXOffset = 1.5f;
            const float kYOffset = 1.5f;
            const float kZOffset = 5.0f;
            float kLeftCameraMatrix[16];
            FillTranslationMatrix(kXOffset, -kYOffset, -kZOffset, kLeftCameraMatrix);
            float kRightCameraMatrix[16];
            FillTranslationMatrix(-kXOffset, -kYOffset, -kZOffset, kRightCameraMatrix);

            // Bind and clear the multiview framebuffer.
            glBindFramebuffer(GL_FRAMEBUFFER, mMultiviewFBO);
            glClearColor(0, 0, 0, 1);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            // Set the viewport to be the size as one of the views.
            glViewport(0, 0, viewWidth, viewHeight);

            // Bind multiview program and set matrices.
            glUseProgram(mMultiviewProgram);
            glUniformMatrix4fv(mMultiviewPersperiveUniformLoc, 1, GL_TRUE,
                               kPerspectiveProjectionMatrix);
            glUniformMatrix4fv(mMultiviewLeftEyeCameraUniformLoc, 1, GL_TRUE, kLeftCameraMatrix);
            glUniformMatrix4fv(mMultiviewRightEyeCameraUniformLoc, 1, GL_TRUE, kRightCameraMatrix);

            glBindVertexArray(mCubeVAO);

            // Draw first cube.
            float kTranslationMatrix[16];
            FillTranslationMatrix(0.0f, 0.0f, 0.0f, kTranslationMatrix);
            glUniformMatrix4fv(mMultiviewTranslationUniformLoc, 1, GL_TRUE, kTranslationMatrix);
            glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(mCube.indices.size()),
                           GL_UNSIGNED_SHORT, nullptr);

            // Draw second cube.
            FillTranslationMatrix(1.0f, 1.0f, -2.0f, kTranslationMatrix);
            glUniformMatrix4fv(mMultiviewTranslationUniformLoc, 1, GL_TRUE, kTranslationMatrix);
            glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(mCube.indices.size()),
                           GL_UNSIGNED_SHORT, nullptr);

            glBindVertexArray(0);
        }

        // Combine both views.
        {
            // Bind the default framebuffer object and clear.
            glBindFramebuffer(GL_FRAMEBUFFER, 0);

            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            // Set the viewport to cover the whole screen.
            glViewport(0, 0, getWindow()->getWidth(), getWindow()->getHeight());

            glUseProgram(mCombineProgram);

            // Bind the 2D array texture to be used as a sampler.
            glUniform1i(glGetUniformLocation(mCombineProgram, "uMultiviewTex"), 0);
            glBindTexture(GL_TEXTURE_2D_ARRAY, mMultiviewFBO);
            glActiveTexture(GL_TEXTURE0);

            // Draw a quad which covers the whole screen. Layer and texture coordinates are
            // calculated in the vertex shader based on the UV coordinates of the quad.
            glBindVertexArray(mQuadVAO);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            glBindVertexArray(0);
        }
    }

  private:
    GLuint mMultiviewProgram;
    GLint mMultiviewPersperiveUniformLoc;
    GLint mMultiviewLeftEyeCameraUniformLoc;
    GLint mMultiviewRightEyeCameraUniformLoc;
    GLint mMultiviewTranslationUniformLoc;

    GLuint mMultiviewFBO;
    GLuint mColorTexture;
    GLuint mDepthTexture;

    GLuint mQuadVAO;
    GLuint mQuadVBO;

    CubeGeometry mCube;
    GLuint mCubeVAO;
    GLuint mCubePosVBO;
    GLuint mCubeNormalVBO;
    GLuint mCubeIBO;

    GLuint mCombineProgram;
};

int main(int argc, char **argv)
{
    MultiviewSample app(argc, argv);
    return app.run();
}
