/*=========================================================================
 *
 *  Copyright UMC Utrecht and contributors
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *=========================================================================*/
#ifndef itkAdvancedBSplineDeformableTransformBase_hxx
#define itkAdvancedBSplineDeformableTransformBase_hxx

#include "itkAdvancedBSplineDeformableTransformBase.h"
#include "itkContinuousIndex.h"
#include "itkIdentityTransform.h"
#include <vnl/vnl_math.h>

namespace itk
{

// Constructor with default arguments
template <typename TScalarType, unsigned int NDimensions>
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::AdvancedBSplineDeformableTransformBase(
  const unsigned splineOrder)
  : Superclass(SpaceDimension)
  , m_SplineOrder(splineOrder)
{
  // Initialize coeffient images
  for (unsigned int j = 0; j < SpaceDimension; ++j)
  {
    m_WrappedImage[j] = ImageType::New();
  }

  /** Fixed Parameters store the following information:
   *     Grid Size
   *     Grid Origin
   *     Grid Spacing
   *     Grid Direction
   *  The size of these is equal to the  NInputDimensions
   */
  Superclass::m_FixedParameters.SetSize(NumberOfFixedParameters);
  Superclass::m_FixedParameters.Fill(0.0);
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    Superclass::m_FixedParameters[2 * NDimensions + i] = m_GridSpacing[i];
  }
  for (unsigned int di = 0; di < NDimensions; ++di)
  {
    for (unsigned int dj = 0; dj < NDimensions; ++dj)
    {
      Superclass::m_FixedParameters[3 * NDimensions + (di * NDimensions + dj)] = m_GridDirection[di][dj];
    }
  }

  this->UpdatePointIndexConversions();
}


// Get the number of parameters
template <typename TScalarType, unsigned int NDimensions>
auto
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::GetNumberOfParameters() const
  -> NumberOfParametersType
{

  // The number of parameters equal SpaceDimension * number of
  // of pixels in the grid region.
  return (NumberOfParametersType{ SpaceDimension } *
          static_cast<NumberOfParametersType>(m_GridRegion.GetNumberOfPixels()));
}


// Get the number of parameters per dimension
template <typename TScalarType, unsigned int NDimensions>
auto
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::GetNumberOfParametersPerDimension() const
  -> NumberOfParametersType
{
  // The number of parameters per dimension equal number of
  // of pixels in the grid region.
  return (static_cast<NumberOfParametersType>(m_GridRegion.GetNumberOfPixels()));
}


// Set the grid spacing
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::UpdatePointIndexConversions()
{
  DirectionType scale;
  for (unsigned int i = 0; i < SpaceDimension; ++i)
  {
    scale[i][i] = m_GridSpacing[i];
  }

  m_IndexToPoint = m_GridDirection * scale;
  m_PointToIndexMatrix = m_IndexToPoint.GetInverse();
  m_PointToIndexMatrixTransposed = m_PointToIndexMatrix.GetTranspose();
  m_PointToIndexMatrixIsDiagonal = true;
  for (unsigned int i = 0; i < SpaceDimension; ++i)
  {
    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_PointToIndexMatrix2[i][j] = static_cast<ScalarType>(m_PointToIndexMatrix[i][j]);
      m_PointToIndexMatrixTransposed2[i][j] = static_cast<ScalarType>(m_PointToIndexMatrixTransposed[i][j]);
      if (i != j && m_PointToIndexMatrix[i][j] != 0.0)
      {
        m_PointToIndexMatrixIsDiagonal = false;
      }
    }
  }

  m_PointToIndexMatrixDiagonal = m_PointToIndexMatrixTransposed2.GetVnlMatrix().get_diagonal().data_block();
  for (unsigned int i = 0; i < SpaceDimension; ++i)
  {
    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_PointToIndexMatrixDiagonalProducts[i + SpaceDimension * j] =
        m_PointToIndexMatrixDiagonal[i] * m_PointToIndexMatrixDiagonal[j];
    }
  }

} // end UpdatePointIndexConversions()


