/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "opengl-precomp.h"  // Precompiled header
//
#include <mrpt/opengl/CRenderizableShaderTexturedTriangles.h>
#include <mrpt/opengl/TLightParameters.h>
#include <mrpt/opengl/opengl_api.h>
#include <mrpt/serialization/CArchive.h>
#include <mrpt/system/CTimeLogger.h>

#include <iostream>
#include <memory>  // std::align
#include <thread>

using namespace mrpt;
using namespace mrpt::opengl;
using namespace mrpt::poses;
using namespace mrpt::math;
using namespace std;
using mrpt::img::CImage;

IMPLEMENTS_VIRTUAL_SERIALIZABLE(CRenderizableShaderTexturedTriangles, CRenderizable, mrpt::opengl)

void CRenderizableShaderTexturedTriangles::renderUpdateBuffers() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  // Generate vertices & colors into m_triangles
  const_cast<CRenderizableShaderTexturedTriangles&>(*this).onUpdateBuffers_TexturedTriangles();

  std::shared_lock<std::shared_mutex> readLock(m_trianglesMtx.data);

  const auto n = m_triangles.size();

  // Define OpenGL buffers:
  (*m_vbo)->createOnce();
  (*m_vbo)->bind();
  (*m_vbo)->allocate(m_triangles.data(), sizeof(m_triangles[0]) * n);

  // VAO: required to use glEnableVertexAttribArray()
  (*m_vao)->createOnce();

#endif
}

