/* +------------------------------------------------------------------------+
   |                     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          |
   +------------------------------------------------------------------------+ */
#pragma once

#include <mrpt/core/exceptions.h>
#include <mrpt/gui/MRPT2NanoguiGLCanvas.h>
#include <mrpt/gui/internal/NanoGUICanvasHeadless.h>
#include <mrpt/opengl/Scene.h>
#include <mrpt/system/string_utils.h>  // firstNLines()

#include <mutex>
#include <string>

// Expose nanogui API to mrpt users, for direct use of nanogui classes.
#include <mrpt/config.h>
#if MRPT_HAS_NANOGUI

#ifdef None  // X11 headers conflict...
#undef None
#endif
#include <nanogui/nanogui.h>

namespace mrpt::gui
{
/** Additional parameters to change the window behavior and OpenGL context
 */
struct CDisplayWindowGUI_Params
{
  CDisplayWindowGUI_Params() = default;

  bool resizable = true;
  bool fullscreen = false;
  int colorBits = 8;
  int alphaBits = 8;
  int depthBits = 24;
  int stencilBits = 8;
  int nSamples = 0;
  unsigned int glMajor = 3;
  unsigned int glMinor = 3;
  bool maximized = false;

  /// If true, will try to use GLES instead of OpenGL.
  /// Note that the glMajor and glMinor version should then be changed
  /// accordingly (e.g. to 1.5 instead of 3.3)
  bool gles_context = false;
};

/** A window with powerful GUI capabilities, via the nanogui library.
 *
 * You can add a background mrpt::opengl::Scene object rendered on the
 * background of the entire window by setting an object in field
 * `background_scene`, locking its mutex `background_scene_mtx`.
 *
 * Refer to nanogui API docs or [MRPT examples](examples.html) for further usage
 * examples. A typical lifecycle of a GUI app with this class might look like:
 *
 * \code
 * nanogui::init();
 * {
 *   mrpt::gui::CDisplayWindowGUI win;
 *   // Populate win adding UI controls, etc.
 *   // ...
 *   win.performLayout();
 *
 *   win.drawAll();
 *   win.setVisible(true);
 *   nanogui::mainloop();
 * }
 * nanogui::shutdown();
 * \endcode
 *
 *
 * ![mrpt::gui::CDisplayWindowGUI screenshot](preview_CDisplayWindowGUI.png)
 *
 * Create managed subwindows with createManagedSubWindow(), with built-in
 * support for minimize and restore.
 * See demo video in: https://www.youtube.com/watch?v=QKMzdlZRW50
 *
 *
 * \ingroup mrpt_gui_grp
 */
class CDisplayWindowGUI : public nanogui::Screen
{
 public:
  /** @name Callback functor types
   * @{ */

  using loop_callback_t = std::function<void(void)>;
  using drop_files_callback_t =
      std::function<bool /*handled*/ (const std::vector<std::string>& /* filenames */)>;
  using keyboard_callback_t = std::function<bool /*handled*/ (
      int /*key*/, int /*scancode*/, int /*action*/, int /*modifiers*/)>;
  /** @} */

  /** @name Ctor and basic window set up
   * @{ */
  using Ptr = std::shared_ptr<CDisplayWindowGUI>;
  using ConstPtr = std::shared_ptr<const CDisplayWindowGUI>;

  /// Regular ctor from caption and size
  CDisplayWindowGUI(
      const std::string& caption = std::string(),
      unsigned int width = 400,
      unsigned int height = 300,
      const CDisplayWindowGUI_Params& p = CDisplayWindowGUI_Params());

  virtual ~CDisplayWindowGUI() override;

  /** Class factory returning a smart pointer, equivalent to
   * `std::make_shared<>(...)` */
  template <typename... Args>
  static Ptr Create(Args&&... args)
  {
    return std::make_shared<CDisplayWindowGUI>(std::forward<Args>(args)...);
  }

  /** Resizes the window */
  void resize(unsigned int width, unsigned int height);

  /** Changes the position of the window on the screen. */
  void setPos(int x, int y);

  /**  Changes the window title. */
  void setWindowTitle(const std::string& str);

  /** Sets the window icon, which must must either RGB or (preferred) RGBA
   * channels. You can read it from a .png or .ico file using
   * mrpt::img::CImage::loadFromFile().
   *
   * The image will be resized as needed. Good sizes include 16x16, 32x32 and
   * 48x48.
   *
   * \note (New in MRPT 2.4.2)
   */
  void setIcon(const mrpt::img::CImage& img);

