/* +------------------------------------------------------------------------+
   |                     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 <Eigen/Dense>  // First! to avoid conflicts with X.h
//
#include <mrpt/core/get_env.h>
#include <mrpt/math/TLine3D.h>
#include <mrpt/math/geometry.h>  // crossProduct3D()
#include <mrpt/opengl/CSetOfObjects.h>
#include <mrpt/opengl/CTexturedPlane.h>
#include <mrpt/opengl/DefaultShaders.h>
#include <mrpt/opengl/Scene.h>
#include <mrpt/opengl/Viewport.h>
#include <mrpt/opengl/opengl_api.h>
#include <mrpt/serialization/CArchive.h>
#include <mrpt/serialization/metaprogramming_serialization.h>
#include <mrpt/serialization/stl_serialization.h>
#include <mrpt/system/CTimeLogger.h>

using namespace mrpt;
using namespace mrpt::poses;
using namespace mrpt::opengl;
using namespace mrpt::math;
using namespace mrpt::serialization::metaprogramming;
using namespace std;

IMPLEMENTS_SERIALIZABLE(Viewport, CSerializable, mrpt::opengl)

const bool MRPT_OPENGL_DEBUG_SHOW_SHADOW_MAP =
    mrpt::get_env<bool>("MRPT_OPENGL_DEBUG_SHOW_SHADOW_MAP", false);

// #define OPENGLVIEWPORT_ENABLE_TIMEPROFILING

#if defined(OPENGLVIEWPORT_ENABLE_TIMEPROFILING)
mrpt::system::CTimeLogger glv_timlog;
#endif

/*--------------------------------------------------------------

      IMPLEMENTATION OF Viewport

  ---------------------------------------------------------------*/

/*--------------------------------------------------------------
          Constructor
  ---------------------------------------------------------------*/
Viewport::Viewport(Scene* parent, const string& name) : m_parent(parent), m_name(name) {}

Viewport::~Viewport() { clear(); }

void Viewport::setCloneView(const string& clonedViewport)
{
  clear();
  m_isCloned = true;
  m_clonedViewport = clonedViewport;
}

void Viewport::setViewportPosition(
    const double x, const double y, const double width, const double height)
{
  MRPT_START

  m_view_x = x;
  m_view_y = y;
  m_view_width = width;
  m_view_height = height;

  MRPT_END
}

/*--------------------------------------------------------------
          getViewportPosition
  ---------------------------------------------------------------*/
void Viewport::getViewportPosition(double& x, double& y, double& width, double& height)
{
  x = m_view_x;
  y = m_view_y;
  width = m_view_width;
  height = m_view_height;
}

/*--------------------------------------------------------------
          clear
  ---------------------------------------------------------------*/
void Viewport::clear() { m_objects.clear(); }
/*--------------------------------------------------------------
          insert
  ---------------------------------------------------------------*/
void Viewport::insert(const CRenderizable::Ptr& newObject) { m_objects.push_back(newObject); }

// Maps [0,1] to [0,Len], wrap negative numbers, etc.
namespace
{
int sizeFromRatio(const int startCoord, const double dSize, const int iLength)
{
  if (dSize > 1)  // >1 -> absolute pixels:
    return static_cast<int>(dSize);
  else if (dSize < 0)
  {  // Negative numbers: Specify the right side coordinates
     // instead of
    // the width:
    if (dSize >= -1)
      return static_cast<int>(-iLength * dSize - startCoord + 1);
    else
      return static_cast<int>(iLength + dSize - startCoord + 1);
  }
  // Otherwise: a fraction
  return static_cast<int>(iLength * dSize);
}
int startFromRatio(const double frac, const int dSize)
{
  const bool doWrap = (frac < 0);
  const auto fracAbs = std::abs(frac);

  const int L = fracAbs > 1 ? static_cast<int>(fracAbs) : static_cast<int>(dSize * fracAbs);

  int ret = doWrap ? dSize - L : L;

  return ret;
}
}  // namespace

// "Image mode" rendering:
void Viewport::renderImageMode() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
#if defined(OPENGLVIEWPORT_ENABLE_TIMEPROFILING)
  mrpt::system::CTimeLoggerEntry tle(glv_timlog, "Viewport::render imageview");
#endif

  // Do we have an actual image to render?
  if (!m_imageViewPlane || m_imageViewPlane->getTextureImage().isEmpty()) return;

  auto _ = m_threadedData.get().state;  // make a copy

  glDisable(GL_DEPTH_TEST);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  // Adjust the aspect ratio:
  const auto img_w = m_imageViewPlane->getTextureImage().getWidth();
  const auto img_h = m_imageViewPlane->getTextureImage().getHeight();
  const double img_ratio = double(img_w) / img_h;
  const double vw_ratio = double(_.viewport_width) / _.viewport_height;
  const double ratio = vw_ratio / img_ratio;

  _.matricesSetIdentity();

  if (ratio > 1)
    _.p_matrix(1, 1) *= ratio;
  else if (ratio > 0)
    _.p_matrix(0, 0) /= ratio;

  auto &p00 = _.p_matrix(0, 0), &p11 = _.p_matrix(1, 1);
  if (p00 > 0 && p11 > 0)
  {
    const double s = (p00 > p11) ? p00 : p11;
    p00 /= s;
    p11 /= s;
  }

  _.pmv_matrix.asEigen() = _.p_matrix.asEigen() * _.v_matrix.asEigen() * _.m_matrix.asEigen();

  // Pass 1: Process all objects (recursively for sets of objects):
  CListOpenGLObjects lst;
  lst.push_back(m_imageViewPlane);
  mrpt::opengl::RenderQueue rq;
  mrpt::opengl::enqueueForRendering(lst, _, rq, true /*skip cull*/, false /* isShadowMap*/);

  // pass 2: render, sorted by shader program:
  mrpt::opengl::processRenderQueue(rq, m_threadedData.get().shaders, m_light);