//
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::UpdateGridOffsetTable()
{
  SizeType gridSize = m_GridRegion.GetSize();
  m_GridOffsetTable.Fill(1);
  for (unsigned int j = 1; j < SpaceDimension; ++j)
  {
    m_GridOffsetTable[j] = m_GridOffsetTable[j - 1] * gridSize[j - 1];
  }

} // end UpdateGridOffsetTable()


// Set the grid spacing
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetGridSpacing(const SpacingType & spacing)
{
  if (m_GridSpacing != spacing)
  {
    m_GridSpacing = spacing;

    // set spacing for each coefficient and Jacobian image
    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_WrappedImage[j]->SetSpacing(m_GridSpacing.GetDataPointer());
    }

    this->UpdatePointIndexConversions();

    this->Modified();
  }
}


// Set the grid direction
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetGridDirection(const DirectionType & direction)
{
  if (m_GridDirection != direction)
  {
    m_GridDirection = direction;

    // set direction for each coefficient and Jacobian image
    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_WrappedImage[j]->SetDirection(m_GridDirection);
    }

    this->UpdatePointIndexConversions();

    this->Modified();
  }
}


// Set the grid origin
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetGridOrigin(const OriginType & origin)
{
  if (m_GridOrigin != origin)
  {
    m_GridOrigin = origin;

    // set spacing for each coefficient and jacobianimage
    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_WrappedImage[j]->SetOrigin(m_GridOrigin.GetDataPointer());
    }

    this->Modified();
  }
}


// Set the parameters
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetIdentity()
{
  if (m_InputParametersPointer)
  {
    ParametersType * parameters = const_cast<ParametersType *>(m_InputParametersPointer);
    parameters->Fill(0.0);
    this->Modified();
  }
  else
  {
    itkExceptionMacro("Input parameters for the spline haven't been set ! Set them using the SetParameters or "
                      "SetCoefficientImage method first.");
  }
}


// Set the parameters
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetParameters(const ParametersType & parameters)
{

  // check if the number of parameters match the
  // expected number of parameters
  if (const auto numberOfParameters = parameters.size(); numberOfParameters != this->GetNumberOfParameters())
  {
    itkExceptionMacro("Mismatched between parameters size " << numberOfParameters << " and region size "
                                                            << m_GridRegion.GetNumberOfPixels());
  }

  // Clean up buffered parameters
  m_InternalParametersBuffer = ParametersType(0);

  // Keep a reference to the input parameters
  m_InputParametersPointer = &parameters;

  // Wrap flat array as images of coefficients
  this->WrapAsImages();

  // Modified is always called since we just have a pointer to the
  // parameters and cannot know if the parameters have changed.
  this->Modified();
}


// Set the Fixed Parameters
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetFixedParameters(
  const FixedParametersType & passedParameters)
{
  FixedParametersType parameters(NumberOfFixedParameters);

  // check if the number of parameters match the
  // expected number of parameters
  if (passedParameters.Size() == NDimensions * 3)
  {
    parameters.Fill(0.0);
    for (unsigned int i = 0; i < 3 * NDimensions; ++i)
    {
      parameters[i] = passedParameters[i];
    }
    for (unsigned int di = 0; di < NDimensions; ++di)
    {
      parameters[3 * NDimensions + (di * NDimensions + di)] = 1;
    }
  }
  else if (const auto numberOfPassedParameters = passedParameters.size();
           numberOfPassedParameters != NumberOfFixedParameters)
  {
    itkExceptionMacro("Mismatched between parameters size "
                      << numberOfPassedParameters << " and number of fixed parameters " << NumberOfFixedParameters);
  }
  else
  {
    for (unsigned int i = 0; i < NumberOfFixedParameters; ++i)
    {
      parameters[i] = passedParameters[i];
    }
  }

  /*********************************************************
    Fixed Parameters store the following information:
        Grid Size
        Grid Origin
        Grid Spacing
        Grid Direction
     The size of these is equal to the  NInputDimensions
  *********************************************************/

  /** Set the Grid Parameters */
  SizeType gridSize;
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    gridSize[i] = static_cast<int>(parameters[i]);
  }
  RegionType bsplineRegion;
  bsplineRegion.SetSize(gridSize);

  /** Set the Origin Parameters */
  OriginType origin;
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    origin[i] = parameters[NDimensions + i];
  }

  /** Set the Spacing Parameters */
  SpacingType spacing;
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    spacing[i] = parameters[2 * NDimensions + i];
  }

  /** Set the Direction Parameters */
  DirectionType direction;
  for (unsigned int di = 0; di < NDimensions; ++di)
  {
    for (unsigned int dj = 0; dj < NDimensions; ++dj)
    {
      direction[di][dj] = parameters[3 * NDimensions + (di * NDimensions + dj)];
    }
  }

  this->SetGridSpacing(spacing);
  this->SetGridDirection(direction);
  this->SetGridOrigin(origin);
  this->SetGridRegion(bsplineRegion);
  this->UpdateGridOffsetTable();

  this->Modified();
}


