/*=========================================================================
 *
 *  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.
 *
 *=========================================================================*/
#include "itkTestHelper.h"

// GPU include files
#include "itkGPUResampleImageFilter.h"

#include "itkOpenCLContextScopeGuard.h"

// GPU copiers
#include "itkGPUTransformCopier.h"
#include "itkGPUAdvancedCombinationTransformCopier.h"
#include "itkGPUInterpolatorCopier.h"

// GPU factory includes
#include "itkGPUImageFactory.h"
#include "itkGPUResampleImageFilterFactory.h"
#include "itkGPUCastImageFilterFactory.h"
#include "itkGPUAffineTransformFactory.h"
#include "itkGPUTranslationTransformFactory.h"
#include "itkGPUBSplineTransformFactory.h"
#include "itkGPUEuler3DTransformFactory.h"
#include "itkGPUSimilarity3DTransformFactory.h"
#include "itkGPUNearestNeighborInterpolateImageFunctionFactory.h"
#include "itkGPULinearInterpolateImageFunctionFactory.h"
#include "itkGPUBSplineInterpolateImageFunctionFactory.h"
#include "itkGPUBSplineDecompositionImageFilterFactory.h"

// elastix GPU factory includes
#include "itkGPUAdvancedCombinationTransformFactory.h"
#include "itkGPUAdvancedMatrixOffsetTransformBaseFactory.h"
#include "itkGPUAdvancedTranslationTransformFactory.h"
#include "itkGPUAdvancedBSplineDeformableTransformFactory.h"
#include "itkGPUAdvancedSimilarity3DTransformFactory.h"

// ITK include files
#include "itkImageFileReader.h"
#include "itkImageFileWriter.h"
#include "itkImageRegionConstIterator.h"
#include "itkMersenneTwisterRandomVariateGenerator.h"
#include "itkOutputWindow.h"
#include "itkTimeProbe.h"

// elastix include files
#include "itkCommandLineArgumentParser.h"

// Other include files
#include <iomanip> // setprecision, etc.
#include <random>  // For mt19937.
#include <sstream>

//------------------------------------------------------------------------------
// GetHelpString
std::string
GetHelpString()
{
  return "Usage:\n"
         "itkGPUResampleImageFilterTest\n"
         "  -in           input file name\n"
         "  -out          output file names.(outputCPU outputGPU)\n"
         "  -rmse         acceptable rmse error\n"
         "  [-i]          interpolator, one of {NearestNeighbor, Linear, BSpline}, default NearestNeighbor\n"
         "  [-t]          transforms, one of {Affine, Translation, BSpline, Euler, Similarity} or combinations with "
         "option \"-c\", default Affine\n"
         "  [-c]          use combo transform, default false\n"
         "  [-p]          parameter file for the B-spline transform\n"
         "  [-threads]    number of threads, default maximum\n";

} // end GetHelpString()


//------------------------------------------------------------------------------
template <typename TransformType, typename CompositeTransformType>
void
PrintTransform(typename TransformType::Pointer & transform)
{
  std::cout << "Transform type: " << transform->GetNameOfClass();

  const CompositeTransformType * compositeTransform =
    dynamic_cast<const CompositeTransformType *>(transform.GetPointer());

  if (compositeTransform)
  {
    std::cout << " [";
    for (std::size_t i = 0; i < compositeTransform->GetNumberOfTransforms(); ++i)
    {
      std::cout << compositeTransform->GetNthTransform(i)->GetNameOfClass();
      if (i != compositeTransform->GetNumberOfTransforms() - 1)
      {
        std::cout << ", ";
      }
    }
    std::cout << "]" << std::endl;
  }
  else
  {
    std::cout << std::endl;
  }
}


//------------------------------------------------------------------------------
template <typename InputImageType>
typename InputImageType::PointType
ComputeCenterOfTheImage(const typename InputImageType::ConstPointer & image)
{
  const unsigned int Dimension = image->GetImageDimension();

  const typename InputImageType::SizeType  size = image->GetLargestPossibleRegion().GetSize();
  const typename InputImageType::IndexType index = image->GetLargestPossibleRegion().GetIndex();

  using ContinuousIndexType = itk::ContinuousIndex<double, InputImageType::ImageDimension>;
  ContinuousIndexType centerAsContInd;
  for (std::size_t i = 0; i < Dimension; ++i)
  {
    centerAsContInd[i] = static_cast<double>(index[i]) + static_cast<double>(size[i] - 1) / 2.0;
  }

  typename InputImageType::PointType center;
  image->TransformContinuousIndexToPhysicalPoint(centerAsContInd, center);
  return center;
}


