/*=========================================================================
 *
 *  Copyright Insight Software Consortium
 *
 *  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 itkLevelSetNeighborhoodExtractor_hxx
#define itkLevelSetNeighborhoodExtractor_hxx

#include "itkLevelSetNeighborhoodExtractor.h"
#include "itkImageRegionIterator.h"
#include "itkNumericTraits.h"
#include "itkMath.h"

#include <algorithm>

namespace itk
{
/**
 *
 */
template< typename TLevelSet >
LevelSetNeighborhoodExtractor< TLevelSet >
::LevelSetNeighborhoodExtractor() :
  m_InsidePoints(nullptr),
  m_OutsidePoints(nullptr),
  m_InputLevelSet(nullptr),
  m_InputNarrowBand(nullptr),
  m_LargeValue(NumericTraits< PixelType >::max())

{
  m_NodesUsed.resize(SetDimension);
}

/*
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::PrintSelf(std::ostream & os, Indent indent) const
{
  Superclass::PrintSelf(os, indent);
  os << indent << "Input level set: " << m_InputLevelSet.GetPointer();
  os << std::endl;
  os << indent << "Level set value: " << m_LevelSetValue << std::endl;
  os << indent << "Narrow bandwidth: " << m_NarrowBandwidth << std::endl;
  os << indent << "Narrowbanding: " << m_NarrowBanding << std::endl;
  os << indent << "Input narrow band: ";
  os << m_InputNarrowBand.GetPointer() << std::endl;
}

/*
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::SetInputNarrowBand(
  NodeContainer *ptr)
{
  if ( m_InputNarrowBand != ptr )
    {
    m_InputNarrowBand = ptr;
    this->Modified();
    }
}

/**
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::Locate()
{
  this->GenerateData();
}

/**
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::Initialize()
{
  // create new emtpy points containers
  m_InsidePoints = NodeContainer::New();
  m_OutsidePoints = NodeContainer::New();

  m_ImageRegion = this->m_InputLevelSet->GetBufferedRegion();
}

/*
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::GenerateData()
{
  if ( !m_InputLevelSet )
    {
    itkExceptionMacro(<< "Input level set is nullptr");
    }

  this->Initialize();

  if ( m_NarrowBanding )
    {
    this->GenerateDataNarrowBand();
    }
  else
    {
    this->GenerateDataFull();
    }

  itkDebugMacro( << "No. inside points: " << m_InsidePoints->Size() );
  itkDebugMacro( << "No. outside points: " << m_OutsidePoints->Size() );
}

/*
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::GenerateDataFull()
{
  using InputConstIterator = ImageRegionConstIterator< LevelSetImageType >;

  InputConstIterator inIt ( m_InputLevelSet,
                            m_InputLevelSet->GetBufferedRegion() );

  IndexType inputIndex;

  SizeValueType totalPixels  =
    m_InputLevelSet->GetBufferedRegion().GetNumberOfPixels();
  SizeValueType updateVisits = totalPixels / 10;
  if ( updateVisits < 1 ) { updateVisits = 1; }

  SizeValueType i;
  for ( i = 0; !inIt.IsAtEnd(); ++inIt, ++i )
    {
    // update progress
    if ( !( i % updateVisits ) )
      {
      this->UpdateProgress( (float)i / (float)totalPixels );
      }

    inputIndex = inIt.GetIndex();
    this->CalculateDistance(inputIndex);
    }
}

/**
 *
 */