// Wrap flat parameters as images
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::WrapAsImages()
{
  /**
   * Wrap flat parameters array into SpaceDimension number of ITK images
   * NOTE: For efficiency, parameters are not copied locally. The parameters
   * are assumed to be maintained by the caller.
   */
  auto *       dataPointer = const_cast<PixelType *>((m_InputParametersPointer->data_block()));
  unsigned int numberOfPixels = m_GridRegion.GetNumberOfPixels();

  for (unsigned int j = 0; j < SpaceDimension; ++j)
  {
    m_WrappedImage[j]->GetPixelContainer()->SetImportPointer(dataPointer, numberOfPixels);
    dataPointer += numberOfPixels;
  }
  m_CoefficientImages = m_WrappedImage;
}


// Set the parameters by value
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetParametersByValue(
  const ParametersType & parameters)
{
  // check if the number of parameters match the
  // expected number of parameters
  if (const auto numberOfParameters = parameters.size(); numberOfParameters != this->GetNumberOfParameters())
  {
    itkExceptionMacro("Mismatched between parameters size " << numberOfParameters << " and region size "
                                                            << m_GridRegion.GetNumberOfPixels());
  }

  // copy it
  m_InternalParametersBuffer = parameters;
  m_InputParametersPointer = &(m_InternalParametersBuffer);

  // wrap flat array as images of coefficients
  this->WrapAsImages();

  // Modified is always called since we just have a pointer to the
  // parameters and cannot know if the parameters have changed.
  this->Modified();
}


// Get the parameters
template <typename TScalarType, unsigned int NDimensions>
auto
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::GetParameters() const -> const ParametersType &
{
  /** NOTE: For efficiency, this class does not keep a copy of the parameters -
   * it just keeps pointer to input parameters.
   */
  if (nullptr == m_InputParametersPointer)
  {
    itkExceptionMacro("Cannot GetParameters() because m_InputParametersPointer is NULL. Perhaps "
                      "SetCoefficientImages() has been called causing the NULL pointer.");
  }

  return *m_InputParametersPointer;
}

// Get the parameters
template <typename TScalarType, unsigned int NDimensions>
auto
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::GetFixedParameters() const
  -> const FixedParametersType &
{
  RegionType resRegion = this->GetGridRegion();

  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    Superclass::m_FixedParameters[i] = (resRegion.GetSize())[i];
  }
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    Superclass::m_FixedParameters[NDimensions + i] = (this->GetGridOrigin())[i];
  }
  for (unsigned int i = 0; i < NDimensions; ++i)
  {
    Superclass::m_FixedParameters[2 * NDimensions + i] = (this->GetGridSpacing())[i];
  }
  for (unsigned int di = 0; di < NDimensions; ++di)
  {
    for (unsigned int dj = 0; dj < NDimensions; ++dj)
    {
      Superclass::m_FixedParameters[3 * NDimensions + (di * NDimensions + dj)] = (this->GetGridDirection())[di][dj];
    }
  }

  return (Superclass::m_FixedParameters);
}