//------------------------------------------------------------------------------
template <typename InterpolatorType>
void
DefineInterpolator(typename InterpolatorType::Pointer & interpolator,
                   const std::string &                  interpolatorName,
                   const unsigned int                   splineOrderInterpolator)
{
  // Interpolator typedefs
  using InputImageType = typename InterpolatorType::InputImageType;
  using CoordRepType = typename InterpolatorType::CoordRepType;
  using CoefficientType = CoordRepType;

  // Typedefs for all interpolators
  using NearestNeighborInterpolatorType = itk::NearestNeighborInterpolateImageFunction<InputImageType, CoordRepType>;
  using LinearInterpolatorType = itk::LinearInterpolateImageFunction<InputImageType, CoordRepType>;
  using BSplineInterpolatorType = itk::BSplineInterpolateImageFunction<InputImageType, CoordRepType, CoefficientType>;

  if (interpolatorName == "NearestNeighbor")
  {
    auto tmpInterpolator = NearestNeighborInterpolatorType::New();
    interpolator = tmpInterpolator;
  }
  else if (interpolatorName == "Linear")
  {
    auto tmpInterpolator = LinearInterpolatorType::New();
    interpolator = tmpInterpolator;
  }
  else if (interpolatorName == "BSpline")
  {
    auto tmpInterpolator = BSplineInterpolatorType::New();
    tmpInterpolator->SetSplineOrder(splineOrderInterpolator);
    interpolator = tmpInterpolator;
  }
}


//------------------------------------------------------------------------------
template <typename AffineTransformType>
void
DefineAffineParameters(typename AffineTransformType::ParametersType & parameters)
{
  const unsigned int Dimension = AffineTransformType::InputSpaceDimension;

  // Setup parameters
  parameters.SetSize(Dimension * Dimension + Dimension);
  unsigned int par = 0;
  if (Dimension == 2)
  {
    const double matrix[] = {
      0.9, 0.1, // matrix part
      0.2, 1.1, // matrix part
      0.0, 0.0, // translation
    };

    for (std::size_t i = 0; i < 6; ++i)
    {
      parameters[par++] = matrix[i];
    }
  }
  else if (Dimension == 3)
  {
    const double matrix[] = {
      1.0,    -0.045, 0.02,  // matrix part
      0.0,    1.0,    0.0,   // matrix part
      -0.075, 0.09,   1.0,   // matrix part
      -3.02,  1.3,    -0.045 // translation
    };

    for (std::size_t i = 0; i < 12; ++i)
    {
      parameters[par++] = matrix[i];
    }
  }
}


//------------------------------------------------------------------------------
template <typename TranslationTransformType>
void
DefineTranslationParameters(const std::size_t                                   transformIndex,
                            typename TranslationTransformType::ParametersType & parameters)
{
  const std::size_t Dimension = TranslationTransformType::SpaceDimension;

  // Setup parameters
  parameters.SetSize(Dimension);
  for (std::size_t i = 0; i < Dimension; ++i)
  {
    parameters[i] = (double)i * (double)transformIndex + (double)transformIndex;
  }
}


//------------------------------------------------------------------------------
template <typename BSplineTransformType>
void
DefineBSplineParameters(const std::size_t                               transformIndex,
                        typename BSplineTransformType::ParametersType & parameters,
                        const typename BSplineTransformType::Pointer &  transform,
                        const std::string &                             parametersFileName)
{
  const unsigned int numberOfParameters = transform->GetNumberOfParameters();
  const unsigned int Dimension = BSplineTransformType::SpaceDimension;
  const unsigned int numberOfNodes = numberOfParameters / Dimension;

  parameters.SetSize(numberOfParameters);

  // Open file and read parameters
  std::ifstream infile;
  infile.open(parametersFileName.c_str());

  // Skip number of elements to make unique coefficients per each transformIndex
  for (std::size_t n = 0; n < transformIndex; ++n)
  {
    double parValue;
    infile >> parValue;
  }

  // Read it
  for (std::size_t n = 0; n < numberOfNodes * Dimension; ++n)
  {
    double parValue;
    infile >> parValue;
    parameters[n] = parValue;
  }

  infile.close();
}


//------------------------------------------------------------------------------
template <typename EulerTransformType>
void
DefineEulerParameters(const std::size_t transformIndex, typename EulerTransformType::ParametersType & parameters)
{
  const std::size_t Dimension = EulerTransformType::InputSpaceDimension;

  // Setup parameters
  // 2D: angle 1, translation 2
  // 3D: 6 angle, translation 3
  parameters.SetSize(EulerTransformType::ParametersDimension);

  // Angle
  const double angle = (double)transformIndex * -0.05;

  std::size_t par = 0;
  if (Dimension == 2)
  {
    // See implementation of Rigid2DTransform::SetParameters()
    parameters[0] = angle;
    ++par;
  }
  else if (Dimension == 3)
  {
    // See implementation of Rigid3DTransform::SetParameters()
    for (std::size_t i = 0; i < 3; ++i)
    {
      parameters[par] = angle;
      ++par;
    }
  }

  for (std::size_t i = 0; i < Dimension; ++i)
  {
    parameters[i + par] = (double)i * (double)transformIndex + (double)transformIndex;
  }
}


//------------------------------------------------------------------------------
template <typename SimilarityTransformType>
void
DefineSimilarityParameters(const std::size_t                                  transformIndex,
                           typename SimilarityTransformType::ParametersType & parameters)
{
  const std::size_t Dimension = SimilarityTransformType::InputSpaceDimension;

  // Setup parameters
  // 2D: 2 translation, angle 1, scale 1
  // 3D: 3 translation, angle 3, scale 1
  parameters.SetSize(SimilarityTransformType::ParametersDimension);

  // Scale, Angle
  const double scale = ((double)transformIndex + 1.0) * 0.05 + 1.0;
  const double angle = (double)transformIndex * -0.06;

  if (Dimension == 2)
  {
    // See implementation of Similarity2DTransform::SetParameters()
    parameters[0] = scale;
    parameters[1] = angle;
  }
  else if (Dimension == 3)
  {
    // See implementation of Similarity3DTransform::SetParameters()
    for (std::size_t i = 0; i < Dimension; ++i)
    {
      parameters[i] = angle;
    }
    parameters[6] = scale;
  }

  // Translation
  for (std::size_t i = 0; i < Dimension; ++i)
  {
    parameters[i + Dimension] = -1.0 * ((double)i * (double)transformIndex + (double)transformIndex);
  }
}


