/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2021, 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/CFBORender.h>
#include <mrpt/opengl/opengl_api.h>

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

/*---------------------------------------------------------------
						Constructor
---------------------------------------------------------------*/
CFBORender::CFBORender(
	unsigned int width, unsigned int height, const bool skip_glut_window)
	: m_width(width),
	  m_height(height),
	  m_win_used(!skip_glut_window),
	  m_default_bk_color(.6f, .6f, .6f, 1)
{
#if MRPT_HAS_OPENCV && MRPT_HAS_OPENGL_GLUT

	MRPT_START

	if (m_win_used)
	{
		// check a previous initialization of the GLUT
		if (!glutGet(GLUT_INIT_STATE))
		{
			// create the context (a little trick)
			int argc = 1;
			char* argv[1] = {nullptr};
			glutInit(&argc, argv);
		}

		// create a hidden window
		m_win = glutCreateWindow("CFBORender");
		glutHideWindow();
	}

	// call after creating the hidden window
	if (!isExtensionSupported("GL_EXT_framebuffer_object"))
		THROW_EXCEPTION("Framebuffer Object extension unsupported");

// In win32 we have to load the pointers to the functions:
#ifdef _WIN32
	glGenFramebuffersEXT =
		(PFNGLGENFRAMEBUFFERSEXTPROC)wglGetProcAddress("glGenFramebuffersEXT");
	glDeleteFramebuffersEXT = (PFNGLDELETEFRAMEBUFFERSEXTPROC)wglGetProcAddress(
		"glDeleteFramebuffersEXT");
	glBindFramebufferEXT =
		(PFNGLBINDFRAMEBUFFEREXTPROC)wglGetProcAddress("glBindFramebufferEXT");
	glFramebufferTexture2DEXT =
		(PFNGLFRAMEBUFFERTEXTURE2DEXTPROC)wglGetProcAddress(
			"glFramebufferTexture2DEXT");

	ASSERT_(glGenFramebuffersEXT != nullptr);
	ASSERT_(glDeleteFramebuffersEXT != nullptr);
	ASSERT_(glBindFramebufferEXT != nullptr);
	ASSERT_(glFramebufferTexture2DEXT != nullptr);
#endif

	// gen the frambuffer object (FBO), similar manner as a texture
	glGenFramebuffersEXT(1, &m_fbo);

	// bind the framebuffer, fbo, so operations will now occur on it
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_fbo);

	// change viewport size (in pixels)
	glViewport(0, 0, m_width, m_height);

	// make a texture
	glGenTextures(1, &m_tex);

	// initialize texture that will store the framebuffer image
	const GLenum texTarget =
#if defined(GL_TEXTURE_RECTANGLE_NV)
		GL_TEXTURE_RECTANGLE_NV;
#elif defined(GL_TEXTURE_RECTANGLE_ARB)
		GL_TEXTURE_RECTANGLE_ARB;
#else
		GL_TEXTURE_RECTANGLE_EXT;
#endif

	glBindTexture(texTarget, m_tex);
	glTexImage2D(
		texTarget, 0, GL_RGB, m_width, m_height, 0, GL_RGB, GL_UNSIGNED_BYTE,
		nullptr);

	// bind this texture to the current framebuffer obj. as color_attachement_0
	glFramebufferTexture2DEXT(
		GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, texTarget, m_tex, 0);

	//'unbind' the frambuffer object, so subsequent drawing ops are not drawn
	// into the FBO.
	// '0' means "windowing system provided framebuffer
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

	MRPT_END

#else
	THROW_EXCEPTION("MRPT compiled without OpenCV and/or OpenGL support!!");
#endif
}

/*---------------------------------------------------------------
						Destructor:
 ---------------------------------------------------------------*/
CFBORender::~CFBORender()
{
#if MRPT_HAS_OPENGL_GLUT
	// delete the current texture, the framebuffer object and the GLUT window
	glDeleteTextures(1, &m_tex);
	glDeleteFramebuffersEXT(1, &m_fbo);
	if (m_win_used) glutDestroyWindow(m_win);
#endif
}

/*---------------------------------------------------------------
					Set the scene camera
 ---------------------------------------------------------------*/
