/*=========================================================================
 *
 *  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 itkConstantVelocityFieldTransform_hxx
#define itkConstantVelocityFieldTransform_hxx

#include "itkConstantVelocityFieldTransform.h"

#include "itkExponentialDisplacementFieldImageFilter.h"
#include "itkImageRegionConstIterator.h"
#include "itkImageRegionIterator.h"
#include "itkVectorLinearInterpolateImageFunction.h"

namespace itk
{

/**
 * Constructor
 */
template <typename TParametersValueType, unsigned int NDimensions>
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::ConstantVelocityFieldTransform()
  : m_ConstantVelocityField(nullptr)

{
  this->m_FixedParameters.SetSize(ConstantVelocityFieldDimension * (ConstantVelocityFieldDimension + 3));
  this->m_FixedParameters.Fill(0.0);

  this->m_LowerTimeBound = 0.0;
  this->m_UpperTimeBound = 1.0;

  this->m_NumberOfIntegrationSteps = 10;

  // Setup and assign default interpolator
  using DefaultInterpolatorType = VectorLinearInterpolateImageFunction<ConstantVelocityFieldType, ScalarType>;
  typename DefaultInterpolatorType::Pointer interpolator = DefaultInterpolatorType::New();
  this->m_ConstantVelocityFieldInterpolator = interpolator;

  // Setup and assign parameter helper. This will hold the displacement field
  // for access through the common OptimizerParameters interface.
  auto * helper = new OptimizerParametersHelperType;
  // After assigning this, this->m_Parameter will manage this,
  // deleting when appropriate.
  this->m_Parameters.SetHelper(helper);
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::UpdateTransformParameters(
  const DerivativeType & update,
  ScalarType             factor)
{
  // This simply adds the values.
  // TODO: This should be multi-threaded probably, via image add filter.
  Superclass::UpdateTransformParameters(update, factor);

  this->IntegrateVelocityField();
}

/**
 * return an inverse transformation
 */
template <typename TParametersValueType, unsigned int NDimensions>
bool
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::GetInverse(Self * inverse) const
{
  if (!inverse || !this->m_ConstantVelocityField)
  {
    return false;
  }
  else
  {
    inverse->SetFixedParameters(this->GetFixedParameters());
    inverse->SetUpperTimeBound(this->GetLowerTimeBound());
    inverse->SetLowerTimeBound(this->GetUpperTimeBound());
    inverse->SetDisplacementField(this->m_InverseDisplacementField);
    inverse->SetInverseDisplacementField(this->m_DisplacementField);
    inverse->SetInterpolator(this->m_Interpolator);
    inverse->SetConstantVelocityField(this->m_ConstantVelocityField);
    inverse->SetConstantVelocityFieldInterpolator(this->m_ConstantVelocityFieldInterpolator);
    return true;
  }
}

// Return an inverse of this transform
template <typename TParametersValueType, unsigned int NDimensions>
typename ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::InverseTransformBasePointer
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::GetInverseTransform() const
{
  Pointer inverseTransform = New();
  if (this->GetInverse(inverseTransform))
  {
    return inverseTransform.GetPointer();
  }
  else
  {
    return nullptr;
  }
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::SetConstantVelocityField(
  ConstantVelocityFieldType * field)
{
  itkDebugMacro("setting VelocityField to " << field);
  if (this->m_ConstantVelocityField != field)
  {
    this->m_ConstantVelocityField = field;

    this->Modified();
    /* Store this separately for use in smoothing because we only want
     * to know when the displacement field object has changed, not just
     * its contents. */
    this->m_ConstantVelocityFieldSetTime = this->GetMTime();
    if (!this->m_ConstantVelocityFieldInterpolator.IsNull())
    {
      this->m_ConstantVelocityFieldInterpolator->SetInputImage(this->m_ConstantVelocityField);
    }
    // Assign to parameters object
    this->m_Parameters.SetParametersObject(this->m_ConstantVelocityField);
  }
  this->SetFixedParametersFromConstantVelocityField();
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::SetConstantVelocityFieldInterpolator(
  ConstantVelocityFieldInterpolatorType * interpolator)
{
  itkDebugMacro("setting ConstantVelocityFieldInterpolator to " << interpolator);
  if (this->m_ConstantVelocityFieldInterpolator != interpolator)
  {
    this->m_ConstantVelocityFieldInterpolator = interpolator;
    this->Modified();
    if (!this->m_ConstantVelocityField.IsNull())
    {
      this->m_ConstantVelocityFieldInterpolator->SetInputImage(this->m_ConstantVelocityField);
    }
  }
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::SetFixedParameters(
  const FixedParametersType & fixedParameters)
{
  if (fixedParameters.Size() != ConstantVelocityFieldDimension * (ConstantVelocityFieldDimension + 3))
  {
    itkExceptionMacro("The fixed parameters are not the right size.");
  }

  SizeType size;
  for (unsigned int d = 0; d < ConstantVelocityFieldDimension; d++)
  {
    size[d] = static_cast<SizeValueType>(fixedParameters[d]);
  }

  PointType origin;
  for (unsigned int d = 0; d < ConstantVelocityFieldDimension; d++)
  {
    origin[d] = fixedParameters[d + ConstantVelocityFieldDimension];
  }

  SpacingType spacing;
  for (unsigned int d = 0; d < ConstantVelocityFieldDimension; d++)
  {
    spacing[d] = fixedParameters[d + 2 * ConstantVelocityFieldDimension];
  }

  DirectionType direction;
  for (unsigned int di = 0; di < ConstantVelocityFieldDimension; di++)
  {
    for (unsigned int dj = 0; dj < ConstantVelocityFieldDimension; dj++)
    {
      direction[di][dj] =
        fixedParameters[3 * ConstantVelocityFieldDimension + (di * ConstantVelocityFieldDimension + dj)];
    }
  }

  PixelType zeroDisplacement;
  zeroDisplacement.Fill(0.0);

  typename ConstantVelocityFieldType::Pointer velocityField = ConstantVelocityFieldType::New();
  velocityField->SetSpacing(spacing);
  velocityField->SetOrigin(origin);
  velocityField->SetDirection(direction);
  velocityField->SetRegions(size);
  velocityField->Allocate();
  velocityField->FillBuffer(zeroDisplacement);

  this->SetConstantVelocityField(velocityField);
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::SetFixedParametersFromConstantVelocityField() const
{
  this->m_FixedParameters.SetSize(ConstantVelocityFieldDimension * (ConstantVelocityFieldDimension + 3));

  const typename ConstantVelocityFieldType::RegionType & fieldRegion =
    this->m_ConstantVelocityField->GetLargestPossibleRegion();

  // Set the field size parameters
  SizeType fieldSize = fieldRegion.GetSize();
  for (unsigned int i = 0; i < ConstantVelocityFieldDimension; i++)
  {
    this->m_FixedParameters[i] = static_cast<FixedParametersValueType>(fieldSize[i]);
  }

  // Set the origin parameters
  PointType fieldOrigin = this->m_ConstantVelocityField->GetOrigin();
  for (unsigned int i = 0; i < ConstantVelocityFieldDimension; i++)
  {
    this->m_FixedParameters[ConstantVelocityFieldDimension + i] = fieldOrigin[i];
  }

  // Set the spacing parameters
  SpacingType fieldSpacing = this->m_ConstantVelocityField->GetSpacing();
  for (unsigned int i = 0; i < ConstantVelocityFieldDimension; i++)
  {
    this->m_FixedParameters[2 * ConstantVelocityFieldDimension + i] =
      static_cast<FixedParametersValueType>(fieldSpacing[i]);
  }

  // Set the direction parameters
  DirectionType fieldDirection = this->m_ConstantVelocityField->GetDirection();
  for (unsigned int di = 0; di < ConstantVelocityFieldDimension; di++)
  {
    for (unsigned int dj = 0; dj < ConstantVelocityFieldDimension; dj++)
    {
      this->m_FixedParameters[3 * ConstantVelocityFieldDimension + (di * ConstantVelocityFieldDimension + dj)] =
        static_cast<FixedParametersValueType>(fieldDirection[di][dj]);
    }
  }
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::IntegrateVelocityField()
{
  using ExponentiatorType =
    ExponentialDisplacementFieldImageFilter<ConstantVelocityFieldType, ConstantVelocityFieldType>;

  ConstantVelocityFieldPointer constantVelocityField = this->GetModifiableConstantVelocityField();

  typename ExponentiatorType::Pointer exponentiator = ExponentiatorType::New();
  exponentiator->SetInput(constantVelocityField);
  if (this->m_CalculateNumberOfIntegrationStepsAutomatically || this->GetNumberOfIntegrationSteps() == 0)
  {
    exponentiator->SetAutomaticNumberOfIterations(true);
    if (!this->m_CalculateNumberOfIntegrationStepsAutomatically && this->m_NumberOfIntegrationSteps == 0)
    {
      itkWarningMacro("Number of integration steps is 0.  Calculating the number of integration steps automatically.");
    }
  }
  else
  {
    exponentiator->SetAutomaticNumberOfIterations(false);
    exponentiator->SetMaximumNumberOfIterations(this->GetNumberOfIntegrationSteps());
  }
  exponentiator->SetComputeInverse(false);
  exponentiator->Update();

  // Calculate inverse displacement field

  typename ExponentiatorType::Pointer exponentiatorInv = ExponentiatorType::New();
  exponentiatorInv->SetInput(constantVelocityField);
  if (this->m_CalculateNumberOfIntegrationStepsAutomatically || this->m_NumberOfIntegrationSteps == 0)
  {
    exponentiatorInv->SetAutomaticNumberOfIterations(true);
    if (!this->m_CalculateNumberOfIntegrationStepsAutomatically && this->m_NumberOfIntegrationSteps == 0)
    {
      itkWarningMacro("Number of integration steps is 0.  Calculating the number of integration steps automatically.");
    }
  }
  else
  {
    exponentiatorInv->SetAutomaticNumberOfIterations(false);
    exponentiatorInv->SetMaximumNumberOfIterations(this->GetNumberOfIntegrationSteps());
  }
  exponentiatorInv->SetComputeInverse(true);
  exponentiatorInv->Update();

  // We use the lower and upper time bounds to keep track of which results should go in
  // the forward and inverse displacement fields.  This is useful when calling and tracking
  // the inverse transform where the velocity field is the same for both the forward and
  // inverse transforms but the upper and lower time bounds are switched as well as the
  // forward and inverse displacement fields.

  if (this->GetLowerTimeBound() <= this->GetUpperTimeBound())
  {
    this->SetDisplacementField(exponentiator->GetOutput());
    this->SetInverseDisplacementField(exponentiatorInv->GetOutput());
  }
  else
  {
    this->SetDisplacementField(exponentiatorInv->GetOutput());
    this->SetInverseDisplacementField(exponentiator->GetOutput());
  }
}

template <typename TParametersValueType, unsigned int NDimensions>
typename ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::DisplacementFieldType::Pointer
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::CopyDisplacementField(
  const DisplacementFieldType * toCopy) const
{
  typename DisplacementFieldType::Pointer rval = DisplacementFieldType::New();
  rval->SetOrigin(toCopy->GetOrigin());
  rval->SetSpacing(toCopy->GetSpacing());
  rval->SetDirection(toCopy->GetDirection());
  rval->SetRegions(toCopy->GetLargestPossibleRegion());
  rval->Allocate();

  ImageRegionConstIterator<DisplacementFieldType> dispIt(toCopy, toCopy->GetLargestPossibleRegion());
  ImageRegionIterator<DisplacementFieldType>      cloneDispIt(rval, rval->GetLargestPossibleRegion());
  for (dispIt.GoToBegin(), cloneDispIt.GoToBegin(); !dispIt.IsAtEnd() && !cloneDispIt.IsAtEnd();
       ++dispIt, ++cloneDispIt)
  {
    cloneDispIt.Set(dispIt.Get());
  }
  return rval;
}

template <typename TParametersValueType, unsigned int NDimensions>
typename LightObject::Pointer
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::InternalClone() const
{
  // create a new instance
  LightObject::Pointer   loPtr = Superclass::InternalClone();
  typename Self::Pointer rval = dynamic_cast<Self *>(loPtr.GetPointer());
  if (rval.IsNull())
  {
    itkExceptionMacro(<< "downcast to type " << this->GetNameOfClass() << " failed.");
  }

  // set the fixed/moving parameters.
  // Not sure these do anything at all useful!
  rval->SetFixedParameters(this->GetFixedParameters());
  rval->SetParameters(this->GetParameters());

  // need the displacement field but GetDisplacementField is non-const.
  auto *                                       nonConstThis = const_cast<Self *>(this);
  typename DisplacementFieldType::ConstPointer dispField = nonConstThis->GetDisplacementField();
  typename DisplacementFieldType::Pointer      cloneDispField = this->CopyDisplacementField(dispField);
  rval->GetModifiableInterpolator()->SetInputImage(cloneDispField);
  rval->SetDisplacementField(cloneDispField);

  // now do the inverse -- it actually gets created as a side effect?
  typename DisplacementFieldType::ConstPointer invDispField = nonConstThis->GetInverseDisplacementField();
  typename DisplacementFieldType::Pointer      cloneInvDispField = this->CopyDisplacementField(invDispField);
  rval->SetInverseDisplacementField(cloneInvDispField);

  // copy the VelocityField
  // SetFixedParameters allocates the VelocityField
  ImageRegionConstIterator<ConstantVelocityFieldType> thisIt(this->m_ConstantVelocityField,
                                                             this->m_ConstantVelocityField->GetLargestPossibleRegion());
  ImageRegionIterator<ConstantVelocityFieldType>      cloneIt(rval->m_ConstantVelocityField,
                                                         rval->m_ConstantVelocityField->GetLargestPossibleRegion());
  for (thisIt.GoToBegin(), cloneIt.GoToBegin(); !thisIt.IsAtEnd() && !cloneIt.IsAtEnd(); ++thisIt, ++cloneIt)
  {
    cloneIt.Set(thisIt.Get());
  }

  // set config parameters
  rval->SetLowerTimeBound(this->GetLowerTimeBound());
  rval->SetUpperTimeBound(this->GetUpperTimeBound());
  rval->SetNumberOfIntegrationSteps(this->GetNumberOfIntegrationSteps());

  // copy the interpolator
  ConstantVelocityFieldInterpolatorPointer newInterp = dynamic_cast<ConstantVelocityFieldInterpolatorType *>(
    this->m_ConstantVelocityFieldInterpolator->CreateAnother().GetPointer());
  // interpolator needs to know about the velocity field
  newInterp->SetInputImage(rval->GetConstantVelocityField());
  rval->SetConstantVelocityFieldInterpolator(newInterp);
  return loPtr;
}

template <typename TParametersValueType, unsigned int NDimensions>
void
ConstantVelocityFieldTransform<TParametersValueType, NDimensions>::PrintSelf(std::ostream & os, Indent indent) const
{
  Superclass::PrintSelf(os, indent);

  itkPrintSelfObjectMacro(ConstantVelocityFieldInterpolator);
  itkPrintSelfObjectMacro(ConstantVelocityField);

  os << indent << "LowerTimeBound: " << this->m_LowerTimeBound << std::endl;
  os << indent << "UpperTimeBound: " << this->m_UpperTimeBound << std::endl;
  os << indent << "NumberOfIntegrationSteps: " << this->m_NumberOfIntegrationSteps << std::endl;
}

} // namespace itk

#endif