#endif
}

void Viewport::unloadShaders() { m_threadedData.get().shaders.clear(); }

void Viewport::loadDefaultShaders() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  MRPT_START

  using ID = DefaultShaderID;  // save space below

  std::vector<shader_id_t> lstShaderIDs = {
      ID::POINTS,
      ID::WIREFRAME,
      ID::TRIANGLES_NO_LIGHT,
      ID::TRIANGLES_LIGHT,
      ID::TEXTURED_TRIANGLES_NO_LIGHT,
      ID::TEXTURED_TRIANGLES_LIGHT,
      ID::TEXT,
      ID::TRIANGLES_SHADOW_1ST,
      ID::TRIANGLES_SHADOW_2ND,
      ID::TEXTURED_TRIANGLES_SHADOW_1ST,
      ID::TEXTURED_TRIANGLES_SHADOW_2ND,
      ID::SKYBOX,
      ID::DEBUG_TEXTURE_TO_SCREEN};

  // -----------------------------------------------------------
  // Load general list of shaders
  // (and use them for the "no shadows" case):
  // -----------------------------------------------------------
  auto& shaders = m_threadedData.get().shaders;
  for (const auto& id : lstShaderIDs)
  {
    shaders[id] = mrpt::opengl::LoadDefaultShader(id);

    ASSERT_(shaders[id]);
    ASSERT_(!shaders[id]->empty());
  }

  // -----------------------------------------------------------
  // Shaders for rendering with shadows: 1st and 2nd passes use
  // different shader programs. Apply a replacement table.
  // -----------------------------------------------------------
  // clang-format off
	const std::map<shader_id_t, std::pair<shader_id_t, shader_id_t>>
	  replacements = {
		{ID::TRIANGLES_LIGHT,             {ID::TRIANGLES_SHADOW_1ST, ID::TRIANGLES_SHADOW_2ND}},
		{ID::TEXTURED_TRIANGLES_LIGHT,    {ID::TEXTURED_TRIANGLES_SHADOW_1ST, ID::TEXTURED_TRIANGLES_SHADOW_2ND}},
		{ID::POINTS,                      {ID::NONE, ID::POINTS}},
		{ID::WIREFRAME,                   {ID::NONE, ID::WIREFRAME}},
		{ID::TEXT,                        {ID::NONE, ID::TEXT}},
		{ID::TRIANGLES_NO_LIGHT,          {ID::TRIANGLES_SHADOW_1ST, ID::TRIANGLES_NO_LIGHT}},
		{ID::TEXTURED_TRIANGLES_NO_LIGHT, {ID::TRIANGLES_SHADOW_1ST, ID::TEXTURED_TRIANGLES_NO_LIGHT}},
		{ID::SKYBOX,                      {ID::NONE, ID::SKYBOX}}
		};
	//clang-format on

	// 1st pass. Replace shaders: we only need depth in the 1st stage.
	// 2nd pass. Replace shaders: we need to account for the shadow map.
	auto& shadowShaders1st = m_threadedData.get().shadersShadow1st;
	shadowShaders1st = shaders;
	auto& shadowShaders2nd = m_threadedData.get().shadersShadow2nd;
	shadowShaders2nd = shaders;

	for (const auto& kv : replacements)
	{
		if (kv.second.first!=ID::NONE)
			shadowShaders1st[kv.first] = shaders.at(kv.second.first);

		if (kv.second.second!=ID::NONE)
			shadowShaders2nd[kv.first] = shaders.at(kv.second.second);
	}

	MRPT_END
#endif
}

// for debugging only if MRPT_OPENGL_DEBUG_SHOW_SHADOW_MAP
#if (MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL)
// From: https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping

// debugRenderQuad() renders a 1x1 XY quad in NDC
// -----------------------------------------
static unsigned int quadVAO = 0;
static unsigned int quadVBO;
namespace
{
 void debugRenderQuad()
{
	if (quadVAO == 0)
	{
		// clang-format off
        float quadVertices[] = {
            // positions        // texture Coords
            -1.0f, -0.5f, 0.0f, 0.0f, 1.0f,
            -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
            -0.5f, -0.5f, 0.0f, 1.0f, 1.0f,
            -0.5f, -1.0f, 0.0f, 1.0f, 0.0f,
        };
    // clang-format on

    // setup plane VAO
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
  }
  glBindVertexArray(quadVAO);
  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  glBindVertexArray(0);
}
}  // namespace
#endif

/** Render a normal scene with 3D objects */
void Viewport::renderNormalSceneMode(const CCamera& useThisCamera, bool is1stShadowMapPass) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  MRPT_START

#ifdef MRPT_OPENGL_PROFILER
  mrpt::system::CTimeLoggerEntry tle(opengl_profiler(), "Viewport.renderNormalSceneMode");
