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

  Program:   Visualization Toolkit
  Module:    vtkAreaPicker.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.

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

#include "vtkAreaPicker.h"
#include "vtkObjectFactory.h"
#include "vtkMapper.h"
#include "vtkAbstractVolumeMapper.h"
#include "vtkImageMapper3D.h"
#include "vtkAbstractMapper3D.h"
#include "vtkProp.h"
#include "vtkLODProp3D.h"
#include "vtkActor.h"
#include "vtkPropCollection.h"
#include "vtkImageSlice.h"
#include "vtkProp3DCollection.h"
#include "vtkAssemblyPath.h"
#include "vtkImageData.h"
#include "vtkVolume.h"
#include "vtkRenderer.h"
#include "vtkProperty.h"
#include "vtkCommand.h"
#include "vtkPlanes.h"
#include "vtkPlane.h"
#include "vtkPoints.h"
#include "vtkExtractSelectedFrustum.h"

vtkStandardNewMacro(vtkAreaPicker);

//--------------------------------------------------------------------------
vtkAreaPicker::vtkAreaPicker()
{
  this->FrustumExtractor = vtkExtractSelectedFrustum::New();
  this->Frustum = this->FrustumExtractor->GetFrustum();
  this->Frustum->Register(this);

  this->ClipPoints = this->FrustumExtractor->GetClipPoints();
  this->ClipPoints->Register(this);

  this->Prop3Ds = vtkProp3DCollection::New();
  this->Mapper = nullptr;
  this->DataSet = nullptr;

  this->X0 = 0.0;
  this->Y0 = 0.0;
  this->X1 = 0.0;
  this->Y1 = 0.0;
}

//--------------------------------------------------------------------------
vtkAreaPicker::~vtkAreaPicker()
{
  this->Prop3Ds->Delete();
  this->ClipPoints->Delete();
  this->Frustum->Delete();
  this->FrustumExtractor->Delete();
}

//--------------------------------------------------------------------------
// Initialize the picking process.
void vtkAreaPicker::Initialize()
{
  this->vtkAbstractPropPicker::Initialize();
  this->Prop3Ds->RemoveAllItems();
  this->Mapper = nullptr;
}

//--------------------------------------------------------------------------
void vtkAreaPicker::SetRenderer(vtkRenderer *renderer)
{
  this->Renderer = renderer;
}
//--------------------------------------------------------------------------
void vtkAreaPicker::SetPickCoords(double x0, double y0, double x1, double y1)
{
  this->X0 = x0;
  this->Y0 = y0;
  this->X1 = x1;
  this->Y1 = y1;
}
//--------------------------------------------------------------------------
int vtkAreaPicker::Pick()
{
  return
    this->AreaPick(this->X0, this->Y0, this->X1, this->Y1, this->Renderer);
}

//--------------------------------------------------------------------------
// Does what this class is meant to do.
int vtkAreaPicker::AreaPick(double x0, double y0, double x1, double y1,
                            vtkRenderer *renderer)
{
  this->Initialize();
  this->X0 = x0;
  this->Y0 = y0;
  this->X1 = x1;
  this->Y1 = y1;
  if (renderer)
  {
    this->Renderer = renderer;
  }

  this->SelectionPoint[0] = (this->X0+this->X1)*0.5;
  this->SelectionPoint[1] = (this->Y0+this->Y1)*0.5;
  this->SelectionPoint[2] = 0.0;

  if ( this->Renderer == nullptr )
  {
    vtkErrorMacro(<<"Must specify renderer!");
    return 0;
  }

  this->DefineFrustum(this->X0, this->Y0, this->X1, this->Y1, this->Renderer);

  return this->PickProps(this->Renderer);
}

