// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause

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

VTK_ABI_NAMESPACE_BEGIN
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->IndexOfFirstOccurence(prop) < 0)
            {
              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";
}
VTK_ABI_NAMESPACE_END