#endif

  // Prepare camera (projection matrix):
  // Needed for both, regular, and 1st/2nd shadow passes:
  // Note: if we were in the 1st stage of shadow rendering,
  // the projection matrix is actually the light projection matrix,
  // but that's handled by the shaders themselves, not here.
  updateMatricesFromCamera(useThisCamera);

  const auto& _ = m_threadedData.get().state;

  // Get objects to render:
  const CListOpenGLObjects* objectsToRender = nullptr;

  if (m_isCloned)
  {  // Clone: render someone's else objects.
    ASSERT_(m_parent.get() != nullptr);

    const auto view = m_parent->getViewport(m_clonedViewport);
    if (!view)
      THROW_EXCEPTION_FMT(
          "Cloned viewport '%s' not found in parent Scene", m_clonedViewport.c_str());

    objectsToRender = &view->m_objects;
  }
  else
  {  // Normal case: render our own objects:
    objectsToRender = &m_objects;
  }

  // Optional pre-Render user code:
  if (hasSubscribers())
  {
    mrptEventGLPreRender ev(this);
    this->publishEvent(ev);
  }

  // Global OpenGL settings:
  // ---------------------------------
#if !defined(__EMSCRIPTEN__)
  glHint(GL_POLYGON_SMOOTH_HINT, m_OpenGL_enablePolygonNicest ? GL_NICEST : GL_FASTEST);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  glEnable(GL_LINE_SMOOTH);
  CHECK_OPENGL_ERROR_IN_DEBUG();
#endif

  // Regular depth model:
  // 0: far, 1: near
  // -------------------------------
  glEnable(GL_DEPTH_TEST);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  glDepthFunc(GL_LESS);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

// Enable point sizes>1
#if !defined(__EMSCRIPTEN__) && defined(GL_PROGRAM_POINT_SIZE)  // OSX undef?
  glEnable(GL_PROGRAM_POINT_SIZE);
  CHECK_OPENGL_ERROR_IN_DEBUG();
#endif

  // Enables Multisampling
  glEnable(GL_MULTISAMPLE);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  // Pass 1: Process all objects (recursively for sets of objects):
  mrpt::opengl::RenderQueue rq;
  mrpt::opengl::RenderQueueStats rqStats;
  mrpt::opengl::enqueueForRendering(
      *objectsToRender, _, rq, false /*skip cull checks*/,
      is1stShadowMapPass /* is shadow map pass */, &rqStats);

  // pass 2: render, sorted by shader program:
  auto& shaders = m_shadowsEnabled ? (is1stShadowMapPass ? m_threadedData.get().shadersShadow1st
                                                         : m_threadedData.get().shadersShadow2nd)
                                   : m_threadedData.get().shaders;

  // shadow map, if enabled:
  std::optional<unsigned int> depthMapTextureId;
  if (m_shadowsEnabled && m_ShadowMapFBO.initialized())
    depthMapTextureId = m_ShadowMapFBO.depthMapTextureId();

  // go render:
  mrpt::opengl::processRenderQueue(rq, shaders, m_light, depthMapTextureId);

#ifdef MRPT_OPENGL_PROFILER
  opengl_profiler().registerUserMeasure("render.totalObjects", rqStats.numObjTotal);
  opengl_profiler().registerUserMeasure("render.numObjRendered", rqStats.numObjRendered);
  opengl_profiler().registerUserMeasure(
      "render.ObjRenderedPercent",
      rqStats.numObjTotal != 0 ? 100.0 * rqStats.numObjRendered / rqStats.numObjTotal : 0);
#endif

  MRPT_END

#endif
}

void Viewport::renderViewportBorder() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  MRPT_START
  if (m_borderWidth < 1) return;

  auto _ = m_threadedData.get().state;

  _.matricesSetIdentity();

  glDisable(GL_DEPTH_TEST);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  //
  if (!m_borderLines)
  {
    m_borderLines = mrpt::opengl::CSetOfLines::Create();
    m_borderLines->appendLine(-1, -1, 0, -1, 1, 0);
    m_borderLines->appendLine(-1, 1, 0, 1, 1, 0);
    m_borderLines->appendLine(1, 1, 0, 1, -1, 0);
    m_borderLines->appendLine(1, -1, 0, -1, -1, 0);
  }
  m_borderLines->setLineWidth(m_borderWidth);
  m_borderLines->setColor_u8(m_borderColor);

  CListOpenGLObjects lst;
  lst.push_back(m_borderLines);

  // Pass 1: Process all objects (recursively for sets of objects):
  mrpt::opengl::RenderQueue rq;
  mrpt::opengl::enqueueForRendering(lst, _, rq, true /*skip cull*/, false /* isShadowMap*/);

  // pass 2: render, sorted by shader program:
  mrpt::opengl::processRenderQueue(rq, m_threadedData.get().shaders, m_light);
  MRPT_END
#endif
}

