/*=auto=========================================================================

Portions (c) Copyright 2005 Brigham and Women\"s Hospital (BWH) All Rights Reserved.

See COPYRIGHT.txt
or http://www.slicer.org/copyright/copyright.txt for details.

Program:   3D Slicer
Module:    $RCSfile: vtkMRMLVolumeNode.cxx,v $
Date:      $Date: 2006/03/17 17:01:53 $
Version:   $Revision: 1.14 $

=========================================================================auto=*/

// MRML includes
#include "vtkMRMLLinearTransformNode.h"
#include "vtkMRMLScalarVolumeDisplayNode.h"
#include "vtkMRMLScene.h"
#include "vtkMRMLSubjectHierarchyNode.h"
#include "vtkMRMLVolumeNode.h"
#include "vtkMRMLTransformNode.h"

// VTK includes
#include <vtkAddonMathUtilities.h>
#include <vtkAlgorithmOutput.h>
#include <vtkAppendPolyData.h>
#include <vtkBoundingBox.h>
#include <vtkEventForwarderCommand.h>
#include <vtkGeneralTransform.h>
#include <vtkHomogeneousTransform.h>
#include <vtkImageData.h>
#include <vtkImageDataGeometryFilter.h>
#include <vtkImageFlip.h>
#include <vtkImageReslice.h>
#include <vtkMathUtilities.h>
#include <vtkMatrix3x3.h>
#include <vtkMatrix4x4.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkSmartPointer.h>
#include <vtkTransform.h>
#include <vtkTrivialProducer.h>

#include <algorithm> // For std::min
#include <array>
#include <cassert>
#include <vector>

//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
vtkMRMLVolumeNode::vtkMRMLVolumeNode()
{
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      this->IJKToRASDirections[i][j] = (i == j) ? 1.0 : 0.0;
    }
  }

  for (int i = 0; i < 3; i++)
  {
    this->Spacing[i] = 1.0;
  }

  for (int i = 0; i < 3; i++)
  {
    this->Origin[i] = 0.0;
  }

  this->ImageDataConnection = nullptr;
  this->DataEventForwarder = nullptr;

  this->VoxelVectorType = vtkMRMLVolumeNode::VoxelVectorTypeUndefined;

  this->ContentModifiedEvents->InsertNextValue(vtkMRMLVolumeNode::ImageDataModifiedEvent);
}