//------------------------------------------------------------------------------
// This helper function completely set the transform
// We are using ITK elastix transforms:
// ITK transforms:
// TransformType, AffineTransformType, TranslationTransformType,
// BSplineTransformType, EulerTransformType, SimilarityTransformType
// elastix Transforms:
// AdvancedCombinationTransformType, AdvancedAffineTransformType,
// AdvancedTranslationTransformType, AdvancedBSplineTransformType,
// AdvancedEulerTransformType, AdvancedSimilarityTransformType
template <typename TransformType,
          typename AffineTransformType,
          typename TranslationTransformType,
          typename BSplineTransformType,
          typename EulerTransformType,
          typename SimilarityTransformType,
          typename AdvancedCombinationTransformType,
          typename AdvancedAffineTransformType,
          typename AdvancedTranslationTransformType,
          typename AdvancedBSplineTransformType,
          typename AdvancedEulerTransformType,
          typename AdvancedSimilarityTransformType,
          typename InputImageType>
void
SetTransform(const std::size_t                                            transformIndex,
             const std::string &                                          transformName,
             typename TransformType::Pointer &                            transform,
             typename AdvancedCombinationTransformType::Pointer &         advancedTransform,
             const typename InputImageType::ConstPointer &                image,
             std::vector<typename BSplineTransformType::ParametersType> & bsplineParameters,
             const std::string &                                          parametersFileName)
{
  if (transformName == "Affine")
  {
    if (advancedTransform.IsNull())
    {
      // Create Affine transform
      auto affineTransform = AffineTransformType::New();

      // Define and set affine parameters
      typename AffineTransformType::ParametersType parameters;
      DefineAffineParameters<AffineTransformType>(parameters);
      affineTransform->SetParameters(parameters);

      transform = affineTransform;
    }
    else
    {
      // Create Advanced Affine transform
      auto affineTransform = AdvancedAffineTransformType::New();
      advancedTransform->SetCurrentTransform(affineTransform);

      // Define and set advanced affine parameters
      typename AdvancedAffineTransformType::ParametersType parameters;
      DefineAffineParameters<AdvancedAffineTransformType>(parameters);
      affineTransform->SetParameters(parameters);
    }
  }
  else if (transformName == "Translation")
  {
    if (advancedTransform.IsNull())
    {
      // Create Translation transform
      auto translationTransform = TranslationTransformType::New();

      // Define and set translation parameters
      typename TranslationTransformType::ParametersType parameters;
      DefineTranslationParameters<TranslationTransformType>(transformIndex, parameters);
      translationTransform->SetParameters(parameters);

      transform = translationTransform;
    }
    else
    {
      // Create Advanced Translation transform
      auto translationTransform = AdvancedTranslationTransformType::New();
      advancedTransform->SetCurrentTransform(translationTransform);

      // Define and set advanced translation parameters
      typename AdvancedTranslationTransformType::ParametersType parameters;
      DefineTranslationParameters<AdvancedTranslationTransformType>(transformIndex, parameters);
      translationTransform->SetParameters(parameters);
    }
  }
  else if (transformName == "BSpline")
  {
    const unsigned int                           Dimension = image->GetImageDimension();
    const typename InputImageType::SpacingType   inputSpacing = image->GetSpacing();
    const typename InputImageType::PointType     inputOrigin = image->GetOrigin();
    const typename InputImageType::DirectionType inputDirection = image->GetDirection();
    const typename InputImageType::RegionType    inputRegion = image->GetBufferedRegion();
    const typename InputImageType::SizeType      inputSize = inputRegion.GetSize();

    using MeshSizeType = typename BSplineTransformType::MeshSizeType;
    MeshSizeType gridSize;
    gridSize.Fill(4);

    using PhysicalDimensionsType = typename BSplineTransformType::PhysicalDimensionsType;
    PhysicalDimensionsType gridSpacing;
    for (unsigned int d = 0; d < Dimension; ++d)
    {
      gridSpacing[d] = inputSpacing[d] * (inputSize[d] - 1.0);
    }

    if (advancedTransform.IsNull())
    {
      // Create BSpline transform
      auto bsplineTransform = BSplineTransformType::New();

      // Set grid properties
      bsplineTransform->SetTransformDomainOrigin(inputOrigin);
      bsplineTransform->SetTransformDomainDirection(inputDirection);
      bsplineTransform->SetTransformDomainPhysicalDimensions(gridSpacing);
      bsplineTransform->SetTransformDomainMeshSize(gridSize);

      // Define and set b-spline parameters
      typename BSplineTransformType::ParametersType parameters;
      DefineBSplineParameters<BSplineTransformType>(transformIndex, parameters, bsplineTransform, parametersFileName);

      // Keep them in memory first by copying to the bsplineParameters
      bsplineParameters.push_back(parameters);
      const std::size_t indexAt = bsplineParameters.size() - 1;

      // Do not set parameters, the will be destroyed going out of scope
      // instead, set the ones from the bsplineParameters array
      bsplineTransform->SetParameters(bsplineParameters[indexAt]);

      transform = bsplineTransform;
    }
    else
    {
      // Create Advanced BSpline transform
      auto bsplineTransform = AdvancedBSplineTransformType::New();
      advancedTransform->SetCurrentTransform(bsplineTransform);

      // Set grid properties
      bsplineTransform->SetGridOrigin(inputOrigin);
      bsplineTransform->SetGridDirection(inputDirection);
      bsplineTransform->SetGridSpacing(gridSpacing);
      bsplineTransform->SetGridRegion(gridSize);

      // Define and set b-spline parameters
      typename AdvancedBSplineTransformType::ParametersType parameters;
      DefineBSplineParameters<AdvancedBSplineTransformType>(
        transformIndex, parameters, bsplineTransform, parametersFileName);

      // Keep them in memory first by copying to the bsplineParameters
      bsplineParameters.push_back(parameters);
      const std::size_t indexAt = bsplineParameters.size() - 1;

      // Do not set parameters, the will be destroyed going out of scope
      // instead, set the ones from the bsplineParameters array
      bsplineTransform->SetParameters(bsplineParameters[indexAt]);
    }
  }
  else if (transformName == "Euler")
  {
    // Compute center
    const typename InputImageType::PointType center = ComputeCenterOfTheImage<InputImageType>(image);

    if (advancedTransform.IsNull())
    {
      // Create Euler transform
      auto eulerTransform = EulerTransformType::New();

      // Set center
      eulerTransform->SetCenter(center);

      // Define and set euler parameters
      typename EulerTransformType::ParametersType parameters;
      DefineEulerParameters<EulerTransformType>(transformIndex, parameters);
      eulerTransform->SetParameters(parameters);

      transform = eulerTransform;
    }
    else
    {
      // Create Advanced Euler transform
      auto eulerTransform = AdvancedEulerTransformType::New();
      advancedTransform->SetCurrentTransform(eulerTransform);

      // Set center
      eulerTransform->SetCenter(center);

      // Define and set advanced euler parameters
      typename AdvancedEulerTransformType::ParametersType parameters;
      DefineEulerParameters<AdvancedEulerTransformType>(transformIndex, parameters);
      eulerTransform->SetParameters(parameters);
    }
  }
  else if (transformName == "Similarity")
  {
    // Compute center
    const typename InputImageType::PointType center = ComputeCenterOfTheImage<InputImageType>(image);

    if (advancedTransform.IsNull())
    {
      // Create Similarity transform
      auto similarityTransform = SimilarityTransformType::New();

      // Set center
      similarityTransform->SetCenter(center);

      // Define and set similarity parameters
      typename SimilarityTransformType::ParametersType parameters;
      DefineSimilarityParameters<SimilarityTransformType>(transformIndex, parameters);
      similarityTransform->SetParameters(parameters);

      transform = similarityTransform;
    }
    else
    {
      // Create Advanced Similarity transform
      auto similarityTransform = AdvancedSimilarityTransformType::New();
      advancedTransform->SetCurrentTransform(similarityTransform);

      // Set center
      similarityTransform->SetCenter(center);

      // Define and set advanced similarity parameters
      typename AdvancedSimilarityTransformType::ParametersType parameters;
      DefineSimilarityParameters<AdvancedSimilarityTransformType>(transformIndex, parameters);
      similarityTransform->SetParameters(parameters);
    }
  }
}


