/*=========================================================================

  Program: LidarView
  Module:  lqRulerReaction.cxx

  Copyright (c) Kitware Inc.
  All rights reserved.
  See LICENSE or http://www.apache.org/licenses/LICENSE-2.0 for details.

  This software is distributed WITHOUT ANY WARRANTY; without even
  the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  PURPOSE.  See the above copyright notice for more information.

=========================================================================*/

#include "lqRulerReaction.h"

#include <QtDebug>

#include <pqActiveObjects.h>
#include <pqApplicationCore.h>
#include <pqRenderView.h>
#include <pqSMAdaptor.h>
#include <pqServerManagerModel.h>
#include <pqView.h>

#include <vtkCallbackCommand.h>
#include <vtkEventQtSlotConnect.h>
#include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h>
#include <vtkSMProxyManager.h>
#include <vtkSMProxyProperty.h>
#include <vtkSMRenderViewProxy.h>

//-----------------------------------------------------------------------------
void lqRulerReaction::mousePressCallback(vtkObject* caller,
  unsigned long vtkNotUsed(eventId),
  void* clientData,
  void* vtkNotUsed(callData))
{

  // Get Mouse Pos
  vtkRenderWindowInteractor* iren = static_cast<vtkRenderWindowInteractor*>(caller);
  int mousepos[2];
  iren->GetEventPosition(mousepos);

  // Get Reaction
  lqRulerReaction* rulerReact = reinterpret_cast<lqRulerReaction*>(clientData);
  if (!rulerReact)
  {
    qCritical() << "Null lqRulerReaction in Ruler callback";
  }

  // if not enabled return
  if (!rulerReact->isEnabled())
  {
    return;
  }

  // if no control key return
  if (!iren->GetControlKey())
  {
    return;
  }

  // Helper lambda to update the ruler widget coordinates points
  auto updateRulerPointWithMousePosition = [&](const char* pointPropertyName, const QPoint& coord)
  {
    auto point = rulerReact->get3DPoint(coord);
    pqSMAdaptor::setMultipleElementProperty(rulerReact->dwr->GetProperty(pointPropertyName), point);
    rulerReact->dwr->UpdateVTKObjects();
    rulerReact->view->getProxy()->UpdateVTKObjects();
    rulerReact->view->render();
  };

  QPoint mpos{ mousepos[0], mousepos[1] };
  if (!rulerReact->started)
  {
    updateRulerPointWithMousePosition("Point1WorldPosition", mpos);
    updateRulerPointWithMousePosition("Point2WorldPosition", mpos);
  }
  else
  {
    updateRulerPointWithMousePosition("Point2WorldPosition", mpos);
  }
  rulerReact->started = !rulerReact->started;
}

//-----------------------------------------------------------------------------
lqRulerReaction::lqRulerReaction(QAction* parent, lqRulerReaction::Mode m)
  : pqReaction(parent)
  , mode(m)
  , view(nullptr)
  , dwr(nullptr)
{

  // Connect View changes
  QObject::connect(
    &pqActiveObjects::instance(), SIGNAL(viewChanged(pqView*)), this, SLOT(onViewChanged(pqView*)));
  // Update for current View if any
  this->onViewChanged(pqActiveObjects::instance().activeView());

  // Connect to View destruction
  pqServerManagerModel* smmodel = pqApplicationCore::instance()->getServerManagerModel();
  QObject::connect(smmodel, SIGNAL(preViewRemoved(pqView*)), this, SLOT(onViewRemoved(pqView*)));

  this->onTriggered(); // Update

  // Set Tooltip
  QString toolTipText;
  if (this->mode == Mode::BETWEEN_2D_POINTS)
  {
    toolTipText = "Measure the distance (in meters) between two screen points.      \
                  <br/>To draw a ruler line, click this button, then hold down      \
                  the <b>Ctrl key</b> and the <b>left button</b> of your mouse.     \
                  <br/><u>This feature is only available in orthogonal projection.</u>";
  }
  else
  {
    toolTipText = "Measure the distance (in meters) between two 3D points.          \
                  <br/>To draw a ruler line, click this button, then hold down      \
                  the <b>Ctrl key</b> and the <b>left button</b> of your mouse.";
  }
  this->parentAction()->setToolTip(toolTipText);

  // Set State
  this->parentAction()->setCheckable(true);
  this->parentAction()->setChecked(false);

  // Update UI for good measure
  this->updateUI();
}

//-----------------------------------------------------------------------------
void lqRulerReaction::onViewChanged(pqView* view)
{
  pqRenderView* rview = qobject_cast<pqRenderView*>(view);

  // Not a Render View selected
  if (!rview)
  {
    return;
  }

  // Did switch current renderView
  if (this->view == nullptr || this->view != view)
  {
    // Destroy old stuff if any
    this->destroyState();

    // Save Active View
    this->setView(rview);

    // Create associated dwr
    this->createDWR();
  }

  // Prevent 2D usage when CameraParallelProjection is off
  if (this->mode == Mode::BETWEEN_2D_POINTS)
  {
    // The Ui needs to be updated, each time the ParalllelProjection property is modified
    this->connection = vtkSmartPointer<vtkEventQtSlotConnect>::New();
    this->connection->Connect(view->getProxy()->GetProperty("CameraParallelProjection"),
      vtkCommand::ModifiedEvent,
      this,
      SLOT(onTriggered()));
  }

  // Update Button / Ruler
  this->onTriggered();

  // Update UI
  this->updateUI();
}