//----------------------------------------------------------------------------
vtkMRMLVolumeNode::~vtkMRMLVolumeNode()
{
  this->SetAndObserveImageData(nullptr);
  if (this->DataEventForwarder)
  {
    this->DataEventForwarder->Delete();
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::WriteXML(ostream& of, int nIndent)
{
  Superclass::WriteXML(of, nIndent);

  vtkMRMLWriteXMLBeginMacro(of);
  vtkMRMLWriteXMLVectorMacro(spacing, Spacing, double, 3);
  vtkMRMLWriteXMLVectorMacro(origin, Origin, double, 3);
  vtkMRMLWriteXMLEnumMacro(voxelVectorType, VoxelVectorType);

  // IJKToRASDirections 3x3 C array
  std::stringstream ss;
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      ss << this->IJKToRASDirections[i][j] << " ";
      if (i != 2 && j != 2)
      {
        ss << "  ";
      }
    }
  }
  of << " ijkToRASDirections=\"" << ss.str() << "\"";

  vtkMRMLWriteXMLEndMacro();
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::ReadXMLAttributes(const char** atts)
{
  int disabledModify = this->StartModify();

  Superclass::ReadXMLAttributes(atts);

  vtkMRMLReadXMLBeginMacro(atts);
  vtkMRMLReadXMLVectorMacro(spacing, Spacing, double, 3);
  vtkMRMLReadXMLVectorMacro(origin, Origin, double, 3);
  vtkMRMLReadXMLEnumMacro(voxelVectorType, VoxelVectorType);
  vtkMRMLReadXMLEndMacro();

  const char* attName;
  const char* attValue;
  while (*atts != nullptr)
  {
    attName = *(atts++);
    attValue = *(atts++);
    if (!strcmp(attName, "ijkToRASDirections"))
    {
      std::stringstream ss;
      double val;
      ss << attValue;
      double dirs[3][3];
      for (int i = 0; i < 3; i++)
      {
        for (int j = 0; j < 3; j++)
        {
          ss >> val;
          dirs[i][j] = val;
        }
      }
      this->SetIJKToRASDirections(dirs);
    }
  }

  this->EndModify(disabledModify);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::CopyContent(vtkMRMLNode* anode, bool deepCopy /*=true*/)
{
  MRMLNodeModifyBlocker blocker(this);
  Superclass::CopyContent(anode, deepCopy);

  vtkMRMLVolumeNode* node = vtkMRMLVolumeNode::SafeDownCast(anode);
  if (!node)
  {
    return;
  }
  if (deepCopy)
  {
    vtkSmartPointer<vtkImageData> targetImageData = node->GetImageData();
    if (targetImageData.GetPointer() != nullptr)
    {
      targetImageData = vtkSmartPointer<vtkImageData>::Take(node->GetImageData()->NewInstance());
      targetImageData->DeepCopy(node->GetImageData());
    }
    this->SetAndObserveImageData(targetImageData); // invokes vtkMRMLVolumeNode::ImageDataModifiedEvent, which is not masked by StartModify
  }
  else
  {
    // shallow-copy
    this->SetAndObserveImageData(node->GetImageData()); // invokes vtkMRMLVolumeNode::ImageDataModifiedEvent, which is not masked by StartModify
  }

  // targetScalarVolumeNode->SetAndObserveTransformNodeID is not called, as we want to keep the currently applied transform
  this->CopyOrientation(node);
  this->SetVoxelVectorType(node->GetVoxelVectorType());
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::CopyOrientation(vtkMRMLVolumeNode* node)
{
  MRMLNodeModifyBlocker blocker(this);
  vtkSmartPointer<vtkMatrix4x4> ijkToRasmatrix = vtkSmartPointer<vtkMatrix4x4>::New();
  node->GetIJKToRASMatrix(ijkToRasmatrix);
  this->SetIJKToRASMatrix(ijkToRasmatrix);
}

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

  vtkMRMLPrintBeginMacro(os, indent);
  vtkMRMLPrintVectorMacro(Spacing, double, 3);
  vtkMRMLPrintVectorMacro(Origin, double, 3);
  vtkMRMLPrintEnumMacro(VoxelVectorType);

  os << indent << "IJKToRASDirections:\n";
  for (int i = 0; i < 3; i++)
  {
    os << indent.GetNextIndent()                //
       << this->IJKToRASDirections[i][0] << " " //
       << this->IJKToRASDirections[i][1] << " " //
       << this->IJKToRASDirections[i][2] << "\n";
  }

  if (this->GetImageData() != nullptr)
  {
    os << indent << "ImageData:\n";
    this->GetImageData()->PrintSelf(os, indent.GetNextIndent());
  }

  vtkMRMLPrintEndMacro();
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIJKToRASDirections(double dirs[3][3])
{
  bool isModified = false;
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      if (!vtkMathUtilities::FuzzyCompare<double>(this->IJKToRASDirections[i][j], dirs[i][j]))
      {
        this->IJKToRASDirections[i][j] = dirs[i][j];
        isModified = true;
      }
    }
  }
  if (isModified)
  {
    this->StorableModifiedTime.Modified();
    this->Modified();
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIJKToRASDirections(double ir, double jr, double kr, double ia, double ja, double ka, double is, double js, double ks)
{
  double dirs[3][3] = { { ir, jr, kr }, { ia, ja, ka }, { is, js, ks } };
  this->SetIJKToRASDirections(dirs);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIToRASDirection(double ir, double ia, double is)
{
  double dirs[3][3] = { { ir, this->IJKToRASDirections[0][1], this->IJKToRASDirections[0][2] },
                        { ia, this->IJKToRASDirections[1][1], this->IJKToRASDirections[1][2] },
                        { is, this->IJKToRASDirections[2][1], this->IJKToRASDirections[2][2] } };
  this->SetIJKToRASDirections(dirs);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetJToRASDirection(double jr, double ja, double js)
{
  double dirs[3][3] = { { this->IJKToRASDirections[0][0], jr, this->IJKToRASDirections[0][2] },
                        { this->IJKToRASDirections[1][0], ja, this->IJKToRASDirections[1][2] },
                        { this->IJKToRASDirections[2][0], js, this->IJKToRASDirections[2][2] } };
  this->SetIJKToRASDirections(dirs);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetKToRASDirection(double kr, double ka, double ks)
{
  double dirs[3][3] = { { this->IJKToRASDirections[0][0], this->IJKToRASDirections[0][1], kr },
                        { this->IJKToRASDirections[1][0], this->IJKToRASDirections[1][1], ka },
                        { this->IJKToRASDirections[2][0], this->IJKToRASDirections[2][1], ks } };
  this->SetIJKToRASDirections(dirs);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetIToRASDirection(double dirs[3])
{
  for (int i = 0; i < 3; i++)
  {
    dirs[i] = IJKToRASDirections[i][0];
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetJToRASDirection(double dirs[3])
{
  for (int i = 0; i < 3; i++)
  {
    dirs[i] = IJKToRASDirections[i][1];
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetKToRASDirection(double dirs[3])
{
  for (int i = 0; i < 3; i++)
  {
    dirs[i] = IJKToRASDirections[i][2];
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetSpacing(double arg1, double arg2, double arg3)
{
  if (!vtkMathUtilities::FuzzyCompare<double>(this->Spacing[0], arg1) || //
      !vtkMathUtilities::FuzzyCompare<double>(this->Spacing[1], arg2) || //
      !vtkMathUtilities::FuzzyCompare<double>(this->Spacing[2], arg3))
  {
    this->Spacing[0] = arg1;
    this->Spacing[1] = arg2;
    this->Spacing[2] = arg3;
    this->StorableModifiedTime.Modified();
    this->Modified();
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetSpacing(double arg[3])
{
  this->SetSpacing(arg[0], arg[1], arg[2]);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetOrigin(double arg1, double arg2, double arg3)
{
  if (!vtkMathUtilities::FuzzyCompare<double>(this->Origin[0], arg1) || //
      !vtkMathUtilities::FuzzyCompare<double>(this->Origin[1], arg2) || //
      !vtkMathUtilities::FuzzyCompare<double>(this->Origin[2], arg3))
  {
    this->Origin[0] = arg1;
    this->Origin[1] = arg2;
    this->Origin[2] = arg3;
    this->StorableModifiedTime.Modified();
    this->Modified();
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetOrigin(double arg[3])
{
  this->SetOrigin(arg[0], arg[1], arg[2]);
}

//----------------------------------------------------------------------------
double vtkMRMLVolumeNode::GetMinSpacing()
{
  if (this->GetSpacing() == nullptr)
  {
    return 0;
  }
  double minSpace = this->GetSpacing()[0];
  for (int i = 1; i < 3; ++i)
  {
    minSpace = std::min(this->GetSpacing()[i], minSpace);
  }
  return minSpace;
}

//----------------------------------------------------------------------------
double vtkMRMLVolumeNode::GetMaxSpacing()
{
  if (this->GetSpacing() == nullptr)
  {
    return 0;
  }
  double maxSpace = this->GetSpacing()[0];
  for (int i = 1; i < 3; ++i)
  {
    maxSpace = std::max(this->GetSpacing()[i], maxSpace);
  }
  return maxSpace;
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIJKToRASMatrix(vtkMatrix4x4* argMat)
{
  if (argMat == nullptr)
  {
    return;
  }
  vtkNew<vtkMatrix4x4> mat;
  mat->DeepCopy(argMat);

  // normalize direction vectors
  double spacing[3];
  int col;
  for (col = 0; col < 3; col++)
  {
    double len = 0;
    int row;
    for (row = 0; row < 3; row++)
    {
      len += mat->GetElement(row, col) * mat->GetElement(row, col);
    }
    len = sqrt(len);
    spacing[col] = len;
    for (row = 0; row < 3; row++)
    {
      mat->SetElement(row, col, mat->GetElement(row, col) / len);
    }
  }

  double dirs[3][3];
  double origin[3];
  for (int row = 0; row < 3; row++)
  {
    for (int col = 0; col < 3; col++)
    {
      dirs[row][col] = mat->GetElement(row, col);
    }
    origin[row] = mat->GetElement(row, 3);
  }

  int disabledModify = this->StartModify();
  this->SetIJKToRASDirections(dirs);
  this->SetSpacing(spacing);
  this->SetOrigin(origin);
  this->EndModify(disabledModify);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetRASToIJKMatrix(vtkMatrix4x4* mat)
{
  vtkNew<vtkMatrix4x4> m;
  m->Identity();
  if (mat)
  {
    m->DeepCopy(mat);
  }
  m->Invert();
  this->SetIJKToRASMatrix(m.GetPointer());
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetIJKToRASMatrix(vtkMatrix4x4* mat)
{
  // this is the full matrix including the spacing and origin
  mat->Identity();
  int row, col;
  for (row = 0; row < 3; row++)
  {
    for (col = 0; col < 3; col++)
    {
      mat->SetElement(row, col, this->Spacing[col] * IJKToRASDirections[row][col]);
    }
    mat->SetElement(row, 3, this->Origin[row]);
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetRASToIJKMatrix(vtkMatrix4x4* mat)
{
  this->GetIJKToRASMatrix(mat);
  mat->Invert();
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetIJKToRASDirections(double dirs[3][3])
{
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      dirs[i][j] = IJKToRASDirections[i][j];
    }
  }
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIJKToRASDirectionMatrix(vtkMatrix4x4* ijkToRASDirectionMatrix)
{
  double dirs[3][3];
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      dirs[i][j] = ijkToRASDirectionMatrix->Element[i][j];
    }
  }
  this->SetIJKToRASDirections(dirs);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetIJKToRASDirectionMatrix(vtkMatrix4x4* ijkToRASDirectionMatrix)
{
  double dirs[3][3];
  this->GetIJKToRASDirections(dirs);
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      ijkToRASDirectionMatrix->Element[i][j] = dirs[i][j];
    }
  }
}

//----------------------------------------------------------------------------
bool vtkMRMLVolumeNode::ComputeIJKToRASFromScanOrder(const char* order, const double* spacing, const int* dims, bool centerImage, vtkMatrix4x4* IJKToRAS)
{
  IJKToRAS->Identity();
  if (order == nullptr)
  {
    return false;
  }

  vtkNew<vtkMatrix4x4> scaleMat;
  scaleMat->Identity();
  scaleMat->SetElement(0, 0, spacing[0]);
  scaleMat->SetElement(1, 1, spacing[1]);
  scaleMat->SetElement(2, 2, spacing[2]);

  vtkNew<vtkMatrix4x4> orientMat;
  orientMat->Identity();

  if (!strcmp(order, "IS") ||       //
      !strcmp(order, "Axial IS") || //
      !strcmp(order, "Axial"))
  {
    const double elems[] = { -1, 0,  0, 0, //
                             0,  -1, 0, 0, //
                             0,  0,  1, 0, //
                             0,  0,  0, 1 };
    orientMat->DeepCopy(elems);
  }
  else if (!strcmp(order, "SI") || //
           !strcmp(order, "Axial SI"))
  {
    const double elems[] = { -1, 0,  0,  0, //
                             0,  -1, 0,  0, //
                             0,  0,  -1, 0, //
                             0,  0,  0,  1 };
    orientMat->DeepCopy(elems);
  }
  else if (!strcmp(order, "RL") ||          //
           !strcmp(order, "Sagittal RL") || //
           !strcmp(order, "Sagittal"))
  {
    const double elems[] = { 0,  0,  -1, 0, //
                             -1, 0,  0,  0, //
                             0,  -1, 0,  0, //
                             0,  0,  0,  1 };
    orientMat->DeepCopy(elems);
  }
  else if (!strcmp(order, "LR") || //
           !strcmp(order, "Sagittal LR"))
  {
    const double elems[] = { 0,  0,  1, 0, //
                             -1, 0,  0, 0, //
                             0,  -1, 0, 0, //
                             0,  0,  0, 1 };
    orientMat->DeepCopy(elems);
  }
  else if (!strcmp(order, "PA") ||         //
           !strcmp(order, "Coronal PA") || //
           !strcmp(order, "Coronal"))
  {
    const double elems[] = { -1, 0,  0, 0, //
                             0,  0,  1, 0, //
                             0,  -1, 0, 0, //
                             0,  0,  0, 1 };
    orientMat->DeepCopy(elems);
  }
  else if (!strcmp(order, "AP") || //
           !strcmp(order, "Coronal AP"))
  {
    const double elems[] = { -1, 0,  0,  0, //
                             0,  0,  -1, 0, //
                             0,  -1, 0,  0, //
                             0,  0,  0,  1 };
    orientMat->DeepCopy(elems);
  }
  else
  {
    return false;
  }

  vtkMatrix4x4::Multiply4x4(orientMat.GetPointer(), scaleMat.GetPointer(), IJKToRAS);

  const double pnt[] = { static_cast<double>(-dims[0] / 2), //
                         static_cast<double>(-dims[1] / 2), //
                         static_cast<double>(-dims[2] / 2), //
                         static_cast<double>(0.0) };

  const double* const pnt1 = IJKToRAS->MultiplyDoublePoint(pnt);

  if (centerImage)
  {
    for (int j = 0; j < 3; j++)
    {
      IJKToRAS->SetElement(j, 3, pnt1[j]);
    }
  }
  return true;
}

//----------------------------------------------------------------------------
const char* vtkMRMLVolumeNode::ComputeScanOrderFromIJKToRAS(vtkMatrix4x4* ijkToRAS)
{
  double dir[4] = { 0, 0, 1, 0 };
  double kvec[4];

  ijkToRAS->MultiplyPoint(dir, kvec);
  int max_comp = 0;
  double max = fabs(kvec[0]);

  for (int i = 1; i < 3; i++)
  {
    if (fabs(kvec[i]) > max)
    {
      max = fabs(kvec[i]);
      max_comp = i;
    }
  }

  switch (max_comp)
  {
    case 0:
      if (kvec[max_comp] > 0)
      {
        return "LR";
      }
      else
      {
        return "RL";
      }
      break;
    case 1:
      if (kvec[max_comp] > 0)
      {
        return "PA";
      }
      else
      {
        return "AP";
      }
      break;
    case 2:
      if (kvec[max_comp] > 0)
      {
        return "IS";
      }
      else
      {
        return "SI";
      }
      break;
    default: vtkErrorWithObjectMacro(ijkToRAS, "Max components " << max_comp << " not in valid range 0,1,2\n"); return "";
  }
}

//----------------------------------------------------------------------------
namespace
{

//----------------------------------------------------------------------------
// Checks that a vtkImageData has origin (0,0,0), spacing (1,1,1), and an identity direction matrix
bool IsImageDataGeometryValid(vtkImageData* imageData)
{
  double spacing[3] = { 0.0 };
  imageData->GetSpacing(spacing);
  if (spacing[0] != 1.0 || spacing[1] != 1.0 || spacing[2] != 1.0)
  {
    return false;
  }
  double origin[3] = { 0.0 };
  imageData->GetOrigin(origin);
  if (origin[0] != 0.0 || origin[1] != 0.0 || origin[2] != 0.0)
  {
    return false;
  }
  vtkMatrix3x3* directionMatrix = imageData->GetDirectionMatrix();
  vtkNew<vtkMatrix3x3> identityMatrix;
  identityMatrix->Identity();
  if (!vtkAddonMathUtilities::MatrixAreEqual(directionMatrix, identityMatrix, /* tolerance= */ 0.0))
  {
    return false;
  }
  return true;
}

} // end of anonymous namespace

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetAndObserveImageData(vtkImageData* imageData)
{
  if (imageData == nullptr)
  {
    vtkTrivialProducer* oldProducer = vtkTrivialProducer::SafeDownCast(this->GetImageDataConnection() ? this->GetImageDataConnection()->GetProducer() : nullptr);
    if (oldProducer && oldProducer->GetOutputDataObject(0))
    {
      oldProducer->GetOutputDataObject(0)->RemoveObservers(vtkCommand::ModifiedEvent, this->DataEventForwarder);
    }
    this->SetImageDataConnection(nullptr);
  }
  else
  {
    if (!IsImageDataGeometryValid(imageData))
    {
      vtkWarningMacro(                                                              //
        "SetAndObserveImageData: The vtkImageData associated with the Volume node " //
        << (this->GetID() != nullptr ? this->GetID() : "(unknown)")                 //
        << " does not meet the required properties:"                                //
        << " Origin must be (0,0,0), Spacing must be (1,1,1),"                      //
        << " and Direction must be an identity matrix.");
    }
    vtkTrivialProducer* oldProducer = vtkTrivialProducer::SafeDownCast(this->GetImageDataConnection() ? this->GetImageDataConnection()->GetProducer() : nullptr);
    if (oldProducer && oldProducer->GetOutputDataObject(0) == imageData)
    {
      return;
    }
    else if (oldProducer && oldProducer->GetOutputDataObject(0))
    {
      oldProducer->GetOutputDataObject(0)->RemoveObservers(vtkCommand::ModifiedEvent, this->DataEventForwarder);
    }
    vtkNew<vtkTrivialProducer> tp;
    tp->SetOutput(imageData);
    // Propagate ModifiedEvent onto the trivial producer to make sure
    // ImageDataModifiedEvent is triggered.
    if (!this->DataEventForwarder)
    {
      this->DataEventForwarder = vtkEventForwarderCommand::New();
    }
    this->DataEventForwarder->SetTarget(tp.GetPointer());
    imageData->AddObserver(vtkCommand::ModifiedEvent, this->DataEventForwarder);
    this->SetImageDataConnection(tp->GetOutputPort());
  }
}

//---------------------------------------------------------------------------
vtkImageData* vtkMRMLVolumeNode::GetImageData()
{
  vtkAlgorithm* producer = this->ImageDataConnection ? this->ImageDataConnection->GetProducer() : nullptr;
  return vtkImageData::SafeDownCast(producer ? producer->GetOutputDataObject(this->ImageDataConnection->GetIndex()) : nullptr);
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetImageDataConnection(vtkAlgorithmOutput* newImageDataConnection)
{
  if (newImageDataConnection == this->ImageDataConnection)
  {
    return;
  }

  vtkAlgorithm* oldImageDataAlgorithm = this->ImageDataConnection ? this->ImageDataConnection->GetProducer() : nullptr;

  this->ImageDataConnection = newImageDataConnection;

  vtkAlgorithm* imageDataAlgorithm = this->ImageDataConnection ? this->ImageDataConnection->GetProducer() : nullptr;
  if (imageDataAlgorithm != nullptr)
  {
    vtkObserveMRMLObjectMacro(imageDataAlgorithm);
    imageDataAlgorithm->Register(this);
  }

  this->SetImageDataToDisplayNodes();

  if (oldImageDataAlgorithm != nullptr)
  {
    vtkUnObserveMRMLObjectMacro(oldImageDataAlgorithm);
    oldImageDataAlgorithm->UnRegister(this);
  }

  this->StorableModifiedTime.Modified();
  this->Modified();
  this->InvokeCustomModifiedEvent(vtkMRMLVolumeNode::ImageDataModifiedEvent);
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetImageDataToDisplayNodes()
{
  int ndisp = this->GetNumberOfDisplayNodes();
  for (int n = 0; n < ndisp; n++)
  {
    vtkMRMLVolumeDisplayNode* dnode = vtkMRMLVolumeDisplayNode::SafeDownCast(this->GetNthDisplayNode(n));
    if (dnode)
    {
      this->SetImageDataToDisplayNode(dnode);
    }
  }
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetImageDataToDisplayNode(vtkMRMLVolumeDisplayNode* volumeDisplayNode)
{
  assert(volumeDisplayNode);
  volumeDisplayNode->SetInputImageDataConnection(this->GetImageDataConnection());
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::OnNodeReferenceAdded(vtkMRMLNodeReference* reference)
{
  this->UpdateDisplayNodeImageData(vtkMRMLDisplayNode::SafeDownCast(reference->GetReferencedNode()));
  Superclass::OnNodeReferenceAdded(reference);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::OnNodeReferenceModified(vtkMRMLNodeReference* reference)
{
  this->UpdateDisplayNodeImageData(vtkMRMLDisplayNode::SafeDownCast(reference->GetReferencedNode()));
  Superclass::OnNodeReferenceModified(reference);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::UpdateDisplayNodeImageData(vtkMRMLDisplayNode* dNode)
{
  vtkMRMLVolumeDisplayNode* vNode = vtkMRMLVolumeDisplayNode::SafeDownCast(dNode);
  if (vNode)
  {
    vNode->SetInputImageDataConnection(this->ImageDataConnection);
  }
}

//-----------------------------------------------------------
void vtkMRMLVolumeNode::UpdateScene(vtkMRMLScene* scene)
{
  Superclass::UpdateScene(scene);

  this->SetAndObserveImageData(this->GetImageData());
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::ProcessMRMLEvents(vtkObject* caller, unsigned long event, void* callData)
{
  Superclass::ProcessMRMLEvents(caller, event, callData);

  // did the image data change?
  if (this->ImageDataConnection != nullptr &&                                           //
      this->ImageDataConnection->GetProducer() == vtkAlgorithm::SafeDownCast(caller) && //
      event == vtkCommand::ModifiedEvent)
  {
    this->InvokeCustomModifiedEvent(vtkMRMLVolumeNode::ImageDataModifiedEvent);
    return;
  }

  return;
}

//---------------------------------------------------------------------------
vtkMRMLVolumeDisplayNode* vtkMRMLVolumeNode::GetVolumeDisplayNode()
{
  int ndnodes = this->GetNumberOfDisplayNodes();
  for (int i = 0; i < ndnodes; i++)
  {
    vtkMRMLVolumeDisplayNode* dnode = vtkMRMLVolumeDisplayNode::SafeDownCast(this->GetNthDisplayNode(i));
    if (dnode)
    {
      return dnode;
    }
  }
  return nullptr;
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetMetaDataDictionary(const itk::MetaDataDictionary& dictionary)
{
  this->Dictionary = dictionary;
  this->Modified();
}

//---------------------------------------------------------------------------
const itk::MetaDataDictionary& vtkMRMLVolumeNode::GetMetaDataDictionary() const
{
  return this->Dictionary;
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::ApplyTransformMatrix(vtkMatrix4x4* transformMatrix)
{
  vtkNew<vtkMatrix4x4> ijkToRASMatrix;
  vtkNew<vtkMatrix4x4> newIJKToRASMatrix;

  this->GetIJKToRASMatrix(ijkToRASMatrix.GetPointer());
  vtkMatrix4x4::Multiply4x4(transformMatrix, ijkToRASMatrix.GetPointer(), newIJKToRASMatrix.GetPointer());

  this->SetIJKToRASMatrix(newIJKToRASMatrix.GetPointer());
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetRASBounds(double bounds[6])
{
  this->GetSliceBounds(bounds, nullptr);
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetSliceBounds(double bounds[6], vtkMatrix4x4* rasToSlice, bool useVoxelCenter /*=false*/)
{
  vtkMRMLVolumeNode::GetBoundsInternal(bounds, rasToSlice, true, useVoxelCenter);
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetBounds(double bounds[6])
{
  vtkMRMLVolumeNode::GetBoundsInternal(bounds, nullptr, false);
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetBoundsInternal(double bounds[6], vtkMatrix4x4* rasToSlice, bool useTransform, bool useVoxelCenter /*=false*/)
{
  vtkMath::UninitializeBounds(bounds);
  vtkImageData* volumeImage = this->GetImageData();
  if (!volumeImage)
  {
    return;
  }

  //
  // Get the size of the volume in RAS space
  // - map the size of the volume in IJK into RAS
  // - map the middle of the volume to RAS for the center
  //   (IJK space always has origin at first pixel)
  //

  vtkNew<vtkGeneralTransform> transform;
  transform->PostMultiply();
  transform->Identity();

  vtkNew<vtkMatrix4x4> ijkToRAS;
  this->GetIJKToRASMatrix(ijkToRAS.GetPointer());
  transform->Concatenate(ijkToRAS.GetPointer());

  vtkMRMLTransformNode* transformNode = this->GetParentTransformNode();
  bool isTransformLinear = true;

  if (useTransform && transformNode)
  {
    isTransformLinear = transformNode->IsTransformToWorldLinear();
    vtkNew<vtkGeneralTransform> worldTransform;
    worldTransform->Identity();
    transformNode->GetTransformToWorld(worldTransform.GetPointer());
    transform->Concatenate(worldTransform.GetPointer());
  }
  if (rasToSlice)
  {
    transform->Concatenate(rasToSlice);
  }

  int dimensions[3] = { 0 };
  volumeImage->GetDimensions(dimensions);
  double doubleDimensions[4] = { 0, 0, 0, 1 };
  vtkBoundingBox boundingBox;

  // If volume is linearly transformed, it is enough to get bounds of the 8 corner points,
  // but if non-linear transform is applied then sides of the volume may bulge out, therefore
  // we need to take samples all over the volume.
  // Note: In most cases, it would be enough to take samples from the volume boundaries,
  // but in some cases non-linear transform may transform the internals of the volume outside its boundaries,
  // so it is more robust to sample the internals as well. Since taking samples all over the volume only approximately
  // doubles number of point samples compared to sampling only the surface, the additional computation time is
  // not likely to cause any performance issues. Also, the number of samples (8x8x8 = 512 samples) is negligible
  // compared to the number of points that are resampled when an image is transformed (hundreds of thousand of points per single slice).
  int numberOfSubdivisions = isTransformLinear ? 2 : 8;
  double positionScale[3] = { 1.0, 1.0, 1.0 };
  double positionOffset = 0.0;
  if (useVoxelCenter)
  {
    positionScale[0] = dimensions[0] / (numberOfSubdivisions - 1.0) - 1;
    positionScale[1] = dimensions[1] / (numberOfSubdivisions - 1.0) - 1;
    positionScale[2] = dimensions[2] / (numberOfSubdivisions - 1.0) - 1;
  }
  else
  {
    positionScale[0] = dimensions[0] / (numberOfSubdivisions - 1.0);
    positionScale[1] = dimensions[1] / (numberOfSubdivisions - 1.0);
    positionScale[2] = dimensions[2] / (numberOfSubdivisions - 1.0);
    positionOffset = -0.5;
  }
  for (int i = 0; i < numberOfSubdivisions; i++)
  {
    for (int j = 0; j < numberOfSubdivisions; j++)
    {
      for (int k = 0; k < numberOfSubdivisions; k++)
      {
        doubleDimensions[0] = i * positionScale[0] + positionOffset;
        doubleDimensions[1] = j * positionScale[1] + positionOffset;
        doubleDimensions[2] = k * positionScale[2] + positionOffset;
        double* rasHDimensions = transform->TransformDoublePoint(doubleDimensions);
        boundingBox.AddPoint(rasHDimensions);
      }
    }
  }

  boundingBox.GetBounds(bounds);
}

//---------------------------------------------------------------------------
bool vtkMRMLVolumeNode::GetModifiedSinceRead()
{
  return this->Superclass::GetModifiedSinceRead() || //
         (this->GetImageData() && this->GetImageData()->GetMTime() > this->GetStoredTime());
}

//---------------------------------------------------------------------------
bool vtkMRMLVolumeNode::CanApplyNonLinearTransforms() const
{
  return true;
}

//-----------------------------------------------------------
void vtkMRMLVolumeNode::ApplyTransform(vtkAbstractTransform* transform)
{
  vtkHomogeneousTransform* linearTransform = vtkHomogeneousTransform::SafeDownCast(transform);
  if (linearTransform)
  {
    this->ApplyTransformMatrix(linearTransform->GetMatrix());
  }
  else
  {
    this->ApplyNonLinearTransform(transform);
  }
  return;
}
//-----------------------------------------------------------
void vtkMRMLVolumeNode::ApplyNonLinearTransform(vtkAbstractTransform* transform)
{
  if (this->GetImageData() == nullptr || !this->CanApplyNonLinearTransforms())
  {
    return;
  }

  vtkNew<vtkImageData> transformedImage;
  vtkNew<vtkMatrix4x4> transformedIJKToRAS;

  if (!vtkMRMLVolumeNode::GetTransformedImageData(this, transform, transformedImage, transformedIJKToRAS))
  {
    vtkErrorMacro("ApplyNonLinearTransform: failed to get transformed image data");
    return;
  }

  // Perform image data and origin update in one step
  int wasModified = this->StartModify();
  this->SetAndObserveImageData(transformedImage);
  this->SetIJKToRASMatrix(transformedIJKToRAS);
  this->EndModify(wasModified);
}

//-----------------------------------------------------------
bool vtkMRMLVolumeNode::GetTransformedImageData( //
  vtkMRMLVolumeNode* volumeNode,                 //
  vtkAbstractTransform* transform,               //
  vtkImageData* outputImage,                     //
  vtkMatrix4x4* outputIJKToWorld)
{
  if (!volumeNode || volumeNode->GetImageData() == nullptr)
  {
    vtkGenericWarningMacro("GetTransformedImageData: invalid input volume node");
    return false;
  }
  if (!transform)
  {
    vtkGenericWarningMacro("GetTransformedImageData: invalid transform");
    return false;
  }
  if (!volumeNode->CanApplyNonLinearTransforms())
  {
    vtkGenericWarningMacro("GetTransformedImageData: cannot apply non-linear transform to this volume node");
    return false;
  }

  int extent[6];
  volumeNode->GetImageData()->GetExtent(extent);

  vtkNew<vtkMatrix4x4> rasToIJK;

  vtkNew<vtkImageReslice> reslice;
  reslice->GenerateStencilOutputOn();

  vtkNew<vtkGeneralTransform> resampleXform;
  resampleXform->Identity();
  resampleXform->PostMultiply();

  volumeNode->GetRASToIJKMatrix(rasToIJK.GetPointer());

  vtkNew<vtkMatrix4x4> IJKToRAS;
  vtkMatrix4x4::Invert(rasToIJK.GetPointer(), IJKToRAS.GetPointer());
  vtkAbstractTransform* transformFromWorld = transform->GetInverse();

  resampleXform->Concatenate(IJKToRAS.GetPointer());
  resampleXform->Concatenate(transformFromWorld);
  resampleXform->Concatenate(rasToIJK.GetPointer());

  // vtkImageReslice works faster if the input is a linear transform, so try to convert it
  // to a linear transform
  vtkNew<vtkTransform> linearResampleXform;
  if (vtkMRMLTransformNode::IsGeneralTransformLinear(resampleXform.GetPointer(), linearResampleXform.GetPointer()))
  {
    reslice->SetResliceTransform(linearResampleXform.GetPointer());
  }
  else
  {
    reslice->SetResliceTransform(resampleXform.GetPointer());
  }

  reslice->SetInputConnection(volumeNode->ImageDataConnection);

  // GetResamplingInterpolationMode does not use VTK_RESLICE... constants because it is an implementation
  // detail that currently vtkImageReslice is used for resampling.
  int resamplingMode = volumeNode->GetResamplingInterpolationMode();
  switch (resamplingMode)
  {
    case VTK_NEAREST_INTERPOLATION: reslice->SetInterpolationModeToNearestNeighbor(); break;
    case VTK_LINEAR_INTERPOLATION: reslice->SetInterpolationModeToLinear(); break;
    case VTK_CUBIC_INTERPOLATION: reslice->SetInterpolationModeToCubic(); break;
    default: vtkGenericWarningMacro("ApplyNonLinearTransform: invalid interpolation mode: " << volumeNode->GetResamplingInterpolationMode());
  }

  double backgroundColor[4] = { 0, 0, 0, 0 };
  for (int i = 0; i < 4; i++)
  {
    backgroundColor[i] = volumeNode->GetImageBackgroundScalarComponentAsDouble(i);
  }
  reslice->SetBackgroundColor(backgroundColor);

  // Do not use vtkImageReslice::AutoCropOutput because that computes bounding box
  // by transforming the 8 corners of the volume, therefore all parts of the volume
  // that bulge out (due to the warping transform) would be cut off.
  // GetBoundsInternal method takes samples all over the volume, therefore the
  // complete transformed volume is included in the output.
  double transformedBounds[6] = { 0.0, -1.0, 0.0, -1.0, 0.0, -1.0 };
  volumeNode->GetBoundsInternal(transformedBounds, rasToIJK, true);
  double spacing[3] = { 1.0, 1.0, 1.0 }; // output is specified in IJK coordinate system
  // transformedBounds is computed so that it includes the voxel corners, therefore the origin is half voxel towards the image center
  reslice->SetOutputOrigin(transformedBounds[0] + 0.5 * spacing[0], transformedBounds[2] + 0.5 * spacing[1], transformedBounds[4] + 0.5 * spacing[2]);
  reslice->SetOutputSpacing(spacing);
  const double voxelExpandTolerance = 1e-3; // do not expand the volume with a new voxel if only expanding by 1/1000th of a voxel
  // clang-format off
  reslice->SetOutputExtent(
    0, ceil((transformedBounds[1] - transformedBounds[0]) / spacing[0] - voxelExpandTolerance) - 1,
    0, ceil((transformedBounds[3] - transformedBounds[2]) / spacing[1] - voxelExpandTolerance) - 1,
    0, ceil((transformedBounds[5] - transformedBounds[4]) / spacing[2] - voxelExpandTolerance) - 1);
  // clang-format on

  // Keep output spacing (1,1,1)
  reslice->TransformInputSamplingOff();

  reslice->SetOptimization(1);
  reslice->SetOutputDimensionality(3);

  reslice->Update();
  outputImage->DeepCopy(reslice->GetOutput());

  // Origin is stored in image node therefore origin in
  // image data object must be set to (0,0,0).
  double resampledOrigin_IJK[4] = { 0, 0, 0, 0 };
  outputImage->GetOrigin(resampledOrigin_IJK);
  double resampledOrigin_RAS[4] = { 0, 0, 0, 0 };
  IJKToRAS->MultiplyPoint(resampledOrigin_IJK, resampledOrigin_RAS);
  double* origin = volumeNode->GetOrigin();
  outputIJKToWorld->DeepCopy(IJKToRAS);
  outputIJKToWorld->SetElement(0, 3, origin[0] + resampledOrigin_RAS[0]);
  outputIJKToWorld->SetElement(1, 3, origin[1] + resampledOrigin_RAS[1]);
  outputIJKToWorld->SetElement(2, 3, origin[2] + resampledOrigin_RAS[2]);
  outputImage->SetOrigin(0, 0, 0);

  return true;
}

//---------------------------------------------------------------------------
int vtkMRMLVolumeNode::GetResamplingInterpolationMode()
{
  // By default linear interpolation is used for all volumes.
  return VTK_LINEAR_INTERPOLATION;
}

//---------------------------------------------------------------------------
void vtkMRMLVolumeNode::ShiftImageDataExtentToZeroStart()
{
  vtkImageData* imageData = this->GetImageData();
  if (!imageData)
  {
    return;
  }

  int extent[6] = { 0, -1, 0, -1, 0, -1 };
  imageData->GetExtent(extent);

  // No need to shift if extent already starts at zeros
  if (extent[0] == 0 && extent[2] == 0 && extent[4] == 0)
  {
    return;
  }

  // Shift the origin to the extent's start
  vtkSmartPointer<vtkMatrix4x4> ijkToRasMatrix = vtkSmartPointer<vtkMatrix4x4>::New();
  this->GetIJKToRASMatrix(ijkToRasMatrix);
  double shiftedOrigin_IJK[4] = { static_cast<double>(extent[0]), //
                                  static_cast<double>(extent[2]), //
                                  static_cast<double>(extent[4]), //
                                  1.0 };
  double shiftedOrigin_RAS[4] = { 0.0, 0.0, 0.0, 1.0 };
  ijkToRasMatrix->MultiplyPoint(shiftedOrigin_IJK, shiftedOrigin_RAS);
  this->SetOrigin(shiftedOrigin_RAS);

  for (int i = 0; i < 3; ++i)
  {
    extent[2 * i + 1] -= extent[2 * i];
    extent[2 * i] = 0;
  }
  imageData->SetExtent(extent);
}

//---------------------------------------------------------------------------
double vtkMRMLVolumeNode::GetImageBackgroundScalarComponentAsDouble(int component)
{
  vtkImageData* imageData = this->GetImageData();
  if (!imageData || component >= imageData->GetNumberOfScalarComponents())
  {
    return 0.0;
  }

  int extent[6] = { 0, -1, 0, -1, 0, -1 };
  imageData->GetExtent(extent);

  if (extent[0] > extent[1] || extent[2] > extent[3] || extent[4] > extent[5])
  {
    return 0.0;
  }

  std::vector<double> scalarValues;
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[0], extent[2], extent[4], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[0], extent[2], extent[5], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[0], extent[3], extent[4], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[0], extent[3], extent[5], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[1], extent[2], extent[4], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[1], extent[2], extent[5], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[1], extent[3], extent[4], component));
  scalarValues.push_back(imageData->GetScalarComponentAsDouble(extent[1], extent[3], extent[5], component));

  // Get the median value (nth_element performs partial sorting until nth largest element is found)
  const int medianElementIndex = 3;
  std::nth_element(scalarValues.begin(), scalarValues.begin() + medianElementIndex, scalarValues.end());
  double medianValue = scalarValues[medianElementIndex];

  return medianValue;
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::CreateDefaultSequenceDisplayNodes()
{
  if (this->GetDisplayNode())
  {
    // there is a display node already
    return;
  }
  this->CreateDefaultDisplayNodes();

  // Turn off auto window/level for scalar volumes (it is costly to compute recommended ww/wl and image would appear to be flickering)
  vtkMRMLScalarVolumeDisplayNode* scalarVolumeDisplayNode = vtkMRMLScalarVolumeDisplayNode::SafeDownCast(this->GetDisplayNode());
  if (scalarVolumeDisplayNode)
  {
    scalarVolumeDisplayNode->AutoWindowLevelOff();
  }
}

//------------------------------------------------------------------------------
bool vtkMRMLVolumeNode::IsCentered()
{
  double centerPosition[3] = { 0.0, 0.0, 0.0 };
  this->GetCenterPositionRAS(centerPosition);
  double tolerance = this->GetMaxSpacing() * 0.1;
  bool centered = (fabs(centerPosition[0]) <= tolerance    //
                   && fabs(centerPosition[1]) <= tolerance //
                   && fabs(centerPosition[2]) <= tolerance);
  return centered;
}

//------------------------------------------------------------------------------
void vtkMRMLVolumeNode::GetCenterPositionRAS(double* centerPositionRAS, bool useParentTransform /*=true*/)
{
  vtkImageData* imageData = this->GetImageData();
  if (!imageData)
  {
    centerPositionRAS[0] = 0.0;
    centerPositionRAS[1] = 0.0;
    centerPositionRAS[2] = 0.0;
    return;
  }

  int* extent = imageData->GetExtent();
  double centerPositionIJK[4] = { double(extent[0] + extent[1]) / 2.0, //
                                  double(extent[2] + extent[3]) / 2.0, //
                                  double(extent[4] + extent[5]) / 2.0, //
                                  1.0 };

  vtkNew<vtkMatrix4x4> ijkToRasMatrix;
  this->GetIJKToRASMatrix(ijkToRasMatrix);
  double centerPositionRAS1[4] = { 0.0, 0.0, 0.0, 1.0 };
  ijkToRasMatrix->MultiplyPoint(centerPositionIJK, centerPositionRAS1);

  if (useParentTransform)
  {
    vtkNew<vtkGeneralTransform> volumeRasToRasTransform;
    vtkMRMLTransformNode::GetTransformBetweenNodes(this->GetParentTransformNode(), nullptr, volumeRasToRasTransform);
    volumeRasToRasTransform->TransformPoint(centerPositionRAS1, centerPositionRAS1);
  }

  centerPositionRAS[0] = centerPositionRAS1[0];
  centerPositionRAS[1] = centerPositionRAS1[1];
  centerPositionRAS[2] = centerPositionRAS1[2];
}

//------------------------------------------------------------------------------
bool vtkMRMLVolumeNode::AddCenteringTransform()
{
  if (this->IsCentered())
  {
    // already centered (without changing parent transform)
    return false;
  }
  this->SetAndObserveTransformNodeID(nullptr);
  if (this->IsCentered())
  {
    // already centered (without adding a transform), no need to add a parent transform
    return true;
  }
  double centerPositionRAS[3];
  this->GetCenterPositionRAS(centerPositionRAS, false);
  vtkMRMLScene* scene = this->GetScene();
  if (!scene)
  {
    vtkErrorMacro("vtkMRMLVolumeNode::AddCenteringTransform failed: invalid scene");
    return false;
  }
  std::string transformName = this->GetName();
  transformName.append(" centering transform");
  vtkMRMLLinearTransformNode* centeringTransform = vtkMRMLLinearTransformNode::SafeDownCast(scene->AddNewNodeByClass("vtkMRMLLinearTransformNode", transformName));
  vtkNew<vtkMatrix4x4> centeringTransformMatrix;
  centeringTransformMatrix->SetElement(0, 3, -centerPositionRAS[0]);
  centeringTransformMatrix->SetElement(1, 3, -centerPositionRAS[1]);
  centeringTransformMatrix->SetElement(2, 3, -centerPositionRAS[2]);
  centeringTransform->SetMatrixTransformToParent(centeringTransformMatrix);
  this->SetAndObserveTransformNodeID(centeringTransform->GetID());

  // Place transform in the same subject hierarchy folder as the volume node
  vtkMRMLSubjectHierarchyNode* shNode = vtkMRMLSubjectHierarchyNode::GetSubjectHierarchyNode(scene);
  if (shNode)
  {
    vtkIdType volumeItemId = shNode->GetItemByDataNode(this);
    if (volumeItemId != vtkMRMLSubjectHierarchyNode::INVALID_ITEM_ID)
    {
      // Volume is in subject hierarchy
      vtkIdType volumeParentItemId = shNode->GetItemParent(volumeItemId);
      shNode->SetItemParent(shNode->GetItemByDataNode(centeringTransform), volumeParentItemId);
    }
  }
  return true;
}

//-----------------------------------------------------------
const char* vtkMRMLVolumeNode::GetVoxelVectorTypeAsString(int id)
{
  switch (id)
  {
    case VoxelVectorTypeUndefined: return "undefined";
    case VoxelVectorTypeSpatial: return "spatial";
    case VoxelVectorTypeColorRGB: return "colorRGB";
    case VoxelVectorTypeColorRGBA: return "colorRGBA";
    case VoxelVectorTypeSpatialCovariant: return "spatialCovariant";
    default:
      // invalid id
      return "";
  }
}

//-----------------------------------------------------------
int vtkMRMLVolumeNode::GetVoxelVectorTypeFromString(const char* name)
{
  if (name == nullptr)
  {
    // invalid name
    return -1;
  }
  for (int ii = 0; ii < VoxelVectorType_Last; ii++)
  {
    if (strcmp(name, vtkMRMLVolumeNode::GetVoxelVectorTypeAsString(ii)) == 0)
    {
      // found a matching name
      return ii;
    }
  }
  // unknown name
  return -1;
}

//----------------------------------------------------------------------------
bool vtkMRMLVolumeNode::IsIJKCoordinateSystemRightHanded(vtkMatrix4x4* ijkToRasMatrix)
{
  // Check if the determinant of the orientation matrix is less than 0.
  vtkNew<vtkMatrix3x3> orientation;
  vtkAddonMathUtilities::GetOrientationMatrix(ijkToRasMatrix, orientation);
  return orientation->Determinant() >= 0.;
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::ReverseSliceOrder(vtkImageData* imageData, vtkMatrix4x4* ijkToRasMatrix)
{
  if (ijkToRasMatrix == nullptr)
  {
    vtkGenericWarningMacro("vtkMRMLVolumeNode::ReverseSliceOrder failed: ijkToRasMatrix is invalid");
    return;
  }
  int imageDimensions[3] = { 0, 0, 0 };
  if (imageData && imageData->GetPointData())
  {
    // Flip third image axis (K) direction

    // In DTI data sets, data is stored in tensors (scalars is nullptr),
    // but vtkImageFlip always works on the scalar data array, so we need to temporarily
    // move the data array that contains the data to the scalar data array.
    vtkPointData* pointData = imageData->GetPointData();
    vtkDataArray* pointDataArray = nullptr;
    int pointDataType = -1;
    std::array<int, 4> candidatePointDataTypes = { vtkDataSetAttributes::SCALARS, vtkDataSetAttributes::VECTORS, vtkDataSetAttributes::NORMALS, vtkDataSetAttributes::TENSORS };
    for (int candidatePointDataType : candidatePointDataTypes)
    {
      vtkDataArray* candidatePointDataArray = pointData->GetAttribute(candidatePointDataType);
      if (candidatePointDataArray == nullptr)
      {
        continue;
      }
      // Found a valid data array
      if (pointDataType == -1)
      {
        // There has not been any other data arrays
        pointDataArray = candidatePointDataArray;
        pointDataType = candidatePointDataType;
      }
      else
      {
        // There has been other data arrays, log a warning because we only flip the first one
        vtkGenericWarningMacro("vtkMRMLVolumeNode::ReverseSliceOrder: Multiple types of point data arrays were found,"
                               " only the "
                               << vtkDataSetAttributes::GetAttributeTypeAsString(pointDataType) << " array will be flipped");
      }
    }

    if (pointDataType != -1)
    {
      // There is data to process
      vtkNew<vtkImageFlip> flip;
      flip->SetFilteredAxes(2);
      flip->SetInputData(imageData);
      if (pointDataType != vtkDataSetAttributes::SCALARS)
      {
        // Temporarily move the data array to the scalar data array
        // because vtkImageFlip always processes the scalar data array
        vtkSmartPointer<vtkDataArray> dataArrayToFlip = pointDataArray;
        imageData->GetPointData()->SetAttribute(nullptr, pointDataType);
        pointData->SetScalars(dataArrayToFlip);
      }
      flip->Update();
      imageData->ShallowCopy(flip->GetOutput());
      if (pointDataType != vtkDataSetAttributes::SCALARS)
      {
        // Move the processed data array back to where it was
        vtkSmartPointer<vtkDataArray> flippedDataArray = imageData->GetPointData()->GetScalars();
        imageData->GetPointData()->SetScalars(nullptr);
        imageData->GetPointData()->SetAttribute(flippedDataArray, pointDataType);
      }
      imageData->GetDimensions(imageDimensions);
    }
  }

  // Update rasToIJK to reflect flip around the third axis and shift of the origin to the opposite corner.
  vtkNew<vtkTransform> flipTransform;
  flipTransform->Concatenate(ijkToRasMatrix);
  if (imageDimensions[2] > 1)
  {
    // There are more than 1 slice in the image, move the origin to the opposite corner
    flipTransform->Translate(0.0, 0.0, imageDimensions[2] - 1);
  }
  flipTransform->Scale(1.0, 1.0, -1.0);
  flipTransform->GetMatrix(ijkToRasMatrix);
}

//----------------------------------------------------------------------------
void vtkMRMLVolumeNode::SetIJKCoordinateSystemToRightHanded()
{
  vtkNew<vtkMatrix4x4> ijkToRAS;
  this->GetIJKToRASMatrix(ijkToRAS);
  if (vtkMRMLVolumeNode::IsIJKCoordinateSystemRightHanded(ijkToRAS))
  {
    // already right-handed
    return;
  }
  vtkMRMLVolumeNode::ReverseSliceOrder(this->GetImageData(), ijkToRAS);
  this->SetIJKToRASMatrix(ijkToRAS);
}