// Set the B-Spline coefficients using input images
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::SetCoefficientImages(ImagePointer images[])
{
  if (images[0])
  {
    this->SetGridRegion(images[0]->GetBufferedRegion());
    this->SetGridSpacing(images[0]->GetSpacing());
    this->SetGridDirection(images[0]->GetDirection());
    this->SetGridOrigin(images[0]->GetOrigin());
    this->UpdateGridOffsetTable();

    for (unsigned int j = 0; j < SpaceDimension; ++j)
    {
      m_CoefficientImages[j] = images[j];
    }

    // Clean up buffered parameters
    m_InternalParametersBuffer = ParametersType(0);
    m_InputParametersPointer = nullptr;
  }
}


// Print self
template <typename TScalarType, unsigned int NDimensions>
void
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::PrintSelf(std::ostream & os, Indent indent) const
{
  this->Superclass::PrintSelf(os, indent);

  os << indent << "GridRegion: " << m_GridRegion << std::endl;
  os << indent << "GridOrigin: " << m_GridOrigin << std::endl;
  os << indent << "GridSpacing: " << m_GridSpacing << std::endl;
  os << indent << "GridDirection:\n" << m_GridDirection << std::endl;
  os << indent << "GridOffsetTable: " << m_GridOffsetTable << std::endl;
  os << indent << "IndexToPoint:\n" << m_IndexToPoint << std::endl;
  os << indent << "PointToIndex:\n" << m_PointToIndexMatrix << std::endl;
  os << indent << "PointToIndex2:\n" << m_PointToIndexMatrix2 << std::endl;
  os << indent << "PointToIndexTransposed:\n" << m_PointToIndexMatrixTransposed << std::endl;
  os << indent << "PointToIndexTransposed2:\n" << m_PointToIndexMatrixTransposed2 << std::endl;

  os << indent << "CoefficientImage: [ " << m_CoefficientImages[0].GetPointer();
  for (unsigned int j = 1; j < SpaceDimension; ++j)
  {
    os << ", " << m_CoefficientImages[j].GetPointer();
  }
  os << " ]" << std::endl;

  os << indent << "WrappedImage: [ " << m_WrappedImage[0].GetPointer();
  for (unsigned int j = 1; j < SpaceDimension; ++j)
  {
    os << ", " << m_WrappedImage[j].GetPointer();
  }
  os << " ]" << std::endl;

  os << indent << "InputParametersPointer: " << m_InputParametersPointer << std::endl;
}


// Transform a point
template <typename TScalarType, unsigned int NDimensions>
bool
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::InsideValidRegion(
  const ContinuousIndexType & index) const
{
  bool inside = true;

  /** Check if index can be evaluated given the current grid. */
  for (unsigned int j = 0; j < SpaceDimension; ++j)
  {
    if (index[j] < m_ValidRegionBegin[j] || index[j] >= m_ValidRegionEnd[j])
    {
      inside = false;
      break;
    }
  }

  return inside;
}


template <typename TScalarType, unsigned int NDimensions>
auto
AdvancedBSplineDeformableTransformBase<TScalarType, NDimensions>::TransformPointToContinuousGridIndex(
  const InputPointType & point) const -> ContinuousIndexType
{
  Vector<double, SpaceDimension> tvector;

  for (unsigned int j = 0; j < SpaceDimension; ++j)
  {
    tvector[j] = point[j] - m_GridOrigin[j];
  }

  Vector<double, SpaceDimension> cvector = m_PointToIndexMatrix * tvector;
  ContinuousIndexType            cindex;

  for (unsigned int j = 0; j < SpaceDimension; ++j)
  {
    cindex[j] = static_cast<typename ContinuousIndexType::CoordinateType>(cvector[j]);
  }
  return cindex;
}


} // namespace itk

#endif
