/*=========================================================================
 *
 *  Copyright NumFOCUS
 *
 *  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 itkKernelTransform_hxx
#define itkKernelTransform_hxx
#include "itkKernelTransform.h"

namespace itk
{

template <typename TParametersValueType, unsigned int NDimensions>
KernelTransform<TParametersValueType, NDimensions>::KernelTransform()
  : Superclass(NDimensions)
// the second NDimensions is associated is provided as
// a tentative number for initializing the Jacobian.
// The matrix can be resized at run time so this number
// here is irrelevant. The correct size of the Jacobian
// will be NDimension X NDimension.NumberOfLandMarks.
{
  this->m_I.set_identity();
  this->m_SourceLandmarks = PointSetType::New();
  this->m_TargetLandmarks = PointSetType::New();
  this->m_Displacements = VectorSetType::New();
  this->m_WMatrixComputed = false;

  this->m_Stiffness = 0.0;
}

template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::SetSourceLandmarks(PointSetType * landmarks)
{
  itkDebugMacro("setting SourceLandmarks to " << landmarks);
  if (this->m_SourceLandmarks != landmarks)
  {
    this->m_SourceLandmarks = landmarks;
    this->UpdateParameters();
    this->Modified();
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::SetTargetLandmarks(PointSetType * landmarks)
{
  itkDebugMacro("setting TargetLandmarks to " << landmarks);
  if (this->m_TargetLandmarks != landmarks)
  {
    this->m_TargetLandmarks = landmarks;
    this->UpdateParameters();
    this->Modified();
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeG(const InputVectorType &,
                                                             GMatrixType & itkNotUsed(gmatrix)) const
{
  itkExceptionMacro(<< "ComputeG(vector,gmatrix) must be reimplemented"
                    << " in subclasses of KernelTransform.");
}


template <typename TParametersValueType, unsigned int NDimensions>
const typename KernelTransform<TParametersValueType, NDimensions>::GMatrixType &
  KernelTransform<TParametersValueType, NDimensions>::ComputeReflexiveG(PointsIterator) const
{
  m_GMatrix.fill(NumericTraits<TParametersValueType>::ZeroValue());
  m_GMatrix.fill_diagonal(m_Stiffness);

  return m_GMatrix;
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeDeformationContribution(const InputPointType & thisPoint,
                                                                                   OutputPointType &      result) const
{
  /*
   * Default implementation of the the method. This can be overloaded
   * in transforms whose kernel produce diagonal G matrices.
   */
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();

  PointsIterator sp = this->m_SourceLandmarks->GetPoints()->Begin();

  GMatrixType Gmatrix;

  for (unsigned int lnd = 0; lnd < numberOfLandmarks; lnd++)
  {
    this->ComputeG(thisPoint - sp->Value(), Gmatrix);
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      for (unsigned int odim = 0; odim < NDimensions; odim++)
      {
        result[odim] += Gmatrix(dim, odim) * m_DMatrix(dim, lnd);
      }
    }
    ++sp;
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeD()
{
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();

  PointsIterator sp = this->m_SourceLandmarks->GetPoints()->Begin();
  PointsIterator tp = this->m_TargetLandmarks->GetPoints()->Begin();
  PointsIterator end = this->m_SourceLandmarks->GetPoints()->End();

  this->m_Displacements->Reserve(numberOfLandmarks);
  typename VectorSetType::Iterator vt = this->m_Displacements->Begin();

  while (sp != end)
  {
    vt->Value() = tp->Value() - sp->Value();
    ++vt;
    ++sp;
    ++tp;
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeWMatrix()
{
  using SVDSolverType = vnl_svd<TParametersValueType>;

  this->ComputeL();
  this->ComputeY();
  SVDSolverType svd(this->m_LMatrix, 1e-8);
  this->m_WMatrix = svd.solve(this->m_YMatrix);

  this->ReorganizeW();
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeL()
{
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();

  vnl_matrix<TParametersValueType> O2(NDimensions * (NDimensions + 1), NDimensions * (NDimensions + 1), 0);

  this->ComputeP();
  this->ComputeK();

  this->m_LMatrix.set_size(NDimensions * (numberOfLandmarks + NDimensions + 1),
                           NDimensions * (numberOfLandmarks + NDimensions + 1));

  this->m_LMatrix.fill(0.0);

  this->m_LMatrix.update(this->m_KMatrix, 0, 0);
  this->m_LMatrix.update(this->m_PMatrix, 0, this->m_KMatrix.columns());
  this->m_LMatrix.update(this->m_PMatrix.transpose(), this->m_KMatrix.rows(), 0);
  this->m_LMatrix.update(O2, this->m_KMatrix.rows(), this->m_KMatrix.columns());
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeK()
{
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();


  this->ComputeD();

  this->m_KMatrix.set_size(NDimensions * numberOfLandmarks, NDimensions * numberOfLandmarks);

  this->m_KMatrix.fill(0.0);

  PointsIterator p1 = this->m_SourceLandmarks->GetPoints()->Begin();
  PointsIterator end = this->m_SourceLandmarks->GetPoints()->End();

  GMatrixType G;
  // K matrix is symmetric, so only evaluate the upper triangle and
  // store the values in bot the upper and lower triangle
  unsigned int i = 0;
  while (p1 != end)
  {
    PointsIterator p2 = p1; // start at the diagonal element
    unsigned int   j = i;

    // Compute the block diagonal element, i.e. kernel for pi->pi
    G = this->ComputeReflexiveG(p1);
    const vnl_matrix_ref<TParametersValueType> G_reference_alias{
      G.as_ref()
    }; // different interfaces require different representations of values.
    this->m_KMatrix.update(
      G_reference_alias, i * NDimensions, i * NDimensions); // update only accepts `const vnl_matrix<T> &`
    ++p2;
    ++j;

    // Compute the upper (and copy into lower) triangular part of K
    while (p2 != end)
    {
      const InputVectorType s = p1.Value() - p2.Value();
      this->ComputeG(s, G);
      // write value in upper and lower triangle of matrix
      this->m_KMatrix.update(G_reference_alias, i * NDimensions, j * NDimensions);
      this->m_KMatrix.update(G_reference_alias, j * NDimensions, i * NDimensions);
      ++p2;
      ++j;
    }
    ++p1;
    ++i;
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeP()
{
  const PointIdentifier                  numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();
  const vnl_matrix<TParametersValueType> I{ IMatrixType().set_identity().as_matrix() };

  InputPointType p{ NumericTraits<ScalarType>::ZeroValue() };

  this->m_PMatrix.set_size(NDimensions * numberOfLandmarks, NDimensions * (NDimensions + 1));
  this->m_PMatrix.fill(0.0);
  for (unsigned long i = 0; i < numberOfLandmarks; i++)
  {
    this->m_SourceLandmarks->GetPoint(i, &p);
    for (unsigned int j = 0; j < NDimensions; j++)
    {
      const vnl_matrix<TParametersValueType> temp_matrix{ I * p[j] }; // be explicit about conversions that occur.
      this->m_PMatrix.update(
        temp_matrix, i * NDimensions, j * NDimensions); // update only takes 'const vnl_matrix<T> &' as input
    }
    this->m_PMatrix.update(I, i * NDimensions, NDimensions * NDimensions);
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeY()
{
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();

  typename VectorSetType::ConstIterator displacement = this->m_Displacements->Begin();

  this->m_YMatrix.set_size(NDimensions * (numberOfLandmarks + NDimensions + 1), 1);

  this->m_YMatrix.fill(0.0);
  for (unsigned int i = 0; i < numberOfLandmarks; i++)
  {
    for (unsigned int j = 0; j < NDimensions; j++)
    {
      this->m_YMatrix.put(i * NDimensions + j, 0, displacement.Value()[j]);
    }
    ++displacement;
  }
  for (unsigned int i = 0; i < NDimensions * (NDimensions + 1); i++)
  {
    this->m_YMatrix.put(numberOfLandmarks * NDimensions + i, 0, 0);
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ReorganizeW()
{
  PointIdentifier numberOfLandmarks = this->m_SourceLandmarks->GetNumberOfPoints();

  // The deformable (non-affine) part of the registration goes here
  this->m_DMatrix.set_size(NDimensions, numberOfLandmarks);
  unsigned int ci = 0;
  for (unsigned int lnd = 0; lnd < numberOfLandmarks; lnd++)
  {
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      this->m_DMatrix(dim, lnd) = this->m_WMatrix(ci++, 0);
    }
  }
  // This matrix holds the rotational part of the Affine component
  for (unsigned int j = 0; j < NDimensions; j++)
  {
    for (unsigned int i = 0; i < NDimensions; i++)
    {
      this->m_AMatrix(i, j) = this->m_WMatrix(ci++, 0);
    }
  }
  // This vector holds the translational part of the Affine component
  for (unsigned int k = 0; k < NDimensions; k++)
  {
    this->m_BVector(k) = this->m_WMatrix(ci++, 0);
  }

  // release WMatrix memory by assigning a small one.
  this->m_WMatrix = WMatrixType(1, 1);
}


template <typename TParametersValueType, unsigned int NDimensions>
typename KernelTransform<TParametersValueType, NDimensions>::OutputPointType
KernelTransform<TParametersValueType, NDimensions>::TransformPoint(const InputPointType & thisPoint) const
{
  OutputPointType result;

  using ValueType = typename OutputPointType::ValueType;

  result.Fill(NumericTraits<ValueType>::ZeroValue());

  // TODO:  It is unclear if the following line is needed.
  this->ComputeDeformationContribution(thisPoint, result);

  // Add the rotational part of the Affine component
  for (unsigned int j = 0; j < NDimensions; j++)
  {
    for (unsigned int i = 0; i < NDimensions; i++)
    {
      result[i] += this->m_AMatrix(i, j) * thisPoint[j];
    }
  }
  // This vector holds the translational part of the Affine component
  for (unsigned int k = 0; k < NDimensions; k++)
  {
    result[k] += this->m_BVector(k) + thisPoint[k];
  }

  return result;
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::ComputeJacobianWithRespectToParameters(
  const InputPointType &,
  JacobianType & jacobian) const
{
  jacobian.Fill(0.0);

  // FIXME: TODO
  // The Jacobian should be computable in terms of the matrices
  // used to Transform points...
  itkExceptionMacro(<< "Get[Local]Jacobian must be implemented in subclasses"
                    << " of KernelTransform.");
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::SetParameters(const ParametersType & parameters)
{
  // Set the parameters
  // NOTE that in this transformation both the Source and Target
  // landmarks could be considered as parameters. It is assumed
  // here that the Target landmarks are provided by the user and
  // are not changed during the optimization process required for
  // registration.

  // Save parameters. Needed for proper operation of TransformUpdateParameters.
  if (&parameters != &(this->m_Parameters))
  {
    const size_t                 parameterSize = this->GetParameters().Size();
    const NumberOfParametersType offset = NumericTraits<NumberOfParametersType>::ZeroValue();
    this->CopyInParameters(&(parameters.data_block())[offset], &(parameters.data_block())[offset] + parameterSize);
  }

  typename PointsContainer::Pointer landmarks = PointsContainer::New();
  const unsigned int                numberOfLandmarks = parameters.Size() / NDimensions;
  landmarks->Reserve(numberOfLandmarks);

  PointsIterator itr = landmarks->Begin();
  PointsIterator end = landmarks->End();

  InputPointType landMark;

  unsigned int pcounter = 0;
  while (itr != end)
  {
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      landMark[dim] = parameters[pcounter];
      ++pcounter;
    }
    itr.Value() = landMark;
    ++itr;
  }

  this->m_SourceLandmarks->SetPoints(landmarks);

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


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::SetFixedParameters(const FixedParametersType & parameters)
{
  // Set the fixed parameters
  // Since the API of the SetParameters() function sets the
  // source landmarks, this function was added to support the
  // setting of the target landmarks, and allowing the Transform
  // I/O mechanism to be supported.

  typename PointsContainer::Pointer landmarks = PointsContainer::New();
  const unsigned int                numberOfLandmarks = parameters.Size() / NDimensions;

  landmarks->Reserve(numberOfLandmarks);

  PointsIterator itr = landmarks->Begin();
  PointsIterator end = landmarks->End();

  InputPointType landMark;

  unsigned int pcounter = 0;
  while (itr != end)
  {
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      landMark[dim] = parameters[pcounter];
      ++pcounter;
    }
    itr.Value() = landMark;
    ++itr;
  }

  this->m_TargetLandmarks->SetPoints(landmarks);
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::UpdateParameters() const
{
  this->m_Parameters = ParametersType(this->m_SourceLandmarks->GetNumberOfPoints() * NDimensions);

  PointsIterator itr = this->m_SourceLandmarks->GetPoints()->Begin();
  PointsIterator end = this->m_SourceLandmarks->GetPoints()->End();

  unsigned int pcounter = 0;
  while (itr != end)
  {
    InputPointType landmark = itr.Value();
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      this->m_Parameters[pcounter] = landmark[dim];
      ++pcounter;
    }
    ++itr;
  }
}


template <typename TParametersValueType, unsigned int NDimensions>
const typename KernelTransform<TParametersValueType, NDimensions>::ParametersType &
KernelTransform<TParametersValueType, NDimensions>::GetParameters() const
{
  this->UpdateParameters();
  return this->m_Parameters;
}


template <typename TParametersValueType, unsigned int NDimensions>
const typename KernelTransform<TParametersValueType, NDimensions>::FixedParametersType &
KernelTransform<TParametersValueType, NDimensions>::GetFixedParameters() const
{
  // Get the fixed parameters
  // This returns the target landmark locations
  // This was added to support the Transform Reader/Writer mechanism
  this->m_FixedParameters = ParametersType(this->m_TargetLandmarks->GetNumberOfPoints() * NDimensions);

  PointsIterator itr = this->m_TargetLandmarks->GetPoints()->Begin();
  PointsIterator end = this->m_TargetLandmarks->GetPoints()->End();

  unsigned int pcounter = 0;
  while (itr != end)
  {
    InputPointType landmark = itr.Value();
    for (unsigned int dim = 0; dim < NDimensions; dim++)
    {
      this->m_FixedParameters[pcounter] = landmark[dim];
      ++pcounter;
    }
    ++itr;
  }

  return this->m_FixedParameters;
}


template <typename TParametersValueType, unsigned int NDimensions>
void
KernelTransform<TParametersValueType, NDimensions>::PrintSelf(std::ostream & os, Indent indent) const
{
  Superclass::PrintSelf(os, indent);
  if (this->m_SourceLandmarks)
  {
    os << indent << "SourceLandmarks: " << std::endl;
    this->m_SourceLandmarks->Print(os, indent.GetNextIndent());
  }
  if (this->m_TargetLandmarks)
  {
    os << indent << "TargetLandmarks: " << std::endl;
    this->m_TargetLandmarks->Print(os, indent.GetNextIndent());
  }
  if (this->m_Displacements)
  {
    os << indent << "Displacements: " << std::endl;
    this->m_Displacements->Print(os, indent.GetNextIndent());
  }
  os << indent << "Stiffness: " << this->m_Stiffness << std::endl;
}

} // end namespace itk

#endif