void CRenderizableShaderTexturedTriangles::render(const RenderContext& rc) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL

  // This will load and/or select our texture, only once:
  initializeTextures();
  ASSERT_(m_glTexture.initialized());

  std::shared_lock<std::shared_mutex> readLock(m_trianglesMtx.data);

  // Set the texture uniform:
  const Program& s = *rc.shader;
  const auto texUnit = m_glTexture.textureUnit();
  if (s.hasUniform("textureSampler") && (!rc.activeTextureUnit || *rc.activeTextureUnit != texUnit))
  {
    rc.activeTextureUnit = texUnit;  // buffer it
    // bound to GL_TEXTURE0 + "i":
    s.setInt("textureSampler", texUnit);
  }

  // Lights:
  if (m_enableLight && rc.lights && rc.shader->hasUniform("light_color") &&
      (!rc.activeLights || rc.activeLights.value() != rc.lights))
  {
    // buffered pointer, to prevent re-setting the opengl state with the
    // same values, a performance killer:
    rc.activeLights = rc.lights;

    const auto& l = rc.lights;

    s.setFloat3("light_color", l->color.R, l->color.G, l->color.B);
    s.setFloat3("light_direction", l->direction.x, l->direction.y, l->direction.z);
    s.setFloat("light_ambient", l->ambient);
    s.setFloat("light_diffuse", l->diffuse);
    if (rc.shader->hasUniform("light_specular")) s.setFloat("light_specular", l->specular);

    if (rc.shader->hasUniform("light_zmax"))
      s.setFloat("light_zmax", rc.state->getLastLightClipZFar());
    if (rc.shader->hasUniform("camera_far_plane"))
      s.setFloat("camera_far_plane", rc.state->getLastClipZFar());

    CHECK_OPENGL_ERROR_IN_DEBUG();
  }

  // Set the texture uniform:
  if (rc.shader->hasUniform("shadowMap"))
  {
    // bound to GL_TEXTURE0 + "i":
    s.setInt("shadowMap", SHADOW_MAP_TEXTURE_UNIT);

    // Shadow params:
    if (const auto& l = rc.lights; l)
    {
      rc.shader->setFloat("shadow_bias", l->shadow_bias);
      rc.shader->setFloat("shadow_bias_cam2frag", l->shadow_bias_cam2frag);
      rc.shader->setFloat("shadow_bias_normal", l->shadow_bias_normal);
    }
  }

  // Set up the vertex array:
  std::optional<GLuint> attr_position;
  if (rc.shader->hasAttribute("position"))
  {
    attr_position = rc.shader->attributeId("position");
    (*m_vao)->bind();
    glEnableVertexAttribArray(*attr_position);
    (*m_vbo)->bind();
    glVertexAttribPointer(
        *attr_position,            /* attribute */
        3,                         /* size */
        GL_FLOAT,                  /* type */
        GL_FALSE,                  /* normalized? */
        sizeof(TTriangle::Vertex), /* stride */
        BUFFER_OFFSET(offsetof(TTriangle::Vertex, xyzrgba.pt.x)));
    CHECK_OPENGL_ERROR_IN_DEBUG();
  }

  // Set up the normals array:
  std::optional<GLuint> attr_normals;
  if (rc.shader->hasAttribute("vertexNormal"))
  {
    attr_normals = rc.shader->attributeId("vertexNormal");
    glEnableVertexAttribArray(*attr_normals);
    (*m_vbo)->bind();
    glVertexAttribPointer(
        *attr_normals,             /* attribute */
        3,                         /* size */
        GL_FLOAT,                  /* type */
        GL_FALSE,                  /* normalized? */
        sizeof(TTriangle::Vertex), /* stride */
        BUFFER_OFFSET(offsetof(TTriangle::Vertex, normal.x)));
    CHECK_OPENGL_ERROR_IN_DEBUG();
  }

  // Set up the UV array:
  std::optional<GLuint> attr_uv;
  if (rc.shader->hasAttribute("vertexUV"))
  {
    attr_uv = rc.shader->attributeId("vertexUV");
    glEnableVertexAttribArray(*attr_uv);
    (*m_vbo)->bind();
    glVertexAttribPointer(
        *attr_uv,                  /* attribute */
        2,                         /* size */
        GL_FLOAT,                  /* type */
        GL_FALSE,                  /* normalized? */
        sizeof(TTriangle::Vertex), /* stride */
        BUFFER_OFFSET(offsetof(TTriangle::Vertex, uv.x)));
    CHECK_OPENGL_ERROR_IN_DEBUG();
  }

  if (m_cullface == TCullFace::NONE &&
      (!rc.activeCullFace || *rc.activeCullFace != TCullFace::NONE))
  {
    rc.activeCullFace = TCullFace::NONE;
    glDisable(GL_CULL_FACE);
  }
  if (m_cullface != TCullFace::NONE && (!rc.activeCullFace || *rc.activeCullFace != m_cullface))
  {
    glEnable(GL_CULL_FACE);
    glCullFace(m_cullface == TCullFace::FRONT ? GL_FRONT : GL_BACK);
    CHECK_OPENGL_ERROR_IN_DEBUG();
    rc.activeCullFace = m_cullface;
  }

  // Draw:
  glDrawArrays(GL_TRIANGLES, 0, 3 * m_triangles.size());
  CHECK_OPENGL_ERROR_IN_DEBUG();

  if (attr_position) glDisableVertexAttribArray(*attr_position);
  if (attr_uv) glDisableVertexAttribArray(*attr_uv);
  if (attr_normals) glDisableVertexAttribArray(*attr_normals);

#endif
}

void CRenderizableShaderTexturedTriangles::assignImage(const CImage& img, const CImage& imgAlpha)
{
  MRPT_START

  CRenderizable::notifyChange();

  m_glTexture.unloadTexture();

  // Make a copy:
  m_textureImage = img;
  m_textureImageAlpha = imgAlpha;
  m_textureImageAssigned = true;

  m_enableTransparency = true;

  MRPT_END
}

void CRenderizableShaderTexturedTriangles::assignImage(const CImage& img)
{
  MRPT_START

  CRenderizable::notifyChange();

  m_glTexture.unloadTexture();

  // Make a shallow copy:
  m_textureImage = img;
  m_textureImageAssigned = true;

  m_enableTransparency = false;

  MRPT_END
}

void CRenderizableShaderTexturedTriangles::assignImage(CImage&& img, CImage&& imgAlpha)
{
  MRPT_START

  CRenderizable::notifyChange();

  m_glTexture.unloadTexture();

  m_textureImage = std::move(img);
  m_textureImageAlpha = std::move(imgAlpha);
  m_textureImageAssigned = true;

  m_enableTransparency = true;

  MRPT_END
}

