#ifndef ARCBALL_H
#define ARCBALL_H

#include <Magnum/Magnum.h>
#include <Magnum/Math/DualQuaternion.h>
#include <Magnum/Math/Functions.h>
#include <Magnum/Math/Vector2.h>
#include <Magnum/Math/Vector3.h>

using namespace Magnum;

Quaternion ndcToArcBall(const Vector2 &p);

/* Implementation of Ken Shoemake's arcball camera with smooth navigation
   feature: https://www.talisman.org/~erlkonig/misc/shoemake92-arcball.pdf */
class ArcBall {
public:
  ArcBall(const Vector3 &cameraPosition, const Vector3 &viewCenter,
          const Vector3 &upDir, Deg fov, const Vector2i &windowSize);

  /* Set the camera view parameters: eye position, view center, up
     direction */
  void setViewParameters(const Vector3 &eye, const Vector3 &viewCenter,
                         const Vector3 &upDir);

  /* Reset the camera to its initial position, view center, and up dir */
  void reset();

  /* Update screen size after the window has been resized */
  void reshape(const Vector2i &windowSize) { m_windowSize = windowSize; }

  /* Update any unfinished transformation due to lagging, return true if
     the camera matrices have changed */
  bool updateTransformation();

  /* Get/set the amount of lagging such that the camera will (slowly)
     smoothly navigate. Lagging must be in [0, 1) */
  Float lagging() const { return m_lagging; }
  void setLagging(Float lagging);

  /* Initialize the first (screen) mouse position for camera
     transformation. This should be called in mouse pressed event. */
  void initTransformation(const Vector2i &mousePos);

  /* Rotate the camera from the previous (screen) mouse position to the
     current (screen) position */
  void rotate(const Vector2i &mousePos);

  /* Translate the camera from the previous (screen) mouse position to
     the current (screen) mouse position */
  void translate(const Vector2i &mousePos);

  /* Translate the camera by the delta amount of (NDC) mouse position.
     Note that NDC position must be in [-1, -1] to [1, 1]. */
  void translateDelta(const Vector2 &translationNDC);

  /* Zoom the camera (positive delta = zoom in, negative = zoom out) */
  void zoom(Float delta);

  /* Get the camera's view transformation as a qual quaternion */
  const DualQuaternion &view() const { return m_view; }

  /* Get the camera's view transformation as a matrix */
  Matrix4 viewMatrix() const { return m_view.toMatrix(); }

  /* Get the camera's inverse view matrix (which also produces
     transformation of the camera) */
  Matrix4 inverseViewMatrix() const { return m_inverseView.toMatrix(); }

  /* Get the camera's transformation as a dual quaternion */
  const DualQuaternion &transformation() const { return m_inverseView; }

  /* Get the camera's transformation matrix */
  Matrix4 transformationMatrix() const { return m_inverseView.toMatrix(); }

  /* Return the distance from the camera position to the center view */
  Float viewDistance() const { return Math::abs(m_targetZooming); }

protected:
  /* Update the camera transformations */
  void updateInternalTransformations();

  /* Transform from screen coordinate to NDC - normalized device
     coordinate. The top-left of the screen corresponds to [-1, 1] NDC,
     and the bottom right is [1, -1] NDC. */
  Vector2 screenCoordToNDC(const Vector2i &mousePos) const;

  Deg m_fov;
  Vector2i m_windowSize;

  Vector2 m_prevMousePosNDC;
  Float m_lagging{};

  Vector3 m_targetPosition, m_currentPosition, m_positionT0;
  Quaternion m_targetQRotation, m_currentQRotation, m_qRotationT0;
  Float m_targetZooming, m_currentZooming, m_zoomingT0;
  DualQuaternion m_view, m_inverseView;
};

#endif