//--------------------------------------------------------------------------
//Converts the given screen rectangle into a selection frustum.
//Saves the results in ClipPoints and Frustum.
void vtkAreaPicker::DefineFrustum(double x0, double y0, double x1, double y1,
                                  vtkRenderer *renderer)
{
  this->X0 = (x0 < x1) ? x0 : x1;
  this->Y0 = (y0 < y1) ? y0 : y1;
  this->X1 = (x0 > x1) ? x0 : x1;
  this->Y1 = (y0 > y1) ? y0 : y1;

  if (this->X0 == this->X1)
  {
    this->X1 += 1.0;
  }
  if (this->Y0 == this->Y1)
  {
    this->Y1 += 1.0;
  }

  //compute world coordinates of the pick volume
  double verts[32];
  renderer->SetDisplayPoint(this->X0, this->Y0, 0);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[0]);

  renderer->SetDisplayPoint(this->X0, this->Y0, 1);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[4]);

  renderer->SetDisplayPoint(this->X0, this->Y1, 0);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[8]);

  renderer->SetDisplayPoint(this->X0, this->Y1, 1);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[12]);

  renderer->SetDisplayPoint(this->X1, this->Y0, 0);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[16]);

  renderer->SetDisplayPoint(this->X1, this->Y0, 1);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[20]);

  renderer->SetDisplayPoint(this->X1, this->Y1, 0);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[24]);

  renderer->SetDisplayPoint(this->X1, this->Y1, 1);
  renderer->DisplayToWorld();
  renderer->GetWorldPoint(&verts[28]);

  //a pick point is required by vtkAbstractPicker
  //return center for now until a better meaning is desired
  double sum[3] = {0.0,0.0,0.0};
  for (int i = 0; i < 8; i++)
  {
    sum[0] += verts[i*3+0];
    sum[1] += verts[i*3+1];
    sum[2] += verts[i*3+2];
  }
  this->PickPosition[0] = sum[0]/8.0;
  this->PickPosition[1] = sum[1]/8.0;
  this->PickPosition[2] = sum[2]/8.0;

  this->FrustumExtractor->CreateFrustum(verts);
}

//--------------------------------------------------------------------------
//Decides which props are within the frustum.
//Adds each to the prop3d list and fires pick events.
//Remembers the dataset, mapper, and assembly path for the nearest.
int vtkAreaPicker::PickProps(vtkRenderer *renderer)
{
  vtkProp *prop;
  int pickable;
  double bounds[6];

  //  Initialize picking process
  this->Initialize();
  this->Renderer = renderer;

  // Invoke start pick method if defined
  this->InvokeEvent(vtkCommand::StartPickEvent,nullptr);

  if ( renderer == nullptr )
  {
    vtkErrorMacro(<<"Must specify renderer!");
    return 0;
  }

  //  Loop over all props.
  //
  vtkPropCollection *props;
  vtkProp *propCandidate;
  if ( this->PickFromList )
  {
    props = this->GetPickList();
  }
  else
  {
    props = renderer->GetViewProps();
  }

  vtkAbstractMapper3D *mapper = nullptr;
  vtkAssemblyPath *path;

  double mindist = VTK_DOUBLE_MAX;

  vtkCollectionSimpleIterator pit;
  for ( props->InitTraversal(pit); (prop=props->GetNextProp(pit)); )
  {
    for ( prop->InitPathTraversal(); (path=prop->GetNextPath()); )
    {
      propCandidate = path->GetLastNode()->GetViewProp();
      pickable = this->TypeDecipher(propCandidate, &mapper);

      //  If actor can be picked, see if it is within the pick frustum.
      if ( pickable )
      {
        if ( mapper )
        {
          propCandidate->PokeMatrix(path->GetLastNode()->GetMatrix());
          const double* bds = propCandidate->GetBounds();
          propCandidate->PokeMatrix(nullptr);
          for (int i = 0; i < 6; i++)
          {
            bounds[i] = bds[i];
          }

          double dist;
          if (this->ABoxFrustumIsect(bounds, dist))
          {
            if ( ! this->Prop3Ds->IsItemPresent(prop) )
            {
              this->Prop3Ds->AddItem(static_cast<vtkProp3D *>(prop));
              if (dist < mindist) //new nearest, remember it
              {
                mindist = dist;
                this->SetPath(path);
                this->Mapper = mapper;
                vtkMapper *map1;
                vtkAbstractVolumeMapper *vmap;
                vtkImageMapper3D *imap;
                if ( (map1=vtkMapper::SafeDownCast(mapper)) != nullptr )
                {
                  this->DataSet = map1->GetInput();
                  this->Mapper = map1;
                }
                else if ( (vmap=vtkAbstractVolumeMapper::SafeDownCast(mapper)) != nullptr )
                {
                  this->DataSet = vmap->GetDataSetInput();
                  this->Mapper = vmap;
                }
                else if ( (imap=vtkImageMapper3D::SafeDownCast(mapper)) != nullptr )
                {
                  this->DataSet = imap->GetDataSetInput();
                  this->Mapper = imap;
                }
                else
                {
                  this->DataSet = nullptr;
                }
              }
            }
          }
        }//mapper
      }//pickable

    }//for all parts
  }//for all props

  int picked = 0;

  if (this->Path)
  {
    // Invoke pick method if one defined - prop goes first
    this->Path->GetFirstNode()->GetViewProp()->Pick();
    this->InvokeEvent(vtkCommand::PickEvent,nullptr);
    picked = 1;
  }

  // Invoke end pick method if defined
  this->InvokeEvent(vtkCommand::EndPickEvent,nullptr);

  return picked;
}