template< typename TLevelSet >
void
LevelSetNeighborhoodExtractor< TLevelSet >
::GenerateDataNarrowBand()
{
  if ( !m_InputNarrowBand )
    {
    itkExceptionMacro(<< "InputNarrowBand has not been set");
    }

  typename NodeContainer::ConstIterator pointsIter;
  typename NodeContainer::ConstIterator pointsEnd;

  pointsIter = m_InputNarrowBand->Begin();
  pointsEnd = m_InputNarrowBand->End();
  NodeType node;
  double   maxValue = m_NarrowBandwidth / 2.0;

  SizeValueType totalPixels  = m_InputNarrowBand->Size();
  SizeValueType updateVisits = totalPixels / 10;
  if ( updateVisits < 1 ) { updateVisits = 1; }

  unsigned int i;
  for ( i = 0; pointsIter != pointsEnd; ++pointsIter, ++i )
    {
    // update progress
    if ( !( i % updateVisits ) )
      {
      this->UpdateProgress( (float)i / (float)totalPixels );
      }

    node = pointsIter.Value();
    if ( itk::Math::abs( node.GetValue() ) <= maxValue )
      {
      this->CalculateDistance( node.GetIndex() );
      }
    }
}

/**
 *
 */
template< typename TLevelSet >
double
LevelSetNeighborhoodExtractor< TLevelSet >
::CalculateDistance(
  IndexType & index)
{
  m_LastPointIsInside = false;

  typename LevelSetImageType::PixelType centerValue;
  PixelType inputPixel;

  inputPixel = m_InputLevelSet->GetPixel(index);
  centerValue = (double)inputPixel;
  centerValue -= m_LevelSetValue;

  NodeType centerNode;
  centerNode.SetIndex(index);

  if ( centerValue == 0.0 )
    {
    centerNode.SetValue(0.0);
    m_InsidePoints->InsertElement(m_InsidePoints->Size(), centerNode);
    m_LastPointIsInside = true;
    return 0.0;
    }

  bool inside = ( centerValue <= 0.0 );

  IndexType neighIndex = index;
  typename LevelSetImageType::PixelType neighValue;
  NodeType neighNode;
  SpacePrecisionType distance;
  SpacePrecisionType spacing;

  // In each dimension, find the distance to the zero set
  // by linear interpolating along the grid line.
  for ( unsigned int j = 0; j < SetDimension; j++ )
    {
    neighNode.SetValue(m_LargeValue);
    spacing = m_InputLevelSet->GetSpacing()[j];

    for ( int s = -1; s < 2; s = s + 2 )
      {
      neighIndex[j] = index[j] + s;

      if ( ! this->m_ImageRegion.IsInside( neighIndex ) )
        {
        continue;
        }

      inputPixel = m_InputLevelSet->GetPixel(neighIndex);
      neighValue = inputPixel;
      neighValue -= m_LevelSetValue;

      if ( ( neighValue > 0 && inside )
           || ( neighValue < 0 && !inside ) )
        {
        distance = centerValue / ( centerValue - neighValue ) * spacing;

        if ( neighNode.GetValue() > distance )
          {
          neighNode.SetValue(distance);
          neighNode.SetIndex(neighIndex);
          }
        }
      } // end one dim loop

    // put the minimum distance neighbor onto the heap
    m_NodesUsed[j] = neighNode;

    // reset neighIndex
    neighIndex[j] = index[j];
    } // end dimension loop

  // sort the neighbors according to distance
  std::sort( m_NodesUsed.begin(), m_NodesUsed.end() );

  // The final distance is given by the minimum distance to the plane
  // crossing formed by the zero set crossing points.
  distance = 0.0;
  for ( unsigned int j = 0; j < SetDimension; j++ )
    {
    neighNode = m_NodesUsed[j];

    if ( neighNode.GetValue() >= m_LargeValue )
      {
      break;
      }

    distance += 1.0 / itk::Math::sqr( (double)neighNode.GetValue() );
    }

  if ( distance == 0.0 )
    {
    return m_LargeValue;
    }

  distance = std::sqrt(1.0 / distance);
  centerNode.SetValue(distance);

  if ( inside )
    {
    m_InsidePoints->InsertElement(m_InsidePoints->Size(), centerNode);
    m_LastPointIsInside = true;
    }
  else
    {
    m_OutsidePoints->InsertElement(m_OutsidePoints->Size(), centerNode);
    m_LastPointIsInside = false;
    }

  return distance;
}
} // namespace itk

#endif