  /** \overload From an icon stored in code as `const char*` exported in the
   * "GIMP header image formar (RGB)", with a transparent color.
   * \note (New in MRPT 2.4.2)
   */
  void setIconFromData(
      const char* imgData, unsigned int width, unsigned int height, const uint8_t transparent);

  /** Every time the window is about to be repainted, an optional callback can
   * be called, if provided via this method. This method can be safely called
   * multiple times to register multiple callbacks.
   *
   * \note (New in MRPT 2.3.2)
   */
  void addLoopCallback(const loop_callback_t& callback) { m_loopCallbacks.push_back(callback); }
  const auto& loopCallbacks() const { return m_loopCallbacks; }

  /** Handles drag-and drop events of files into the window.
   * This method can be safely called multiple times to register multiple
   * callbacks.
   *
   * \note (New in MRPT 2.3.2)
   */
  void addDropFilesCallback(const drop_files_callback_t& callback)
  {
    m_dropFilesCallbacks.push_back(callback);
  }
  const auto& dropFilesCallbacks() const { return m_dropFilesCallbacks; }

  /** Handles keyboard events.
   * This method can be safely called multiple times to register multiple
   * callbacks.
   *
   * \note (New in MRPT 2.3.2)
   */
  void addKeyboardCallback(const keyboard_callback_t& callback)
  {
    m_keyboardCallbacks.push_back(callback);
  }
  const auto& keyboardCallbacks() const { return m_keyboardCallbacks; }

  /** \note This call replaces *all* existing loop callbacks. See
   * addLoopCallback().
   * \deprecated In MRPT 2.3.2, replaced by addLoopCallback()
   */
  void setLoopCallback(const loop_callback_t& callback)
  {
    m_loopCallbacks.clear();
    m_loopCallbacks.push_back(callback);
  }
  /** \deprecated In MRPT 2.3.2, replaced by loopCallbacks() */
  loop_callback_t loopCallback() const
  {
    return m_loopCallbacks.empty() ? loop_callback_t() : m_loopCallbacks.at(0);
  }

  /** \note This call replaces *all* existing drop file callbacks. See
   * addDropFilesCallback().
   * \deprecated In MRPT 2.3.2, replaced by addDropFilesCallback()
   */
  void setDropFilesCallback(const drop_files_callback_t& callback)
  {
    m_dropFilesCallbacks.clear();
    m_dropFilesCallbacks.push_back(callback);
  }
  /** \deprecated In MRPT 2.3.2, replaced by dropFilesCallbacks() */
  drop_files_callback_t dropFilesCallback() const
  {
    return m_dropFilesCallbacks.empty() ? drop_files_callback_t() : m_dropFilesCallbacks.at(0);
  }

  /** \note This call replaces *all* existing keyboard callbacks. See
   * addKeyboardCallback().
   * \deprecated In MRPT 2.3.2, replaced by addKeyboardCallback()
   */
  void setKeyboardCallback(const keyboard_callback_t& callback)
  {
    m_keyboardCallbacks.clear();
    m_keyboardCallbacks.push_back(callback);
  }
  /** \deprecated In MRPT 2.3.2, replaced by keyboardCallbacks() */
  keyboard_callback_t keyboardCallback() const
  {
    return m_keyboardCallbacks.empty() ? keyboard_callback_t() : m_keyboardCallbacks.at(0);
  }

  /** @} */

  /** @name Managed subwindows subsystem
   * @{ */

  /** Creates and return a nanogui::Window, adds to it basic minimize/restore
   * tool buttons and add it to the list of handled subwindows so it gets
   * listed in the subwindows control UI.
   * User should set a layout manager, width, height, etc. in the returned
   * window as desired.
   *
   * The returned object is owned by the nanogui system, you should NOT delete
   * it.
   *
   * The first time this is called, an additional subWindow will be created
   * (default position:bottom left corner) to hold minimized windows.
   * You can access and modify this windows via getSubWindowsUI().
   *
   * \note [New in MRPT 2.1.1]
   */
  nanogui::Window* createManagedSubWindow(const std::string& title);

  nanogui::Window* getSubWindowsUI() { return m_subWindows.ui; }
  const nanogui::Window* getSubWindowsUI() const { return m_subWindows.ui; }

  /** Direct (read-only) access to managed subwindows by 0-based index
   * (creation order).
   *
   * \sa createManagedSubWindow()
   * \note [New in MRPT 2.3.1]
   */
  const nanogui::Window* getSubwindow(size_t index) const { return m_subWindows.windows.at(index); }

