// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause
#include "vtkVolumePicker.h"
#include "vtkObjectFactory.h"

#include "vtkBox.h"
#include "vtkImageData.h"
#include "vtkVolume.h"
#include "vtkVolumeMapper.h"

VTK_ABI_NAMESPACE_BEGIN
vtkStandardNewMacro(vtkVolumePicker);

//------------------------------------------------------------------------------
vtkVolumePicker::vtkVolumePicker()
{
  this->PickCroppingPlanes = 0;
  this->CroppingPlaneId = -1;
}

//------------------------------------------------------------------------------
vtkVolumePicker::~vtkVolumePicker() = default;

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

  os << indent << "PickCroppingPlanes: " << (this->PickCroppingPlanes ? "On" : "Off") << "\n";

  os << indent << "CroppingPlaneId: " << this->CroppingPlaneId << "\n";
}

//------------------------------------------------------------------------------
void vtkVolumePicker::ResetPickInfo()
{
  this->Superclass::ResetPickInfo();

  this->CroppingPlaneId = -1;
}

//------------------------------------------------------------------------------
// Intersect a vtkVolume with a line by ray casting.  Compared to the
// same method in the superclass, this method will look for cropping planes.

double vtkVolumePicker::IntersectVolumeWithLine(const double p1[3], const double p2[3], double t1,
  double t2, vtkProp3D* prop, vtkAbstractVolumeMapper* mapper)
{
  double tMin = VTK_DOUBLE_MAX;

  vtkImageData* data = vtkImageData::SafeDownCast(mapper->GetDataSetInput());
  vtkVolumeMapper* vmapper = vtkVolumeMapper::SafeDownCast(mapper);

  if (data == nullptr)
  {
    // This picker only works with image inputs
    return VTK_DOUBLE_MAX;
  }

  // Convert ray to structured coordinates
  double spacing[3], origin[3];
  int extent[6];
  data->GetSpacing(spacing);
  data->GetOrigin(origin);
  data->GetExtent(extent);

  double x1[3], x2[3];
  for (int i = 0; i < 3; i++)
  {
    x1[i] = (p1[i] - origin[i]) / spacing[i];
    x2[i] = (p2[i] - origin[i]) / spacing[i];
  }

  // These are set to the plane that the ray enters through
  int planeId = -1;
  int extentPlaneId = -1;

  // There might be multiple regions, depending on cropping flags
  int numSegments = 1;
  double t1List[16], t2List[16], s1List[16];
  int planeIdList[16];
  t1List[0] = t1;
  t2List[0] = t2;
  // s1 is the cropping plane intersection, initialize to large value
  double s1 = s1List[0] = VTK_DOUBLE_MAX;
  planeIdList[0] = -1;

  // Find the cropping bounds in structured coordinates
  double bounds[6];
  for (int j = 0; j < 6; j++)
  {
    bounds[j] = extent[j];
  }

  if (vmapper && vmapper->GetCropping())
  {
    vmapper->GetCroppingRegionPlanes(bounds);
    for (int j = 0; j < 3; j++)
    {
      double b1 = (bounds[2 * j] - origin[j]) / spacing[j];
      double b2 = (bounds[2 * j + 1] - origin[j]) / spacing[j];
      bounds[2 * j] = (b1 < b2 ? b1 : b2);
      bounds[2 * j + 1] = (b1 < b2 ? b2 : b1);
      if (bounds[2 * j] < extent[2 * j])
      {
        bounds[2 * j] = extent[2 * j];
      }
      if (bounds[2 * j + 1] > extent[2 * j + 1])
      {
        bounds[2 * j + 1] = extent[2 * j + 1];
      }
      if (bounds[2 * j] > bounds[2 * j + 1])
      {
        return VTK_DOUBLE_MAX;
      }
    }

    // Get all of the line segments that intersect the visible blocks
    int flags = vmapper->GetCroppingRegionFlags();
    if (!vtkVolumePicker::ClipLineWithCroppingRegion(bounds, extent, flags, x1, x2, t1, t2,
          extentPlaneId, numSegments, t1List, t2List, s1List, planeIdList))
    {
      return VTK_DOUBLE_MAX;
    }
  }
  else
  {
    // If no cropping, then use volume bounds
    double s2;
    if (!vtkVolumePicker::ClipLineWithExtent(extent, x1, x2, s1, s2, extentPlaneId))
    {
      return VTK_DOUBLE_MAX;
    }
    s1List[0] = s1;
    t1List[0] = ((s1 > t1) ? s1 : t1);
    t2List[0] = ((s2 < t2) ? s2 : t2);
  }

  if (this->PickCroppingPlanes && vmapper && vmapper->GetCropping())
  {
    // Only require information about the first intersection
    s1 = s1List[0];
    if (s1 > t1)
    {
      planeId = planeIdList[0];
    }

    // Set data values at the intersected cropping or clipping plane
    if ((tMin = t1List[0]) < this->GlobalTMin)
    {
      this->ResetPickInfo();
      this->DataSet = data;
      this->Mapper = vmapper;

      double x[3];
      for (int j = 0; j < 3; j++)
      {
        x[j] = x1[j] * (1.0 - tMin) + x2[j] * tMin;
        if (planeId >= 0 && j == planeId / 2)
        {
          x[j] = bounds[planeId];
        }
        else if (planeId < 0 && extentPlaneId >= 0 && j == extentPlaneId / 2)
        {
          x[j] = extent[extentPlaneId];
        }
        this->MapperPosition[j] = x[j] * spacing[j] + origin[j];
      }

      this->SetImageDataPickInfo(x, extent);
    }
  }
  else
  {
    // Go through the segments in order, until a hit occurs
    for (int segment = 0; segment < numSegments; segment++)
    {
      if ((tMin = this->Superclass::IntersectVolumeWithLine(
             p1, p2, t1List[segment], t2List[segment], prop, mapper)) < VTK_DOUBLE_MAX)
      {
        s1 = s1List[segment];
        // Keep the first planeId that was set at the first intersection
        // that occurred after t1
        if (planeId < 0 && s1 > t1)
        {
          planeId = planeIdList[segment];
        }
        break;
      }
    }
  }

  if (tMin < this->GlobalTMin)
  {
    this->CroppingPlaneId = planeId;
    // If t1 is at a cropping or extent plane, use the plane normal
    if (planeId < 0)
    {
      planeId = extentPlaneId;
    }
    if (planeId >= 0 && tMin == s1)
    {
      this->MapperNormal[0] = 0.0;
      this->MapperNormal[1] = 0.0;
      this->MapperNormal[2] = 0.0;
      this->MapperNormal[planeId / 2] = 2.0 * (planeId % 2) - 1.0;
      if (spacing[planeId / 2] < 0)
      {
        this->MapperNormal[planeId / 2] = -this->MapperNormal[planeId / 2];
      }
    }
  }

  return tMin;
}