//------------------------------------------------------------------------------
// This test compares the CPU with the GPU version of the ResampleImageFilter.
// The filter takes an input image and produces an output image.
// We compare the CPU and GPU output image using RMSE and speed.
//
// The following ITK interpolations are supported:
// itk::NearestNeighborInterpolateImageFunction
// itk::LinearInterpolateImageFunction
// itk::BSplineInterpolateImageFunction
//
// The following ITK transforms are supported:
// itk::CompositeTransform
// itk::AffineTransform
// itk::TranslationTransform
// itk::BSplineTransform
// itk::Euler3DTransform
// itk::Similarity3DTransform
//
// The following elastix transforms are supported:
// itk::AdvancedCombinationTransform
// itk::AdvancedMatrixOffsetTransformBase
// itk::AdvancedTranslationTransform
// itk::AdvancedBSplineDeformableTransform
// itk::AdvancedEuler3DTransform
// itk::AdvancedSimilarity3DTransform
//
int
main(int argc, char * argv[])
{
  // Setup for debugging
  itk::SetupForDebugging();

  // Create and check OpenCL context
  if (!itk::CreateContext())
  {
    return EXIT_FAILURE;
  }
  const itk::OpenCLContextScopeGuard openCLContextScopeGuard{};

  // Check for the device 'double' support
  itk::OpenCLContext::Pointer context = itk::OpenCLContext::GetInstance();
  if (!context->GetDefaultDevice().HasDouble())
  {
    std::cerr << "Your OpenCL device: " << context->GetDefaultDevice().GetName()
              << ", does not support 'double' computations. Consider updating it." << std::endl;
    return EXIT_FAILURE;
  }

  // Create a command line argument parser
  itk::CommandLineArgumentParser::Pointer parser = itk::CommandLineArgumentParser::New();
  parser->SetCommandLineArguments(argc, argv);
  parser->SetProgramHelpText(GetHelpString());

  parser->MarkArgumentAsRequired("-in", "The input filename");
  parser->MarkArgumentAsRequired("-out", "The output filenames");
  parser->MarkArgumentAsRequired("-rmse", "The acceptable rmse error");

  itk::CommandLineArgumentParser::ReturnValue validateArguments = parser->CheckForRequiredArguments();

  if (validateArguments == itk::CommandLineArgumentParser::FAILED)
  {
    return EXIT_FAILURE;
  }
  else if (validateArguments == itk::CommandLineArgumentParser::HELPREQUESTED)
  {
    return EXIT_SUCCESS;
  }

  // Get command line arguments
  std::string inputFileName = "";
  parser->GetCommandLineArgument("-in", inputFileName);

  std::vector<std::string> outputFileNames(2, "");
  parser->GetCommandLineArgument("-out", outputFileNames);

  // Get acceptable rmse error
  double rmseError;
  parser->GetCommandLineArgument("-rmse", rmseError);

  // interpolator argument
  std::string interpolator = "NearestNeighbor";
  parser->GetCommandLineArgument("-i", interpolator);

  if (interpolator != "NearestNeighbor" && interpolator != "Linear" && interpolator != "BSpline")
  {
    std::cerr << "ERROR: interpolator \"-i\" should be one of {NearestNeighbor, Linear, BSpline}." << std::endl;
    return EXIT_FAILURE;
  }

  // transform argument
  const bool               useComboTransform = parser->ArgumentExists("-c");
  std::vector<std::string> transforms;
  transforms.push_back("Affine");
  parser->GetCommandLineArgument("-t", transforms);

  // check that use combo transform provided when used multiple transforms
  if (transforms.size() > 1 && !useComboTransform)
  {
    std::cerr << "ERROR: for multiple transforms option \"-c\" should provided." << std::endl;
    return EXIT_FAILURE;
  }

  // check for supported transforms
  for (std::size_t i = 0; i < transforms.size(); ++i)
  {
    const std::string transformName = transforms[i];
    if (transformName != "Affine" && transformName != "Translation" && transformName != "BSpline" &&
        transformName != "Euler" && transformName != "Similarity")
    {
      std::cerr << "ERROR: transforms \"-t\" should be one of {Affine, Translation, BSpline, Euler, Similarity} or "
                   "combination of them."
                << std::endl;
      return EXIT_FAILURE;
    }
  }

  unsigned int runTimes = 1;
  std::string  parametersFileName = "";
  for (std::size_t i = 0; i < transforms.size(); ++i)
  {
    if (transforms[i] == "BSpline")
    {
      const bool retp = parser->GetCommandLineArgument("-p", parametersFileName);
      if (!retp)
      {
        std::cerr << "ERROR: You should specify parameters file \"-p\" for the B-spline transform." << std::endl;
        return EXIT_FAILURE;
      }
      // Faster B-spline tests
      runTimes = 1;
    }
  }

  // Threads.
  unsigned int maximumNumberOfThreads = itk::MultiThreaderBase::GetGlobalDefaultNumberOfThreads();
  parser->GetCommandLineArgument("-threads", maximumNumberOfThreads);
  itk::MultiThreaderBase::SetGlobalMaximumNumberOfThreads(maximumNumberOfThreads);

  // Setup for debugging.
  itk::SetupForDebugging();

  const unsigned int splineOrderInterpolator = 3;
  std::cout << std::showpoint << std::setprecision(4);

  // Typedefs.
  const unsigned int Dimension = 3;
  using InputPixelType = short;
  using OutputPixelType = short;
  using InputImageType = itk::Image<InputPixelType, Dimension>;
  using OutputImageType = itk::Image<OutputPixelType, Dimension>;
  using SizeValueType = InputImageType::SizeType::SizeValueType;
  using OCLImageTypes = typelist::MakeTypeList<short>::Type;

  // CPU typedefs
  using InterpolatorPrecisionType = float;
  using ScalarType = float;
  using FilterType = itk::ResampleImageFilter<InputImageType, OutputImageType, InterpolatorPrecisionType>;

  // Transform typedefs
  using TransformType = itk::Transform<ScalarType, Dimension, Dimension>;
  using AffineTransformType = itk::AffineTransform<ScalarType, Dimension>;
  using TranslationTransformType = itk::TranslationTransform<ScalarType, Dimension>;
  using BSplineTransformType = itk::BSplineTransform<ScalarType, Dimension, 3>;
  using EulerTransformType = itk::Euler3DTransform<ScalarType>;
  using SimilarityTransformType = itk::Similarity3DTransform<ScalarType>;

  // Advanced transform typedefs
  using AdvancedCombinationTransformType = itk::AdvancedCombinationTransform<ScalarType, Dimension>;
  using AdvancedTransformType = itk::AdvancedTransform<ScalarType, Dimension, Dimension>;
  using AdvancedAffineTransformType = itk::AdvancedMatrixOffsetTransformBase<ScalarType, Dimension, Dimension>;
  using AdvancedTranslationTransformType = itk::AdvancedTranslationTransform<ScalarType, Dimension>;
  using AdvancedBSplineTransformType = itk::AdvancedBSplineDeformableTransform<ScalarType, Dimension, 3>;
  using AdvancedEulerTransformType = itk::AdvancedEuler3DTransform<ScalarType>;
  using AdvancedSimilarityTransformType = itk::AdvancedSimilarity3DTransform<ScalarType>;

  // Transform copiers
  using AdvancedTransformCopierType = itk::
    GPUAdvancedCombinationTransformCopier<OCLImageTypes, OCLImageDims, AdvancedCombinationTransformType, ScalarType>;
  using TransformCopierType = itk::GPUTransformCopier<OCLImageTypes, OCLImageDims, TransformType, ScalarType>;

  // Interpolate typedefs
  using InterpolatorType = itk::InterpolateImageFunction<InputImageType, InterpolatorPrecisionType>;
  //  using NearestNeighborInterpolatorType = itk::NearestNeighborInterpolateImageFunction<
  //    InputImageType, InterpolatorPrecisionType >;
  //  using LinearInterpolatorType = itk::LinearInterpolateImageFunction<
  //    InputImageType, InterpolatorPrecisionType >;
  //  using BSplineInterpolatorType = itk::BSplineInterpolateImageFunction<
  //    InputImageType, ScalarType, InterpolatorPrecisionType >;

  // Interpolator copier
  using InterpolateCopierType =
    itk::GPUInterpolatorCopier<OCLImageTypes, OCLImageDims, InterpolatorType, InterpolatorPrecisionType>;

  using ReaderType = itk::ImageFileReader<InputImageType>;
  using WriterType = itk::ImageFileWriter<OutputImageType>;

  // CPU part
  ReaderType::Pointer       cpuReader;
  FilterType::Pointer       cpuFilter;
  InterpolatorType::Pointer cpuInterpolator;
  TransformType::Pointer    cpuTransform;

  // Keep BSpline transform parameters in memory
  using BSplineParametersType = BSplineTransformType::ParametersType;
  std::vector<BSplineParametersType> bsplineParameters;

  // CPU Reader
  cpuReader = ReaderType::New();
  cpuReader->SetFileName(inputFileName);
  try
  {
    cpuReader->Update();
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "ERROR: Caught ITK exception during cpuReader->Update(): " << e << std::endl;
    return EXIT_FAILURE;
  }

  // Construct and setup the resample filter
  cpuFilter = FilterType::New();

  using RandomNumberGeneratorType = itk::Statistics::MersenneTwisterRandomVariateGenerator;
  RandomNumberGeneratorType::Pointer randomNum = RandomNumberGeneratorType::GetInstance();
  randomNum->SetSeed(std::mt19937::default_seed);

  InputImageType::ConstPointer        inputImage = cpuReader->GetOutput();
  const InputImageType::SpacingType   inputSpacing = inputImage->GetSpacing();
  const InputImageType::PointType     inputOrigin = inputImage->GetOrigin();
  const InputImageType::DirectionType inputDirection = inputImage->GetDirection();
  const InputImageType::RegionType    inputRegion = inputImage->GetBufferedRegion();
  const InputImageType::SizeType      inputSize = inputRegion.GetSize();

  OutputImageType::SpacingType   outputSpacing;
  OutputImageType::PointType     outputOrigin;
  OutputImageType::DirectionType outputDirection;
  OutputImageType::SizeType      outputSize;
  std::stringstream              s;
  s << std::setprecision(4) << std::setiosflags(std::ios_base::fixed);
  double tmp1, tmp2;
  for (std::size_t i = 0; i < Dimension; ++i)
  {
    tmp1 = randomNum->GetUniformVariate(0.9, 1.1);
    tmp2 = inputSpacing[i] * tmp1;
    s << tmp2;
    s >> outputSpacing[i];
    s.clear();

    tmp1 = randomNum->GetUniformVariate(-10.0, 10.0);
    tmp2 = inputOrigin[i] + tmp1;
    s << tmp2;
    s >> outputOrigin[i];
    s.clear();

    for (unsigned int j = 0; j < Dimension; ++j)
    {
      // tmp = randomNum->GetUniformVariate( 0.9 * inputOrigin[ i ], 1.1 *
      // inputOrigin[ i ] );
      outputDirection[i][j] = inputDirection[i][j]; // * tmp;
    }

    tmp1 = randomNum->GetUniformVariate(0.9, 1.1);
    outputSize[i] = itk::Math::Round<SizeValueType>(inputSize[i] * tmp1);
  }

  cpuFilter->SetDefaultPixelValue(-1.0);
  cpuFilter->SetOutputSpacing(outputSpacing);
  cpuFilter->SetOutputOrigin(outputOrigin);
  cpuFilter->SetOutputDirection(outputDirection);
  cpuFilter->SetSize(outputSize);
  cpuFilter->SetOutputStartIndex(inputRegion.GetIndex());

  // Construct, select and setup transform
  if (!useComboTransform)
  {
    AdvancedCombinationTransformType::Pointer dummy;
    SetTransform<
      // ITK Transforms
      TransformType,
      AffineTransformType,
      TranslationTransformType,
      BSplineTransformType,
      EulerTransformType,
      SimilarityTransformType,
      // elastix Transforms
      AdvancedCombinationTransformType,
      AdvancedAffineTransformType,
      AdvancedTranslationTransformType,
      AdvancedBSplineTransformType,
      AdvancedEulerTransformType,
      AdvancedSimilarityTransformType,
      InputImageType>(0, transforms[0], cpuTransform, dummy, inputImage, bsplineParameters, parametersFileName);
  }
  else
  {
    AdvancedTransformType::Pointer            currentTransform;
    AdvancedCombinationTransformType::Pointer initialTransform;
    auto                                      tmpTransform = AdvancedCombinationTransformType::New();
    initialTransform = tmpTransform;
    cpuTransform = tmpTransform;

    for (std::size_t i = 0; i < transforms.size(); ++i)
    {
      if (i == 0)
      {
        SetTransform<
          // ITK Transforms
          TransformType,
          AffineTransformType,
          TranslationTransformType,
          BSplineTransformType,
          EulerTransformType,
          SimilarityTransformType,
          // elastix Transforms
          AdvancedCombinationTransformType,
          AdvancedAffineTransformType,
          AdvancedTranslationTransformType,
          AdvancedBSplineTransformType,
          AdvancedEulerTransformType,
          AdvancedSimilarityTransformType,
          InputImageType>(
          i, transforms[i], cpuTransform, initialTransform, inputImage, bsplineParameters, parametersFileName);
      }
      else
      {
        auto initialNext = AdvancedCombinationTransformType::New();

        SetTransform<
          // ITK Transforms
          TransformType,
          AffineTransformType,
          TranslationTransformType,
          BSplineTransformType,
          EulerTransformType,
          SimilarityTransformType,
          // elastix Transforms
          AdvancedCombinationTransformType,
          AdvancedAffineTransformType,
          AdvancedTranslationTransformType,
          AdvancedBSplineTransformType,
          AdvancedEulerTransformType,
          AdvancedSimilarityTransformType,
          InputImageType>(
          i, transforms[i], cpuTransform, initialNext, inputImage, bsplineParameters, parametersFileName);

        initialTransform->SetInitialTransform(initialNext);
        initialTransform = initialNext;
      }
    }
  }

  // Create CPU interpolator here
  DefineInterpolator<InterpolatorType>(cpuInterpolator, interpolator, splineOrderInterpolator);

  // Print info
  std::cout << "Testing the ResampleImageFilter, CPU vs GPU:\n";
  std::cout << "CPU/GPU transform interpolator #threads time speedup RMSE\n";

  // Time the filter, run on the CPU
  itk::TimeProbe cputimer;
  cputimer.Start();

  for (std::size_t i = 0; i < runTimes; ++i)
  {
    cpuFilter->SetInput(cpuReader->GetOutput());
    cpuFilter->SetTransform(cpuTransform);
    cpuFilter->SetInterpolator(cpuInterpolator);
    try
    {
      cpuFilter->Update();
    }
    catch (itk::ExceptionObject & e)
    {
      std::cerr << "ERROR: Caught ITK exception during cpuFilter->Update(): " << e << std::endl;
      return EXIT_FAILURE;
    }

    // Modify the filter, only not the last iteration
    if (i != runTimes - 1)
    {
      cpuFilter->Modified();
    }
  }
  cputimer.Stop();

  std::cout << "CPU " << cpuTransform->GetNameOfClass() << " " << cpuInterpolator->GetNameOfClass() << " "
            << cpuFilter->GetNumberOfWorkUnits() << " " << cputimer.GetMean() / runTimes << std::endl;

  /** Write the CPU result. */
  auto cpuWriter = WriterType::New();
  cpuWriter->SetInput(cpuFilter->GetOutput());
  cpuWriter->SetFileName(outputFileNames[0].c_str());
  try
  {
    cpuWriter->Update();
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "ERROR: Caught ITK exception during cpuWriter->Update(): " << e << std::endl;
    return EXIT_FAILURE;
  }

  // Register object factory for GPU image and filter
  // All these filters that are constructed after this point are
  // turned into a GPU filter.
  itk::GPUImageFactory2<OCLImageTypes, OCLImageDims>::RegisterOneFactory();
  itk::GPUResampleImageFilterFactory2<OCLImageTypes, OCLImageTypes, OCLImageDims>::RegisterOneFactory();
  itk::GPUCastImageFilterFactory2<OCLImageTypes, OCLImageTypes, OCLImageDims>::RegisterOneFactory();

  // Transforms factory registration
  itk::GPUAffineTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUTranslationTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUBSplineTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUEuler3DTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUSimilarity3DTransformFactory2<OCLImageDims>::RegisterOneFactory();

  // Interpolators factory registration
  itk::GPUNearestNeighborInterpolateImageFunctionFactory2<OCLImageTypes, OCLImageDims>::RegisterOneFactory();
  itk::GPULinearInterpolateImageFunctionFactory2<OCLImageTypes, OCLImageDims>::RegisterOneFactory();
  itk::GPUBSplineInterpolateImageFunctionFactory2<OCLImageTypes, OCLImageDims>::RegisterOneFactory();
  itk::GPUBSplineDecompositionImageFilterFactory2<OCLImageTypes, OCLImageTypes, OCLImageDims>::RegisterOneFactory();

  // Advanced transforms factory registration
  itk::GPUAdvancedCombinationTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUAdvancedMatrixOffsetTransformBaseFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUAdvancedTranslationTransformFactory2<OCLImageDims>::RegisterOneFactory();
  itk::GPUAdvancedBSplineDeformableTransformFactory2<OCLImageDims>::RegisterOneFactory();
  // itk::GPUAdvancedEuler3DTransformFactory2< OCLImageDims >::RegisterOneFactory();
  itk::GPUAdvancedSimilarity3DTransformFactory2<OCLImageDims>::RegisterOneFactory();

  // GPU part
  ReaderType::Pointer       gpuReader;
  FilterType::Pointer       gpuFilter;
  InterpolatorType::Pointer gpuInterpolator;
  TransformType::Pointer    gpuTransform;

  // Construct the filter
  // Use a try/catch, because construction of this filter will trigger
  // OpenCL compilation, which may fail.
  try
  {
    gpuFilter = FilterType::New();
    itk::ITKObjectEnableWarnings(gpuFilter.GetPointer());
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "Caught ITK exception during gpuFilter::New(): " << e << std::endl;
    return EXIT_FAILURE;
  }

  gpuFilter->SetDefaultPixelValue(-1.0);
  gpuFilter->SetOutputSpacing(outputSpacing);
  gpuFilter->SetOutputOrigin(outputOrigin);
  gpuFilter->SetOutputDirection(outputDirection);
  gpuFilter->SetSize(outputSize);
  gpuFilter->SetOutputStartIndex(inputRegion.GetIndex());

  // Also need to re-construct the image reader, so that it now
  // reads a GPUImage instead of a normal image.
  // Otherwise, you will get an exception when running the GPU filter:
  // "ERROR: The GPU InputImage is NULL. Filter unable to perform."
  gpuReader = ReaderType::New();
  gpuReader->SetFileName(inputFileName);
  try
  {
    gpuReader->Update(); // needed?
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "ERROR: Caught ITK exception during cpuReader->Update(): " << e << std::endl;
    return EXIT_FAILURE;
  }

  try
  {
    if (!useComboTransform)
    {
      auto copier = TransformCopierType::New();
      copier->SetInputTransform(cpuTransform);
      copier->SetExplicitMode(false);
      copier->Update();
      gpuTransform = copier->GetModifiableOutput();
    }
    else
    {
      // Get CPU AdvancedCombinationTransform
      const AdvancedCombinationTransformType * CPUAdvancedCombinationTransform =
        dynamic_cast<const AdvancedCombinationTransformType *>(cpuTransform.GetPointer());
      if (CPUAdvancedCombinationTransform)
      {
        auto copier = AdvancedTransformCopierType::New();
        copier->SetInputTransform(CPUAdvancedCombinationTransform);
        copier->SetExplicitMode(false);
        copier->Update();
        gpuTransform = copier->GetModifiableOutput();
      }
      else
      {
        std::cerr << "ERROR: Unable to retrieve CPU AdvancedCombinationTransform." << std::endl;
        return EXIT_FAILURE;
      }
    }
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "ERROR: Caught ITK exception during copy transforms: " << e << std::endl;
    return EXIT_FAILURE;
  }

  // Create GPU copy for interpolator here
  auto interpolateCopier = InterpolateCopierType::New();
  interpolateCopier->SetInputInterpolator(cpuInterpolator);
  interpolateCopier->SetExplicitMode(false);
  interpolateCopier->Update();
  gpuInterpolator = interpolateCopier->GetModifiableOutput();

  // Time the filter, run on the GPU
  itk::TimeProbe gputimer;
  gputimer.Start();
  for (std::size_t i = 0; i < runTimes; ++i)
  {
    try
    {
      gpuFilter->SetInput(gpuReader->GetOutput());
      gpuFilter->SetTransform(gpuTransform);
      gpuFilter->SetInterpolator(gpuInterpolator);
    }
    catch (itk::ExceptionObject & e)
    {
      std::cerr << "ERROR: " << e << std::endl;
      return EXIT_FAILURE;
    }

    try
    {
      gpuFilter->Update();
    }
    catch (itk::ExceptionObject & e)
    {
      std::cerr << "ERROR: " << e << std::endl;
      return EXIT_FAILURE;
    }
    // Modify the filter, only not the last iteration
    if (i != runTimes - 1)
    {
      gpuFilter->Modified();
    }
  }
  gputimer.Stop();

  std::cout << "GPU " << cpuTransform->GetNameOfClass() << " " << cpuInterpolator->GetNameOfClass() << " x "
            << gputimer.GetMean() / runTimes << " " << cputimer.GetMean() / gputimer.GetMean();

  /** Write the GPU result. */
  auto gpuWriter = WriterType::New();
  gpuWriter->SetInput(gpuFilter->GetOutput());
  gpuWriter->SetFileName(outputFileNames[1].c_str());
  try
  {
    gpuWriter->Update();
  }
  catch (itk::ExceptionObject & e)
  {
    std::cerr << "ERROR: " << e << std::endl;
    return EXIT_FAILURE;
  }

  // Compute RMSE
  double       RMSrelative = 0.0;
  const double RMSerror = itk::ComputeRMSE<double, OutputImageType, OutputImageType>(
    cpuFilter->GetOutput(), gpuFilter->GetOutput(), RMSrelative);
  std::cout << " " << RMSerror << std::endl;

  // Check
  if (RMSerror > rmseError)
  {
    std::cerr << "ERROR: the RMSE between the CPU and GPU results is " << RMSerror
              << ", which is larger than the expected " << rmseError << std::endl;
    return EXIT_FAILURE;
  }

  // End program.
  return EXIT_SUCCESS;
}
