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

  Program:   Visualization Toolkit
  Module:    vtkInteractorStyleUnicam.cxx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm 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.

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

/*
 * This work was produced under a grant from the Department of Energy to Brown 
 * University.  Neither Brown University nor the authors assert any copyright 
 * with respect to this work and it may be used, reproduced, and distributed 
 * without permission.
 */

#include "vtkInteractorStyleUnicam.h"

#include "vtkActor.h"
#include "vtkCamera.h"
#include "vtkMath.h"
#include "vtkObjectFactory.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkRenderer.h"
#include "vtkSphereSource.h"
#include "vtkTransform.h"
#include "vtkWorldPointPicker.h"

vtkCxxRevisionMacro(vtkInteractorStyleUnicam, "1.36");
vtkStandardNewMacro(vtkInteractorStyleUnicam);

// define 'TheTime()' function-- returns time in elapsed seconds
#if defined(_WIN32) || defined(WIN32)
# include "vtkWindows.h"

static double TheTime() 
  {return double(GetTickCount())/1000.0;}
#else
#include <sys/time.h>

static double TheTime() 
{
  struct timeval ts; struct timezone tz;
  gettimeofday(&ts, &tz);
  return (double)(ts.tv_sec + ts.tv_usec/1e6);
}
#endif

vtkInteractorStyleUnicam::vtkInteractorStyleUnicam()
{
  // use z-buffer picking
  this->InteractionPicker = vtkWorldPointPicker::New();

  // set to default modes
  this->IsDot = 0;
  this->ButtonDown = VTK_UNICAM_NONE;
  state = 0;            // which camera mode is being used?

  // create focus sphere actor
  vtkSphereSource *sphere = vtkSphereSource::New();
  sphere->SetThetaResolution(6); 
  sphere->SetPhiResolution(6);
  vtkPolyDataMapper *sphereMapper = vtkPolyDataMapper::New();
  sphereMapper->SetInput(sphere->GetOutput());
  sphere->Delete();
  
  // XXX - would like to make the focus sphere not be affected by
  // XXX - the lights-- i.e., always be easily easily seen.  i'm not sure
  // XXX - how to do that.
  this->FocusSphere = vtkActor::New();
  this->FocusSphere->SetMapper(sphereMapper);
  this->FocusSphere->GetProperty()->SetColor(0.8900,0.6600,0.4100);
  this->FocusSphere->GetProperty()->SetRepresentationToWireframe();
  sphereMapper->Delete();
  
  // set WorldUpVector to be z-axis by default
  WorldUpVector[0] = 0;
  WorldUpVector[1] = 0;
  WorldUpVector[2] = 1;
}

vtkInteractorStyleUnicam::~vtkInteractorStyleUnicam() 
{
  this->InteractionPicker->Delete();
  this->FocusSphere->Delete();
}

void vtkInteractorStyleUnicam::PrintSelf(ostream& os, vtkIndent indent) 
{
  this->Superclass::PrintSelf(os,indent);

  os << indent << "Interaction Picker: " << this->InteractionPicker;
//   os << indent << "WorldUpVector: " << this->WorldUpVector;
}

void vtkInteractorStyleUnicam::OnTimer() 
{
  if (this->ButtonDown != VTK_UNICAM_NONE) 
    {
    // restart timer-- we want to keep getting 'OnMouseMove' events
    if (this->UseTimers) 
      {
      this->Interactor->CreateTimer(VTKI_TIMER_UPDATE);
      }
    }
}

