/* +------------------------------------------------------------------------+
   |                     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/containers/yaml.h>
#include <mrpt/opengl/CRenderizable.h>
#include <mrpt/opengl/Viewport.h>

/** The namespace for 3D scene representation and rendering. See also the <a
 * href="mrpt-opengl.html" > summary page</a> of the mrpt-opengl library for
 * more info and thumbnails of many of the render primitive.
 */
namespace mrpt::opengl
{
/** This class allows the user to create, load, save, and render 3D scenes using
 * OpenGL primitives.
 * The class can be understood as a program to be run over OpenGL, containing
 * a sequence of viewport definitions,
 * rendering primitives, etc.
 *
 * It can contain from 1 up to any number of <b>Viewports</b>, each one
 * associated a set of OpenGL objects and, optionally, a preferred camera
 * position. Both orthogonal (2D/3D) and projection
 * camera models can be used for each viewport independently, greatly
 * increasing the possibilities of rendered scenes.
 *
 * An object of Scene always contains at least one viewport
 * (mrpt::opengl::Viewport), named "main". Optionally, any
 * number of other viewports may exist. Viewports are referenced by their
 * names, case-sensitive strings. Each viewport contains
 * a different 3D scene (i.e. they render different objects), though a
 * mechanism exist to share the same 3D scene by a number of
 * viewports so memory is not wasted replicating the same objects (see
 * Viewport::setCloneView ).
 *
 * The main rendering method, Scene::render(), assumes a viewport has
 * been set-up for the entire target window. That
 * method will internally make the required calls to opengl for creating the
 * additional viewports. Note that only the depth
 * buffer is cleared by default for each (non-main) viewport, to allow
 * transparencies. This can be disabled by the approppriate
 * member in Viewport.
 *
 * An object Scene can be saved to a [".3Dscene"
 * file](robotics_file_formats.html) using CFileOutputStream or with the direct
 * method Scene::saveToFile() for posterior visualization from the
 * standalone application \ref app_SceneViewer3D.
 *
 * It can be also displayed in real-time using windows in mrpt::gui or
 * serialized over a network socket, etc.
 *
 * \ingroup mrpt_opengl_grp
 */
class Scene : public mrpt::serialization::CSerializable
{
  DEFINE_SERIALIZABLE(Scene, mrpt::opengl)
 public:
  using TListViewports = std::vector<Viewport::Ptr>;

  Scene();
  ~Scene() override;
  Scene& operator=(const Scene& obj);
  Scene(const Scene& obj);

  /**
   * Inserts a set of objects into the scene, in the given viewport ("main"
   * by default). Any iterable object will be accepted.
   * \sa createViewport,getViewport
   */
  template <class T>
  inline void insertCollection(const T& objs, const std::string& vpn = std::string("main"))
  {
    insert(objs.begin(), objs.end(), vpn);
  }
  /** Insert a new object into the scene, in the given viewport (by default,
   * into the "main" viewport).
   *  The viewport must be created previously, an exception will be raised if
   * the given name does not correspond to
   *   an existing viewport.
   * \sa createViewport, getViewport
   */
  void insert(
      const CRenderizable::Ptr& newObject, const std::string& viewportName = std::string("main"));

  /**
   * Inserts a set of objects into the scene, in the given viewport ("main"
   * by default).
   * \sa createViewport,getViewport
   */
  template <class T_it>
  inline void insert(
      const T_it& begin, const T_it& end, const std::string& vpn = std::string("main"))
  {
    for (T_it it = begin; it != end; it++) insert(*it, vpn);
  }

  /** Creates a new viewport, adding it to the scene and returning a pointer
   * to the new object. Names (case-sensitive) cannot be duplicated: if the
   * name provided coincides with an already existing viewport, a pointer to
   * the existing object will be returned. The first, default viewport, is
   * named "main".
   */
  Viewport::Ptr createViewport(const std::string& viewportName);

  /** Returns the viewport with the given name, or nullptr if it does not
   * exist; note that the default viewport is named "main" and initially
   * occupies the entire rendering area.
   */
  Viewport::Ptr getViewport(const std::string& viewportName = std::string("main")) const;

  const TListViewports& viewports() const { return m_viewports; }

  /** Render this scene */
  void render() const;

  size_t viewportsCount() const { return m_viewports.size(); }
  /** Clear the list of objects and viewports in the scene, deleting objects'
   * memory, and leaving just the default viewport with the default values.
   */
  void clear(bool createMainViewport = true);