//------------------------------------------------------------------------------
// This method does several things.  Given the volume CroppingRegionPlanes
// stored in bounds (in structured coords), and the volume extent, it
// casts a ray through the 27 "blocks" that the volume has been divided into.
// Each "block" is turned on or off by a bit in "flags".  The result
// of the ray cast is a collection of line segments: the parametric
// start and end of each segment is stored in t1List and t2List respectively.
// If the segment starts at a cropping plane, the planeIdList will store
// the Id of that plane, otherwise planeIdList will store -1 for that segment.

int vtkVolumePicker::ClipLineWithCroppingRegion(const double bounds[6], const int extent[6],
  int flags, const double x1[3], const double x2[3], double t1, double t2, int& extentPlaneId,
  int& numSegments, double* t1List, double* t2List, double* s1List, int* planeIdList)
{
  extentPlaneId = -1;
  numSegments = 0;
  double s1, s2;

  // Start by clipping the line with the volume extent
  if (!vtkVolumePicker::ClipLineWithExtent(extent, x1, x2, s1, s2, extentPlaneId))
  {
    return 0;
  }

  if (s1 >= t1)
  {
    t1 = s1;
  }
  if (s2 <= t2)
  {
    t2 = s2;
  }

  if (t2 < t1)
  {
    return 0;
  }

  // Compute the coordinates that correspond to t1
  double x[3];
  for (int i = 0; i < 3; i++)
  {
    x[i] = x1[i] * (1.0 - t1) + x2[i] * t1;
    // Watch for out-of-bounds due to numerical roundoff
    if (x[i] < extent[2 * i])
    {
      x[i] = extent[2 * i];
    }
    if (x[i] > extent[2 * i + 1])
    {
      x[i] = extent[2 * i + 1];
    }
  }
  if (t1 == s1 && extentPlaneId >= 0)
  {
    // If right on the boundary, set position exactly
    x[extentPlaneId / 2] = extent[extentPlaneId];
  }

  // Find out which block is hit first, store indices and bounds
  int xi[3];
  double blockBounds[6];
  for (int j = 0; j < 3; j++)
  {
    xi[j] = 0;
    blockBounds[2 * j] = extent[2 * j];
    blockBounds[2 * j + 1] = bounds[2 * j];
    // Be particular about the ray direction
    if (x[j] > bounds[2 * j] || (x[j] == bounds[2 * j] && x1[j] < x2[j]))
    {
      xi[j] = 1;
      blockBounds[2 * j] = bounds[2 * j];
      blockBounds[2 * j + 1] = bounds[2 * j + 1];
    }
    if (x[j] > bounds[2 * j + 1] || (x[j] == bounds[2 * j + 1] && x1[j] < x2[j]))
    {
      xi[j] = 2;
      blockBounds[2 * j] = bounds[2 * j + 1];
      blockBounds[2 * j + 1] = extent[2 * j + 1];
    }
  }

  // Loop through the blocks along the ray path
  int plane1 = -1;
  int plane2 = -1;
  for (;;)
  {
    if (!vtkBox::IntersectWithLine(blockBounds, x1, x2, s1, s2, nullptr, nullptr, plane1, plane2))
    {
      // This should never happen, but if it does, stop here
      break;
    }

    int blockId = xi[0] + xi[1] * 3 + xi[2] * 9;
    if ((flags >> blockId) & 1)
    {
      t1List[numSegments] = (t1 > s1 ? t1 : s1);
      t2List[numSegments] = (t2 < s2 ? t2 : s2);
      s1List[numSegments] = s1;
      planeIdList[numSegments] = -1;
      if (plane1 >= 0)
      {
        // Compute  plane1/2  and  plane1%2
        int k = (plane1 >> 1);
        int l = (plane1 & 1);

        // Need to know if the ray is entering the volume, i.e. whether
        // the adjacent block that the ray is coming from is "off", because
        // we can't define a clip plane unless it is off.
        static const int blockInc[3] = { 1, 3, 9 };
        int noPlane = 1;

        if (xi[k] == 1)
        {
          noPlane = (flags >> (blockId + blockInc[k] * (2 * l - 1)) & 1);
          if (!noPlane)
          {
            planeIdList[numSegments] = plane1;
          }
        }
        else if (xi[k] == 0)
        {
          noPlane = (flags >> (blockId + blockInc[k]) & 1);
          if (!noPlane && l == 1)
          {
            planeIdList[numSegments] = 2 * k;
          }
        }
        else if (xi[k] == 2)
        {
          noPlane = (flags >> (blockId - blockInc[k]) & 1);
          if (!noPlane && l == 0)
          {
            planeIdList[numSegments] = 2 * k + 1;
          }
        }
      }

      // Sanity check: allow no segments with negative length
      if (t1List[numSegments] <= t2List[numSegments])
      {
        if (numSegments > 0 && t1List[numSegments] == t2List[numSegments - 1])
        {
          // Concatenate this segment with the previous one
          t2List[numSegments - 1] = t2List[numSegments];
        }
        else
        {
          // Add this segment as a new segment
          numSegments++;
        }
      }
    }

    // If there is no exit plane, the ray terminated and the search is over
    if (plane2 < 0)
    {
      break;
    }

    // Use the exit plane to choose the next block
    int k = plane2 / 2;
    xi[k] += 2 * (plane2 - 2 * k) - 1;

    if (xi[k] == 0)
    {
      blockBounds[2 * k] = extent[2 * k];
      blockBounds[2 * k + 1] = bounds[2 * k];
    }
    else if (xi[k] == 1)
    {
      blockBounds[2 * k] = bounds[2 * k];
      blockBounds[2 * k + 1] = bounds[2 * k + 1];
    }
    else if (xi[k] == 2)
    {
      blockBounds[2 * k] = bounds[2 * k + 1];
      blockBounds[2 * k + 1] = extent[2 * k + 1];
    }
    else
    {
      // Exit, stage right
      break;
    }
  }

  return numSegments;
}
VTK_ABI_NAMESPACE_END
