/*=========================================================================
 *
 *  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.
 *
 *=========================================================================*/
#ifndef itkBinaryMorphologicalClosingImageFilter_hxx
#define itkBinaryMorphologicalClosingImageFilter_hxx

#include "itkBinaryErodeImageFilter.h"
#include "itkBinaryDilateImageFilter.h"
#include "itkProgressAccumulator.h"
#include "itkCropImageFilter.h"
#include "itkConstantPadImageFilter.h"
#include "itkNeighborhoodIterator.h"
#include "itkProgressReporter.h"

/*
 * This code was contributed in the Insight Journal paper:
 * "Binary morphological closing and opening image filters"
 * by Lehmann G.
 * https://doi.org/10.54294/bcwtvq
 *
 */

namespace itk
{
template <typename TInputImage, typename TOutputImage, typename TKernel>
BinaryMorphologicalClosingImageFilter<TInputImage, TOutputImage, TKernel>::BinaryMorphologicalClosingImageFilter()
  : m_ForegroundValue(NumericTraits<InputPixelType>::max())
  , m_SafeBorder(true)
{}

template <typename TInputImage, typename TOutputImage, typename TKernel>
void
BinaryMorphologicalClosingImageFilter<TInputImage, TOutputImage, TKernel>::GenerateData()
{
  // Allocate the outputs
  this->AllocateOutputs();

  // let choose a background value. Background value should not be given by user
  // because closing is extensive so no background pixels will be added
  // it is just needed for internal erosion filter and constant padder
  InputPixelType backgroundValue{};
  if (m_ForegroundValue == backgroundValue)
  {
    // current background value is already used for foreground value
    // choose another one
    backgroundValue = NumericTraits<InputPixelType>::max();
  }

  /** set up erosion and dilation methods */
  auto dilate = BinaryDilateImageFilter<TInputImage, TInputImage, TKernel>::New();

  auto erode = BinaryErodeImageFilter<TInputImage, TOutputImage, TKernel>::New();

  // create the pipeline without input and output image
  dilate->ReleaseDataFlagOn();
  dilate->SetKernel(this->GetKernel());
  dilate->SetForegroundValue(m_ForegroundValue); // Intensity value to dilate

  erode->SetKernel(this->GetKernel());
  erode->ReleaseDataFlagOn();
  erode->SetForegroundValue(m_ForegroundValue); // Intensity value to erode
  erode->SetBackgroundValue(backgroundValue);   // Replacement value for eroded voxel
  erode->SetInput(dilate->GetOutput());

  // now we have 2 cases:
  // + SafeBorder is true so we need to create a bigger image use it as input
  //   and crop the image to the normal output image size
  // + SafeBorder is false; we just have to connect filters
  if (m_SafeBorder)
  {
    using PadType = ConstantPadImageFilter<InputImageType, InputImageType>;
    auto pad = PadType::New();
    pad->SetPadLowerBound(this->GetKernel().GetRadius());
    pad->SetPadUpperBound(this->GetKernel().GetRadius());
    pad->SetConstant(backgroundValue);
    pad->SetInput(this->GetInput());

    dilate->SetInput(pad->GetOutput());

    using CropType = CropImageFilter<TOutputImage, TOutputImage>;
    auto crop = CropType::New();
    crop->SetInput(erode->GetOutput());
    crop->SetUpperBoundaryCropSize(this->GetKernel().GetRadius());
    crop->SetLowerBoundaryCropSize(this->GetKernel().GetRadius());

    auto progress = ProgressAccumulator::New();
    progress->SetMiniPipelineFilter(this);
    progress->RegisterInternalFilter(pad, .1f);
    progress->RegisterInternalFilter(erode, .35f);
    progress->RegisterInternalFilter(dilate, .35f);
    progress->RegisterInternalFilter(crop, .1f);

    crop->GraftOutput(this->GetOutput());
    /** execute the minipipeline */
    crop->Update();

    /** graft the minipipeline output back into this filter's output */
    this->GraftOutput(crop->GetOutput());
  }
  else
  {
    /** set up the minipipeline */
    auto progress = ProgressAccumulator::New();
    progress->SetMiniPipelineFilter(this);
    progress->RegisterInternalFilter(erode, .45f);
    progress->RegisterInternalFilter(dilate, .45f);

    dilate->SetInput(this->GetInput());
    erode->GraftOutput(this->GetOutput());

    /** execute the minipipeline */
    erode->Update();

    /** graft the minipipeline output back into this filter's output */
    this->GraftOutput(erode->GetOutput());
  }

  // finally copy background which should have been eroded
  //
  // iterator on input image
  ImageRegionConstIterator<InputImageType> inIt(this->GetInput(), this->GetOutput()->GetRequestedRegion());
  // iterator on output image
  ImageRegionIterator<OutputImageType> outIt(this->GetOutput(), this->GetOutput()->GetRequestedRegion());

  ProgressReporter progress2(this, 0, this->GetOutput()->GetRequestedRegion().GetNumberOfPixels(), 20, 0.9, 0.1);
  while (!outIt.IsAtEnd())
  {
    if (outIt.Get() != m_ForegroundValue)
    {
      outIt.Set(static_cast<OutputPixelType>(inIt.Get()));
    }
    ++outIt;
    ++inIt;
    progress2.CompletedPixel();
  }

  // the end !
}

template <typename TInputImage, typename TOutputImage, typename TKernel>
void
BinaryMorphologicalClosingImageFilter<TInputImage, TOutputImage, TKernel>::PrintSelf(std::ostream & os,
                                                                                     Indent         indent) const
{
  Superclass::PrintSelf(os, indent);

  os << indent
     << "ForegroundValue: " << static_cast<typename NumericTraits<InputPixelType>::PrintType>(m_ForegroundValue)
     << std::endl;
  os << indent << "SafeBorder: " << m_SafeBorder << std::endl;
}
} // end namespace itk
#endif