void CFBORender::setCamera(const COpenGLScene& scene, const CCamera& camera)
{
	MRPT_START

	scene.getViewport("main")->getCamera() = camera;

	MRPT_END
}

CCamera& CFBORender::getCamera(const COpenGLScene& scene)
{
	MRPT_START

	return scene.getViewport("main")->getCamera();

	MRPT_END
}

void CFBORender::getFrame(
	[[maybe_unused]] const COpenGLScene& scene, [[maybe_unused]] CImage& buffer)
{
#if MRPT_HAS_OPENCV && MRPT_HAS_OPENGL_GLUT

	MRPT_START

	// resize the buffer if it is necessary
	if (buffer.getWidth() != static_cast<size_t>(m_width) ||
		buffer.getHeight() != static_cast<size_t>(m_height) ||
		buffer.getChannelCount() != 3)
	{
		buffer.resize(m_width, m_height, mrpt::img::CH_RGB);
	}

	// Go on.
	getFrame2(scene, buffer);

	MRPT_END
#endif
}

void CFBORender::getFrame2(
	[[maybe_unused]] const COpenGLScene& scene, [[maybe_unused]] CImage& buffer)
{
#if MRPT_HAS_OPENGL_GLUT

	MRPT_START

	// check the buffer size
	ASSERT_EQUAL_(buffer.getWidth(), static_cast<size_t>(m_width));
	ASSERT_EQUAL_(buffer.getHeight(), static_cast<size_t>(m_height));
	ASSERT_EQUAL_(buffer.getChannelCount(), 3);
	// bind the framebuffer, fbo, so operations will now occur on it
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_fbo);

	glClearColor(
		m_default_bk_color.R, m_default_bk_color.G, m_default_bk_color.B,
		m_default_bk_color.A);

	// Render opengl objects:
	// ---------------------------
	scene.render();

	// TODO NOTE: This should fail if the image has padding bytes. See
	// glPixelStore() etc.
	glReadPixels(
		0, 0, m_width, m_height, GL_BGR_EXT, GL_UNSIGNED_BYTE, buffer(0, 0));

	// Flip vertically if needed:
	if (buffer.isOriginTopLeft())
	{
		buffer.flipVertical();
	}

	//'unbind' the frambuffer object, so subsequent drawing ops are not drawn
	// into the FBO.
	// '0' means "windowing system provided framebuffer
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

	MRPT_END
#endif
}

void CFBORender::resize(
	[[maybe_unused]] unsigned int width, [[maybe_unused]] unsigned int height)
{
#if MRPT_HAS_OPENCV && MRPT_HAS_OPENGL_GLUT

	MRPT_START

	// update members
	m_width = width;
	m_height = height;

	// bind the framebuffer, fbo, so operations will now occur on it
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_fbo);

	// change viewport size (in pixels)
	glViewport(0, 0, m_width, m_height);

	// change texture size
	const GLenum texTarget =
#if defined(GL_TEXTURE_RECTANGLE_NV)
		GL_TEXTURE_RECTANGLE_NV;
#elif defined(GL_TEXTURE_RECTANGLE_ARB)
		GL_TEXTURE_RECTANGLE_ARB;
#else
		GL_TEXTURE_RECTANGLE_EXT;
#endif

	glBindTexture(texTarget, m_tex);
	glTexImage2D(
		texTarget, 0, GL_RGB, m_width, m_height, 0, GL_RGB, GL_UNSIGNED_BYTE,
		nullptr);

	//'unbind' the frambuffer object, so subsequent drawing ops are not drawn
	// into the FBO.
	// '0' means "windowing system provided framebuffer
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

	MRPT_END

//#else
//	THROW_EXCEPTION("MRPT compiled without OpenCV and/or OpenGL support!!");
#endif
}

bool CFBORender::isExtensionSupported([
	[maybe_unused]] const std::string& extension)
{
#if MRPT_HAS_OPENGL_GLUT
	MRPT_START
	for (int index = 0;; index++)
	{
		const auto extName = glGetStringi(GL_EXTENSIONS, index);
		if (!extName) break;

		if (std::string(reinterpret_cast<const char*>(extName)) == extension)
			return true;
	}
	MRPT_END
#endif
	return false;
}
