/*
 * Copyright 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//--------------------------------------------------------------------------------
// MoreTeapotsRenderer.cpp
// Render teapots
//--------------------------------------------------------------------------------
//--------------------------------------------------------------------------------
// Include files
//--------------------------------------------------------------------------------
#include "MoreTeapotsRenderer.h"

#include <string.h>

//--------------------------------------------------------------------------------
// Teapot model data
//--------------------------------------------------------------------------------
#include "teapot.inl"

//--------------------------------------------------------------------------------
// Ctor
//--------------------------------------------------------------------------------
MoreTeapotsRenderer::MoreTeapotsRenderer()
    : geometry_instancing_support_(false) {}

//--------------------------------------------------------------------------------
// Dtor
//--------------------------------------------------------------------------------
MoreTeapotsRenderer::~MoreTeapotsRenderer() { Unload(); }

//--------------------------------------------------------------------------------
// Init
//--------------------------------------------------------------------------------
void MoreTeapotsRenderer::Init(const int32_t numX, const int32_t numY,
                               const int32_t numZ) {
  if (ndk_helper::GLContext::GetInstance()->GetGLVersion() >= 3.0) {
    geometry_instancing_support_ = true;
  } else if (ndk_helper::GLContext::GetInstance()->CheckExtension(
                 "GL_NV_draw_instanced") &&
             ndk_helper::GLContext::GetInstance()->CheckExtension(
                 "GL_NV_uniform_buffer_object")) {
    LOGI("Supported via extension!");
    //_bGeometryInstancingSupport = true;
    //_bARBSupport = true; //Need to patch shaders
    // Currently this has been disabled
  }

  // Settings
  glFrontFace(GL_CCW);

  // Create Index buffer
  num_indices_ = sizeof(teapotIndices) / sizeof(teapotIndices[0]);
  glGenBuffers(1, &ibo_);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(teapotIndices), teapotIndices,
               GL_STATIC_DRAW);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

  // Create VBO
  num_vertices_ = sizeof(teapotPositions) / sizeof(teapotPositions[0]) / 3;
  int32_t stride = sizeof(TEAPOT_VERTEX);
  int32_t index = 0;
  TEAPOT_VERTEX* p = new TEAPOT_VERTEX[num_vertices_];
  for (int32_t i = 0; i < num_vertices_; ++i) {
    p[i].pos[0] = teapotPositions[index];
    p[i].pos[1] = teapotPositions[index + 1];
    p[i].pos[2] = teapotPositions[index + 2];

    p[i].normal[0] = teapotNormals[index];
    p[i].normal[1] = teapotNormals[index + 1];
    p[i].normal[2] = teapotNormals[index + 2];
    index += 3;
  }
  glGenBuffers(1, &vbo_);
  glBindBuffer(GL_ARRAY_BUFFER, vbo_);
  glBufferData(GL_ARRAY_BUFFER, stride * num_vertices_, p, GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  delete[] p;

  // Init Projection matrices
  teapot_x_ = numX;
  teapot_y_ = numY;
  teapot_z_ = numZ;
  vec_mat_models_.reserve(teapot_x_ * teapot_y_ * teapot_z_);

  UpdateViewport();

  const float total_width = 500.f;
  float gap_x = total_width / (teapot_x_ - 1);
  float gap_y = total_width / (teapot_y_ - 1);
  float gap_z = total_width / (teapot_z_ - 1);
  float offset_x = -total_width / 2.f;
  float offset_y = -total_width / 2.f;
  float offset_z = -total_width / 2.f;

  for (int32_t x = 0; x < teapot_x_; ++x)
    for (int32_t y = 0; y < teapot_y_; ++y)
      for (int32_t z = 0; z < teapot_z_; ++z) {
        vec_mat_models_.push_back(ndk_helper::Mat4::Translation(
            x * gap_x + offset_x, y * gap_y + offset_y,
            z * gap_z + offset_z));
        vec_colors_.push_back(ndk_helper::Vec3(
            random() / float(RAND_MAX * 1.1), random() / float(RAND_MAX * 1.1),
            random() / float(RAND_MAX * 1.1)));

        float rotation_x = random() / float(RAND_MAX) - 0.5f;
        float rotation_y = random() / float(RAND_MAX) - 0.5f;
        vec_rotations_.push_back(ndk_helper::Vec2(rotation_x * 0.05f, rotation_y * 0.05f));
        vec_current_rotations_.push_back(
            ndk_helper::Vec2(rotation_x * M_PI, rotation_y * M_PI));
      }

  if (geometry_instancing_support_) {
    //
    // Create parameter dictionary for shader patch
    std::map<std::string, std::string> param;
    param[std::string("%NUM_TEAPOT%")] =
        ToString(teapot_x_ * teapot_y_ * teapot_z_);
    param[std::string("%LOCATION_VERTEX%")] = ToString(ATTRIB_VERTEX);
    param[std::string("%LOCATION_NORMAL%")] = ToString(ATTRIB_NORMAL);
    if (arb_support_)
      param[std::string("%ARB%")] = std::string("ARB");
    else
      param[std::string("%ARB%")] = std::string("");

    // Load shader
    bool b = LoadShadersES3(&shader_param_, "Shaders/VS_ShaderPlainES3.vsh",
                            "Shaders/ShaderPlainES3.fsh", param);
    if (b) {
      //
      // Create uniform buffer
      //
      GLuint bindingPoint = 1;
      GLuint blockIndex;
      blockIndex = glGetUniformBlockIndex(shader_param_.program_, "ParamBlock");
      glUniformBlockBinding(shader_param_.program_, blockIndex, bindingPoint);

      // Retrieve array stride value
      int32_t num_indices;
      glGetActiveUniformBlockiv(shader_param_.program_, blockIndex,
                                GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &num_indices);
      GLint i[num_indices];
      GLint stride[num_indices];
      glGetActiveUniformBlockiv(shader_param_.program_, blockIndex,
                                GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, i);
      glGetActiveUniformsiv(shader_param_.program_, num_indices, (GLuint*)i,
                            GL_UNIFORM_ARRAY_STRIDE, stride);

      ubo_matrix_stride_ = stride[0] / sizeof(float);
      ubo_vector_stride_ = stride[2] / sizeof(float);

      glGenBuffers(1, &ubo_);
      glBindBuffer(GL_UNIFORM_BUFFER, ubo_);
      glBindBufferBase(GL_UNIFORM_BUFFER, bindingPoint, ubo_);

      // Store color value which wouldn't be updated every frame
      int32_t size = teapot_x_ * teapot_y_ * teapot_z_ *
                      (ubo_matrix_stride_ + ubo_matrix_stride_ +
                       ubo_vector_stride_);  // Mat4 + Mat4 + Vec3 + 1 stride
      float* pBuffer = new float[size];
      float* pColor =
          pBuffer + teapot_x_ * teapot_y_ * teapot_z_ * ubo_matrix_stride_ * 2;
      for (int32_t i = 0; i < teapot_x_ * teapot_y_ * teapot_z_; ++i) {
        memcpy(pColor, &vec_colors_[i], 3 * sizeof(float));
        pColor += ubo_vector_stride_;  // Assuming std140 layout which is 4
                                       // DWORD stride for vectors
      }

      glBufferData(GL_UNIFORM_BUFFER, size * sizeof(float), pBuffer,
                   GL_DYNAMIC_DRAW);
      delete[] pBuffer;
    } else {
      LOGI("Shader compilation failed!! Falls back to ES2.0 pass");
      // This happens some devices.
      geometry_instancing_support_ = false;
      // Load shader for GLES2.0
      LoadShaders(&shader_param_, "Shaders/VS_ShaderPlain.vsh",
                  "Shaders/ShaderPlain.fsh");
    }
  } else {
    // Load shader for GLES2.0
    LoadShaders(&shader_param_, "Shaders/VS_ShaderPlain.vsh",
                "Shaders/ShaderPlain.fsh");
  }
}

void MoreTeapotsRenderer::UpdateViewport() {
  int32_t viewport[4];
  glGetIntegerv(GL_VIEWPORT, viewport);

  const float CAM_NEAR = 5.f;
  const float CAM_FAR = 10000.f;
  if (viewport[2] < viewport[3]) {
    float aspect =
            static_cast<float>(viewport[2]) / static_cast<float>(viewport[3]);
    mat_projection_ =
            ndk_helper::Mat4::Perspective(aspect, 1.0f, CAM_NEAR, CAM_FAR);
  } else {
    float aspect =
            static_cast<float>(viewport[3]) / static_cast<float>(viewport[2]);
    mat_projection_ =
            ndk_helper::Mat4::Perspective(1.0f, aspect, CAM_NEAR, CAM_FAR);
  }
}

//--------------------------------------------------------------------------------
// Unload
//--------------------------------------------------------------------------------
void MoreTeapotsRenderer::Unload() {
  if (vbo_) {
    glDeleteBuffers(1, &vbo_);
    vbo_ = 0;
  }
  if (ubo_) {
    glDeleteBuffers(1, &ubo_);
    ubo_ = 0;
  }
  if (ibo_) {
    glDeleteBuffers(1, &ibo_);
    ibo_ = 0;
  }
  if (shader_param_.program_) {
    glDeleteProgram(shader_param_.program_);
    shader_param_.program_ = 0;
  }
}

//--------------------------------------------------------------------------------
// Update
//--------------------------------------------------------------------------------
void MoreTeapotsRenderer::Update(float fTime) {
  const float CAM_X = 0.f;
  const float CAM_Y = 0.f;
  const float CAM_Z = 2000.f;

  mat_view_ = ndk_helper::Mat4::LookAt(ndk_helper::Vec3(CAM_X, CAM_Y, CAM_Z),
                                       ndk_helper::Vec3(0.f, 0.f, 0.f),
                                       ndk_helper::Vec3(0.f, 1.f, 0.f));

  if (camera_) {
    camera_->Update();
    mat_view_ = camera_->GetTransformMatrix() * mat_view_ *
                camera_->GetRotationMatrix();
  }
}

//--------------------------------------------------------------------------------
// Render
//--------------------------------------------------------------------------------
void MoreTeapotsRenderer::Render() {
  // Bind the VBO
  glBindBuffer(GL_ARRAY_BUFFER, vbo_);

  int32_t iStride = sizeof(TEAPOT_VERTEX);
  // Pass the vertex data
  glVertexAttribPointer(ATTRIB_VERTEX, 3, GL_FLOAT, GL_FALSE, iStride,
                        BUFFER_OFFSET(0));
  glEnableVertexAttribArray(ATTRIB_VERTEX);

  glVertexAttribPointer(ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, iStride,
                        BUFFER_OFFSET(3 * sizeof(GLfloat)));
  glEnableVertexAttribArray(ATTRIB_NORMAL);

  // Bind the IB
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);

  glUseProgram(shader_param_.program_);

  TEAPOT_MATERIALS material = {{1.0f, 1.0f, 1.0f, 10.f}, {0.1f, 0.1f, 0.1f}, };

  // Update uniforms
  //
  // using glUniform3fv here was troublesome..
  //
  glUniform4f(shader_param_.material_specular_, material.specular_color[0],
              material.specular_color[1], material.specular_color[2],
              material.specular_color[3]);
  glUniform3f(shader_param_.material_ambient_, material.ambient_color[0],
              material.ambient_color[1], material.ambient_color[2]);

  glUniform3f(shader_param_.light0_, 100.f, -200.f, -600.f);

  if (geometry_instancing_support_) {
    //
    // Geometry instancing, new feature in GLES3.0
    //

    // Update UBO
    glBindBuffer(GL_UNIFORM_BUFFER, ubo_);
    float* p = (float*)glMapBufferRange(
        GL_UNIFORM_BUFFER, 0, teapot_x_ * teapot_y_ * teapot_z_ *
                                  (ubo_matrix_stride_ * 2) * sizeof(float),
        GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT);
    float* mat_mvp = p;
    float* mat_mv = p + teapot_x_ * teapot_y_ * teapot_z_ * ubo_matrix_stride_;
    for (int32_t i = 0; i < teapot_x_ * teapot_y_ * teapot_z_; ++i) {
      // Rotation
      float x, y;
      vec_current_rotations_[i] += vec_rotations_[i];
      vec_current_rotations_[i].Value(x, y);
      ndk_helper::Mat4 mat_rotation =
          ndk_helper::Mat4::RotationX(x) * ndk_helper::Mat4::RotationY(y);

      // Feed Projection and Model View matrices to the shaders
      ndk_helper::Mat4 mat_v = mat_view_ * vec_mat_models_[i] * mat_rotation;
      ndk_helper::Mat4 mat_vp = mat_projection_ * mat_v;

      memcpy(mat_mvp, mat_vp.Ptr(), sizeof(mat_v));
      mat_mvp += ubo_matrix_stride_;

      memcpy(mat_mv, mat_v.Ptr(), sizeof(mat_v));
      mat_mv += ubo_matrix_stride_;
    }
    glUnmapBuffer(GL_UNIFORM_BUFFER);

    // Instanced rendering
    glDrawElementsInstanced(GL_TRIANGLES, num_indices_, GL_UNSIGNED_SHORT,
                            BUFFER_OFFSET(0),
                            teapot_x_ * teapot_y_ * teapot_z_);

  } else {
    // Regular rendering pass
    for (int32_t i = 0; i < teapot_x_ * teapot_y_ * teapot_z_; ++i) {
      // Set diffuse
      float x, y, z;
      vec_colors_[i].Value(x, y, z);
      glUniform4f(shader_param_.material_diffuse_, x, y, z, 1.f);

      // Rotation
      vec_current_rotations_[i] += vec_rotations_[i];
      vec_current_rotations_[i].Value(x, y);
      ndk_helper::Mat4 mat_rotation =
          ndk_helper::Mat4::RotationX(x) * ndk_helper::Mat4::RotationY(y);

      // Feed Projection and Model View matrices to the shaders
      ndk_helper::Mat4 mat_v = mat_view_ * vec_mat_models_[i] * mat_rotation;
      ndk_helper::Mat4 mat_vp = mat_projection_ * mat_v;
      glUniformMatrix4fv(shader_param_.matrix_projection_, 1, GL_FALSE,
                         mat_vp.Ptr());
      glUniformMatrix4fv(shader_param_.matrix_view_, 1, GL_FALSE, mat_v.Ptr());

      glDrawElements(GL_TRIANGLES, num_indices_, GL_UNSIGNED_SHORT,
                     BUFFER_OFFSET(0));
    }
  }

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

//--------------------------------------------------------------------------------
// LoadShaders
//--------------------------------------------------------------------------------
bool MoreTeapotsRenderer::LoadShaders(SHADER_PARAMS* params, const char* strVsh,
                                      const char* strFsh) {
  //
  // Shader load for GLES2
  // In GLES2.0, shader attribute locations need to be explicitly specified
  // before linking
  //
  GLuint program;
  GLuint vertShader, fragShader;

  // Create shader program
  program = glCreateProgram();
  LOGI("Created Shader %d", program);

  // Create and compile vertex shader
  if (!ndk_helper::shader::CompileShader(&vertShader, GL_VERTEX_SHADER,
                                         strVsh)) {
    LOGI("Failed to compile vertex shader");
    glDeleteProgram(program);
    return false;
  }

  // Create and compile fragment shader
  if (!ndk_helper::shader::CompileShader(&fragShader, GL_FRAGMENT_SHADER,
                                         strFsh)) {
    LOGI("Failed to compile fragment shader");
    glDeleteProgram(program);
    return false;
  }

  // Attach vertex shader to program
  glAttachShader(program, vertShader);

  // Attach fragment shader to program
  glAttachShader(program, fragShader);

  // Bind attribute locations
  // this needs to be done prior to linking
  glBindAttribLocation(program, ATTRIB_VERTEX, "myVertex");
  glBindAttribLocation(program, ATTRIB_NORMAL, "myNormal");

  // Link program
  if (!ndk_helper::shader::LinkProgram(program)) {
    LOGI("Failed to link program: %d", program);

    if (vertShader) {
      glDeleteShader(vertShader);
      vertShader = 0;
    }
    if (fragShader) {
      glDeleteShader(fragShader);
      fragShader = 0;
    }
    if (program) {
      glDeleteProgram(program);
    }
    return false;
  }

  // Get uniform locations
  params->matrix_projection_ = glGetUniformLocation(program, "uPMatrix");
  params->matrix_view_ = glGetUniformLocation(program, "uMVMatrix");

  params->light0_ = glGetUniformLocation(program, "vLight0");
  params->material_diffuse_ = glGetUniformLocation(program, "vMaterialDiffuse");
  params->material_ambient_ = glGetUniformLocation(program, "vMaterialAmbient");
  params->material_specular_ =
      glGetUniformLocation(program, "vMaterialSpecular");

  // Release vertex and fragment shaders
  if (vertShader) glDeleteShader(vertShader);
  if (fragShader) glDeleteShader(fragShader);

  params->program_ = program;
  return true;
}

bool MoreTeapotsRenderer::LoadShadersES3(
    SHADER_PARAMS* params, const char* strVsh, const char* strFsh,
    std::map<std::string, std::string>& shaderParams) {
  //
  // Shader load for GLES3
  // In GLES3.0, shader attribute index can be described in a shader code
  // directly with layout() attribute
  //
  GLuint program;
  GLuint vertShader, fragShader;

  // Create shader program
  program = glCreateProgram();
  LOGI("Created Shader %d", program);

  // Create and compile vertex shader
  if (!ndk_helper::shader::CompileShader(&vertShader, GL_VERTEX_SHADER, strVsh,
                                         shaderParams)) {
    LOGI("Failed to compile vertex shader");
    glDeleteProgram(program);
    return false;
  }

  // Create and compile fragment shader
  if (!ndk_helper::shader::CompileShader(&fragShader, GL_FRAGMENT_SHADER,
                                         strFsh, shaderParams)) {
    LOGI("Failed to compile fragment shader");
    glDeleteProgram(program);
    return false;
  }

  // Attach vertex shader to program
  glAttachShader(program, vertShader);

  // Attach fragment shader to program
  glAttachShader(program, fragShader);

  // Link program
  if (!ndk_helper::shader::LinkProgram(program)) {
    LOGI("Failed to link program: %d", program);

    if (vertShader) {
      glDeleteShader(vertShader);
      vertShader = 0;
    }
    if (fragShader) {
      glDeleteShader(fragShader);
      fragShader = 0;
    }
    if (program) {
      glDeleteProgram(program);
    }

    return false;
  }

  // Get uniform locations
  params->light0_ = glGetUniformLocation(program, "vLight0");
  params->material_ambient_ = glGetUniformLocation(program, "vMaterialAmbient");
  params->material_specular_ =
      glGetUniformLocation(program, "vMaterialSpecular");

  // Release vertex and fragment shaders
  if (vertShader) glDeleteShader(vertShader);
  if (fragShader) glDeleteShader(fragShader);

  params->program_ = program;
  return true;
}

//--------------------------------------------------------------------------------
// Bind
//--------------------------------------------------------------------------------
bool MoreTeapotsRenderer::Bind(ndk_helper::TapCamera* camera) {
  camera_ = camera;
  return true;
}

//--------------------------------------------------------------------------------
// Helper functions
//--------------------------------------------------------------------------------
std::string MoreTeapotsRenderer::ToString(const int32_t i) {
  char str[64];
  snprintf(str, sizeof(str), "%d", i);
  return std::string(str);
}