void vtkInteractorStyleUnicam::SetWorldUpVector(double x, double y, double z) 
{
  WorldUpVector[0] = x;
  WorldUpVector[1] = y;
  WorldUpVector[2] = z;
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::OnLeftButtonDown() 
{
  int x = this->Interactor->GetEventPosition()[0];
  int y = this->Interactor->GetEventPosition()[1];

  this->ButtonDown = VTK_UNICAM_BUTTON_LEFT;
    if (this->UseTimers) 
      {
      this->Interactor->CreateTimer(VTKI_TIMER_UPDATE);
      }

  this->DTime    = TheTime();
  this->Dist     = 0;

  // cam manip init
  double curpt[2];
  this->NormalizeMouseXY(x, y, &curpt[0], &curpt[1]);
  this->LastPos[0] = curpt[0];
  this->LastPos[1] = curpt[1];

  this->StartPix[0] = this->LastPix[0] = x;
  this->StartPix[1] = this->LastPix[1] = y;

  // Find 'this->DownPt'  (point in world space under the cursor tip)
  // 
  // Note: If no object has been rendered to the pixel (X, Y), then
  // vtkWorldPointPicker will return a z-value with depth equal
  // to the distance from the camera's position to the focal point.
  // This seems like an arbitrary, but perhaps reasonable, default value.
  // 
  this->FindPokedRenderer(x, y);
  this->InteractionPicker->Pick(x, y, 0.0, this->CurrentRenderer);
  this->InteractionPicker->GetPickPosition(this->DownPt);
  
  // if someone has already clicked to make a dot and they're not clicking 
  // on it now, OR if the user is clicking on the perimeter of the screen, 
  // then we want to go into rotation mode.
  if ((fabs(curpt[0]) > .85 || fabs(curpt[1]) > .9) || this->IsDot) 
    {
    if (this->IsDot)
      {
      this->FocusSphere->GetPosition(this->Center);
      }
    state = VTK_UNICAM_CAM_INT_ROT;
    }
  else 
    {
    state = VTK_UNICAM_CAM_INT_CHOOSE;
    }
}

//----------------------------------------------------------------------------
double vtkInteractorStyleUnicam::WindowAspect()
{
  double w = Interactor->GetRenderWindow()->GetSize()[0];
  double h = Interactor->GetRenderWindow()->GetSize()[1];

  return w/h;
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::NormalizeMouseXY(int X, int Y,
                                                double *NX, double *NY)
{
  double w = Interactor->GetRenderWindow()->GetSize()[0];
  double h = Interactor->GetRenderWindow()->GetSize()[1];

  *NX = -1.0 + 2.0 * double(X) / w;
  *NY = -1.0 + 2.0 * double(Y) / h;
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::OnMouseMove()
{
  int x = this->Interactor->GetEventPosition()[0];
  int y = this->Interactor->GetEventPosition()[1];

 // filter out any repeated events
  static int last_X = 0;
  static int last_Y = 0;
  if (x == last_X && y == last_Y)
    {
    return;
    }

  // channel event to right method handler.
  switch (this->ButtonDown) 
    {
    case VTK_UNICAM_BUTTON_LEFT:
      OnLeftButtonMove();
      break;
    }

  last_X = x;
  last_Y = y;

  this->Interactor->Render();  // re-draw scene.. it should have changed
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::OnLeftButtonUp() 
{
  int x = this->Interactor->GetEventPosition()[0];
  int y = this->Interactor->GetEventPosition()[1];

  this->ButtonDown = VTK_UNICAM_NONE;

  if (state == VTK_UNICAM_CAM_INT_ROT && this->IsDot ) 
    {
    this->FocusSphereRenderer->RemoveActor(this->FocusSphere);
    this->IsDot = 0;
    } 
  else if (state == VTK_UNICAM_CAM_INT_CHOOSE) 
    {
    if (this->IsDot) 
      {
      this->FocusSphereRenderer->RemoveActor(this->FocusSphere);
      this->IsDot = 0;
      } 
    else 
      {
      this->FocusSphere->SetPosition(this->DownPt[0],
                                     this->DownPt[1],
                                     this->DownPt[2]);

      double from[3];
      this->FindPokedRenderer(x, y);
      vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
      camera->GetPosition(from);

      double vec[3];
      vec[0] = this->DownPt[0] - from[0];
      vec[1] = this->DownPt[1] - from[1];
      vec[2] = this->DownPt[2] - from[2];

      double at_v[4];
      camera->GetDirectionOfProjection(at_v);
      vtkMath::Normalize(at_v);

      // calculate scale so focus sphere always is the same size on the screen
      double s = 0.02 * vtkMath::Dot(at_v, vec);

      this->FocusSphere->SetScale   (s, s, s);

      this->FindPokedRenderer(x, y);
      this->FocusSphereRenderer = this->CurrentRenderer;
      this->FocusSphereRenderer->AddActor(this->FocusSphere);

      this->IsDot = 1;
      }
    this->Interactor->Render();
    }

  vtkRenderWindowInteractor *rwi = this->Interactor;
  rwi->GetRenderWindow()->SetDesiredUpdateRate(rwi->GetStillUpdateRate());
  rwi->Render();
  if (this->UseTimers)
    {
    rwi->DestroyTimer();
    }
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::OnLeftButtonMove()
{
  int x = this->Interactor->GetEventPosition()[0];
  int y = this->Interactor->GetEventPosition()[1];

  switch (state) 
    {
    case VTK_UNICAM_CAM_INT_CHOOSE:   this->ChooseXY(x, y); break;
    case VTK_UNICAM_CAM_INT_ROT:      this->RotateXY(x, y); break;
    case VTK_UNICAM_CAM_INT_PAN:      this->PanXY(x, y); break;
    case VTK_UNICAM_CAM_INT_DOLLY:    this->DollyXY(x, y); break;
    }
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::ChooseXY( int X, int Y )
{
  int   te[2];  // pixel location
  te[0] = X;
  te[1] = Y;

  double curpt[2];
  this->NormalizeMouseXY(X, Y, &curpt[0], &curpt[1]);
  
  double delta[2];
  delta[0] = curpt[0] - this->LastPos[0];
  delta[1] = curpt[1] - this->LastPos[1];
  this->LastPos[0] = te[0];
  this->LastPos[1] = te[1];

  double tdelt(TheTime() - this->DTime);

  this->Dist += sqrt(delta[0] * delta[0] + delta[1] * delta[1]);

  double sdelt[2];
  sdelt[0] = te[0] - this->StartPix[0];
  sdelt[1] = te[1] - this->StartPix[1];

  int xa=0,ya=1;
  if (getenv("FLIP_CAM_MANIP")) 
    {
    int tmp = xa;
    xa = ya;
    ya = tmp;
    }
     
  double len = sqrt(sdelt[0] * sdelt[0] + sdelt[1] * sdelt[1]);
  if (fabs(sdelt[ya])/len > 0.9 && tdelt > 0.05) 
    {
    state = VTK_UNICAM_CAM_INT_DOLLY;
    } 
  else if (tdelt < 0.1 && this->Dist < 0.03)
    {
    return;
    }
  else 
    {
    if (fabs(sdelt[xa])/len > 0.6 )
      {
      state = VTK_UNICAM_CAM_INT_PAN;
      }
    else 
      {
      state = VTK_UNICAM_CAM_INT_DOLLY;
      }
    }
}

// define some utilty functions
template <class Type>
inline Type clamp(const Type a,
                  const Type b,
                  const Type c) { return a > b ? (a < c ? a : c) : b ; }
inline int  Sign (double a)     { return a > 0 ? 1 : a < 0 ? -1 : 0; }

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::RotateXY( int X, int Y )
{
  double cpt[3];
  double center[3];
  this->FocusSphere->GetPosition(center);
  this->ComputeWorldToDisplay(center[0], center[1], center[2], cpt);
  this->NormalizeMouseXY(static_cast<int>(cpt[0]), static_cast<int>(cpt[1]), 
                         &cpt[0], &cpt[1]);

  double radsq = pow(1.0+fabs(cpt[0]),2.0); // squared rad of virtual cylinder

  double tp[2], te[2];
  this->NormalizeMouseXY(static_cast<int>(this->LastPix[0]), 
                         static_cast<int>(this->LastPix[1]), &tp[0], &tp[1]);
  this->NormalizeMouseXY(X, Y, &te[0], &te[1]);
  this->LastPix[0] = X;
  this->LastPix[1] = Y;

  double op[3], oe[3];
  op[0] = tp[0];
  op[1] = 0;
  op[2] = 0;
  oe[0] = te[0];
  oe[1] = 0;
  oe[2] = 0;

  double opsq = op[0] * op[0], oesq = oe[0] * oe[0];

  double lop  = opsq > radsq ? 0 : sqrt(radsq - opsq);
  double loe  = oesq > radsq ? 0 : sqrt(radsq - oesq);

  double nop[3], noe[3];
  nop[0] = op[0];
  nop[1] = 0;
  nop[2] = lop;
  vtkMath::Normalize(nop);
  noe[0] = oe[0];
  noe[1] = 0;
  noe[2] = loe;
  vtkMath::Normalize(noe);

  double dot = vtkMath::Dot(nop, noe);

  if (fabs(dot) > 0.0001) 
    {
    this->FindPokedRenderer(X, Y);

    double angle = -2*acos(clamp(dot,(double)-1.0,(double)1.0)) * Sign(te[0]-tp[0]);

    double UPvec[3];
    UPvec[0] = WorldUpVector[0];
    UPvec[1] = WorldUpVector[1];
    UPvec[2] = WorldUpVector[2];
    vtkMath::Normalize(UPvec);

    MyRotateCamera(center[0], center[1], center[2], 
                   UPvec[0], UPvec[1], UPvec[2],
                   angle);

    double dvec[3];
    double from[3];
    vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
    camera->GetPosition(from);
    for(int i=0; i<3; i++)
      {
      dvec[i] = from[i] - center[i];
      }
     
    double rdist = te[1]-tp[1];
    vtkMath::Normalize(dvec);

    double atV[4], upV[4], rightV[4];
    camera->GetViewPlaneNormal(atV);
    camera->GetViewUp(upV);
    vtkMath::Cross(upV, atV, rightV);
    vtkMath::Normalize(rightV);

    // 
    // The following two tests try to prevent chaotic camera movement
    // that results from rotating over the poles defined by the
    // "WorldUpVector".  The problem is the constraint to keep the
    // camera's up vector in line w/ the WorldUpVector is at odds with
    // the action of rotating tover the top of the virtual sphere used
    // for rotation.  The solution here is to prevent the user from
    // rotating the last bit required to "go over the top"-- as a
    // consequence, you can never look directly down on the poles.
    // 
    // The "0.99" value is somewhat arbitrary, but seems to produce
    // reasonable results.  (Theoretically, some sort of clamping
    // function could probably be used rather than a hard cutoff, but
    // time constraints prevent figuring that out right now.)
    // 
    const double OVER_THE_TOP_THRESHOLD = 0.99;
    if (vtkMath::Dot(UPvec, atV) >  OVER_THE_TOP_THRESHOLD && rdist < 0)
      rdist = 0;
    if (vtkMath::Dot(UPvec, atV) < -OVER_THE_TOP_THRESHOLD && rdist > 0)
      rdist = 0;

    MyRotateCamera(center[0], center[1], center[2],
                   rightV[0], rightV[1], rightV[2],
                   rdist);

    camera->SetViewUp(UPvec[0], UPvec[1], UPvec[2]);
    }
}

//----------------------------------------------------------------------------
void vtkInteractorStyleUnicam::DollyXY( int X, int Y )
{
  int i;
  double cn[2], ln[2];
  this->NormalizeMouseXY(X, Y, &cn[0], &cn[1]);
  this->NormalizeMouseXY(static_cast<int>(this->LastPix[0]), 
                         static_cast<int>(this->LastPix[1]), &ln[0], &ln[1]);

  double delta[2];
  delta[0] = cn[0] - ln[0];
  delta[1] = cn[1] - ln[1];
  this->LastPix[0] = X;
  this->LastPix[1] = Y;

  // 1. handle dollying
  // XXX - assume perspective projection for now.
  double from[3];
  this->FindPokedRenderer(X, Y);
  vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
  camera->GetPosition(from);
  
  double movec[3];
  for(i=0; i<3; i++)
    {
    movec[i] = this->DownPt[i] - from[i];
    }

  double offset1[3];
  for(i=0; i<3; i++)
    {
    offset1[i] = movec[i] * delta[1] * -4;
    }

  this->MyTranslateCamera(offset1);


  // 2. now handle side-to-side panning
  double rightV[3], upV[3];
  this->GetRightVandUpV(this->DownPt, camera,
                        rightV, upV);

  double offset2[3];
  for(i=0; i<3; i++)
    {
    offset2[i] = (-delta[0] * rightV[i]);
    }

  this->MyTranslateCamera(offset2);
}

//----------------------------------------------------------------------------
// 
// Transform mouse horizontal & vertical movements to a world
// space offset for the camera that maintains pick correlation.
// 
void vtkInteractorStyleUnicam::PanXY( int X, int Y )
{
  double delta[2];
  double cn[2], ln[2];
  int i;

  this->NormalizeMouseXY(X, Y, &cn[0], &cn[1]);
  this->NormalizeMouseXY(static_cast<int>(this->LastPix[0]), 
                         static_cast<int>(this->LastPix[1]), &ln[0], &ln[1]);
  delta[0] = cn[0] - ln[0];
  delta[1] = cn[1] - ln[1];
  this->LastPix[0] = X;
  this->LastPix[1] = Y;

  // XXX - assume perspective projection for now

  this->FindPokedRenderer(X, Y);

  double rightV[3], upV[3];
  vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
  this->GetRightVandUpV(this->DownPt, camera,
                        rightV, upV);

  double offset[3];
  for(i=0; i<3; i++)
    {
    offset[i] = (-delta[0] * rightV[i] +
                 -delta[1] * upV   [i]);
    }

  this->MyTranslateCamera(offset);
}

// 
// Given a 3D point & a vtkCamera, compute the vectors that extend
// from the projection of the center of projection to the center of
// the right-edge and the center of the top-edge onto the plane
// containing the 3D point & with normal parallel to the camera's
// projection plane.
// 
void vtkInteractorStyleUnicam::GetRightVandUpV(double *p, vtkCamera *cam,
                                               double *rightV, double *upV)
{
  int i;

  // Compute the horizontal & vertical scaling ('scalex' and 'scaley')
  // factors as function of the down point & camera params.
  double from[3];
  cam->GetPosition(from);

  // construct a vector from the viewing position to the picked point
  double vec[3];
  for(i=0; i<3; i++)
    {
      vec[i] = p[i] - from[i];
    }

  // Get shortest distance 'l' between the viewing position and
  // plane parallel to the projection plane that contains the 'DownPt'.
  double atV[4];
  cam->GetViewPlaneNormal(atV);
  vtkMath::Normalize(atV);
  double l = -vtkMath::Dot(vec, atV);

  double view_angle = cam->GetViewAngle() * vtkMath::Pi() / 180.0;
  double w = Interactor->GetRenderWindow()->GetSize()[0];
  double h = Interactor->GetRenderWindow()->GetSize()[1];
  double scalex = w/h*((2*l*tan(view_angle/2))/2);
  double scaley =     ((2*l*tan(view_angle/2))/2);

  // construct the camera offset vector as function of delta mouse X & Y.
  cam->GetViewUp(upV);
  vtkMath::Cross(upV, atV, rightV);
  vtkMath::Cross(atV, rightV, upV); // (make sure 'upV' is orthogonal
                                    //  to 'atV' & 'rightV')
  vtkMath::Normalize(rightV);
  vtkMath::Normalize(upV);

  
  for(i=0; i<3; i++)
    {
    rightV[i] = rightV[i] * scalex;
    upV   [i] = upV   [i] * scaley;
    }
}

// 
// Rotate the camera by 'angle' degrees about the point <cx, cy, cz>
// and around the vector/axis <ax, ay, az>.
// 
void vtkInteractorStyleUnicam::MyRotateCamera(double cx, double cy, double cz,
                                              double ax, double ay, double az,
                                              double angle)
{
  angle *= 180.0 / vtkMath::Pi();   // vtk uses degrees, not radians

  double p[4], f[4], u[4];
  vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
  camera->GetPosition  (p);
  camera->GetFocalPoint(f);
  camera->GetViewUp    (u);
  p[3] = f[3] = 1.0; // (points)
  u[3] = 0.0;        // (a vector)

  vtkTransform *t = vtkTransform::New();
  t->PostMultiply();
  t->Identity();
  t->Translate(-cx, -cy, -cz);
  t->RotateWXYZ(angle, ax, ay, az);
  t->Translate( cx,  cy,  cz);

  double new_p[4], new_f[4];
  t->MultiplyPoint(p, new_p);
  t->MultiplyPoint(f, new_f);

  double new_u[4];
  t->Identity();
  t->RotateWXYZ(angle, ax, ay, az);
  t->MultiplyPoint(u, new_u);

  camera->SetPosition  (new_p[0], new_p[1], new_p[2]);
  camera->SetFocalPoint(new_f[0], new_f[1], new_f[2]);
  camera->SetViewUp    (new_u[0], new_u[1], new_u[2]);

  // IMPORTANT!  If you don't re-compute view plane normal, the camera
  // view gets all messed up.
  camera->ComputeViewPlaneNormal();

  t->Delete();
}

// Translate the camera by the offset <v[0], v[1], v[2]>.  Update
// the camera clipping range.
// 
void vtkInteractorStyleUnicam::MyTranslateCamera(double v[3])
{
  double p[3], f[3];
  vtkCamera* camera = this->CurrentRenderer->GetActiveCamera();
  camera->GetPosition  (p);
  camera->GetFocalPoint(f);

  double newP[3], newF[3];
  for(int i=0;i<3;i++) 
    {
    newP[i] = p[i] + v[i];
    newF[i] = f[i] + v[i];
    }

  camera->SetPosition  (newP);
  camera->SetFocalPoint(newF);

  if (this->AutoAdjustCameraClippingRange)
    {
    this->CurrentRenderer->ResetCameraClippingRange();
    }
}