//------------------------------------------------------------------------------
//converts the propCandidate into a vtkAbstractMapper3D
//and returns its pickability
int vtkAreaPicker::TypeDecipher(vtkProp *propCandidate,
                                vtkAbstractMapper3D **mapper)
{
  int pickable = 0;
  *mapper = nullptr;

  vtkActor *actor;
  vtkLODProp3D *prop3D;
  vtkProperty *tempProperty;
  vtkVolume *volume;
  vtkImageSlice *imageSlice;

  if ( propCandidate->GetPickable() && propCandidate->GetVisibility() )
  {
    pickable = 1;
    if ( (actor=vtkActor::SafeDownCast(propCandidate)) != nullptr )
    {
      *mapper = actor->GetMapper();
      if ( actor->GetProperty()->GetOpacity() <= 0.0 )
      {
        pickable = 0;
      }
    }
    else if ( (prop3D=vtkLODProp3D::SafeDownCast(propCandidate)) != nullptr )
    {
      int LODId = prop3D->GetPickLODID();
      *mapper = prop3D->GetLODMapper(LODId);
      if ( vtkMapper::SafeDownCast(*mapper) != nullptr)
      {
        prop3D->GetLODProperty(LODId, &tempProperty);
        if ( tempProperty->GetOpacity() <= 0.0 )
        {
          pickable = 0;
        }
      }
    }
    else if ( (volume=vtkVolume::SafeDownCast(propCandidate)) != nullptr )
    {
      *mapper = volume->GetMapper();
    }
    else if ( (imageSlice=vtkImageSlice::SafeDownCast(propCandidate)) != nullptr )
    {
      *mapper = imageSlice->GetMapper();
    }
    else
    {
      pickable = 0; //only vtkProp3D's (actors and volumes) can be picked
    }
  }
  return pickable;
}

//--------------------------------------------------------------------------
//Intersect the bbox represented by the bounds with the clipping frustum.
//Return true if partially inside.
//Also return a distance to the near plane.
int vtkAreaPicker::ABoxFrustumIsect(double *bounds, double &mindist)
{
  if (bounds[0] > bounds[1] ||
      bounds[2] > bounds[3] ||
      bounds[4] > bounds[5])
  {
    return 0;
  }

  double verts[8][3];
  int x, y, z;
  int vid = 0;
  for (x = 0; x < 2; x++)
  {
    for (y = 0; y < 2; y++)
    {
      for (z = 0; z < 2; z++)
      {
        verts[vid][0] = bounds[0+x];
        verts[vid][1] = bounds[2+y];
        verts[vid][2] = bounds[4+z];
        vid++;
      }
    }
  }

  //find distance to the corner nearest the near plane for 'closest' prop
  mindist = -VTK_DOUBLE_MAX;
  vtkPlane *plane = this->Frustum->GetPlane(4); //near plane
  for (vid = 0; vid < 8; vid++)
  {
    double dist = plane->EvaluateFunction(verts[vid]);
    if (dist < 0 && dist > mindist)
    {
      mindist = dist;
    }
  }
  mindist = -mindist;

  //leave the intersection test to the frustum extractor class
  return this->FrustumExtractor->OverallBoundsTest(bounds);
}

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

  os << indent << "Frustum: " << this->Frustum << "\n";
  os << indent << "ClipPoints: " << this->ClipPoints << "\n";
  os << indent << "Mapper: " << this->Mapper << "\n";
  os << indent << "DataSet: " << this->DataSet << "\n";
}
