/*=========================================================================

  Program:   Visualization Toolkit
  Module:    vtkFiniteDifferenceGradientEstimator.cxx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkFiniteDifferenceGradientEstimator.h"

#include "vtkCharArray.h"
#include "vtkDirectionEncoder.h"
#include "vtkDoubleArray.h"
#include "vtkFloatArray.h"
#include "vtkImageData.h"
#include "vtkIntArray.h"
#include "vtkLongArray.h"
#include "vtkMultiThreader.h"
#include "vtkObjectFactory.h"
#include "vtkPointData.h"
#include "vtkShortArray.h"
#include "vtkUnsignedCharArray.h"
#include "vtkUnsignedIntArray.h"
#include "vtkUnsignedLongArray.h"
#include "vtkUnsignedShortArray.h"

#include <cmath>

vtkStandardNewMacro(vtkFiniteDifferenceGradientEstimator);

// This is the templated function that actually computes the EncodedNormal
// and the GradientMagnitude
template <class T>
void vtkComputeGradients(
  vtkFiniteDifferenceGradientEstimator *estimator, T *data_ptr,
  int thread_id, int thread_count )
{
  vtkIdType           xstep, ystep, zstep;
  int                 x, y, z;
  vtkIdType           offset;
  int                 x_start, x_limit;
  int                 y_start, y_limit;
  int                 z_start, z_limit;
  int                 useClip;
  int                 *clip;
  T                   *dptr;
  unsigned char       *gptr;
  unsigned short      *nptr;
  float               n[3], t;
  float               gvalue;
  float               zeroNormalThreshold;
  int                 useBounds;
  int                 bounds[6];
  int                 size[3];
  float               aspect[3];
  int                 xlow, xhigh;
  float               scale, bias;
  int                 computeGradientMagnitudes;
  vtkDirectionEncoder *direction_encoder;
  int                 zeroPad;

  estimator->GetInputSize( size );
  estimator->GetInputAspect( aspect );
  computeGradientMagnitudes = estimator->GetComputeGradientMagnitudes();
  scale = estimator->GetGradientMagnitudeScale();
  bias = estimator->GetGradientMagnitudeBias();
  zeroPad = estimator->GetZeroPad();

  // adjust the aspect
  aspect[0] = aspect[0] * 2.0 * estimator->SampleSpacingInVoxels;
  aspect[1] = aspect[1] * 2.0 * estimator->SampleSpacingInVoxels;
  aspect[2] = aspect[2] * 2.0 * estimator->SampleSpacingInVoxels;

  // Compute steps through the volume in x, y, and z
  xstep = 1;
  ystep = size[0];
  zstep = ystep * size[1];

  // Multiply by the spacing used for normal estimation
  xstep *= estimator->SampleSpacingInVoxels;
  ystep *= estimator->SampleSpacingInVoxels;
  zstep *= estimator->SampleSpacingInVoxels;

  // Get the length at or below which normals are considered to
  // be "zero"
  zeroNormalThreshold = estimator->GetZeroNormalThreshold();

  useBounds = estimator->GetBoundsClip();

  // Compute an offset based on the thread_id. The volume will
  // be broken into large slabs (thread_count slabs). For this thread
  // we need to access the correct slab. Also compute the z plane that
  // this slab starts on, and the z limit of this slab (one past the
  // end of the slab)
  if ( useBounds )
  {
    estimator->GetBounds( bounds );
    x_start = bounds[0];
    x_limit = bounds[1]+1;
    y_start = bounds[2];
    y_limit = bounds[3]+1;
    z_start = static_cast<int>(
      ( thread_id / static_cast<float>(thread_count) ) *
      (bounds[5]-bounds[4]+1) ) + bounds[4];
    z_limit = static_cast<int>(
      ( (thread_id + 1) / static_cast<float>(thread_count) ) *
      (bounds[5]-bounds[4]+1) ) + bounds[4];
  }
  else
  {
    x_start = 0;
    x_limit = size[0];
    y_start = 0;
    y_limit = size[1];
    z_start = static_cast<int>(
      ( thread_id /static_cast<float>(thread_count) ) * size[2] );
    z_limit = static_cast<int>(
      ( (thread_id + 1) / static_cast<float>(thread_count) ) * size[2] );
  }

  // Do final error checking on limits - make sure they are all within bounds
  // of the scalar input

  x_start = (x_start<0)?(0):(x_start);
  y_start = (y_start<0)?(0):(y_start);
  z_start = (z_start<0)?(0):(z_start);

  x_limit = (x_limit>size[0])?(size[0]):(x_limit);
  y_limit = (y_limit>size[1])?(size[1]):(y_limit);
  z_limit = (z_limit>size[2])?(size[2]):(z_limit);


  direction_encoder = estimator->GetDirectionEncoder();

  useClip = estimator->GetUseCylinderClip();
  clip = estimator->GetCircleLimits();

  // Loop through all the data and compute the encoded normal and
  // gradient magnitude for each scalar location
  for ( z = z_start; z < z_limit; z++ )
  {
    for ( y = y_start; y < y_limit; y++ )
    {
      if ( useClip )
      {
        xlow = ((clip[2*y])>x_start)?(clip[2*y]):(x_start);
        xhigh = ((clip[2*y+1]+1)<x_limit)?(clip[2*y+1]+1):(x_limit);
      }
      else
      {
        xlow = x_start;
        xhigh = x_limit;
      }
      offset = z * zstep + y * ystep + xlow;

      // Set some pointers
      dptr = data_ptr + offset;
      nptr = estimator->EncodedNormals + offset;
      gptr = estimator->GradientMagnitudes + offset;

      for ( x = xlow; x < xhigh; x++ )
      {

        // Use a central difference method if possible,
        // otherwise use a forward or backward difference if
        // we are on the edge

        // Compute the X component
        if ( x < estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[0] = -(static_cast<float>(*(dptr+xstep)));
          }
          else
          {
            n[0] = 2.0*(static_cast<float>(*(dptr)) - static_cast<float>(*(dptr+xstep)));
          }
        }
        else if ( x >= size[0] - estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[0] =  static_cast<float>(*(dptr-xstep));
          }
          else
          {
            n[0] = 2.0*(static_cast<float>(*(dptr-xstep)) - static_cast<float>(*(dptr)));
          }
        }
        else
        {
          n[0] = static_cast<float>(*(dptr-xstep)) - static_cast<float>(*(dptr+xstep));
        }

        // Compute the Y component
        if ( y < estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[1] = -static_cast<float>(*(dptr+ystep));
          }
          else
          {
            n[1] = 2.0*(static_cast<float>(*(dptr)) - static_cast<float>(*(dptr+ystep)));
          }
        }
        else if ( y >= size[1] - estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[1] =  static_cast<float>(*(dptr-ystep));
          }
          else
          {
            n[1] = 2.0*(static_cast<float>(*(dptr-ystep)) - static_cast<float>(*(dptr)));
          }
        }
        else
        {
          n[1] = static_cast<float>(*(dptr-ystep)) - static_cast<float>(*(dptr+ystep));
        }

        // Compute the Z component
        if ( z < estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[2] = -static_cast<float>(*(dptr+zstep));
          }
          else
          {
            n[2] = 2.0*(static_cast<float>(*(dptr)) - static_cast<float>(*(dptr+zstep)));
          }
        }
        else if ( z >= size[2] - estimator->SampleSpacingInVoxels )
        {
          if ( zeroPad )
          {
            n[2] =  static_cast<float>(*(dptr-zstep));
          }
          else
          {
            n[2] = 2.0*(static_cast<float>(*(dptr-zstep)) - static_cast<float>(*(dptr)));
          }
        }
        else
        {
          n[2] = static_cast<float>(*(dptr-zstep)) - static_cast<float>(*(dptr+zstep));
        }

        // Take care of the aspect ratio of the data
        // Scaling in the vtkVolume is isotropic, so this is the
        // only place we have to worry about non-isotropic scaling.
        n[0] /= aspect[0];
        n[1] /= aspect[1];
        n[2] /= aspect[2];

        // Compute the gradient magnitude
        t = sqrt( static_cast<double>( n[0]*n[0] + n[1]*n[1] + n[2]*n[2] ) );

        if ( computeGradientMagnitudes )
        {
          // Encode this into an 8 bit value
          gvalue = (t + bias) * scale;

          if ( gvalue < 0.0 )
          {
            *gptr = 0;
          }
          else if ( gvalue > 255.0 )
          {
            *gptr = 255;
          }
          else
          {
            *gptr = static_cast<unsigned char>(gvalue);
          }
          gptr++;
        }

        // Normalize the gradient direction
        if ( t > zeroNormalThreshold )
        {
          n[0] /= t;
          n[1] /= t;
          n[2] /= t;
        }
        else
        {
          n[0] = n[1] = n[2] = 0.0;
        }

        // Convert the gradient direction into an encoded index value
        *nptr = direction_encoder->GetEncodedDirection( n );
        nptr++;
        dptr++;

      }
    }
  }
}

// Construct a vtkFiniteDifferenceGradientEstimator
vtkFiniteDifferenceGradientEstimator::vtkFiniteDifferenceGradientEstimator()
{
  this->SampleSpacingInVoxels      = 1;
}

// Destruct a vtkFiniteDifferenceGradientEstimator - free up any memory used
vtkFiniteDifferenceGradientEstimator::~vtkFiniteDifferenceGradientEstimator() = default;

static VTK_THREAD_RETURN_TYPE vtkSwitchOnDataType( void *arg )
{
  vtkFiniteDifferenceGradientEstimator   *estimator;
  int                                    thread_count;
  int                                    thread_id;
  vtkDataArray                           *scalars;

  thread_id = ((vtkMultiThreader::ThreadInfo *)(arg))->ThreadID;
  thread_count = ((vtkMultiThreader::ThreadInfo *)(arg))->NumberOfThreads;
  estimator = (vtkFiniteDifferenceGradientEstimator *)
    (((vtkMultiThreader::ThreadInfo *)(arg))->UserData);
  scalars = estimator->InputData->GetPointData()->GetScalars();

  if (scalars == nullptr)
  {
    return VTK_THREAD_RETURN_VALUE;
  }

  // Find the data type of the Input and call the correct
  // templated function to actually compute the normals and magnitudes

  switch ( scalars->GetDataType() )
  {
    vtkTemplateMacro(
      vtkComputeGradients(estimator,
                          static_cast<VTK_TT*>(scalars->GetVoidPointer(0)),
                          thread_id, thread_count)
      );
    default:
      vtkGenericWarningMacro("unable to encode scalar type!");
  }

  return VTK_THREAD_RETURN_VALUE;
}


// This method is used to compute the encoded normal and the
// magnitude of the gradient for each voxel location in the
// Input.
void vtkFiniteDifferenceGradientEstimator::UpdateNormals( )
{
  vtkDebugMacro( << "Updating Normals!" );
  this->Threader->SetNumberOfThreads( this->NumberOfThreads );

  this->Threader->SetSingleMethod( vtkSwitchOnDataType,this );

  this->Threader->SingleMethodExecute();
}

// Print the vtkFiniteDifferenceGradientEstimator
void vtkFiniteDifferenceGradientEstimator::PrintSelf(ostream& os,
                                                     vtkIndent indent)
{
  this->Superclass::PrintSelf(os, indent);

  os << indent << "Sample spacing in voxels: " <<
    this->SampleSpacingInVoxels << endl;
}
