/****************************************************************************

 Copyright (c) 2018  GeometryFactory Sarl (France).
 Copyright (C) 2002-2014 Gilles Debunne. All rights reserved.

 This file is part of a fork of the QGLViewer library version 2.7.0.

*****************************************************************************/
// $URL: https://github.com/CGAL/cgal/blob/v6.0.1/GraphicsView/include/CGAL/Qt/manipulatedCameraFrame_impl.h $
// $Id: include/CGAL/Qt/manipulatedCameraFrame_impl.h 50cfbde3b84 $
// SPDX-License-Identifier: GPL-3.0-only

#ifdef CGAL_HEADER_ONLY
#define CGAL_INLINE_FUNCTION inline

#include <CGAL/license/GraphicsView.h>

#else
#define CGAL_INLINE_FUNCTION
#endif
#include <CGAL/number_type_config.h>
#include <CGAL/Qt/manipulatedCameraFrame.h>
#include <CGAL/Qt/camera.h>
#include <CGAL/Qt/qglviewer.h>

#include <QMouseEvent>

namespace CGAL{
namespace qglviewer{

/*! Default constructor.

 flySpeed() is set to 0.0 and sceneUpVector() is (0,1,0). The pivotPoint() is
 set to (0,0,0).

  \attention Created object is removeFromMouseGrabberPool(). */
CGAL_INLINE_FUNCTION
ManipulatedCameraFrame::ManipulatedCameraFrame()
    : driveSpeed_(0.0), sceneUpVector_(0.0, 1.0, 0.0),
      rotatesAroundUpVector_(false), zoomsOnPivotPoint_(false) {
  setFlySpeed(0.0);
  removeFromMouseGrabberPool();
  connect(&flyTimer_, SIGNAL(timeout()), SLOT(flyUpdate()));
}

/*! Equal operator. Calls ManipulatedFrame::operator=() and then copy
 * attributes. */
CGAL_INLINE_FUNCTION
ManipulatedCameraFrame &ManipulatedCameraFrame::
operator=(const ManipulatedCameraFrame &mcf) {
  ManipulatedFrame::operator=(mcf);

  setFlySpeed(mcf.flySpeed());
  setSceneUpVector(mcf.sceneUpVector());
  setRotatesAroundUpVector(mcf.rotatesAroundUpVector_);
  setZoomsOnPivotPoint(mcf.zoomsOnPivotPoint_);

  return *this;
}

/*! Copy constructor. Performs a deep copy of all members using operator=(). */
CGAL_INLINE_FUNCTION
ManipulatedCameraFrame::ManipulatedCameraFrame(
    const ManipulatedCameraFrame &mcf)
    : ManipulatedFrame(mcf) {
  removeFromMouseGrabberPool();
  connect(&flyTimer_, SIGNAL(timeout()), SLOT(flyUpdate()));
  (*this) = (mcf);
}

////////////////////////////////////////////////////////////////////////////////

/*! Overloading of ManipulatedFrame::spin().

Rotates the ManipulatedCameraFrame around its pivotPoint() instead of its
origin. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::spin() {
  rotateAroundPoint(spinningQuaternion(), pivotPoint());
}

#ifndef DOXYGEN
/*! Called for continuous frame motion in fly mode (see
  MOVE_FORWARD). Emits manipulated(). */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::flyUpdate() {
  static Vec flyDisp(0.0, 0.0, 0.0);
  switch (action_) {
  case MOVE_FORWARD:
    flyDisp.z = -flySpeed();
    translate(localInverseTransformOf(flyDisp));
    break;
  case MOVE_BACKWARD:
    flyDisp.z = flySpeed();
    translate(localInverseTransformOf(flyDisp));
    break;
  case DRIVE:
    flyDisp.z = flySpeed() * driveSpeed_;
    translate(localInverseTransformOf(flyDisp));
    break;
  default:
    break;
  }

  // Needs to be out of the switch since ZOOM/fastDraw()/wheelEvent use this
  // callback to trigger a final draw(). #CONNECTION# wheelEvent.
  Q_EMIT manipulated();
}
#endif

/*! This method will be called by the Camera when its orientation is changed, so
that the sceneUpVector (private) is changed accordingly. You should not need to
call this method. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::updateSceneUpVector() {
  sceneUpVector_ = inverseTransformOf(Vec(0.0, 1.0, 0.0));
}


////////////////////////////////////////////////////////////////////////////////
//                 M o u s e    h a n d l i n g                               //
////////////////////////////////////////////////////////////////////////////////

#ifndef DOXYGEN
/*! Protected internal method used to handle mouse events. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::startAction(int ma, bool withConstraint) {
  ManipulatedFrame::startAction(ma, withConstraint);

  switch (action_) {
  case MOVE_FORWARD:
  case MOVE_BACKWARD:
  case DRIVE:
    flyTimer_.setSingleShot(false);
    flyTimer_.start(10);
    break;
  case ROTATE:
    constrainedRotationIsReversed_ = transformOf(sceneUpVector_).y < 0.0;
    break;
  default:
    break;
  }
}

CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::zoom(qreal delta, const Camera *const camera) {
  const qreal sceneRadius = camera->sceneRadius();
  if (zoomsOnPivotPoint_) {
    Vec direction = position() - camera->pivotPoint();
    if (direction.norm() > 0.02 * sceneRadius || delta > 0.0)
      translate(delta * direction);
  } else {
    const qreal coef =
        qMax(fabs((camera->frame()->coordinatesOf(camera->pivotPoint())).z),
             qreal(0.2) * sceneRadius);
    Vec trans(0.0, 0.0, -coef * delta);
    translate(inverseTransformOf(trans));
  }
}

#endif

/*! Overloading of ManipulatedFrame::mouseMoveEvent().

Motion depends on mouse binding (see <a href="../mouse.html">mouse page</a> for
details). The resulting displacements are basically inverted from those of a
ManipulatedFrame. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::mouseMoveEvent(QMouseEvent *const event,
                                            Camera *const camera) {
  // #CONNECTION# mouseMoveEvent does the update().
  switch (action_) {
  case TRANSLATE: {
    const QPoint delta = prevPos_ - event->pos();
    Vec trans(delta.x(), -delta.y(), 0.0);
    // Scale to fit the screen mouse displacement
    switch (camera->type()) {
    case Camera::PERSPECTIVE:
      trans *= 2.0 * tan(camera->fieldOfView() / 2.0) *
               fabs((camera->frame()->coordinatesOf(pivotPoint())).z) /
               camera->screenHeight();
      break;
    case Camera::ORTHOGRAPHIC: {
      GLdouble w, h;
      camera->getOrthoWidthHeight(w, h);
      trans[0] *= 2.0 * w / camera->screenWidth();
      trans[1] *= 2.0 * h / camera->screenHeight();
      break;
    }
    }
    translate(inverseTransformOf(translationSensitivity() * trans));
    break;
  }

  case MOVE_FORWARD: {
    Quaternion rot = pitchYawQuaternion(event->position().x(), event->position().y(), camera);
    rotate(rot);
    //#CONNECTION# wheelEvent MOVE_FORWARD case
    // actual translation is made in flyUpdate().
    // translate(inverseTransformOf(Vec(0.0, 0.0, -flySpeed())));
    break;
  }

  case MOVE_BACKWARD: {
    Quaternion rot = pitchYawQuaternion(event->position().x(), event->position().y(), camera);
    rotate(rot);
    // actual translation is made in flyUpdate().
    // translate(inverseTransformOf(Vec(0.0, 0.0, flySpeed())));
    break;
  }

  case DRIVE: {
    Quaternion rot = turnQuaternion(event->position().x(), camera);
    rotate(rot);
    // actual translation is made in flyUpdate().
    driveSpeed_ = 0.01 * (event->position().y() - pressPos_.y());
    break;
  }

  case ZOOM: {
    zoom(deltaWithPrevPos(event, camera), camera);
    break;
  }

  case LOOK_AROUND: {
    Quaternion rot = pitchYawQuaternion(event->position().x(), event->position().y(), camera);
    rotate(rot);
    break;
  }

  case ROTATE: {
    Quaternion rot;
    if (rotatesAroundUpVector_) {
      // Multiply by 2.0 to get on average about the same speed as with the
      // deformed ball
      qreal dx = 2.0 * rotationSensitivity() * (prevPos_.x() - event->position().x()) /
                 camera->screenWidth();
      qreal dy = 2.0 * rotationSensitivity() * (prevPos_.y() - event->position().y()) /
                 camera->screenHeight();
      if (constrainedRotationIsReversed_)
        dx = -dx;
      Vec verticalAxis = transformOf(sceneUpVector_);
      rot = Quaternion(verticalAxis, dx) * Quaternion(Vec(1.0, 0.0, 0.0), dy);
    } else {
      Vec trans = camera->projectedCoordinatesOf(pivotPoint());
      rot = deformedBallQuaternion(event->position().x(), event->position().y(), trans[0], trans[1],
                                   camera);
    }
    //#CONNECTION# These two methods should go together (spinning detection and
    // activation)
    computeMouseSpeed(event);
    setSpinningQuaternion(rot);
    spin();
    break;
  }

  case SCREEN_ROTATE: {
    Vec trans = camera->projectedCoordinatesOf(pivotPoint());

    const qreal angle = atan2(event->position().y() - trans[1], event->position().x() - trans[0]) -
                        atan2(prevPos_.y() - trans[1], prevPos_.x() - trans[0]);

    Quaternion rot(Vec(0.0, 0.0, 1.0), angle);
    //#CONNECTION# These two methods should go together (spinning detection and
    // activation)
    computeMouseSpeed(event);
    setSpinningQuaternion(rot);
    spin();
    updateSceneUpVector();
    break;
  }

  case ROLL: {
    const qreal angle =
      CGAL_PI * (event->position().x() - prevPos_.x()) / camera->screenWidth();
    Quaternion rot(Vec(0.0, 0.0, 1.0), angle);
    rotate(rot);
    setSpinningQuaternion(rot);
    updateSceneUpVector();
    break;
  }

  case SCREEN_TRANSLATE: {
    Vec trans;
    int dir = mouseOriginalDirection(event);
    if (dir == 1)
      trans.setValue(prevPos_.x() - event->position().x(), 0.0, 0.0);
    else if (dir == -1)
      trans.setValue(0.0, event->position().y() - prevPos_.y(), 0.0);

    switch (camera->type()) {
    case Camera::PERSPECTIVE:
      trans *= 2.0 * tan(camera->fieldOfView() / 2.0) *
               fabs((camera->frame()->coordinatesOf(pivotPoint())).z) /
               camera->screenHeight();
      break;
    case Camera::ORTHOGRAPHIC: {
      GLdouble w, h;
      camera->getOrthoWidthHeight(w, h);
      trans[0] *= 2.0 * w / camera->screenWidth();
      trans[1] *= 2.0 * h / camera->screenHeight();
      break;
    }
    }

    translate(inverseTransformOf(translationSensitivity() * trans));
    break;
  }

  default:
    break;
  }

  if (action_ != NO_MOUSE_ACTION) {
    prevPos_ = event->pos();
    if (action_ != ZOOM_ON_REGION)
      // ZOOM_ON_REGION should not emit manipulated().
      // prevPos_ is used to draw rectangle feedback.
      Q_EMIT manipulated();
  }
}

/*! This is an overload of ManipulatedFrame::mouseReleaseEvent(). The
  MouseAction is terminated. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::mouseReleaseEvent(QMouseEvent *const event,
                                               Camera *const camera) {
  if ((action_ == MOVE_FORWARD) ||
      (action_ == MOVE_BACKWARD) || (action_ == DRIVE))
    flyTimer_.stop();

  if (action_ == ZOOM_ON_REGION)
    camera->fitScreenRegion(QRect(pressPos_, event->pos()));

  ManipulatedFrame::mouseReleaseEvent(event, camera);
}

/*! This is an overload of ManipulatedFrame::wheelEvent().

The wheel behavior depends on the wheel binded action. Current possible actions
are ZOOM, MOVE_FORWARD, MOVE_BACKWARD.
ZOOM speed depends on wheelSensitivity() while
MOVE_FORWARD and MOVE_BACKWARD depend on flySpeed(). See
CGAL::QGLViewer::setWheelBinding() to customize the binding. */
CGAL_INLINE_FUNCTION
void ManipulatedCameraFrame::wheelEvent(QWheelEvent *const event,
                                        Camera *const camera) {
  //#CONNECTION# CGAL::QGLViewer::setWheelBinding, ManipulatedFrame::wheelEvent.
  switch (action_) {
  case ZOOM: {
    zoom(wheelDelta(event), camera);
    Q_EMIT manipulated();
    break;
  }
  case MOVE_FORWARD:
  case MOVE_BACKWARD:
    //#CONNECTION# mouseMoveEvent() MOVE_FORWARD case
    translate(
        inverseTransformOf(Vec(0.0, 0.0, 0.2 * flySpeed() * event->angleDelta().y())));
    Q_EMIT manipulated();
    break;
  case ZOOM_FOV:
  {
    qreal delta = - wheelDelta(event);//- sign to keep the same behavior as for the ZOOM action.
    qreal new_fov = delta/100 + camera->fieldOfView();
    if(new_fov > CGAL_PI/180.0)
    {
      new_fov = delta + camera->fieldOfView();
    }
    if(new_fov > CGAL_PI/4.0)
      new_fov = CGAL_PI/4.0;
    if( new_fov >= 0.0)
    {
      camera->setFieldOfView(new_fov);
    }
    Q_EMIT manipulated();
    break;
  }
  default:
    break;
  }

  // #CONNECTION# startAction should always be called before
  if (previousConstraint_)
    setConstraint(previousConstraint_);

  // The wheel triggers a fastDraw. A final update() is needed after the last
  // wheel event to polish the rendering using draw(). Since the last wheel
  // event does not say its name, we use the flyTimer_ to trigger flyUpdate(),
  // which emits manipulated. Two wheel events separated by more than this delay
  // milliseconds will trigger a draw().
  const int finalDrawAfterWheelEventDelay = 400;

  // Starts (or prolungates) the timer.
  flyTimer_.setSingleShot(true);
  flyTimer_.start(finalDrawAfterWheelEventDelay);

  // This could also be done *before* manipulated is emitted, so that
  // isManipulated() returns false. But then fastDraw would not be used with
  // wheel. Detecting the last wheel event and forcing a final draw() is done
  // using the timer_.
  if(action_ != ZOOM_FOV)
    action_ = NO_MOUSE_ACTION;
  //else done after postDraw().

}

////////////////////////////////////////////////////////////////////////////////

/*! Returns a Quaternion that is a rotation around current camera Y,
 * proportionnal to the horizontal mouse position. */
CGAL_INLINE_FUNCTION
Quaternion ManipulatedCameraFrame::turnQuaternion(int x,
                                                  const Camera *const camera) {
  return Quaternion(Vec(0.0, 1.0, 0.0), rotationSensitivity() *
                                            (prevPos_.x() - x) /
                                            camera->screenWidth());
}

/*! Returns a Quaternion that is the composition of two rotations, inferred from
  the mouse pitch (X axis) and yaw (sceneUpVector() axis). */
Quaternion
CGAL_INLINE_FUNCTION
ManipulatedCameraFrame::pitchYawQuaternion(int x, int y,
                                           const Camera *const camera) {
  const Quaternion rotX(Vec(1.0, 0.0, 0.0), rotationSensitivity() *
                                                (prevPos_.y() - y) /
                                                camera->screenHeight());
  const Quaternion rotY(transformOf(sceneUpVector()),
                        rotationSensitivity() * (prevPos_.x() - x) /
                            camera->screenWidth());
  return rotY * rotX;
}

}}