void Viewport::renderTextMessages() const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  MRPT_START

  // Ensure GL objects are up-to-date:
  m_2D_texts.regenerateGLobjects();

  // Prepare shaders upon first invocation:
  if (m_threadedData.get().shaders.empty()) loadDefaultShaders();

  // Prepare camera (projection matrix):
  TRenderMatrices _ = m_threadedData.get().state;  // make a copy

  // Compute the projection matrix (p_matrix):
  // was: glLoadIdentity(); glOrtho(0, w, 0, h, -1, 1);
  const auto w = _.viewport_width, h = _.viewport_height;
  _.is_projective = false;

  // Reset model-view 4x4 matrix to the identity transformation:
  _.matricesSetIdentity();
  //_.computeOrthoProjectionMatrix(0, w, 0, h, m_clip_min, m_clip_max);

  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  // Text messages should always be visible on top the rest:
  glDisable(GL_DEPTH_TEST);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  // Collect all 2D text objects, and update their properties:
  CListOpenGLObjects objs;

  std::shared_lock<std::shared_mutex> lckRead2DTexts(m_2D_texts.mtx.data);
  for (auto& kv : m_2D_texts.messages)
  {
    const DataPerText& label = kv.second;

    // If (x,y) \in [0,1[, it's interpreted as a ratio, otherwise, as an
    // actual coordinate in pixels
    float x = label.x >= 1 ? label.x : (label.x < 0 ? w + label.x : label.x * w);
    float y = label.y >= 1 ? label.y : (label.y < 0 ? h + label.y : label.y * h);

    if (CText::Ptr& o = label.gl_text_shadow; o)
    {
      o->setFont(label.vfont_name, label.vfont_scale * 2);
      o->setString(label.text);
      o->setColor(label.shadow_color);
      // Change coordinates: mrpt text (0,0)-(1,1) to OpenGL
      // (-1,-1)-(+1,+1):
      o->setLocation(-1.0f + 2 * (x + 1) / w, -1.0f + 2 * (y - 1) / h, 0.1);
      objs.push_back(o);
    }
    if (CText::Ptr& o = label.gl_text; o)
    {
      o->setFont(label.vfont_name, label.vfont_scale * 2);
      o->setString(label.text);
      o->setColor(label.color);
      // Change coordinates: mrpt text (0,0)-(1,1) to OpenGL
      // (-1,-1)-(+1,+1):
      o->setLocation(-1.0f + 2 * x / w, -1.0f + 2 * y / h, 0);
      objs.push_back(o);
    }
  }
  lckRead2DTexts.unlock();

  // Pass 1: Process all objects (recursively for sets of objects):
  mrpt::opengl::RenderQueue rq;
  mrpt::opengl::enqueueForRendering(objs, _, rq, true /*skip cull*/, false /* isShadowMap*/);

  // pass 2: render, sorted by shader program:
  mrpt::opengl::processRenderQueue(rq, m_threadedData.get().shaders, m_light);
  MRPT_END
#endif
}

void Viewport::render(
    [[maybe_unused]] const int render_width,
    [[maybe_unused]] const int render_height,
    [[maybe_unused]] const int render_offset_x,
    [[maybe_unused]] const int render_offset_y,
    [[maybe_unused]] const CCamera* forceThisCamera) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  MRPT_START

#ifdef MRPT_OPENGL_PROFILER
  mrpt::system::CTimeLoggerEntry tle(opengl_profiler(), "Viewport.render");
#endif

  if (!m_isViewportVisible) return;

  // Prepare shaders upon first invocation:
  if (m_threadedData.get().shaders.empty()) loadDefaultShaders();

  auto& _ = m_threadedData.get().state;

  auto* activeCameraPtr = internalResolveActiveCamera(forceThisCamera);

  // make a copy so the camera remains const over the rendering:
  const CCamera activeCamera = *activeCameraPtr;

  // Get former viewport
  GLint oldViewport[4];
  glGetIntegerv(GL_VIEWPORT, oldViewport);

  const GLint vx = render_offset_x + startFromRatio(m_view_x, render_width);
  const GLint vy = render_offset_y + startFromRatio(m_view_y, render_height);
  const GLint vw = sizeFromRatio(vx, m_view_width, render_width);
  const GLint vh = sizeFromRatio(vy, m_view_height, render_height);

  // Let the matrix generation stuff what's the viewport size (for real
  // rendering, not shadow 1st pass shadow map)
  _.viewport_width = vw;
  _.viewport_height = vh;

  // If we are rendering with shadows, run a camera-view depth map first
  // (Shadows 1st pass)
  // ----------------------------------------------------------------------
  if (m_shadowsEnabled && !isImageViewMode())
  {
    if (!m_ShadowMapFBO.initialized())
    {
      m_ShadowMapFBO.createDepthMap(m_ShadowMapSizeX, m_ShadowMapSizeY);
    }

    glEnable(GL_DEPTH_TEST);

    // Render scene to depth map, as seen from the light point of view:
    glViewport(0, 0, m_ShadowMapSizeX, m_ShadowMapSizeY);

    const auto oldFBs = m_ShadowMapFBO.bind();
    CHECK_OPENGL_ERROR_IN_DEBUG();

    glClear(GL_DEPTH_BUFFER_BIT);

    renderNormalSceneMode(activeCamera, true /* is1stShadowMapPass */);

    m_ShadowMapFBO.Bind(oldFBs);

    // The 2nd pass is done inside renderNormalSceneMode()

    // TODO: Any way to refactor the whole pipeline to avoid recursive
    // rendering twice?
  }

  // Change viewport:
  // -------------------------------------------
  glViewport(vx, vy, vw, vh);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  // Clear depth&/color buffers:
  // -------------------------------------------

  glScissor(vx, vy, vw, vh);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  glEnable(GL_SCISSOR_TEST);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  if (!m_isTransparent)
  {  // Clear color & depth buffers:
    // Save:
    GLfloat prevBkCol[4];
    glGetFloatv(GL_COLOR_CLEAR_VALUE, prevBkCol);

    glClearColor(
        m_background_color.R, m_background_color.G, m_background_color.B, m_background_color.A);
    CHECK_OPENGL_ERROR_IN_DEBUG();

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    CHECK_OPENGL_ERROR_IN_DEBUG();

    glClearColor(prevBkCol[0], prevBkCol[1], prevBkCol[2], prevBkCol[3]);
  }
  else
  {  // Clear depth buffer only:
    glClear(GL_DEPTH_BUFFER_BIT);
    CHECK_OPENGL_ERROR_IN_DEBUG();
  }
  glDisable(GL_SCISSOR_TEST);
  CHECK_OPENGL_ERROR_IN_DEBUG();

  // If we are in "image mode", rendering is much simpler: just set
  //  ortho projection and render the image quad:
  if (isImageViewMode())
    renderImageMode();
  else
    renderNormalSceneMode(activeCamera);

  // Draw text messages, if any:
  renderTextMessages();

  // Finally, draw the border:
  renderViewportBorder();

  // Optional post-Render user code:
  if (hasSubscribers())
  {
    mrptEventGLPostRender ev(this);
    this->publishEvent(ev);
  }

  // Debug:
  if (MRPT_OPENGL_DEBUG_SHOW_SHADOW_MAP && m_shadowsEnabled)
  {
    // render Depth map to quad for visual debugging
    auto& sh = shaders().at(DefaultShaderID::DEBUG_TEXTURE_TO_SCREEN);
    sh->use();
    sh->setInt("textureId", 0 /* Use GL_TEXTURE0 */);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_ShadowMapFBO.depthMapTextureId());
    debugRenderQuad();
  }

  // Restore viewport:
  glViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);

  MRPT_END
