#include <Magnum/Math/Matrix3.h>

#include "arc_ball.h"

/* Project a point in NDC onto the arcball sphere */
Quaternion ndcToArcBall(const Vector2 &p) {
  const Float dist = Math::dot(p, p);

  /* Point is on sphere */
  if (dist <= 1.0f)
    return {{p.x(), p.y(), Math::sqrt(1.0f - dist)}, 0.0f};

  /* Point is outside sphere */
  else {
    const Vector2 proj = p.normalized();
    return {{proj.x(), proj.y(), 0.0f}, 0.0f};
  }
}

ArcBall::ArcBall(const Vector3 &eye, const Vector3 &viewCenter,
                 const Vector3 &upDir, Deg fov, const Vector2i &windowSize)
    : m_fov{fov}, m_windowSize{windowSize} {
  setViewParameters(eye, viewCenter, upDir);
}

void ArcBall::setViewParameters(const Vector3 &eye, const Vector3 &viewCenter,
                                const Vector3 &upDir) {
  const Vector3 dir = viewCenter - eye;
  Vector3 zAxis = dir.normalized();
  Vector3 xAxis = (Math::cross(zAxis, upDir.normalized())).normalized();
  Vector3 yAxis = (Math::cross(xAxis, zAxis)).normalized();
  xAxis = (Math::cross(zAxis, yAxis)).normalized();

  m_targetPosition = -viewCenter;
  m_targetZooming = -dir.length();
  m_targetQRotation =
      Quaternion::fromMatrix(Matrix3x3{xAxis, yAxis, -zAxis}.transposed())
          .normalized();

  m_positionT0 = m_currentPosition = m_targetPosition;
  m_zoomingT0 = m_currentZooming = m_targetZooming;
  m_qRotationT0 = m_currentQRotation = m_targetQRotation;

  updateInternalTransformations();
}

void ArcBall::reset() {
  m_targetPosition = m_positionT0;
  m_targetZooming = m_zoomingT0;
  m_targetQRotation = m_qRotationT0;
}

void ArcBall::setLagging(const Float lagging) {
  CORRADE_INTERNAL_ASSERT(lagging >= 0.0f && lagging < 1.0f);
  m_lagging = lagging;
}

void ArcBall::initTransformation(const Vector2i &mousePos) {
  m_prevMousePosNDC = screenCoordToNDC(mousePos);
}

void ArcBall::rotate(const Vector2i &mousePos) {
  const Vector2 mousePosNDC = screenCoordToNDC(mousePos);
  const Quaternion currentQRotation = ndcToArcBall(mousePosNDC);
  const Quaternion prevQRotation = ndcToArcBall(m_prevMousePosNDC);
  m_prevMousePosNDC = mousePosNDC;
  m_targetQRotation =
      (currentQRotation * prevQRotation * m_targetQRotation).normalized();
}

void ArcBall::translate(const Vector2i &mousePos) {
  const Vector2 mousePosNDC = screenCoordToNDC(mousePos);
  const Vector2 translationNDC = mousePosNDC - m_prevMousePosNDC;
  m_prevMousePosNDC = mousePosNDC;
  translateDelta(translationNDC);
}

void ArcBall::translateDelta(const Vector2 &translationNDC) {
  /* Half size of the screen viewport at the view center and perpendicular
     with the viewDir */
  const Float hh = Math::abs(m_targetZooming) * Math::tan(m_fov * 0.5f);
  const Float hw = hh * Vector2{m_windowSize}.aspectRatio();

  m_targetPosition += m_inverseView.transformVector(
      {translationNDC.x() * hw, translationNDC.y() * hh, 0.0f});
}

void ArcBall::zoom(const Float delta) { m_targetZooming += delta; }

bool ArcBall::updateTransformation() {
  const Vector3 diffViewCenter = m_targetPosition - m_currentPosition;
  const Quaternion diffRotation = m_targetQRotation - m_currentQRotation;
  const Float diffZooming = m_targetZooming - m_currentZooming;

  const Float dViewCenter = Math::dot(diffViewCenter, diffViewCenter);
  const Float dRotation = Math::dot(diffRotation, diffRotation);
  const Float dZooming = diffZooming * diffZooming;

  /* Nothing change */
  if (dViewCenter < 1.0e-10f && dRotation < 1.0e-10f && dZooming < 1.0e-10f) {
    return false;
  }

  /* Nearly done: just jump directly to the target */
  if (dViewCenter < 1.0e-6f && dRotation < 1.0e-6f && dZooming < 1.0e-6f) {
    m_currentPosition = m_targetPosition;
    m_currentQRotation = m_targetQRotation;
    m_currentZooming = m_targetZooming;

    /* Interpolate between the current transformation and the target
       transformation */
  } else {
    const Float t = 1 - m_lagging;
    m_currentPosition = Math::lerp(m_currentPosition, m_targetPosition, t);
    m_currentZooming = Math::lerp(m_currentZooming, m_targetZooming, t);
    m_currentQRotation =
        Math::slerpShortestPath(m_currentQRotation, m_targetQRotation, t);
  }

  updateInternalTransformations();
  return true;
}

void ArcBall::updateInternalTransformations() {
  m_view = DualQuaternion::translation(Vector3::zAxis(m_currentZooming)) *
          DualQuaternion{m_currentQRotation} *
          DualQuaternion::translation(m_currentPosition);
  m_inverseView = m_view.inverted();
}

Vector2 ArcBall::screenCoordToNDC(const Vector2i &mousePos) const {
  return {mousePos.x() * 2.0f / m_windowSize.x() - 1.0f,
          1.0f - 2.0f * mousePos.y() / m_windowSize.y()};
}
