/* +------------------------------------------------------------------------+
   |                     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 "gui-precomp.h"  // Precompiled headers
//
#include <mrpt/core/lock_helper.h>
#include <mrpt/gui/CWxGLCanvasBase.h>
#include <mrpt/gui/WxSubsystem.h>
#include <mrpt/gui/WxUtils.h>
#include <mrpt/system/CTicTac.h>

#if MRPT_HAS_WXWIDGETS && MRPT_HAS_OPENGL_GLUT

#if MRPT_HAS_OPENGL_GLUT
#ifdef _WIN32
// Windows:
#include <windows.h>
#endif

#ifdef __APPLE__
#include <GLUT/glut.h>
#include <OpenGL/gl.h>
#include <OpenGL/glu.h>
#else
#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>
#ifdef HAVE_FREEGLUT_EXT_H
#include <GL/freeglut_ext.h>
#endif
#endif

#endif

#if !wxUSE_GLCANVAS
#error "OpenGL required: set wxUSE_GLCANVAS to 1 and rebuild wxWidgets"
#endif

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

/*----------------------------------------------------------------
  Implementation of Test-GLCanvas
-----------------------------------------------------------------*/

std::unique_ptr<wxGLContext> CWxGLCanvasBase::m_gl_context;
std::recursive_mutex CWxGLCanvasBase::m_gl_context_mtx;

void CWxGLCanvasBase::OnWindowCreation(wxWindowCreateEvent& ev)
{
  auto lck = mrpt::lockHelper(m_gl_context_mtx);

  if (!m_gl_context) m_gl_context = std::make_unique<wxGLContext>(this);
}

void CWxGLCanvasBase::swapBuffers()
{
  auto lck = mrpt::lockHelper(m_gl_context_mtx);

  if (m_gl_context)
  {
    SetCurrent(*m_gl_context);
    SwapBuffers();
  }
}
void CWxGLCanvasBase::preRender() { OnPreRender(); }
void CWxGLCanvasBase::postRender() { OnPostRender(); }
void CWxGLCanvasBase::renderError(const string& err_msg) { OnRenderError(err_msg.c_str()); }

void CWxGLCanvasBase::OnMouseDown(wxMouseEvent& event)
{
  updateLastPos(event.GetX(), event.GetY());
  setMousePos(event.GetX(), event.GetY());
  setMouseClicked(true);
}
void CWxGLCanvasBase::OnMouseUp(wxMouseEvent& /*event*/) { setMouseClicked(false); }

void CWxGLCanvasBase::OnMouseMove(wxMouseEvent& event)
{
  bool leftIsDown = event.LeftIsDown();

  int X = event.GetX();
  int Y = event.GetY();
  updateLastPos(X, Y);

  if (leftIsDown || event.RightIsDown() || event.MiddleIsDown())
  {
    // Proxy variables to cache the changes:
    CamaraParams params = cameraParams();

    if (leftIsDown)
    {
      if (event.ShiftDown())
        updateZoom(params, X, Y);
      else if (event.ControlDown())
        updateRotate(params, X, Y);
      else
        updateOrbitCamera(params, X, Y);
    }
    else
      updatePan(params, X, Y);

    setMousePos(X, Y);
    setCameraParams(params);

    Refresh(false);
    Update();
  }

  // ensure we have the focus so we get keyboard events:
  this->SetFocus();
}

void CWxGLCanvasBase::OnMouseWheel(wxMouseEvent& event)
{
  int X = event.GetX();
  int Y = event.GetY();
  updateLastPos(X, Y);
  setMousePos(X, Y);

  CamaraParams params = cameraParams();
  if (!event.ShiftDown())
  {
    // regular zoom:
    updateZoom(params, event.GetWheelRotation());
  }
  else
  {
    // Move vertically +-Z:
    params.cameraPointingZ += event.GetWheelRotation() * params.cameraZoomDistance * 1e-4;
  }
  setCameraParams(params);

  Refresh(false);
  Update();

  // ensure we have the focus so we get keyboard events:
  this->SetFocus();
}

// clang-format off
static int WX_GL_ATTR_LIST[] = {
	WX_GL_DOUBLEBUFFER,    WX_GL_RGBA,
	WX_GL_DEPTH_SIZE,     24,
#if wxCHECK_VERSION(3, 0, 3)
	WX_GL_MAJOR_VERSION,  3,
	WX_GL_MINOR_VERSION,  1,
    WX_GL_CORE_PROFILE, // do not allow using opengl 1.x deprecated stuff
#endif
	0
};
// clang-format on

