/*=========================================================================
 *
 *  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 itkDiscreteLevelSetImage_hxx
#define itkDiscreteLevelSetImage_hxx

#include "itkDiscreteLevelSetImage.h"

namespace itk
{

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::GradientType
DiscreteLevelSetImage< TOutput, VDimension >::EvaluateGradient( const InputType& inputIndex ) const
{
  InputType inputIndexA = inputIndex;
  InputType inputIndexB = inputIndex;

  GradientType dx;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] += 1;
    inputIndexB[dim] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );

    // division by 0 only if image is a single pixel
    const OutputRealType scale = this->m_NeighborhoodScales[dim] / (inputIndexA[dim] - inputIndexB[dim]);

    dx[dim] = ( valueA - valueB ) * scale;

    inputIndexA[dim] = inputIndexB[dim] = inputIndex[dim];

    }

  return dx;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::GradientType
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateForwardGradient( const InputType& inputIndex ) const
{
  const auto centerValue = static_cast< OutputRealType >( this->Evaluate( inputIndex ) );

  InputType inputIndexA = inputIndex;

  GradientType dx;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] += 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const OutputRealType scale = this->m_NeighborhoodScales[dim];

    dx[dim] = ( valueA - centerValue ) * scale;

    inputIndexA[dim] = inputIndex[dim];
    }

  return dx;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::GradientType
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateBackwardGradient( const InputType& inputIndex ) const
{
  const auto centerValue = static_cast< OutputRealType >( this->Evaluate( inputIndex ) );

  InputType inputIndexA = inputIndex;

  GradientType dx;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const OutputRealType scale = this->m_NeighborhoodScales[dim];

    dx[dim] = ( centerValue - valueA ) * scale;

    inputIndexA[dim] = inputIndex[dim];
    }
  return dx;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::HessianType
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateHessian( const InputType& inputIndex ) const
{
  HessianType oHessian;

  const auto centerValue = static_cast< OutputRealType >( this->Evaluate( inputIndex ) );

  InputType inputIndexA = inputIndex;
  InputType inputIndexB = inputIndex;

  InputType inputIndexAa;
  InputType inputIndexBa;
  InputType inputIndexCa;
  InputType inputIndexDa;

  for( unsigned int dim1 = 0; dim1 < Dimension; dim1++ )
    {
    inputIndexA[dim1] += 1;
    inputIndexB[dim1] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim1] = inputIndex[dim1];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim1] = inputIndex[dim1];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );

    oHessian[dim1][dim1] = ( valueA + valueB - 2.0 * centerValue )
        * itk::Math::sqr( this->m_NeighborhoodScales[dim1] );

    inputIndexAa = inputIndexB;
    inputIndexBa = inputIndexB;

    inputIndexCa = inputIndexA;
    inputIndexDa = inputIndexA;

    for( unsigned int dim2 = dim1 + 1; dim2 < Dimension; dim2++ )
      {
      inputIndexAa[dim2] -= 1;
      inputIndexBa[dim2] += 1;

      inputIndexCa[dim2] -= 1;
      inputIndexDa[dim2] += 1;

      if( !this->IsInsideDomain( inputIndexAa ) )
        {
        inputIndexAa[dim2] = inputIndexB[dim2];
        }

      if( !this->IsInsideDomain( inputIndexBa ) )
        {
        inputIndexBa[dim2] = inputIndexB[dim2];
        }

      if( !this->IsInsideDomain( inputIndexCa ) )
        {
        inputIndexCa[dim2] = inputIndexA[dim2];
        }

      if( !this->IsInsideDomain( inputIndexDa ) )
        {
        inputIndexDa[dim2] = inputIndexA[dim2];
        }

      const auto valueAa = static_cast< OutputRealType >( this->Evaluate( inputIndexAa ) );
      const auto valueBa = static_cast< OutputRealType >( this->Evaluate( inputIndexBa ) );
      const auto valueCa = static_cast< OutputRealType >( this->Evaluate( inputIndexCa ) );
      const auto valueDa = static_cast< OutputRealType >( this->Evaluate( inputIndexDa ) );

      oHessian[dim1][dim2] = oHessian[dim2][dim1] =
          0.25 * ( valueAa - valueBa - valueCa + valueDa )
          * this->m_NeighborhoodScales[dim1] * this->m_NeighborhoodScales[dim2];

      inputIndexAa[dim2] = inputIndexB[dim2];
      inputIndexBa[dim2] = inputIndexB[dim2];

      inputIndexCa[dim2] = inputIndexA[dim2];
      inputIndexDa[dim2] = inputIndexA[dim2];
      }

    inputIndexA[dim1] = inputIndex[dim1];
    inputIndexB[dim1] = inputIndex[dim1];
    }

  return oHessian;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::OutputRealType
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateLaplacian( const InputType& inputIndex ) const
{
  OutputRealType oLaplacian = NumericTraits< OutputRealType >::ZeroValue();

  const auto centerValue = static_cast< OutputRealType >( this->Evaluate( inputIndex ) );

  InputType inputIndexA = inputIndex;
  InputType inputIndexB = inputIndex;

  for( unsigned int dim1 = 0; dim1 < Dimension; dim1++ )
    {
    inputIndexA[dim1] += 1;
    inputIndexB[dim1] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim1] = inputIndex[dim1];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim1] = inputIndex[dim1];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );

    oLaplacian += ( valueA + valueB - 2.0 * centerValue )
        * itk::Math::sqr(this->m_NeighborhoodScales[dim1]);

    inputIndexA[dim1] = inputIndex[dim1];
    inputIndexB[dim1] = inputIndex[dim1];
    }

  return oLaplacian;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::Evaluate( const InputType& inputIndex, LevelSetDataType& data ) const
{
  // If it has not already been computed before
  if( data.Value.m_Computed )
    {
    return;
    }

  data.Value.m_Value = this->Evaluate( inputIndex );
  data.Value.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateGradient( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( data.Gradient.m_Computed )
    {
    return;
    }

  // If it has not already been computed before

  // compute the gradient

  InputType inputIndexA = inputIndex;
  InputType inputIndexB = inputIndex;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] += 1;
    inputIndexB[dim] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );
    const OutputRealType scale = this->m_NeighborhoodScales[dim] / (inputIndexA[dim] - inputIndexB[dim]);

    data.Gradient.m_Value[dim] = ( valueA - valueB ) * scale;

    inputIndexA[dim] = inputIndexB[dim] = inputIndex[dim];
    }

  data.Gradient.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateHessian( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( data.Hessian.m_Computed )
    {
    return;
    }

  if( !data.Value.m_Computed )
    {
    data.Value.m_Computed = true;
    data.Value.m_Value = this->Evaluate( inputIndex );
    }

  // compute the hessian
  auto centerValue = static_cast< OutputRealType >( data.Value.m_Value );

  InputType inputIndexA = inputIndex;
  InputType inputIndexB = inputIndex;

  InputType inputIndexAa;
  InputType inputIndexBa;
  InputType inputIndexCa;
  InputType inputIndexDa;

  bool backward = data.BackwardGradient.m_Computed;
  bool forward = data.ForwardGradient.m_Computed;

  for( unsigned int dim1 = 0; dim1 < Dimension; dim1++ )
    {
    inputIndexA[dim1] += 1;
    inputIndexB[dim1] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim1] = inputIndex[dim1];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim1] = inputIndex[dim1];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );

    data.Hessian.m_Value[dim1][dim1] =
        ( valueA + valueB - 2.0 * centerValue ) * itk::Math::sqr( this->m_NeighborhoodScales[dim1] );

    if( !backward )
      {
      data.BackwardGradient.m_Computed = true;
      data.BackwardGradient.m_Value[dim1] =
          ( centerValue - valueB ) * this->m_NeighborhoodScales[dim1];
      }

    if( !forward )
      {
      data.ForwardGradient.m_Computed = true;
      data.ForwardGradient.m_Value[dim1]  =
          ( valueA - centerValue ) * this->m_NeighborhoodScales[dim1];
      }

    inputIndexAa = inputIndexB;
    inputIndexBa = inputIndexB;

    inputIndexCa = inputIndexA;
    inputIndexDa = inputIndexA;

    for( unsigned int dim2 = dim1 + 1; dim2 < Dimension; dim2++ )
      {
      inputIndexAa[dim2] -= 1;
      inputIndexBa[dim2] += 1;

      inputIndexCa[dim2] -= 1;
      inputIndexDa[dim2] += 1;

      if( !this->IsInsideDomain( inputIndexAa ) )
        {
        inputIndexAa[dim2] = inputIndexB[dim2];
        }

      if( !this->IsInsideDomain( inputIndexBa ) )
        {
        inputIndexBa[dim2] = inputIndexB[dim2];
        }

      if( !this->IsInsideDomain( inputIndexCa ) )
        {
        inputIndexCa[dim2] = inputIndexA[dim2];
        }

      if( !this->IsInsideDomain( inputIndexDa ) )
        {
        inputIndexDa[dim2] = inputIndexA[dim2];
        }

      const auto valueAa = static_cast< OutputRealType >( this->Evaluate( inputIndexAa ) );
      const auto valueBa = static_cast< OutputRealType >( this->Evaluate( inputIndexBa ) );
      const auto valueCa = static_cast< OutputRealType >( this->Evaluate( inputIndexCa ) );
      const auto valueDa = static_cast< OutputRealType >( this->Evaluate( inputIndexDa ) );

      data.Hessian.m_Value[dim1][dim2] =
          data.Hessian.m_Value[dim2][dim1] =
          0.25 * ( valueAa - valueBa - valueCa + valueDa )
          * this->m_NeighborhoodScales[dim1] * this->m_NeighborhoodScales[dim2];

      inputIndexAa[dim2] = inputIndexB[dim2];
      inputIndexBa[dim2] = inputIndexB[dim2];

      inputIndexCa[dim2] = inputIndexA[dim2];
      inputIndexDa[dim2] = inputIndexA[dim2];
      }

    inputIndexA[dim1] = inputIndex[dim1];
    inputIndexB[dim1] = inputIndex[dim1];
    }

    data.Hessian.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
typename DiscreteLevelSetImage< TOutput, VDimension >::OutputRealType
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateMeanCurvature( const InputType& inputIndex ) const
{
  OutputRealType oValue = NumericTraits< OutputRealType >::ZeroValue();

  HessianType   hessian = this->EvaluateHessian( inputIndex );
  GradientType  grad = this->EvaluateGradient( inputIndex );

  for( unsigned int i = 0; i < Dimension; i++ )
    {
    for( unsigned int j = 0; j < Dimension; j++ )
      {
      if( j != i )
        {
        oValue -= grad[i] * grad[j] * hessian[i][j];
        oValue += hessian[j][j] * grad[i] * grad[i];
        }
      }
    }

  OutputRealType gradNorm = grad.GetNorm();

  if( gradNorm > itk::Math::eps )
    {
    oValue /= ( gradNorm * gradNorm * gradNorm );
    }
  else
    {
    oValue /= ( NumericTraits< OutputRealType >::OneValue() + gradNorm );
    }

  return oValue;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateLaplacian( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( data.Laplacian.m_Computed )
    {
    return;
    }

  if( !data.Value.m_Computed )
    {
    data.Value.m_Value = this->Evaluate( inputIndex );
    data.Value.m_Computed = true;
    }

  const auto centerValue = static_cast< OutputRealType >( data.Value.m_Value );

  InputType inputIndexA =inputIndex;
  InputType inputIndexB = inputIndex;

  for( unsigned int dim1 = 0; dim1 < Dimension; dim1++ )
    {
    inputIndexA[dim1] += 1;
    inputIndexB[dim1] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim1] = inputIndex[dim1];
      }

    if( !this->IsInsideDomain( inputIndexB ) )
      {
      inputIndexB[dim1] = inputIndex[dim1];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const auto valueB = static_cast< OutputRealType >( this->Evaluate( inputIndexB ) );

    data.Laplacian.m_Value +=
        ( valueA + valueB - 2.0 * centerValue ) * itk::Math::sqr( this->m_NeighborhoodScales[dim1] );

    inputIndexA[dim1] = inputIndex[dim1];
    inputIndexB[dim1] = inputIndex[dim1];
    }

  data.Laplacian.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateMeanCurvature( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( !data.MeanCurvature.m_Computed )
    {
    if( !data.Hessian.m_Computed )
      {
      this->EvaluateHessian( inputIndex, data );
      }

    if( !data.Gradient.m_Computed )
      {
      this->EvaluateGradient( inputIndex, data );
      }

    if( !data.GradientNorm.m_Computed )
      {
      this->EvaluateGradientNorm( inputIndex, data );
      }

    data.MeanCurvature.m_Computed = true;
    data.MeanCurvature.m_Value = NumericTraits< OutputRealType >::ZeroValue();

    for( unsigned int i = 0; i < Dimension; i++ )
      {
      for( unsigned int j = 0; j < Dimension; j++ )
        {
        if( j != i )
          {
          data.MeanCurvature.m_Value -= data.Gradient.m_Value[i]
              * data.Gradient.m_Value[j] * data.Hessian.m_Value[i][j];
          data.MeanCurvature.m_Value += data.Hessian.m_Value[j][j]
              * data.Gradient.m_Value[i] * data.Gradient.m_Value[i];
          }
        }
      }

    OutputRealType temp = data.GradientNorm.m_Value;

    if( temp > itk::Math::eps )
      {
      data.MeanCurvature.m_Value /= ( temp * temp * temp );
      }
    else
      {
      data.MeanCurvature.m_Value /= ( NumericTraits< OutputRealType >::OneValue() + temp );
      }
    }
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateForwardGradient( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( data.ForwardGradient.m_Computed )
    {
    return;
    }

  // compute the gradient
  if( !data.Value.m_Computed )
    {
    data.Value.m_Computed = true;
    data.Value.m_Value = this->Evaluate( inputIndex );
    }

  const auto centerValue = static_cast< OutputRealType >( data.Value.m_Value );

  InputType inputIndexA = inputIndex;

  GradientType dx;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] += 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const OutputRealType scale = this->m_NeighborhoodScales[dim];

    dx[dim] = ( valueA - centerValue ) * scale;

    inputIndexA[dim] = inputIndex[dim];
    }
  data.ForwardGradient.m_Value = dx;

  data.ForwardGradient.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::EvaluateBackwardGradient( const InputType& inputIndex, LevelSetDataType& data ) const
{
  if( data.BackwardGradient.m_Computed )
    {
    return;
    }

  // compute the gradient
  if( !data.Value.m_Computed )
    {
    data.Value.m_Value = this->Evaluate( inputIndex );
    data.Value.m_Computed = true;
    }

  const auto centerValue = static_cast< OutputRealType >( data.Value.m_Value );

  InputType inputIndexA = inputIndex;

  GradientType dx;

  for( unsigned int dim = 0; dim < Dimension; dim++ )
    {
    inputIndexA[dim] -= 1;

    if( !this->IsInsideDomain( inputIndexA ) )
      {
      inputIndexA[dim] = inputIndex[dim];
      }

    const auto valueA = static_cast< OutputRealType >( this->Evaluate( inputIndexA ) );
    const OutputRealType scale = this->m_NeighborhoodScales[dim];

    dx[dim] = ( centerValue - valueA ) * scale;

    inputIndexA[dim] = inputIndex[dim];
    }
  data.BackwardGradient.m_Value = dx;

  data.BackwardGradient.m_Computed = true;
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::Initialize()
{
  Superclass::Initialize();
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::CopyInformation(const DataObject *data)
{
  Superclass::CopyInformation( data );

  const auto * LevelSet = dynamic_cast< const Self * >( data );
  if ( !LevelSet )
    {
    // pointer could not be cast back down
    itkExceptionMacro( << "itk::DiscreteLevelSetImage::CopyInformation() cannot cast "
                       << typeid( data ).name() << " to "
                       << typeid( Self * ).name() );
    }
}

// ----------------------------------------------------------------------------
template< typename TOutput, unsigned int VDimension >
void
DiscreteLevelSetImage< TOutput, VDimension >
::Graft( const DataObject* data )
{
  Superclass::Graft( data );
  const auto * LevelSet = dynamic_cast< const Self* >( data );

  if ( !LevelSet )
    {
    // pointer could not be cast back down
    itkExceptionMacro( << "itk::DiscreteLevelSetImage::CopyInformation() cannot cast "
                       << typeid( data ).name() << " to "
                       << typeid( Self * ).name() );
    }

  this->m_NeighborhoodScales = LevelSet->m_NeighborhoodScales;
}

}
#endif // itkDiscreteLevelSetImage_hxx
