/*=========================================================================
 *
 *  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
 *
 *         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 <iostream>

#include "itkConstantBoundaryCondition.h"
#include "itkConstNeighborhoodIterator.h"
#include "itkVectorImage.h"

using ImageType = itk::Image<int, 2>;
using RegionType = ImageType::RegionType;
using IndexType = ImageType::IndexType;
using SizeType = ImageType::SizeType;
using IteratorType = itk::ConstNeighborhoodIterator<ImageType>;
using RadiusType = IteratorType::RadiusType;

using VectorImageType = itk::VectorImage<int, 2>;
using VectorIteratorType = itk::ConstNeighborhoodIterator<VectorImageType>;

static bool
TestPrintNeighborhood(IteratorType & p, VectorIteratorType & v)
{
  bool success = true;

  std::cout << "Output from operator()(const OffsetType &, const OffsetType &, const NeighborhoodType *) const"
            << std::endl;

  for (unsigned int y = 0, i = 0; y < p.GetSize()[1]; ++y)
  {
    for (unsigned int x = 0; x < p.GetSize()[0]; ++x, ++i)
    {
      std::cout << p.GetPixel(i) << ' ';
    }
    std::cout << std::endl;
  }

  std::cout << "Output from operator()(const OffsetType &, const OffsetType &, const NeighborhoodType *, "
            << "const NeighborhoodAccessorFunctorType &) const" << std::endl;

  for (unsigned int y = 0, i = 0; y < v.GetSize()[1]; ++y)
  {
    for (unsigned int x = 0; x < v.GetSize()[0]; ++x, ++i)
    {
      std::cout << v.GetPixel(i)[0] << ' ';
    }
    std::cout << std::endl;
  }

  std::cout << "Output from GetPixel( const IndexType & index, const TImage * image ) const" << std::endl;

  for (unsigned int y = 0, i = 0; y < p.GetSize()[1]; ++y)
  {
    itk::Index<2> index;
    index[1] = p.GetIndex()[1] - p.GetRadius()[1] + y;
    for (unsigned int x = 0; x < p.GetSize()[0]; ++x, ++i)
    {
      index[0] = p.GetIndex()[0] - p.GetRadius()[0] + x;

      // Access the pixel value through three different methods in the
      // boundary condition.
      const int pixel1 = p.GetBoundaryCondition()->GetPixel(index, p.GetImagePointer());
      const int pixel2 = p.GetPixel(i);
      const int pixel3 = v.GetPixel(i)[0];

      std::cout << pixel1 << ' ';

      // Check agreement of output from three methods of accessing pixel values.
      if (pixel1 != pixel2 || pixel2 != pixel3)
      {
        success = false;
      }
    }
    std::cout << std::endl;
  }

  std::cout << "----" << std::endl;
  if (!success)
  {
    std::cerr << "Unexpected neighborhood value encountered in neighborhoods printed above." << std::endl;
  }

  return success;
}

static bool
CheckInputRequestedRegion(const RegionType & imageRegion,
                          const RegionType & requestedRegion,
                          const RegionType & expectedRegion)
{
  if (requestedRegion != expectedRegion)
  {
    std::cerr << "Unexpected input region for request region: " << std::endl;
    std::cerr << imageRegion << std::endl;
    std::cerr << "Got:" << std::endl;
    std::cerr << requestedRegion << std::endl;
    std::cerr << "Expected: " << std::endl;
    std::cerr << expectedRegion << std::endl;

    return false;
  }

  return true;
}

int
itkConstantBoundaryConditionTest(int, char *[])
{
  // Test an image to cover one operator() method.
  auto               image = ImageType::New();
  constexpr SizeType imageSize{ 5, 5 };
  RegionType         imageRegion{ imageSize };
  image->SetRegions(imageRegion);
  image->Allocate();

  // Test a vector image to cover the other operator() method.
  auto vectorImage = VectorImageType::New();
  vectorImage->SetRegions(imageRegion);
  vectorImage->SetNumberOfComponentsPerPixel(1);
  vectorImage->Allocate();

  ImageType::IndexType pos;
  for (pos[1] = 0; pos[1] < 5; ++pos[1])
  {
    for (pos[0] = 0; pos[0] < 5; ++pos[0])
    {
      image->SetPixel(pos, pos[0] * 10 + pos[1]);
      VectorImageType::PixelType vectorPixel(1);
      vectorPixel[0] = image->GetPixel(pos);
      vectorImage->SetPixel(pos, vectorPixel);
      std::cout << image->GetPixel(pos) << ' ';
    }
    std::cout << std::endl;
  }

  RadiusType radius;
  RadiusType radiusTwo;
  radius[0] = radius[1] = 1;
  IteratorType       it(radius, image, image->GetRequestedRegion());
  VectorIteratorType vit(radius, vectorImage, vectorImage->GetRequestedRegion());

  itk::ConstantBoundaryCondition<ImageType>       bc;
  itk::ConstantBoundaryCondition<VectorImageType> vbc;

  constexpr ImageType::PixelType constant{ 3 };
  bc.SetConstant(constant);

  if (bc.GetConstant() != constant)
  {
    std::cerr << "Got unexpected constant " << bc.GetConstant() << ", expected " << constant << '.' << std::endl;
    return EXIT_FAILURE;
  }

  VectorImageType::PixelType vectorConstant(1);
  vectorConstant[0] = constant;
  vbc.SetConstant(vectorConstant);

  if (vbc.GetConstant()[0] != constant)
  {
    std::cerr << "Got unexpected constant " << vbc.GetConstant() << ", expected " << vectorConstant << '.' << std::endl;
    return EXIT_FAILURE;
  }

  it.OverrideBoundaryCondition(&bc);
  vit.OverrideBoundaryCondition(&vbc);

  pos[0] = pos[1] = 0;
  it.SetLocation(pos);
  vit.SetLocation(pos);

  for (it.GoToBegin(), vit.GoToBegin(); !it.IsAtEnd(); ++it, ++vit)
  {
    std::cout << "Index: " << it.GetIndex() << std::endl;
    const bool success = TestPrintNeighborhood(it, vit);
    if (!success)
    {
      return EXIT_FAILURE;
    }
  }

  radiusTwo[0] = radiusTwo[1] = 2;
  IteratorType       it2(radiusTwo, image, image->GetRequestedRegion());
  VectorIteratorType vit2(radiusTwo, vectorImage, vectorImage->GetRequestedRegion());

  it2.OverrideBoundaryCondition(&bc);
  vit2.OverrideBoundaryCondition(&vbc);

  pos[0] = pos[1] = 0;
  it2.SetLocation(pos);
  vit2.SetLocation(pos);

  for (it2.GoToBegin(), vit2.GoToBegin(); !it2.IsAtEnd(); ++it2, ++vit2)
  {
    std::cout << "Index: " << it2.GetIndex() << std::endl;
    const bool success = TestPrintNeighborhood(it2, vit2);
    if (!success)
    {
      return EXIT_FAILURE;
    }
  }

  // Now test the input region calculation

  // Test 1
  std::cout << "GetInputRequestedRegion() Test 1" << std::endl;
  IndexType  requestIndex{};
  auto       requestSize = SizeType::Filled(2);
  RegionType requestRegion{ requestIndex, requestSize };

  RegionType expectedRegion = requestRegion;

  RegionType inputRegion = bc.GetInputRequestedRegion(imageRegion, requestRegion);
  if (!CheckInputRequestedRegion(imageRegion, inputRegion, expectedRegion))
  {
    std::cerr << "[FAILED]" << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << "[PASSED]" << std::endl;

  // Test 2
  std::cout << "GetInputRequestedRegion() Test 2" << std::endl;
  requestIndex[0] = -2;
  requestIndex[1] = 0;
  requestSize[0] = 3;
  requestSize[1] = 2;
  requestRegion = { requestIndex, requestSize };

  auto expectedIndex = itk::MakeFilled<IndexType>(0);
  expectedRegion.SetIndex(expectedIndex);
  SizeType expectedSize = { 1, 2 };
  expectedSize[0] = 1;
  expectedSize[1] = 2;
  expectedRegion.SetSize(expectedSize);

  inputRegion = bc.GetInputRequestedRegion(imageRegion, requestRegion);
  if (!CheckInputRequestedRegion(imageRegion, inputRegion, expectedRegion))
  {
    std::cerr << "[FAILED]" << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << "[PASSED]" << std::endl;

  // Test 3
  std::cout << "GetInputRequestedRegion() Test 3" << std::endl;
  requestIndex[0] = -2;
  requestIndex[1] = 8;
  requestSize[0] = 3;
  requestSize[1] = 3;
  requestRegion = { requestIndex, requestSize };

  expectedIndex[0] = 0;
  expectedIndex[1] = 0;
  expectedSize[0] = 0;
  expectedSize[1] = 0;
  expectedRegion = { expectedIndex, expectedSize };

  inputRegion = bc.GetInputRequestedRegion(imageRegion, requestRegion);
  if (!CheckInputRequestedRegion(imageRegion, inputRegion, expectedRegion))
  {
    std::cerr << "[FAILED]" << std::endl;
    return EXIT_FAILURE;
  }
  std::cout << "[PASSED]" << std::endl;

  // Other boundary condition tests
  if (bc.RequiresCompleteNeighborhood() != false)
  {
    std::cerr << "RequiresCompleteNeighborhood() expected to return false, got true instead." << std::endl;
    return EXIT_FAILURE;
  }

  // Print boundary conditions
  bc.Print(std::cout);
  vbc.Print(std::cout);

  return EXIT_SUCCESS;
}