#else
  THROW_EXCEPTION(
      "MRPT has been compiled with MRPT_HAS_OPENGL_GLUT=0! OpenGL "
      "functions are not implemented");
#endif
}

uint8_t Viewport::serializeGetVersion() const { return 10; }
void Viewport::serializeTo(mrpt::serialization::CArchive& out) const
{
  // Save data:
  out << m_camera << m_isCloned << m_isClonedCamera << m_clonedViewport << m_name << m_isTransparent
      << m_borderWidth << m_view_x << m_view_y << m_view_width << m_view_height;

  // Added in v1:
  out << m_background_color.R << m_background_color.G << m_background_color.B
      << m_background_color.A;

  // Save objects:
  uint32_t n;
  n = (uint32_t)m_objects.size();
  out << n;
  for (const auto& m_object : m_objects) out << *m_object;

  // Added in v2: Global OpenGL settings:
  out << m_OpenGL_enablePolygonNicest;

  // Added in v3: Lights
  out << m_light;

  // Added in v4: text messages:
  std::shared_lock<std::shared_mutex> lckRead2DTexts(m_2D_texts.mtx.data);

  out.WriteAs<uint32_t>(m_2D_texts.messages.size());
  for (auto& kv : m_2D_texts.messages)
  {
    out << kv.first;  // id
    out << kv.second.x << kv.second.y << kv.second.text;

    const auto& fp = kv.second;

    out << fp.vfont_name << fp.vfont_scale << fp.color << fp.draw_shadow << fp.shadow_color
        << fp.vfont_spacing << fp.vfont_kerning;
    out.WriteAs<uint8_t>(static_cast<uint8_t>(fp.vfont_style));
  }
  lckRead2DTexts.unlock();

  // Added in v5: image mode
  out.WriteAs<bool>(m_imageViewPlane);
  if (m_imageViewPlane) out << *m_imageViewPlane;

  // Added in v6:
  out << m_clonedCameraViewport;

  // Added in v8:
  out << m_shadowsEnabled << m_ShadowMapSizeX << m_ShadowMapSizeY;

  // Added in v9:
  out << m_clip_max << m_clip_min << m_lightShadowClipMin << m_lightShadowClipMax;

  // v10:
  out << m_isViewportVisible;
}

void Viewport::serializeFrom(mrpt::serialization::CArchive& in, uint8_t version)
{
  switch (version)
  {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
    case 10:
    {
      // Load data:
      in >> m_camera >> m_isCloned >> m_isClonedCamera >> m_clonedViewport >> m_name >>
          m_isTransparent >> m_borderWidth >> m_view_x >> m_view_y >> m_view_width >> m_view_height;

      // in v1:
      if (version >= 1)
      {
        if (version < 7)
        {
          // field removed in v7:
          bool was_m_custom_backgb_color;
          in >> was_m_custom_backgb_color;
        }

        in >> m_background_color.R >> m_background_color.G >> m_background_color.B >>
            m_background_color.A;
      }

      // Load objects:
      uint32_t n;
      in >> n;
      clear();
      m_objects.resize(n);

      for_each(m_objects.begin(), m_objects.end(), ObjectReadFromStream(&in));

      // Added in v2: Global OpenGL settings:
      if (version >= 2)
      {
        in >> m_OpenGL_enablePolygonNicest;
      }
      else
      {
        // Defaults
      }

      // Added in v3: Lights
      if (version >= 3)
        in >> m_light;
      else
      {
        // Default:
        m_light = TLightParameters();
      }

      // v4: text:
      m_2D_texts.messages.clear();
      uint32_t nTexts = 0;
      if (version >= 4) nTexts = in.ReadAs<uint32_t>();

      for (uint32_t i = 0; i < nTexts; i++)
      {
        const auto id = in.ReadAs<uint32_t>();
        double x, y;
        std::string text;
        in >> x >> y >> text;

        TFontParams fp;

        in >> fp.vfont_name >> fp.vfont_scale >> fp.color >> fp.draw_shadow >> fp.shadow_color >>
            fp.vfont_spacing >> fp.vfont_kerning;
        fp.vfont_style = static_cast<TOpenGLFontStyle>(in.ReadAs<uint8_t>());

        this->addTextMessage(x, y, text, id, fp);
      }

      // Added in v5: image mode
      if (in.ReadAs<bool>())
      {
        in >> m_imageViewPlane;
      }
      else
      {
        m_imageViewPlane.reset();
      }

      if (version >= 6)
        in >> m_clonedCameraViewport;
      else
        m_clonedCameraViewport.clear();

      if (version >= 8)
      {
        in >> m_shadowsEnabled >> m_ShadowMapSizeX >> m_ShadowMapSizeY;
      }
      else
      {
        m_shadowsEnabled = false;
      }

      if (version >= 9)
      {
        in >> m_clip_max >> m_clip_min >> m_lightShadowClipMin >> m_lightShadowClipMax;
      }

      if (version >= 10)
      {
        in >> m_isViewportVisible;
      }
    }
    break;
    default:
      MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version);
  };
}