  /** If disabled (default), the SceneViewer application will ignore the
   * camera of the "main" viewport and keep the viewport selected by the user
   * by hand; otherwise, the camera in the "main" viewport prevails.
   * \sa followCamera
   */
  void enableFollowCamera(bool enabled) { m_followCamera = enabled; }
  /** Return the value of "followCamera"
   * \sa enableFollowCamera
   */
  bool followCamera() const { return m_followCamera; }
  /** Returns the first object with a given name, or nullptr (an empty smart
   * pointer) if not found.
   */
  CRenderizable::Ptr getByName(
      const std::string& str, const std::string& viewportName = std::string("main"));

  /** Returns the i'th object of a given class (or of a descendant class), or
  nullptr (an empty smart pointer) if not found.
  *  Example:
  * \code
     CSphere::Ptr obs = myscene.getByClass<CSphere>();
  * \endcode
  * By default (ith=0), the first observation is returned.
  */
  template <typename T>
  typename T::Ptr getByClass(size_t ith = 0) const
  {
    MRPT_START
    for (const auto& m_viewport : m_viewports)
    {
      typename T::Ptr o = m_viewport->getByClass<T>(ith);
      if (o) return o;
    }
    return typename T::Ptr();  // Not found: return empty smart pointer
    MRPT_END
  }

  /** Removes the given object from the scene (it also deletes the object to
   * free its memory).
   */
  void removeObject(
      const CRenderizable::Ptr& obj, const std::string& viewportName = std::string("main"));

  /** Initializes all textures in the scene (See
   * opengl::CTexturedPlane::initializeTextures)
   */
  void initializeTextures();

  /** Retrieves a list of all objects in text form.
   * 	\deprecated Prefer asYAML() (since MRPT 2.1.3) */
  void dumpListOfObjects(std::vector<std::string>& lst) const;

  /** Prints all viewports and objects in human-readable YAML form.
   * Note that not all objects data is serialized, so this method is not
   * suitable for deserialization (for that, use saveToFile(), loadFromFile()
   * instead).
   * \note (New in MRPT 2.1.3) */
  mrpt::containers::yaml asYAML() const;

  /** Saves the scene to a [".3Dscene" file](robotics_file_formats.html),
   * loadable by: \ref app_SceneViewer3D
   * \sa loadFromFile
   * \return false on any error.
   */
  bool saveToFile(const std::string& fil) const;

  /** Loads the scene from a [".3Dscene" file](robotics_file_formats.html).
   * \sa saveToFile
   * \return false on any error.
   */
  bool loadFromFile(const std::string& fil);

  /** Traces a ray
   */
  bool traceRay(const mrpt::poses::CPose3D& o, double& dist) const;

  /** Evaluates the bounding box of the scene in the given viewport (default:
   * "main"). */
  mrpt::math::TBoundingBox getBoundingBox(const std::string& vpn = std::string("main")) const;

  /** Recursive depth-first visit all objects in all viewports of the scene,
   * calling the user-supplied function
   *  The passed function must accept only one argument of type "const
   * mrpt::opengl::CRenderizable::Ptr &"
   */
  template <typename FUNCTOR>
  void visitAllObjects(FUNCTOR functor) const
  {
    MRPT_START
    for (const auto& viewport : m_viewports)
      for (auto& o : *viewport) internal_visitAllObjects(functor, o);
    MRPT_END
  }

  /** Ensure all shaders are unloaded in all viewports */
  void unloadShaders();

  /** Ensure all OpenGL buffers are destroyed. */
  void freeOpenGLResources();

 protected:
  bool m_followCamera = false;

  /** The list of viewports, indexed by name. */
  TListViewports m_viewports;

  template <typename FUNCTOR>
  static void internal_visitAllObjects(FUNCTOR functor, const CRenderizable::Ptr& o)
  {
    functor(o);
    if (auto objs = std::dynamic_pointer_cast<CSetOfObjects>(o); objs)
    {
      for (auto& obj : *objs) internal_visitAllObjects(functor, obj);
    }
  }
};

/** Inserts an openGL object into a scene. Allows call chaining. \sa
 * mrpt::opengl::Scene::insert  */
inline Scene::Ptr& operator<<(Scene::Ptr& s, const CRenderizable::Ptr& r)
{
  s->insert(r);
  return s;
}
/**Inserts any iterable collection of openGL objects into a scene, allowing call
 * chaining.  \sa mrpt::opengl::Scene::insert */
template <class T>
inline Scene::Ptr& operator<<(Scene::Ptr& s, const std::vector<T>& v)
{
  s->insert(v.begin(), v.end());
  return s;
}
}  // namespace mrpt::opengl
