/* +------------------------------------------------------------------------+
   |                     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/Shader.h>
#include <mrpt/opengl/opengl_api.h>

#include <iostream>
#include <list>
#include <mutex>
#include <thread>

using namespace std;
using namespace mrpt;
using namespace mrpt::opengl;

// ========= clearPendingIfPossible =============
namespace mrpt::opengl::internal
{
// pending to clear (ppc) lists:
static std::list<std::shared_ptr<Program::Data>> pptc;
static std::list<std::shared_ptr<Shader::Data>> spptc;
static bool inClearPendingIfPossible = false;
static std::mutex pendingToClear_mtx;

// Frees those programs that were still waiting since they were attempted to
// delete from a wrong thread. If we are now at that thread, clean up:
void clearPendingIfPossible()
{
  std::lock_guard<std::mutex> lck(pendingToClear_mtx);
  inClearPendingIfPossible = true;

  for (auto it = pptc.begin(); it != pptc.end();)
  {
    if (!*it)
    {
      it = pptc.erase(it);
      continue;
    }
    if ((*it)->linkedThread == std::this_thread::get_id())
    {
      (*it)->destroy();
      it = pptc.erase(it);
      continue;
    }
    else
    {
      ++it;
    }
  }

  for (auto it = spptc.begin(); it != spptc.end();)
  {
    if (!*it)
    {
      it = spptc.erase(it);
      continue;
    }
    if ((*it)->creationThread == std::this_thread::get_id())
    {
      (*it)->destroy();
      it = spptc.erase(it);
      continue;
    }
    else
    {
      ++it;
    }
  }
  inClearPendingIfPossible = false;
}
}  // namespace mrpt::opengl::internal

// ============ Shader ============
Shader::~Shader() { clear(); }
Shader& Shader::operator=(Shader&& o)
{
  m_data = std::move(o.m_data);
  o.m_data.reset();
  return *this;
}
Shader::Shader(Shader&& o)
{
  m_data = std::move(o.m_data);
  o.m_data.reset();
}

void Shader::clear()
{
  if (!m_data || !m_data->shader) return;

  // If we are in the same thread that created us, ok, clean up.
  // Otherwise, postpone it for later on:
  if (m_data->creationThread == std::this_thread::get_id())
  {
    m_data->destroy();
  }
  else
  {
    // Postpone (except if we are already in the global dtor of the queue!)
    if (!m_data->inPostponedDestructionQueue)
    {
      std::lock_guard<std::mutex> lck(internal::pendingToClear_mtx);
      m_data->inPostponedDestructionQueue = true;
      internal::spptc.emplace_back(m_data);
    }
    m_data = std::make_shared<Data>();
  }
  // if (!internal::inClearPendingIfPossible)
  // internal::clearPendingIfPossible();
}

void Shader::Data::destroy()
{
  if (!shader) return;

#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  // See clear() comments
  ASSERT_(creationThread == std::this_thread::get_id());

  glDeleteShader(shader);
#endif
  shader = 0;
}

bool Shader::compile(
    unsigned int type,
    const std::vector<std::string>& shaderCode,
    mrpt::optional_ref<std::string> outErrorMessages)
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  clear();

  const auto nShaderCodes = shaderCode.size();
  ASSERT_(nShaderCodes >= 1);

  m_data->creationThread = std::this_thread::get_id();

  m_data->shader = glCreateShader(static_cast<GLenum>(type));

  std::vector<const GLchar*> sources(nShaderCodes);
  std::vector<GLint> lengths(nShaderCodes);

  for (size_t i = 0; i < nShaderCodes; i++)
  {
    sources[i] = shaderCode[i].c_str();
    lengths[i] = shaderCode[i].size();
  }

  glShaderSource(m_data->shader, nShaderCodes, sources.data(), lengths.data());
  glCompileShader(m_data->shader);

  GLint shader_ok;
  glGetShaderiv(m_data->shader, GL_COMPILE_STATUS, &shader_ok);
  if (!shader_ok)
  {
    GLint log_length;
    std::string log;
    glGetShaderiv(m_data->shader, GL_INFO_LOG_LENGTH, &log_length);
    log.resize(log_length);
    glGetShaderInfoLog(m_data->shader, log_length, NULL, &log[0]);

    if (outErrorMessages)
      outErrorMessages.value().get() = std::move(log);
    else
      std::cerr << "[Shader::compile] Compile error: " << log << "\n";

    glDeleteShader(m_data->shader);
    m_data->shader = 0;
    return false;  // error
  }

  return true;  // ok
#else
  THROW_EXCEPTION("MRPT built without OpenGL support.");
#endif
}

// ========= Program =============
Program::~Program() { clear(); }

void Program::clear()
{
  if (!m_data || !m_data->program) return;

  // If we are in the same thread that created us, ok, clean up.
  // Otherwise, postpone it for later on:
  if (m_data->linkedThread == std::this_thread::get_id())
  {
    m_data->destroy();
  }
  else
  {
    // Postpone (except if we are already in the global dtor of the queue!)
    if (!m_data->inPostponedDestructionQueue)
    {
      std::lock_guard<std::mutex> lck(internal::pendingToClear_mtx);
      m_data->inPostponedDestructionQueue = true;
      internal::pptc.emplace_back(m_data);
    }
    m_data = std::make_shared<Data>();
  }
  if (!internal::inClearPendingIfPossible) internal::clearPendingIfPossible();
}

void Program::Data::destroy()
{
  if (!program) return;
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL

  // See clear() comments
  ASSERT_(linkedThread == std::this_thread::get_id());

  // 1) Detach shaders from program.
  for (const Shader& s : shaders) glDetachShader(program, s.handle());

  // 2) Delete program.
  glDeleteProgram(program);

  // 3) Delete shaders.
  shaders.clear();

  // 4) Delete all variables:
  uniforms.clear();
  attribs.clear();
#endif

  program = 0;
}

bool Program::linkProgram(
    std::vector<Shader>& shaders, mrpt::optional_ref<std::string> outErrorMessages)
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  clear();

#if defined(MRPT_OS_LINUX)
  // Workaround to enfore wxWidgets to use GLSL>=3.3 even for wxWidgets<3.0.4
  // See CWxGLCanvasBase::CWxGLCanvasBase.
  if (!::getenv("MESA_GL_VERSION_OVERRIDE"))
  {
    ::setenv("MESA_GL_VERSION_OVERRIDE", "3.3", 1 /*overwrite*/);
  }