/*---------------------------------------------------------------
              getByName
  ---------------------------------------------------------------*/
CRenderizable::Ptr Viewport::getByName(const string& str)
{
  for (auto& m_object : m_objects)
  {
    if (m_object->getName() == str)
      return m_object;
    else if (m_object->GetRuntimeClass() == CLASS_ID_NAMESPACE(CSetOfObjects, opengl))
    {
      if (CRenderizable::Ptr ret =
              std::dynamic_pointer_cast<CSetOfObjects>(m_object)->getByName(str);
          ret)
        return ret;
    }
  }
  return {};
}

void Viewport::initializeTextures()
{
  for (auto& obj : m_objects) obj->initializeTextures();
}

void Viewport::dumpListOfObjects(std::vector<std::string>& lst) const
{
  for (auto& obj : m_objects)
  {
    // Single obj:
    string s(obj->GetRuntimeClass()->className);
    if (!obj->getName().empty()) s += string(" (") + obj->getName() + string(")");
    lst.emplace_back(s);

    if (obj->GetRuntimeClass() == CLASS_ID_NAMESPACE(CSetOfObjects, mrpt::opengl))
    {
      std::vector<std::string> auxLst;

      dynamic_cast<CSetOfObjects*>(obj.get())->dumpListOfObjects(auxLst);

      for (const auto& i : auxLst) lst.emplace_back(string(" ") + i);
    }
  }
}

mrpt::containers::yaml Viewport::asYAML() const
{
  mrpt::containers::yaml d = mrpt::containers::yaml::Sequence();

  d.asSequence().resize(m_objects.size());

  for (uint32_t i = 0; i < m_objects.size(); i++)
  {
    const auto obj = m_objects.at(i);
    mrpt::containers::yaml de = mrpt::containers::yaml::Map();

    // class-specific properties:
    obj->toYAMLMap(de);

    de["index"] = i;  // type for "i" must be a stdint type
    if (!obj)
    {
      de["class"] = "nullptr";
      continue;
    }
    de["class"] = obj->GetRuntimeClass()->className;

    // Single obj:
    if (obj->GetRuntimeClass() == CLASS_ID_NAMESPACE(CSetOfObjects, mrpt::opengl))
    {
      de["obj_children"] = dynamic_cast<CSetOfObjects*>(obj.get())->asYAML();
    }
    d.asSequence().at(i) = std::move(de);
  }
  return d;
}

/*--------------------------------------------------------------
          removeObject
  ---------------------------------------------------------------*/
void Viewport::removeObject(const CRenderizable::Ptr& obj)
{
  for (auto it = m_objects.begin(); it != m_objects.end(); ++it)
    if (*it == obj)
    {
      m_objects.erase(it);
      return;
    }
    else if ((*it)->GetRuntimeClass() == CLASS_ID_NAMESPACE(CSetOfObjects, opengl))
      dynamic_cast<CSetOfObjects*>(it->get())->removeObject(obj);
}

void Viewport::setViewportClipDistances(const float clip_min, const float clip_max)
{
  ASSERT_GT_(clip_max, clip_min);

  m_clip_min = clip_min;
  m_clip_max = clip_max;
}

void Viewport::getViewportClipDistances(float& clip_min, float& clip_max) const
{
  clip_min = m_clip_min;
  clip_max = m_clip_max;
}

void Viewport::setLightShadowClipDistances(const float clip_min, const float clip_max)
{
  ASSERT_GT_(clip_max, clip_min);

  m_lightShadowClipMin = clip_min;
  m_lightShadowClipMax = clip_max;
}

void Viewport::getLightShadowClipDistances(float& clip_min, float& clip_max) const
{
  clip_min = m_lightShadowClipMin;
  clip_max = m_lightShadowClipMax;
}

/*--------------------------------------------------------------
          get3DRayForPixelCoord
  ---------------------------------------------------------------*/