//-----------------------------------------------------------------------------
void lqRulerReaction::onViewRemoved(pqView* view)
{
  // Clear State if the tracked renderview is removed
  if (this->view != nullptr && this->view == view)
  {
    this->destroyState();
  }
}

//-----------------------------------------------------------------------------
void lqRulerReaction::onTriggered()
{
  // Return if no view
  if (!this->view)
  {
    return;
  }

  // Disable If not in Ortho mode
  if (this->mode == Mode::BETWEEN_2D_POINTS)
  {
    bool cameraParallelProjection = pqSMAdaptor::getElementProperty(
      this->view->getProxy()->GetProperty("CameraParallelProjection"))
                                      .toBool();
    this->parentAction()->setEnabled(cameraParallelProjection);
    if (!cameraParallelProjection)
    {
      this->parentAction()->setChecked(false);
    }
  }

  // Display Ruler
  this->displayRuler(this->isEnabled());
}

//-----------------------------------------------------------------------------
void lqRulerReaction::onDisable()
{
  if (this->isEnabled())
  {
    this->displayRuler(false);
    this->parentAction()->setChecked(false);
  }
}

//-----------------------------------------------------------------------------
void lqRulerReaction::setView(pqRenderView* rview)
{
  this->view = rview;

  // Set Mouse callback
  mouseCC.TakeReference(vtkCallbackCommand::New()); // Mouse press callback
  mouseCC->SetClientData(this);
  mouseCC->SetCallback(lqRulerReaction::mousePressCallback);
  vtkSMViewProxy::SafeDownCast(this->view->getProxy())
    ->GetRenderWindow()
    ->GetInteractor()
    ->AddObserver(vtkCommand::LeftButtonPressEvent, mouseCC);
}

//-----------------------------------------------------------------------------
void lqRulerReaction::createDWR()
{
  vtkSMProxyManager* proxyManager = vtkSMProxyManager::GetProxyManager();
  this->dwr.TakeReference(
    proxyManager->NewProxy("representations", "DistanceWidgetRepresentation"));
  vtkSMProxyProperty::SafeDownCast(this->view->getProxy()->GetProperty("Representations"))
    ->AddProxy(this->dwr);
}

//-----------------------------------------------------------------------------
void lqRulerReaction::destroyState()
{
  // Remove View / DWR if any
  if (this->view)
  {
    vtkSMProxyProperty::SafeDownCast(this->view->getProxy()->GetProperty("Representations"))
      ->RemoveProxy(this->dwr);
    this->dwr = nullptr;

    this->view->getProxy()->UpdateVTKObjects();
    this->view->render();
    this->view = nullptr;
  }

  // Reset Callback Command
  if (mouseCC)
  {
    mouseCC->SetClientData(nullptr);
    mouseCC->SetCallback(nullptr);
    mouseCC = nullptr;
  }

  // Reset 'started' state
  this->started = false;
}

//-----------------------------------------------------------------------------
void lqRulerReaction::updateUI()
{
  if (this->view)
  {
    this->view->getProxy()->UpdateVTKObjects();
    this->view->render();
  }
}

//-----------------------------------------------------------------------------
void lqRulerReaction::displayRuler(bool value)
{
  if (this->dwr)
  {
    pqSMAdaptor::setElementProperty(this->dwr->GetProperty("Visibility"), value);
    this->dwr->UpdateVTKObjects();
  }

  if (this->view)
  {
    this->view->getProxy()->UpdateVTKObjects();
    this->view->render();
  }
}

//-----------------------------------------------------------------------------
bool lqRulerReaction::isEnabled()
{
  return this->parentAction()->isChecked();
};

//-----------------------------------------------------------------------------
QList<QVariant> lqRulerReaction::get3DPoint(QPoint coord)
{

  if (!this->view)
  {
    qCritical() << "lqRulerReaction: get3DPoint has null view";
    return QList<QVariant>();
  }

  vtkSMRenderViewProxy* viewProxy = this->view->getRenderViewProxy();
  if (!viewProxy)
  {
    qCritical() << "qRulerReaction: get3DPoint view has null proxy";
    return QList<QVariant>();
  }

  // Was required for origin changes
  // auto viewSize =
  // pqSMAdaptor::getMultipleElementProperty(this->view->getProxy()->GetProperty("ViewSize"));

  double position[4];

  if (this->mode == Mode::BETWEEN_2D_POINTS)
  {
    // in the Mode::BETWEEN_2D_POINTS, we will create a 3D points that project to the mouse
    // coordinate To do so we create a point express in display (or screen) coordinates and let the
    // render convert the coordinate to world.
    double midPlaneDistance = 0.5;
    // VTK points have bottom left origin
    double display3DPoint[3] = {
      static_cast<double>(coord.x()), static_cast<double>(coord.y()), midPlaneDistance
    };
    vtkRenderer* renderer = viewProxy->GetRenderer();
    renderer->SetDisplayPoint(display3DPoint);
    renderer->DisplayToWorld();
    renderer->GetWorldPoint(position);
  }
  else
  {
    // int the Mode::BETWEEN_3D_POINTS, we will lock for an existing 3D points that project to the
    // mouse coordinate For more information look at the
    // vtkSMRenderViewProxy::ConvertDisplayToPointOnSurface function
    int display3DPoint[2] = { coord.x(), coord.y() };
    double normal[3] = { 0, 0, 0 };
    if (!viewProxy->ConvertDisplayToPointOnSurface(display3DPoint, position, normal, true))
    {
      return QList<QVariant>();
    }
  }

  QList<QVariant> point3d;
  point3d << position[0] << position[1] << position[2];

  return point3d;
}