#endif

  m_data->program = glCreateProgram();
  CHECK_OPENGL_ERROR();
  ASSERT_(m_data->program != 0);

  // Take ownership of shaders:
  m_data->shaders = std::move(shaders);
  m_data->linkedThread = std::this_thread::get_id();

  for (const auto& shader : m_data->shaders) glAttachShader(m_data->program, shader.handle());

  glLinkProgram(m_data->program);
  CHECK_OPENGL_ERROR();

  GLint program_ok;
  glGetProgramiv(m_data->program, GL_LINK_STATUS, &program_ok);
  if (!program_ok)
  {
    GLint log_length;
    std::string log;
    glGetProgramiv(m_data->program, GL_INFO_LOG_LENGTH, &log_length);
    log.resize(log_length);
    glGetProgramInfoLog(m_data->program, log_length, NULL, &log[0]);

    if (outErrorMessages)
      outErrorMessages.value().get() = std::move(log);
    else
      std::cerr << "[Program::linkProgram] Link error: " << log << "\n";
    clear();
    return false;  // error
  }

  return true;
#else
  THROW_EXCEPTION("MRPT built without OpenGL support.");
#endif
}

void Program::declareUniform(const std::string& name)
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  ASSERT_(!empty());

  if (m_data->uniforms.count(name) != 0)
    THROW_EXCEPTION_FMT("declareUniform: Name `%s` already registered", name.c_str());

  const auto ret = glGetUniformLocation(m_data->program, static_cast<const GLchar*>(name.c_str()));
  if (ret < 0)
    THROW_EXCEPTION_FMT(
        "declareUniform: glGetUniformLocation() returned error for `%s`", name.c_str());
  m_data->uniforms[name] = ret;
#else
  THROW_EXCEPTION("MRPT built without OpenGL support.");
#endif
}
void Program::declareAttribute(const std::string& name)
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  ASSERT_(!empty());

  if (m_data->attribs.count(name) != 0)
    THROW_EXCEPTION_FMT("declareAttribute: Name `%s` already registered", name.c_str());

  const auto ret = glGetAttribLocation(m_data->program, static_cast<const GLchar*>(name.c_str()));
  if (ret < 0)
    THROW_EXCEPTION_FMT(
        "declareAttribute: glGetAttribLocation() returned error for `%s`", name.c_str());
  m_data->attribs[name] = ret;

#else
  THROW_EXCEPTION("MRPT built without OpenGL support.");
#endif
}

void Program::dumpProgramDescription(std::ostream& o) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  ASSERT_(!empty());

  GLint count;

  GLint size;   // size of the variable
  GLenum type;  // type of the variable (float, vec3 or mat4, etc)

  const GLsizei bufSize = 32;  // maximum name length
  GLchar name[bufSize];        // variable name in GLSL
  GLsizei length;              // name length

  // Attributes
  glGetProgramiv(m_data->program, GL_ACTIVE_ATTRIBUTES, &count);
  o << mrpt::format("Active Attributes: %d\n", count);

  for (GLint i = 0; i < count; i++)
  {
    glGetActiveAttrib(m_data->program, (GLuint)i, bufSize, &length, &size, &type, name);

    o << mrpt::format("Attribute #%d Type: %u Name: %s\n", i, type, name);
  }

  // Uniforms
  glGetProgramiv(m_data->program, GL_ACTIVE_UNIFORMS, &count);
  printf("Active Uniforms: %d\n", count);

  for (GLint i = 0; i < count; i++)
  {
    glGetActiveUniform(m_data->program, (GLuint)i, bufSize, &length, &size, &type, name);

    o << mrpt::format("Uniform #%d Type: %u Name: %s\n", i, type, name);
  }
#endif
}

void Program::use()
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  glUseProgram(programId());
  CHECK_OPENGL_ERROR();
#endif
}

void Program::setInt(const char* uniformName, int value) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  glUniform1i(uniformId(uniformName), value);
#endif
}

void Program::setFloat(const char* uniformName, float value, bool failIfNotExists) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  if (!failIfNotExists && !hasUniform(uniformName)) return;

  glUniform1f(uniformId(uniformName), value);
#endif
}

void Program::setFloat3(const char* uniformName, float v1, float v2, float v3) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  glUniform3f(uniformId(uniformName), v1, v2, v3);
#endif
}

void Program::setFloat4(const char* uniformName, float v1, float v2, float v3, float v4) const
{
#if MRPT_HAS_OPENGL_GLUT || MRPT_HAS_EGL
  glUniform4f(uniformId(uniformName), v1, v2, v3, v4);
#endif
}