void CRenderizableShaderTexturedTriangles::assignImage(CImage&& img)
{
  MRPT_START

  CRenderizable::notifyChange();

  m_glTexture.unloadTexture();

  m_textureImage = std::move(img);
  m_textureImageAssigned = true;

  m_enableTransparency = false;

  MRPT_END
}

void CRenderizableShaderTexturedTriangles::initializeTextures() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL

  // Note: if we are rendering and the user assigned us no texture image,
  // let's create a dummy one with the uniform CRenderizable's color:
  if (!textureImageHasBeenAssigned() || m_textureImage.isEmpty())
  {
    mrpt::img::CImage im_rgb(4, 4, mrpt::img::CH_RGB), im_a(4, 4, mrpt::img::CH_GRAY);
    const auto col = getColor_u8();
    im_rgb.filledRectangle(0, 0, 3, 3, col);
    im_a.filledRectangle(0, 0, 3, 3, mrpt::img::TColor(col.A, col.A, col.A, col.A));
    const_cast<CRenderizableShaderTexturedTriangles*>(this)->assignImage(
        std::move(im_rgb), std::move(im_a));
  }

  if (m_glTexture.initialized())
  {
    m_glTexture.bindAsTexture2D();  // activate it:
    return;
  }

  // Reserve the new one --------------------------
  mrpt::opengl::Texture::Options opts;
  opts.enableTransparency = m_enableTransparency;
  opts.magnifyLinearFilter = m_textureInterpolate;
  opts.generateMipMaps = m_textureUseMipMaps;

  m_glTexture.assignImage2D(m_textureImage, m_textureImageAlpha, opts);

#endif
}

CRenderizableShaderTexturedTriangles::~CRenderizableShaderTexturedTriangles()
{
  try
  {
    m_glTexture.unloadTexture();
  }
  catch (const std::exception& e)
  {
    std::cerr << "[~CRenderizableShaderTexturedTriangles] Ignoring exception: "
              << mrpt::exception_to_str(e);
  }
}

void CRenderizableShaderTexturedTriangles::writeToStreamTexturedObject(
    mrpt::serialization::CArchive& out) const
{
  uint8_t ver = 3;

  out << ver;
  out << m_enableTransparency << m_textureInterpolate << m_textureUseMipMaps;
  out << m_textureImage;
  if (m_enableTransparency) out << m_textureImageAlpha;
  out << m_textureImageAssigned;
  out << m_enableLight << static_cast<uint8_t>(m_cullface);  // v2
}

void CRenderizableShaderTexturedTriangles::readFromStreamTexturedObject(
    mrpt::serialization::CArchive& in)
{
  uint8_t version;
  in >> version;

  switch (version)
  {
    case 0:
    case 1:
    case 2:
    case 3:
    {
      in >> m_enableTransparency >> m_textureInterpolate;
      if (version >= 3)
      {
        in >> m_textureUseMipMaps;
      }
      else
      {
        m_textureUseMipMaps = true;
      }
      in >> m_textureImage;
      if (m_enableTransparency)
      {
        in >> m_textureImageAlpha;
        assignImage(m_textureImage, m_textureImageAlpha);
      }
      else
      {
        assignImage(m_textureImage);
      }
      if (version >= 1)
        in >> m_textureImageAssigned;
      else
        m_textureImageAssigned = true;

      if (version >= 2)
      {
        in >> m_enableLight;
        m_cullface = static_cast<TCullFace>(in.ReadAs<uint8_t>());
      }
    }
    break;
    default:
      MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version);
  };

  CRenderizable::notifyChange();
}

const mrpt::math::TBoundingBoxf CRenderizableShaderTexturedTriangles::trianglesBoundingBox() const
{
  mrpt::math::TBoundingBoxf bb;

  std::shared_lock<std::shared_mutex> readLock(m_trianglesMtx.data);

  if (m_triangles.empty()) return bb;

  bb = mrpt::math::TBoundingBoxf::PlusMinusInfinity();

  for (const auto& t : m_triangles)
    for (int i = 0; i < 3; i++) bb.updateWithPoint(t.vertices[i].xyzrgba.pt);

  return bb;
}
