/*=========================================================================
 *
 *  Copyright NumFOCUS
 *
 *  Licensed under the Apache License, Version 2.0 (the "License" << std::endl;
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         https://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 "itkNeighborhoodIteratorTestCommon.hxx"
#include "itkConstNeighborhoodIteratorWithOnlyIndex.h"

template <typename TImage>
typename TImage::Pointer
itkConstNeighborhoodIteratorWithOnlyIndexTestGetTestImage(int d1, int d2, int d3, int d4)
{
  itk::Size<4> sizeND;
  sizeND[0] = d1;
  sizeND[1] = d2;
  sizeND[2] = d3;
  sizeND[3] = d4;

  const itk::ImageRegion<4> RegionND{ sizeND };

  auto imageND = TImage::New();
  imageND->SetRegions(RegionND);

  return imageND;
}

template <typename TImage>
int
itkConstNeighborhoodIteratorWithOnlyIndexTestRun()
{
  const typename TImage::Pointer img = itkConstNeighborhoodIteratorWithOnlyIndexTestGetTestImage<TImage>(10, 10, 5, 3);

  using ImageType = TImage;
  using ConstNeighborhoodIteratorType = itk::ConstNeighborhoodIteratorWithOnlyIndex<ImageType>;
  using IndexType = typename ConstNeighborhoodIteratorType::IndexType;

  IndexType loc{ 4, 4, 2, 1 };

  typename ConstNeighborhoodIteratorType::RadiusType         radius{ 1, 1, 1, 1 };
  constexpr typename ConstNeighborhoodIteratorType::SizeType sz{ 10, 10, 5, 1 };
  constexpr IndexType                                        idx{ 0, 0, 0, 1 };
  typename ConstNeighborhoodIteratorType::RegionType         reg = { idx, sz };

  std::cout << "Creating ConstNeighborhoodIterator" << std::endl;
  ConstNeighborhoodIteratorType it(radius, img, reg);

  std::cout << "Moving iterator using SetLocation() to loc: " << loc << std::endl;
  it.SetLocation(loc);
  it.Print(std::cout);
  if (it.GetIndex() != loc)
  {
    std::cerr << "Error with SetLocation." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Test GetIndex( NeighborhoodIndex )" << std::endl;
  if (it.GetIndex(it.GetCenterNeighborhoodIndex()) != loc)
  {
    std::cerr << "Error getting index from center nhood index. Returned "
              << it.GetIndex(it.GetCenterNeighborhoodIndex()) << std::endl;
    return EXIT_FAILURE;
  }
  IndexType truthIndex;
  for (unsigned int i = 0; i < 4; ++i)
  {
    truthIndex[i] = loc[i] - radius[i];
  }
  if (it.GetIndex(0) != truthIndex)
  {
    std::cerr << "Error getting index from nhood index 0. Returned " << it.GetIndex(0) << std::endl;
  }

  std::cout << "Testing GoToBegin()" << std::endl;
  it.GoToBegin();
  it.Print(std::cout);
  if (it.GetIndex() != idx)
  {
    std::cerr << "Error with GoToBegin. Expected: " << idx << ", GetIndex: " << it.GetIndex() << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Testing IsAtBegin()" << std::endl;
  std::cout << it.IsAtBegin() << std::endl;
  if (!it.IsAtBegin())
  {
    std::cerr << "Error with IsAtBegin." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Testing GoToEnd()" << std::endl;
  it.GoToEnd();
  it.Print(std::cout);

  std::cout << "Testing IsAtEnd()" << std::endl;
  std::cout << it.IsAtEnd() << std::endl;
  if (!it.IsAtEnd())
  {
    std::cerr << "Error with either IsAtEnd or GoToEnd." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Testing forward iteration" << std::endl;
  it.GoToBegin();
  IndexType index = it.GetIndex();
  ++it;
  if (it.GetIndex()[0] != index[0] + 1)
  {
    std::cerr << "Error with fwd iteration" << std::endl;
    return EXIT_FAILURE;
  }
  while (!it.IsAtEnd())
  {
    ++it;
  }

  // fwd iterate by a line
  it.GoToBegin();
  index = it.GetIndex();
  for (unsigned int i = 0; i < sz[0]; ++i)
  {
    ++it;
  }
  if (it.GetIndex()[0] != index[0] || it.GetIndex()[1] != index[1] + 1)
  {
    std::cerr << "Error with fwd iteration by one line." << std::endl;
    return EXIT_FAILURE;
  }

  // fwd iterate by a slice
  index = it.GetIndex();
  for (unsigned int i = 0; i < sz[0] * sz[1]; ++i)
  {
    ++it;
  }
  if (it.GetIndex()[0] != index[0] || it.GetIndex()[1] != index[1] || it.GetIndex()[2] != index[2] + 1)
  {
    std::cerr << "Error with fwd iteration by one slice." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Testing reverse iteration" << std::endl;
  it.GoToEnd();
  index = it.GetIndex();
  --it;
  if (it.GetIndex()[3] != index[3] - 1)
  {
    std::cerr << "Error with reverse iteration" << std::endl;
    return EXIT_FAILURE;
  }

  index = it.GetIndex();
  for (unsigned int i = 0; i < sz[0]; ++i)
  {
    --it;
  }
  if (it.GetIndex()[0] != index[0] || it.GetIndex()[1] != index[1] - 1)
  {
    std::cerr << "Error with reverse iteration by one line." << std::endl;
    return EXIT_FAILURE;
  }

  index = it.GetIndex();
  for (unsigned int i = 0; i < sz[0] * sz[1]; ++i)
  {
    --it;
  }
  if (it.GetIndex()[0] != index[0] || it.GetIndex()[1] != index[1] || it.GetIndex()[2] != index[2] - 1)
  {
    std::cerr << "Error with reverse iteration by one slice." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Moving iterator using SetLocation()" << std::endl;
  it.SetLocation(loc);
  it.Print(std::cout);

  std::cout << "Testing GetIndex()" << std::endl;
  std::cout << it.GetIndex() << std::endl;

  std::cout << "Testing GetBoundingBoxAsImageRegion" << std::endl;
  std::cout << it.GetBoundingBoxAsImageRegion() << std::endl;

  ////

  std::cout << "Testing random access iteration" << std::endl;

  const typename ImageType::Pointer ra_img =
    itkConstNeighborhoodIteratorWithOnlyIndexTestGetTestImage<TImage>(10, 10, 5, 3);
  loc[0] = 4;
  loc[1] = 4;
  loc[2] = 2;
  loc[3] = 1;

  radius[0] = radius[1] = radius[2] = radius[3] = 1;

  std::cout << "Creating ConstNeighborhoodIterator" << std::endl;
  ConstNeighborhoodIteratorType ra_it(radius, ra_img, ra_img->GetRequestedRegion());
  ConstNeighborhoodIteratorType copy_it;

  std::cout << "Test copying." << std::endl;
  copy_it = ra_it;
  if (copy_it != ra_it || !(copy_it == ra_it))
  {
    std::cerr << "Failure with copying or equality comparison." << std::endl;
    return EXIT_FAILURE;
  }
  if (!(copy_it >= ra_it) || !(copy_it <= ra_it))
  {
    std::cerr << "Failure with copying or >= or <= comparison." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Testing random access" << std::endl;
  ra_it.SetLocation(loc);

  std::cout << "Adding [1, 1, 1, 1]" << std::endl;
  auto a_off = itk::MakeFilled<OffsetType>(1);
  ra_it += a_off;
  for (unsigned int i = 0; i < 4; ++i)
  {
    IndexType ind = ra_it.GetIndex();
    if (ind[i] != loc[i] + 1)
    {
      std::cerr << "Error with adding offset " << a_off << std::endl;
      return EXIT_FAILURE;
    }
  }

  std::cout << "Test iterator comparisons" << std::endl;
  if (!(copy_it < ra_it) || !(copy_it <= ra_it))
  {
    std::cerr << "Error with < or <=." << std::endl;
    return EXIT_FAILURE;
  }
  if (!(copy_it < ra_it) || !(copy_it <= ra_it))
  {
    std::cerr << "Error with < or <=." << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "Subtracting [1, 1, 1, 1]" << std::endl;
  ra_it -= a_off;
  for (unsigned int i = 0; i < 4; ++i)
  {
    IndexType ind = ra_it.GetIndex();
    if (ind[i] != loc[i])
    {
      std::cerr << "Error with subtracting offset " << a_off << std::endl;
      return EXIT_FAILURE;
    }
  }

  copy_it = ra_it;

  std::cout << "Adding [0 0 0 2]" << std::endl;
  a_off.Fill(0);
  a_off[3] = 2;
  ra_it += a_off;
  IndexType truth = loc;
  truth += a_off;
  for (unsigned int i = 0; i < 4; ++i)
  {
    IndexType ind = ra_it.GetIndex();
    if (ind[i] != truth[i])
    {
      std::cerr << "Error with adding offset " << a_off << std::endl;
      return EXIT_FAILURE;
    }
  }

  if (ra_it < copy_it || ra_it <= copy_it || copy_it > ra_it || copy_it >= ra_it)
  {
    std::cerr << "Error with > or >= comparison after adding offset." << std::endl;
    return EXIT_FAILURE;
  }

  copy_it = ra_it;

  std::cout << "Adding [0 8 0 0]" << std::endl;
  a_off.Fill(0);
  a_off[1] = 8;
  ra_it += a_off;
  truth += a_off;
  for (unsigned int i = 0; i < 4; ++i)
  {
    IndexType ind = ra_it.GetIndex();
    if (ind[i] != truth[i])
    {
      std::cerr << "Error with adding offset " << a_off << std::endl;
      return EXIT_FAILURE;
    }
  }

  if (ra_it < copy_it || ra_it <= copy_it || copy_it > ra_it || copy_it >= ra_it)
  {
    std::cerr << "Error with comparison after adding offset " << a_off << std::endl;
    std::cerr << (ra_it < copy_it) << ", " << (ra_it <= copy_it) << ", " << (copy_it > ra_it) << ", "
              << (copy_it >= ra_it) << std::endl;
    return EXIT_FAILURE;
  }

  copy_it = ra_it;

  std::cout << "Adding [5 -3 2 -1]" << std::endl;
  a_off[0] = 5;
  a_off[1] = -3;
  a_off[2] = 2;
  a_off[3] = -1;
  ra_it += a_off;
  truth += a_off;
  for (unsigned int i = 0; i < 4; ++i)
  {
    IndexType ind = ra_it.GetIndex();
    if (ind[i] != truth[i])
    {
      std::cerr << "Error with adding offset " << a_off << std::endl;
      return EXIT_FAILURE;
    }
  }

  if (ra_it > copy_it || ra_it >= copy_it || copy_it < ra_it || copy_it <= ra_it)
  {
    std::cerr << "Error with comparison after adding offset." << std::endl;
    return EXIT_FAILURE;
  }

  //
  // Test IndexInBounds
  //
  std::cout << "Testing IndexInBounds" << std::endl;
  constexpr int                     dims[4]{ 13, 11, 9, 7 };
  const typename ImageType::Pointer iib_img =
    itkConstNeighborhoodIteratorWithOnlyIndexTestGetTestImage<TImage>(dims[0], dims[1], dims[2], dims[3]);
  radius[0] = 4;
  radius[1] = 3;
  radius[2] = 2;
  radius[3] = 1;

  std::cout << "Creating ConstNeighborhoodIterator" << std::endl;
  using IteratorType = ConstNeighborhoodIteratorType;
  IteratorType                      iib_it(radius, iib_img, iib_img->GetRequestedRegion());
  typename IteratorType::OffsetType resultOffset;
  typename IteratorType::OffsetType internalIndex;

  typename IteratorType::IndexType centerLoc;
  centerLoc[0] = static_cast<typename IteratorType::IndexType::IndexValueType>(dims[0] / 2);
  centerLoc[1] = static_cast<typename IteratorType::IndexType::IndexValueType>(dims[1] / 2);
  centerLoc[2] = static_cast<typename IteratorType::IndexType::IndexValueType>(dims[2] / 2);
  centerLoc[3] = static_cast<typename IteratorType::IndexType::IndexValueType>(dims[3] / 2);

  iib_it.GoToBegin();
  bool inside = iib_it.IndexInBounds(0, internalIndex, resultOffset);
  if (inside)
  {
    std::cerr << "IndexInBounds failed for index 0, expected false." << std::endl;
    return EXIT_FAILURE;
  }
  for (unsigned int n = 0; n < 4; ++n)
  {
    if (resultOffset[n] != static_cast<itk::OffsetValueType>(radius[n]))
    {
      std::cerr << "IndexInBounds failed. Expected resultOffset of " << radius << ", but got " << resultOffset
                << std::endl;
      return EXIT_FAILURE;
    }
  }
  inside = iib_it.IndexInBounds(iib_it.Size() - 1, internalIndex, resultOffset);
  if (!inside)
  {
    std::cerr << "IndexInBounds failed for index size-1, expected true." << std::endl;
    return EXIT_FAILURE;
  }
  for (unsigned int n = 0; n < 4; ++n)
  {
    if (resultOffset[n] != 0)
    {
      std::cerr << "IndexInBounds failed. Expected resultOffset of 0, but got " << resultOffset << std::endl;
      return EXIT_FAILURE;
    }
  }

  // Test min boundary by dimension
  int result = EXIT_SUCCESS;
  for (unsigned int n = 0; n < 4; ++n)
  {
    typename IteratorType::IndexType boundaryLoc = centerLoc;
    boundaryLoc[n] = 0;
    iib_it.SetLocation(boundaryLoc);
    if (iib_it.IndexInBounds(0, internalIndex, resultOffset))
    {
      std::cerr << "IndexInBounds failed for min boundaryLoc: " << boundaryLoc << " and dimension: " << n
                << ". Expected false." << std::endl;
      result = EXIT_FAILURE;
    }
  }

  // Test max boundary by dimension
  for (unsigned int n = 0; n < 4; ++n)
  {
    typename IteratorType::IndexType boundaryLoc = centerLoc;
    boundaryLoc[n] = dims[n] - 1;
    iib_it.SetLocation(boundaryLoc);
    if (iib_it.IndexInBounds(iib_it.Size() - 1, internalIndex, resultOffset))
    {
      std::cerr << "IndexInBounds failed for max boundaryLoc: " << boundaryLoc << " and dimension: " << n
                << ". Expected false." << std::endl;
      result = EXIT_FAILURE;
    }
  }

  // Test center
  iib_it.SetLocation(centerLoc);
  inside = iib_it.IndexInBounds(0, internalIndex, resultOffset);
  if (!inside)
  {
    std::cerr << "IndexInBounds failed for index 0, expected true." << std::endl;
    result = EXIT_FAILURE;
  }

  return result;
}

int
itkConstNeighborhoodIteratorWithOnlyIndexTest(int, char *[])
{
  std::cout << "*** Testing with itk::Image" << std::endl << std::endl;
  if (itkConstNeighborhoodIteratorWithOnlyIndexTestRun<itk::Image<signed char, 4>>() == EXIT_FAILURE)
  {
    std::cerr << "XXX Failed with itk::Image XXX" << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << std::endl << std::endl << "*** Testing with itk::ImageBase" << std::endl << std::endl;
  if (itkConstNeighborhoodIteratorWithOnlyIndexTestRun<itk::ImageBase<4>>() == EXIT_FAILURE)
  {
    std::cerr << "XXX Failed with itk::ImageBase XXX" << std::endl;
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