  /** Get the number of managed subwindows. \sa createManagedSubWindow()
   * \note [New in MRPT 2.3.1]
   */
  size_t getSubwindowCount() const { return m_subWindows.windows.size(); }

  /** Minimize a subwindow. \note [New in MRPT 2.3.1] */
  void subwindowMinimize(size_t index) { m_subWindows.minimize(index); }

  /** Restore a minimized subwindow. \note [New in MRPT 2.3.1] */
  void subwindowRestore(size_t index) { m_subWindows.restore(index); }

  /** Forces focus on a subwindow. \note [New in MRPT 2.3.1] */
  void subwindowSetFocused(size_t index) { m_subWindows.setFocused(index); }

  /** @} */

  /** @name Access to full-window (background) GL scene
   * @{ */

  mrpt::opengl::Scene::Ptr background_scene;
  std::mutex background_scene_mtx;

  CGlCanvasBase& camera() { return m_background_canvas; }
  const CGlCanvasBase& camera() const { return m_background_canvas; }

  /** @} */

  /** @name Direct access to underlying nanogui API
   * @{ */

  nanogui::Screen* nanogui_screen() { return dynamic_cast<nanogui::Screen*>(this); }

  /** @} */

  virtual void drawContents() override;
  virtual void onIdleLoopTasks() override;

 protected:
  CDisplayWindowGUI(const CDisplayWindowGUI&) = delete;
  CDisplayWindowGUI& operator=(const CDisplayWindowGUI&) = delete;

  CDisplayWindowGUI(CDisplayWindowGUI&&) = delete;
  CDisplayWindowGUI& operator=(CDisplayWindowGUI&&) = delete;

  /** @name Internal virtual functions to handle GUI events
   * @{ */
  virtual bool keyboardEvent(int key, int scancode, int action, int modifiers) override;
  virtual bool mouseMotionEvent(
      const nanogui::Vector2i& p, const nanogui::Vector2i& rel, int button, int modifiers) override;
  virtual bool mouseButtonEvent(
      const nanogui::Vector2i& p, int button, bool down, int modifiers) override;
  virtual bool scrollEvent(const nanogui::Vector2i& p, const nanogui::Vector2f& rel) override;
  virtual bool dropEvent(const std::vector<std::string>& filenames) override;
  /** @} */

  /** Used to keep track of mouse events on the camera */
  internal::NanoGUICanvasHeadless m_background_canvas;

  std::vector<loop_callback_t> m_loopCallbacks;
  std::vector<drop_files_callback_t> m_dropFilesCallbacks;
  std::vector<keyboard_callback_t> m_keyboardCallbacks;

  void createSubWindowsControlUI();

  class SubWindow : public nanogui::Window
  {
   public:
    SubWindow(CDisplayWindowGUI& parentGui, int myIndex, Widget* parent, const std::string& title) :
        nanogui::Window(parent, title), m_parentGui(parentGui), m_subWindowIndex(myIndex)
    {
    }
    CDisplayWindowGUI& m_parentGui;
    const int m_subWindowIndex = 0;

    /// Handle a focus change event (default implementation: record the
    /// focus status, but do nothing)
    bool focusEvent(bool focused) override
    {
      if (focused) m_parentGui.m_subWindows.onSubWindowFocused(m_subWindowIndex);
      return nanogui::Window::focusEvent(focused);
    }
  };

  struct SubWindows
  {
    SubWindows(CDisplayWindowGUI& p) : parent(p) {}

    std::vector<SubWindow*> windows;
    nanogui::Window* ui = nullptr;
    nanogui::ComboBox* uiCombo = nullptr;

    void minimize(int index);
    void restore(int index);
    void setFocused(int index);

    void onSubWindowFocused(int index);

    CDisplayWindowGUI& parent;
  };

  SubWindows m_subWindows{*this};
};  // namespace mrpt::gui

#define NANOGUI_START_TRY \
  try                     \
  {
#define NANOGUI_END_TRY(_parentWindowRef_)                                                   \
  }                                                                                          \
  catch (const std::exception& e)                                                            \
  {                                                                                          \
    const size_t maxLines = 7;                                                               \
    const std::string sErr = mrpt::system::firstNLines(mrpt::exception_to_str(e), maxLines); \
    auto dlg = new nanogui::MessageDialog(                                                   \
        &_parentWindowRef_, nanogui::MessageDialog::Type::Warning, "Exception", sErr);       \
    dlg->requestFocus();                                                                     \
    dlg->setCallback([](int /*result*/) {});                                                 \
  }

}  // namespace mrpt::gui
#endif  // MRPT_HAS_NANOGUI