void Viewport::get3DRayForPixelCoord(
    const double x_coord,
    const double y_coord,
    mrpt::math::TLine3D& out_ray,
    mrpt::poses::CPose3D* out_cameraPose) const
{
  auto& _ = m_threadedData.get().state;

  if (!_.initialized)
  {
    updateMatricesFromCamera(*internalResolveActiveCamera());
    ASSERT_(_.initialized);
  }

  ASSERTDEB_(_.viewport_height > 0 && _.viewport_width > 0);

  const double ASPECT = _.viewport_width / double(_.viewport_height);

  // unitary vector between (eye) -> (pointing):
  const TPoint3D pointing_dir = {
      -cos(_.azimuth) * cos(_.elev), -sin(_.azimuth) * cos(_.elev), -sin(_.elev)};

  // The camera X vector (in 3D) can be computed from the camera azimuth
  // angle:
  const TPoint3D cam_x_3d = {-sin(_.azimuth), cos(_.azimuth), 0};

  // The camera real UP vector (in 3D) is the cross product:
  //     X3d x pointing_dir:
  const auto cam_up_3d = mrpt::math::crossProduct3D(cam_x_3d, pointing_dir);

  if (!_.is_projective)
  {
    // Ortho projection:
    // -------------------------------
    double Ax = _.eyeDistance * 0.25;
    double Ay = Ax;

    if (ASPECT > 1)
      Ax *= ASPECT;
    else
    {
      if (ASPECT != 0) Ay /= ASPECT;
    }

    const double point_lx = (-0.5 + x_coord / _.viewport_width) * 2 * Ax;
    const double point_ly = -(-0.5 + y_coord / _.viewport_height) * 2 * Ay;

    const TPoint3D ray_origin(
        _.eye.x + point_lx * cam_x_3d.x + point_ly * cam_up_3d.x,
        _.eye.y + point_lx * cam_x_3d.y + point_ly * cam_up_3d.y,
        _.eye.z + point_lx * cam_x_3d.z + point_ly * cam_up_3d.z);

    out_ray.pBase = ray_origin;
    out_ray.director[0] = pointing_dir.x;
    out_ray.director[1] = pointing_dir.y;
    out_ray.director[2] = pointing_dir.z;
  }
  else
  {
    // Perspective camera
    // -------------------------------

    // JL: This can be derived from:
    // http://www.opengl.org/sdk/docs/man/xhtml/gluPerspective.xml
    //  where one arrives to:
    //    tan(FOVx/2) = ASPECT_RATIO * tan(FOVy/2)
    //
    const double FOVy = DEG2RAD(_.FOV);
    const double FOVx = 2.0 * atan(ASPECT * tan(FOVy * 0.5));

    const auto vw = _.viewport_width;
    const auto vh = _.viewport_height;
    const double len_horz = 2.0 * (-0.5 + x_coord / vw) * tan(0.5 * FOVx);
    const double len_vert = -2.0 * (-0.5 + y_coord / vh) * tan(0.5 * FOVy);
    // Point in camera local reference frame
    const auto l = mrpt::math::TPoint3D(len_horz, len_vert, 1.0);

    const mrpt::math::TPoint3D ray_director(
        l.x * cam_x_3d.x + l.y * cam_up_3d.x + l.z * pointing_dir.x,
        l.x * cam_x_3d.y + l.y * cam_up_3d.y + l.z * pointing_dir.y,
        l.x * cam_x_3d.z + l.y * cam_up_3d.z + l.z * pointing_dir.z);

    // Set out ray:
    out_ray.pBase = _.eye;
    out_ray.director[0] = ray_director.x;
    out_ray.director[1] = ray_director.y;
    out_ray.director[2] = ray_director.z;

  }  // end projective

  // Camera pose:
  if (out_cameraPose)
  {
    mrpt::math::CMatrixDouble44 M(UNINITIALIZED_MATRIX);
    M(0, 0) = cam_x_3d.x;
    M(1, 0) = cam_x_3d.y;
    M(2, 0) = cam_x_3d.z;
    M(3, 0) = 0;

    M(0, 1) = cam_up_3d.x;
    M(1, 1) = cam_up_3d.y;
    M(2, 1) = cam_up_3d.z;
    M(3, 1) = 0;

    M(0, 2) = pointing_dir.x;
    M(1, 2) = pointing_dir.y;
    M(2, 2) = pointing_dir.z;
    M(3, 2) = 0;

    M(0, 3) = _.eye.x;
    M(1, 3) = _.eye.y;
    M(2, 3) = _.eye.z;
    M(3, 3) = 1;

    *out_cameraPose = CPose3D(M);
  }
}

void Viewport::setCurrentCameraFromPose(mrpt::poses::CPose3D& p)
{
  m_camera.set6DOFMode(true);
  m_camera.setPose(p);
}

void Viewport::getCurrentCameraPose(mrpt::poses::CPose3D& out_cameraPose) const
{
  mrpt::math::TLine3D dum;
  get3DRayForPixelCoord(0, 0, dum, &out_cameraPose);
}

/** Resets the viewport to a normal 3D viewport \sa setCloneView, setImageView
 */
void Viewport::setNormalMode()
{
  // If this was an image-mode viewport, remove the quad object to disable it.
  m_imageViewPlane.reset();

  m_isCloned = false;
  m_isClonedCamera = false;
}

void Viewport::setImageView(const mrpt::img::CImage& img, bool transparentBackground)
{
  internal_enableImageView(transparentBackground);
  m_imageViewPlane->assignImage(img);
}
void Viewport::setImageView(mrpt::img::CImage&& img, bool transparentBackground)
{
  internal_enableImageView(transparentBackground);
  m_imageViewPlane->assignImage(img);
}

void Viewport::internal_enableImageView(bool transparentBackground)
{
  // If this is the first time, we have to create the quad object:
  if (!m_imageViewPlane)
  {
    m_imageViewPlane = mrpt::opengl::CTexturedPlane::Create();
    // Flip vertically:
    m_imageViewPlane->setPlaneCorners(-1, 1, 1, -1);
  }
  setTransparent(transparentBackground);
}

