/*=========================================================================
 *
 *  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.
 *
 *=========================================================================*/
/*=========================================================================
 *
 *  Portions of this file are subject to the VTK Toolkit Version 3 copyright.
 *
 *  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
 *
 *  For complete copyright, license and disclaimer of warranty information
 *  please refer to the NOTICE file at the top of the ITK source tree.
 *
 *=========================================================================*/
#ifndef itkImageBase_hxx
#define itkImageBase_hxx


#include <mutex>
#include "itkProcessObject.h"
#include "itkSpatialOrientation.h"
#include <cstring>
#include "itkMath.h"

namespace itk
{

template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::Allocate(bool)
{}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::Initialize()
{
  //
  // We don't modify ourselves because the "ReleaseData" methods depend upon
  // no modification when initialized. Otherwise BUG: 8490 will
  // reoccur.
  //
  // DO NOT CALL ANY METHODS WHICH MODIFY OURSELVES

  // Call the superclass which should initialize the BufferedRegion ivar.
  Superclass::Initialize();

  // Clear the offset table
  memset(m_OffsetTable, 0, sizeof(m_OffsetTable));

  // Clear the BufferedRegion ivar
  this->InitializeBufferedRegion();
}

template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetSpacing(const SpacingType & spacing)
{
  for (unsigned int i = 0; i < VImageDimension; ++i)
  {
    // Check for zero-valued spacing
    if (spacing[i] == 0.0)
    {
      itkExceptionMacro(
        "Zero-valued spacing is not supported and may result in undefined behavior.\nRefusing to change spacing from "
        << this->m_Spacing << " to " << spacing);
    }

    // Check for negative-valued spacing
    if (spacing[i] < 0.0)
    {
      constexpr char message[] = "Negative spacing is not supported and may result in undefined behavior.\n";
#if !defined(ITK_LEGACY_REMOVE)
      itkWarningMacro(<< message << "Proceeding to set spacing to " << spacing);
      break;
#else
      itkExceptionMacro(<< message << "Refusing to change spacing from " << this->m_Spacing << " to " << spacing);
#endif
    }
  }

  itkDebugMacro("setting Spacing to " << spacing);
  if (this->m_Spacing != spacing)
  {
    this->m_Spacing = spacing;
    this->ComputeIndexToPhysicalPointMatrices();
    this->Modified();
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetSpacing(const double spacing[VImageDimension])
{
  this->SetSpacing(SpacingType(spacing));
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetSpacing(const float spacing[VImageDimension])
{
  this->SetSpacing(SpacingType(spacing));
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetOrigin(const double origin[VImageDimension])
{
  this->SetOrigin(PointType(origin));
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetOrigin(const float origin[VImageDimension])
{
  this->SetOrigin(PointType(origin));
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetDirection(const DirectionType & direction)
{
  bool modified = false;

  if (vnl_determinant(direction.GetVnlMatrix()) == 0.0)
  {
    itkExceptionMacro("Bad direction, determinant is 0. Refusing to change direction from " << this->m_Direction
                                                                                            << " to " << direction);
  }

  for (unsigned int r = 0; r < VImageDimension; ++r)
  {
    for (unsigned int c = 0; c < VImageDimension; ++c)
    {
      if (Math::NotExactlyEquals(m_Direction[r][c], direction[r][c]))
      {
        m_Direction[r][c] = direction[r][c];
        modified = true;
      }
    }
  }

  if (modified)
  {
    this->ComputeIndexToPhysicalPointMatrices();
    this->m_InverseDirection = m_Direction.GetInverse();
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::ComputeIndexToPhysicalPointMatrices()
{
  DirectionType scale;

  for (unsigned int i = 0; i < VImageDimension; ++i)
  {
    scale[i][i] = this->m_Spacing[i];
  }

  this->m_IndexToPhysicalPoint = this->m_Direction * scale;
  this->m_PhysicalPointToIndex = m_IndexToPhysicalPoint.GetInverse();

  this->Modified();
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::ComputeOffsetTable()
{
  // vxl_uint_64 num=1;
  OffsetValueType  num = 1;
  const SizeType & bufferSize = this->GetBufferedRegion().GetSize();

  // m_OffsetTable[0] = (OffsetValueType)num;
  m_OffsetTable[0] = num;
  for (unsigned int i = 0; i < VImageDimension; ++i)
  {
    num *= bufferSize[i];
    // m_OffsetTable[i+1] = (OffsetValueType)num;
    m_OffsetTable[i + 1] = num;
  }
  // if( num > NumericTraits<SizeValueType>::max() )
  //   {
  //   itkExceptionMacro("Requested number of pixels (" << num
  //     << ") is greater than the largest possible number of pixels (" <<
  // NumericTraits<SizeValueType>::max() << ").");
  //   }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::UpdateOutputInformation()
{
  const auto source = this->GetSource();

  if (source)
  {
    source->UpdateOutputInformation();
  }
  else
  {
    // If we don't have a source, we should set our Image to span our
    // buffer (by setting our LargestPossibleRegion to equal our
    // BufferedRegion). However, if the buffer is empty, we leave the
    // LargestPossibleRegion at its prior value.  This allows InPlace
    // filters to overwrite their inputs safely (taking ownership of
    // the pixel buffers), yet respond to subsequent requests for
    // information.
    if (this->GetBufferedRegion().GetNumberOfPixels() > 0)
    {
      this->SetLargestPossibleRegion(this->GetBufferedRegion());
    }
  }

  // Now we should know what our largest possible region is. If our
  // requested region was not set yet, (or has been set to something
  // invalid - with no data in it ) then set it to the largest possible
  // region.
  if (this->GetRequestedRegion().GetNumberOfPixels() == 0)
  {
    this->SetRequestedRegionToLargestPossibleRegion();
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::UpdateOutputData()
{
  // If the requested region does not contain any pixels then there is
  // no reason to Update the output data. This is needed so that
  // filters don't need to update all inputs. This occurs in
  // ImageBase as  oppose to DataObject, but cause this statement
  // requires the specific GetNumberOfPixels methods ( as oppose to a
  // generic Region::IsEmpty method ).
  //
  // Also note, the check of the largest possible region is needed so
  // that an exception will be thrown in the process object when no
  // input has been set. ( This part of the statement could be removed
  // if this check happened earlier in the pipeline )
  if (this->GetRequestedRegion().GetNumberOfPixels() > 0 || this->GetLargestPossibleRegion().GetNumberOfPixels() == 0)
  {
    this->Superclass::UpdateOutputData();
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetRequestedRegionToLargestPossibleRegion()
{
  this->SetRequestedRegion(this->GetLargestPossibleRegion());
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::CopyInformation(const DataObject * data)
{
  // Standard call to the superclass' method
  Superclass::CopyInformation(data);

  if (data)
  {
    // Attempt to cast data to an ImageBase
    const auto * const imgData = dynamic_cast<const ImageBase<VImageDimension> *>(data);

    if (imgData != nullptr)
    {
      // Copy the meta data for this data type
      this->SetLargestPossibleRegion(imgData->GetLargestPossibleRegion());
      this->SetSpacing(imgData->GetSpacing());
      this->SetOrigin(imgData->GetOrigin());
      this->SetDirection(imgData->GetDirection());
      this->SetNumberOfComponentsPerPixel(imgData->GetNumberOfComponentsPerPixel());
    }
    else
    {
      // pointer could not be cast back down
      itkExceptionMacro("itk::ImageBase::CopyInformation() cannot cast " << typeid(data).name() << " to "
                                                                         << typeid(const ImageBase *).name());
    }
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::Graft(const Self * image)
{
  if (!image)
  {
    return;
  }

  // Copy the meta-information
  this->CopyInformation(image);

  // Copy the remaining region information. Subclasses are
  // responsible for copying the pixel container.
  this->SetBufferedRegion(image->GetBufferedRegion());
  this->SetRequestedRegion(image->GetRequestedRegion());
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::Graft(const DataObject * data)
{
  using ImageBaseType = ImageBase<VImageDimension>;

  const auto * image = dynamic_cast<const ImageBaseType *>(data);

  if (!image)
  {
    return;
  }

  // Call Graft() with image input to actually perform the graft operation
  this->Graft(image);
}


template <unsigned int VImageDimension>
bool
ImageBase<VImageDimension>::RequestedRegionIsOutsideOfTheBufferedRegion()
{
  unsigned int      i;
  const IndexType & requestedRegionIndex = this->GetRequestedRegion().GetIndex();
  const IndexType & bufferedRegionIndex = this->GetBufferedRegion().GetIndex();

  const SizeType & requestedRegionSize = this->GetRequestedRegion().GetSize();
  const SizeType & bufferedRegionSize = this->GetBufferedRegion().GetSize();

  for (i = 0; i < VImageDimension; ++i)
  {
    if ((requestedRegionIndex[i] < bufferedRegionIndex[i]) ||
        ((requestedRegionIndex[i] + static_cast<OffsetValueType>(requestedRegionSize[i])) >
         (bufferedRegionIndex[i] + static_cast<OffsetValueType>(bufferedRegionSize[i]))))
    {
      return true;
    }
  }

  return false;
}


template <unsigned int VImageDimension>
bool
ImageBase<VImageDimension>::VerifyRequestedRegion()
{
  bool         retval = true;
  unsigned int i;

  // Is the requested region within the LargestPossibleRegion?
  // Note that the test is indeed against the largest possible region
  // rather than the buffered region; see DataObject::VerifyRequestedRegion.
  const IndexType & requestedRegionIndex = this->GetRequestedRegion().GetIndex();
  const IndexType & largestPossibleRegionIndex = this->GetLargestPossibleRegion().GetIndex();

  const SizeType & requestedRegionSize = this->GetRequestedRegion().GetSize();
  const SizeType & largestPossibleRegionSize = this->GetLargestPossibleRegion().GetSize();

  for (i = 0; i < VImageDimension; ++i)
  {
    if ((requestedRegionIndex[i] < largestPossibleRegionIndex[i]) ||
        ((requestedRegionIndex[i] + static_cast<OffsetValueType>(requestedRegionSize[i])) >
         (largestPossibleRegionIndex[i] + static_cast<OffsetValueType>(largestPossibleRegionSize[i]))))
    {
      retval = false;
    }
  }

  return retval;
}


template <unsigned int VImageDimension>
bool
ImageBase<VImageDimension>::IsCongruentImageGeometry(const ImageBase * otherImage,
                                                     double            coordinateTolerance,
                                                     double            directionTolerance) const
{
  // check that the image occupy the same physical space, and that
  // each index is at the same physical location

  // tolerance for origin and spacing depends on the size of pixel
  // tolerance for directions a fraction of the unit cube.
  const SpacePrecisionType coordinateTol =
    itk::Math::abs(coordinateTolerance * this->GetSpacing()[0]); // use first dimension spacing

  return this->GetOrigin().GetVnlVector().is_equal(otherImage->GetOrigin().GetVnlVector(), coordinateTol) &&
         this->GetSpacing().GetVnlVector().is_equal(otherImage->GetSpacing().GetVnlVector(), coordinateTol) &&
         this->GetDirection().GetVnlMatrix().is_equal(otherImage->GetDirection().GetVnlMatrix(), directionTolerance);
}


template <unsigned int VImageDimension>
bool
ImageBase<VImageDimension>::IsSameImageGeometryAs(const ImageBase * otherImage,
                                                  double            coordinateTolerance,
                                                  double            directionTolerance) const
{
  return this->IsCongruentImageGeometry(otherImage, coordinateTolerance, directionTolerance) &&
         this->GetLargestPossibleRegion() == otherImage->GetLargestPossibleRegion();
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetBufferedRegion(const RegionType & region)
{
  if (m_BufferedRegion != region)
  {
    m_BufferedRegion = region;
    this->ComputeOffsetTable();
    this->Modified();
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::InitializeBufferedRegion()
{
  //
  // We don't modify ourselves because the "ReleaseData" methods depend upon
  // no modification when initialized.
  //
  // Otherwise BUG: 8490 will reoccur

  m_BufferedRegion = RegionType();
  this->ComputeOffsetTable();
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetRequestedRegion(const RegionType & region)
{
  m_RequestedRegion = region;
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetRequestedRegion(const DataObject * data)
{
  const auto * const imgData = dynamic_cast<const ImageBase *>(data);

  if (imgData != nullptr)
  {
    // only copy the RequestedRegion if the parameter object is an image
    this->SetRequestedRegion(imgData->GetRequestedRegion());
  }
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetLargestPossibleRegion(const RegionType & region)
{
  if (m_LargestPossibleRegion != region)
  {
    m_LargestPossibleRegion = region;
    this->Modified();
  }
}


template <unsigned int VImageDimension>
unsigned int
ImageBase<VImageDimension>::GetNumberOfComponentsPerPixel() const
{
  // Returns the number of components in the image.
  // base implementation defaults to 1
  return 1;
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::SetNumberOfComponentsPerPixel(unsigned int)
{
  // does nothing (always 1)
}


template <unsigned int VImageDimension>
void
ImageBase<VImageDimension>::PrintSelf(std::ostream & os, Indent indent) const
{
  Superclass::PrintSelf(os, indent);

  os << indent << "LargestPossibleRegion: " << std::endl;
  this->GetLargestPossibleRegion().PrintSelf(os, indent.GetNextIndent());

  os << indent << "BufferedRegion: " << std::endl;
  this->GetBufferedRegion().PrintSelf(os, indent.GetNextIndent());

  os << indent << "RequestedRegion: " << std::endl;
  this->GetRequestedRegion().PrintSelf(os, indent.GetNextIndent());

  os << indent << "Spacing: " << this->GetSpacing() << std::endl;

  os << indent << "Origin: " << this->GetOrigin() << std::endl;

  os << indent << "Direction: " << std::endl << this->GetDirection() << std::endl;

  os << indent << "IndexToPointMatrix: " << std::endl;
  os << this->m_IndexToPhysicalPoint << std::endl;

  os << indent << "PointToIndexMatrix: " << std::endl;
  os << this->m_PhysicalPointToIndex << std::endl;

  os << indent << "Inverse Direction: " << std::endl;
  os << this->GetInverseDirection() << std::endl;
}

} // end namespace itk

#endif