CWxGLCanvasBase::CWxGLCanvasBase(
    wxWindow* parent,
    wxWindowID id,
    const wxPoint& pos,
    const wxSize& size,
    long style,
    const wxString& name) :
    CGlCanvasBase(),
    wxGLCanvas(parent, id, WX_GL_ATTR_LIST, pos, size, style | wxFULL_REPAINT_ON_RESIZE, name)
{
  this->Bind(wxEVT_LEFT_DOWN, &CWxGLCanvasBase::OnMouseDown, this);
  this->Bind(wxEVT_MIDDLE_DOWN, &CWxGLCanvasBase::OnMouseDown, this);
  this->Bind(wxEVT_RIGHT_DOWN, &CWxGLCanvasBase::OnMouseDown, this);

  this->Bind(wxEVT_LEFT_UP, &CWxGLCanvasBase::OnMouseUp, this);
  this->Bind(wxEVT_MIDDLE_UP, &CWxGLCanvasBase::OnMouseUp, this);
  this->Bind(wxEVT_RIGHT_UP, &CWxGLCanvasBase::OnMouseUp, this);

  this->Bind(wxEVT_MOTION, &CWxGLCanvasBase::OnMouseMove, this);
  this->Bind(wxEVT_MOUSEWHEEL, &CWxGLCanvasBase::OnMouseWheel, this);
  this->Bind(wxEVT_CHAR, &CWxGLCanvasBase::OnChar, this);
  this->Bind(wxEVT_CHAR_HOOK, &CWxGLCanvasBase::OnChar, this);
  this->Bind(wxEVT_CREATE, &CWxGLCanvasBase::OnWindowCreation, this);

  this->Bind(wxEVT_SIZE, &CWxGLCanvasBase::OnSize, this);
  this->Bind(wxEVT_PAINT, &CWxGLCanvasBase::OnPaint, this);
  this->Bind(wxEVT_ERASE_BACKGROUND, &CWxGLCanvasBase::OnEraseBackground, this);
  this->Bind(wxEVT_ENTER_WINDOW, &CWxGLCanvasBase::OnEnterWindow, this);

// JL: There seems to be a problem in MSW we don't receive this event, but
//      in GTK we do and at the right moment to avoid an X server crash.
#ifdef _WIN32
  wxWindowCreateEvent dum;
  OnWindowCreation(dum);
#endif
}

void CWxGLCanvasBase::OnChar(wxKeyEvent& event) { OnCharCustom(event); }
void CWxGLCanvasBase::Render()
{
  wxPaintDC dc(this);

  auto lck = mrpt::lockHelper(m_gl_context_mtx);

  if (!m_gl_context)
  { /*cerr << "[CWxGLCanvasBase::Render] No GL Context!" <<
     endl;*/
    return;
  }

  SetCurrent(*m_gl_context);

  // Init OpenGL once, but after SetCurrent
  if (!m_init)
  {
    InitGL();
    m_init = true;
  }

  lck.unlock();

  const auto sz = mrpt::gui::GetScaledClientSize(this);
  double At = renderCanvas(sz.GetWidth(), sz.GetHeight());

  OnPostRenderSwapBuffers(At, dc);
}

void CWxGLCanvasBase::OnEnterWindow(wxMouseEvent& WXUNUSED(event)) { SetFocus(); }

void CWxGLCanvasBase::OnPaint(wxPaintEvent& WXUNUSED(event)) { Render(); }
void CWxGLCanvasBase::OnSize(wxSizeEvent& event)
{
  if (!m_parent->IsShown()) return;

  // set GL viewport (not called by wxGLCanvas::OnSize on all platforms...)
  const auto sz = mrpt::gui::GetScaledClientSize(this);

  if (this->IsShownOnScreen())
  {
    if (!m_gl_context)
    { /*cerr << "[CWxGLCanvasBase::Render] No GL Context!" <<
       endl;*/
      return;
    }
    else
    {
      auto lck = mrpt::lockHelper(m_gl_context_mtx);
      SetCurrent(*m_gl_context);
    }

    resizeViewport(sz.GetWidth(), sz.GetHeight());
  }
}

void CWxGLCanvasBase::OnEraseBackground(wxEraseEvent& WXUNUSED(event))
{
  // Do nothing, to avoid flashing.
}

void CWxGLCanvasBase::InitGL()
{
  auto lck = mrpt::lockHelper(m_gl_context_mtx);

  if (!m_gl_context)
  { /*cerr << "[CWxGLCanvasBase::Render] No GL Context!" <<
     endl;*/
    return;
  }
  else
    SetCurrent(*m_gl_context);

  static bool GLUT_INIT_DONE = false;

  if (!GLUT_INIT_DONE)
  {
    GLUT_INIT_DONE = true;

    int argc = 1;
    char* argv[1] = {nullptr};
    glutInit(&argc, argv);
  }
}

void CWxGLCanvasBase::setCameraPose(const mrpt::poses::CPose3D& camPose)
{
  THROW_EXCEPTION("todo");
}

#endif  // MRPT_HAS_WXWIDGETS