/** Evaluates the bounding box of this object (including possible children) in
 * the coordinate frame of the object parent. */
auto Viewport::getBoundingBox() const -> mrpt::math::TBoundingBox
{
  mrpt::math::TBoundingBox bb;
  bool first = true;

  for (const auto& o : m_objects)
  {
    if (first)
    {
      bb = o->getBoundingBox();
      first = false;
    }
    else
      bb = bb.unionWith(o->getBoundingBox());
  }

  return bb;
}

void Viewport::setCloneCamera(bool enable)
{
  m_isClonedCamera = enable;
  if (!enable)
  {
    m_clonedCameraViewport.clear();
  }
  else
  {
    ASSERTMSG_(
        !m_clonedViewport.empty(),
        "Error: cannot setCloneCamera(true) on a viewport before calling "
        "setCloneView()");

    m_clonedCameraViewport = m_clonedViewport;
  }
}

const CCamera* Viewport::internalResolveActiveCamera(const CCamera* forceThisCamera) const
{
  // Prepare camera (projection matrix):
  Viewport* viewForGetCamera = nullptr;

  if (!m_clonedCameraViewport.empty())
  {
    const auto view = m_parent->getViewport(m_clonedCameraViewport);
    if (!view)
      THROW_EXCEPTION_FMT(
          "Cloned viewport '%s' not found in parent Scene", m_clonedViewport.c_str());

    viewForGetCamera = m_isClonedCamera ? view.get() : const_cast<Viewport*>(this);
  }
  else
  {  // Normal case: render our own objects:
    viewForGetCamera = const_cast<Viewport*>(this);
  }

  // Get camera:
  // 1st: if there is a CCamera in the scene (nullptr if no camera found):
  const auto camPtr = viewForGetCamera->getByClass<CCamera>();
  const auto* myCamera = camPtr ? camPtr.get() : nullptr;

  // 2nd: the internal camera of all viewports:
  if (!myCamera) myCamera = &viewForGetCamera->m_camera;

  // forced cam?
  if (forceThisCamera) myCamera = forceThisCamera;

  return myCamera;
}

void Viewport::updateMatricesFromCamera(const CCamera& myCamera) const
{
  auto& _ = m_threadedData.get().state;

  if (myCamera.isNoProjection())
  {
    // No translation nor rotation nor perspective:
    _.computeNoProjectionMatrix(m_clip_min, m_clip_max);
  }
  else
  {
    // Projective or orthogonal camera models:
    ASSERT_(myCamera.getZoomDistance() > 0);

    _.is_projective = myCamera.isProjective();

    _.FOV = myCamera.getProjectiveFOVdeg();
    _.pinhole_model = myCamera.getPinholeModel();
    _.eyeDistance = myCamera.getZoomDistance();
    _.azimuth = DEG2RAD(myCamera.getAzimuthDegrees());
    _.elev = DEG2RAD(myCamera.getElevationDegrees());
    _.roll = DEG2RAD(myCamera.getRollDegrees());

    if (myCamera.is6DOFMode())
    {
      // In 6DOFMode eye is set viewing towards the direction of the
      // positive Z axis
      // Up is set as -Y axis
      const auto pose = mrpt::poses::CPose3D(myCamera.getPose());
      const auto viewDirection = mrpt::poses::CPose3D::FromTranslation(0, 0, 1);
      const auto at = pose + viewDirection;

      _.eye = pose.translation();
      _.pointing = at.translation();
      // "UP" = -Y axis
      pose.getRotationMatrix().extractColumn(1, _.up);
      _.up *= -1.0;
    }
    else
    {
      // Normal mode: use "camera orbit" parameters to compute pointing-to
      // point:
      _.pointing = myCamera.getPointingAt();

      const auto c2m_u = mrpt::math::TVector3D(
          cos(_.azimuth) * cos(_.elev),  //
          sin(_.azimuth) * cos(_.elev),  //
          sin(_.elev)                    //
      );

      const double dis = std::max<double>(0.001, myCamera.getZoomDistance());
      _.eye = _.pointing + c2m_u * dis;

      _.up.x = -cos(_.azimuth) * sin(_.elev);
      _.up.y = -sin(_.azimuth) * sin(_.elev);
      _.up.z = cos(_.elev);

      // roll?
      if (_.roll != .0)
      {
        // _.up & c2m_u are both unit vectors and orthogonal to each other.
        const auto w = mrpt::math::crossProduct3D(c2m_u, _.up);
        _.up = _.up * cos(_.roll) + w * sin(_.roll);
      }
    }

    // Compute the projection matrix (p_matrix):
    _.computeProjectionMatrix(m_clip_min, m_clip_max);

    // Apply eye center and lookAt to p_matrix:
    _.computeViewMatrix();
  }

  // Reset model4x4 matrix to the identity transformation:
  _.m_matrix.setIdentity();

  // Compute the directional light projection matrix (light_pv)
  _.computeLightProjectionMatrix(m_lightShadowClipMin, m_lightShadowClipMax, m_light);

  _.initialized = true;
}

void Viewport::enableShadowCasting(
    bool enabled, unsigned int SHADOW_MAP_SIZE_X, unsigned int SHADOW_MAP_SIZE_Y)
{
  m_shadowsEnabled = enabled;
  if (SHADOW_MAP_SIZE_X) m_ShadowMapSizeX = SHADOW_MAP_SIZE_X;
  if (SHADOW_MAP_SIZE_Y) m_ShadowMapSizeY = SHADOW_MAP_SIZE_Y;
}
