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

#include "itkBayesianClassifierInitializationImageFilter.h"
#include "itkScalarImageKmeansImageFilter.h"

#include "itkGaussianMembershipFunction.h"

namespace itk
{
template <typename TInputImage, typename TProbabilityPrecisionType>
BayesianClassifierInitializationImageFilter<TInputImage,
                                            TProbabilityPrecisionType>::BayesianClassifierInitializationImageFilter()

{
  m_MembershipFunctionContainer = nullptr;
}

// GenerateOutputInformation method. Here we force update on the entire input
// image. It does not make sense having K-Means etc otherwise.
template <typename TInputImage, typename TProbabilityPrecisionType>
void
BayesianClassifierInitializationImageFilter<TInputImage, TProbabilityPrecisionType>::GenerateOutputInformation()
{
  // call the superclass' implementation of this method
  Superclass::GenerateOutputInformation();

  // get pointers to the input and output
  typename OutputImageType::Pointer outputPtr = this->GetOutput();
  if (!outputPtr)
  {
    return;
  }

  // Set the size of the output region
  outputPtr->SetBufferedRegion(this->GetInput()->GetLargestPossibleRegion());
  outputPtr->SetLargestPossibleRegion(this->GetInput()->GetLargestPossibleRegion());

  if (m_NumberOfClasses == 0)
  {
    itkExceptionMacro(<< "Number of classes unspecified");
  }
  outputPtr->SetVectorLength(m_NumberOfClasses);
}

template <typename TInputImage, typename TProbabilityPrecisionType>
void
BayesianClassifierInitializationImageFilter<TInputImage, TProbabilityPrecisionType>::InitializeMembershipFunctions()
{
  // Typedefs for the KMeans filter, Covariance calculator...
  using KMeansFilterType = ScalarImageKmeansImageFilter<InputImageType>;
  using KMeansOutputImageType = typename KMeansFilterType::OutputImageType;
  using ConstKMeansIteratorType = ImageRegionConstIterator<KMeansOutputImageType>;

  using CovarianceArrayType = Array<double>;
  using ClassCountArrayType = Array<double>;

  using GaussianMembershipFunctionType = Statistics::GaussianMembershipFunction<MeasurementVectorType>;
  using MeanEstimatorsContainerType =
    VectorContainer<unsigned short, typename GaussianMembershipFunctionType::MeanVectorType *>;
  using CovarianceEstimatorsContainerType =
    VectorContainer<unsigned short, typename GaussianMembershipFunctionType::CovarianceMatrixType *>;

  // Run k means to get the means from the input image
  typename KMeansFilterType::Pointer kmeansFilter = KMeansFilterType::New();
  kmeansFilter->SetInput(this->GetInput());
  kmeansFilter->SetUseNonContiguousLabels(false);

  for (unsigned k = 0; k < m_NumberOfClasses; k++)
  {
    const double userProvidedInitialMean = k;
    // TODO: Choose more reasonable defaults for specifying the initial means
    // to the KMeans filter. We could also add this as an option of the filter.
    kmeansFilter->AddClassWithInitialMean(userProvidedInitialMean);
  }

  try
  {
    kmeansFilter->Update();
  }
  catch (ExceptionObject & err)
  {
    // Pass exception to caller
    throw err;
  }

  typename KMeansFilterType::ParametersType estimatedMeans = kmeansFilter->GetFinalMeans(); // mean of each class

  // find class covariances from the kmeans output to initialize the gaussian
  // density functions.
  ConstKMeansIteratorType itrKMeansImage(kmeansFilter->GetOutput(), kmeansFilter->GetOutput()->GetBufferedRegion());
  CovarianceArrayType     sumsOfSquares(m_NumberOfClasses);    // sum of the
                                                               // square
                                                               // intensities
                                                               // for each
                                                               // class
  CovarianceArrayType sums(m_NumberOfClasses);                 // sum of the
                                                               // intensities
                                                               // for each
                                                               // class
  ClassCountArrayType classCount(m_NumberOfClasses);           // m_Number of
                                                               // pixels
                                                               // belonging to
                                                               // each class
  CovarianceArrayType estimatedCovariances(m_NumberOfClasses); // covariance
                                                               // of each
                                                               // class

  // initialize the arrays
  sumsOfSquares.Fill(0.0);
  sums.Fill(0.0);
  classCount.Fill(0);

  const InputImageType *              inputImage = this->GetInput();
  typename InputImageType::RegionType imageRegion = inputImage->GetLargestPossibleRegion();
  InputImageIteratorType              itrInputImage(inputImage, imageRegion);

  itrInputImage.GoToBegin();
  itrKMeansImage.GoToBegin();

  // find sumsOfSquares, sums, and classCount by indexing using the kmeans
  // output labelmap
  while (!itrInputImage.IsAtEnd())
  {
    sumsOfSquares[(unsigned int)itrKMeansImage.Get()] =
      sumsOfSquares[(unsigned int)itrKMeansImage.Get()] + itrInputImage.Get() * itrInputImage.Get();
    sums[(unsigned int)itrKMeansImage.Get()] = sums[(unsigned int)itrKMeansImage.Get()] + itrInputImage.Get();
    ++classCount[(unsigned int)itrKMeansImage.Get()];
    ++itrInputImage;
    ++itrKMeansImage;
  }

  // calculate the class covariances using the sumsOfSquares, sums, and
  // classCount information
  itkDebugMacro(<< "Estimated parameters after Kmeans filter");
  for (unsigned int i = 0; i < m_NumberOfClasses; ++i)
  {
    estimatedCovariances[i] =
      (sumsOfSquares[i] / classCount[i]) - ((sums[i] * sums[i]) / (classCount[i] * classCount[i]));
    if (estimatedCovariances[i] < 0.0000001) // set lower limit for
                                             // covariance
    {
      estimatedCovariances[i] = 0.0000001;
    }
    itkDebugMacro(<< "cluster[" << i << "]-- ");
    itkDebugMacro(<< " estimated mean : " << estimatedMeans[i]);
    itkDebugMacro(<< " estimated covariance : " << estimatedCovariances[i]);
  }

  // Create gaussian membership functions.
  typename MeanEstimatorsContainerType::Pointer       meanEstimatorsContainer = MeanEstimatorsContainerType::New();
  typename CovarianceEstimatorsContainerType::Pointer covarianceEstimatorsContainer =
    CovarianceEstimatorsContainerType::New();
  meanEstimatorsContainer->Reserve(m_NumberOfClasses);
  covarianceEstimatorsContainer->Reserve(m_NumberOfClasses);

  m_MembershipFunctionContainer = MembershipFunctionContainerType::New();
  m_MembershipFunctionContainer->Initialize(); // Clear elements
  for (unsigned int i = 0; i < m_NumberOfClasses; ++i)
  {
    meanEstimatorsContainer->InsertElement(i, new typename GaussianMembershipFunctionType::MeanVectorType(1));
    covarianceEstimatorsContainer->InsertElement(i,
                                                 new typename GaussianMembershipFunctionType::CovarianceMatrixType());
    auto * meanEstimators =
      const_cast<typename GaussianMembershipFunctionType::MeanVectorType *>(meanEstimatorsContainer->GetElement(i));
    auto * covarianceEstimators = const_cast<typename GaussianMembershipFunctionType::CovarianceMatrixType *>(
      covarianceEstimatorsContainer->GetElement(i));
    covarianceEstimators->SetSize(1, 1);

    meanEstimators->Fill(estimatedMeans[i]);
    covarianceEstimators->Fill(estimatedCovariances[i]);
    typename GaussianMembershipFunctionType::Pointer gaussianDensityFunction = GaussianMembershipFunctionType::New();
    gaussianDensityFunction->SetMean(*(meanEstimatorsContainer->GetElement(i)));
    gaussianDensityFunction->SetCovariance(*(covarianceEstimatorsContainer->GetElement(i)));

    m_MembershipFunctionContainer->InsertElement(
      i, dynamic_cast<MembershipFunctionType *>(gaussianDensityFunction.GetPointer()));
  }

  // delete allocated elements in meanEstimators
  while (!meanEstimatorsContainer->empty())
  {
    delete meanEstimatorsContainer->back();
    meanEstimatorsContainer->pop_back();
  }
  // delete allocated elements in covarianceEstimators
  while (!covarianceEstimatorsContainer->empty())
  {
    delete covarianceEstimatorsContainer->back();
    covarianceEstimatorsContainer->pop_back();
  }
}

template <typename TInputImage, typename TProbabilityPrecisionType>
void
BayesianClassifierInitializationImageFilter<TInputImage, TProbabilityPrecisionType>::GenerateData()
{
  // TODO Check if we need a progress accumulator
  const InputImageType * inputImage = this->GetInput();

  typename InputImageType::RegionType imageRegion = inputImage->GetLargestPossibleRegion();
  InputImageIteratorType              itrInputImage(inputImage, imageRegion);

  if (!m_UserSuppliesMembershipFunctions)
  {
    // Perform Kmeans classification to initialize the gaussian density function
    // find class means via kmeans classification
    this->InitializeMembershipFunctions();
  }

  if (m_MembershipFunctionContainer->Size() != m_NumberOfClasses)
  {
    itkExceptionMacro(<< "Number of membership functions should be the same as the number of classes");
  }

  this->AllocateOutputs();

  // create vector image of membership probabilities
  OutputImageType * membershipImage = this->GetOutput();

  MembershipImageIteratorType itrMembershipImage(membershipImage, imageRegion);
  MembershipPixelType         membershipPixel(m_NumberOfClasses);
  MeasurementVectorType       mv;

  itrMembershipImage.GoToBegin();
  itrInputImage.GoToBegin();
  while (!itrMembershipImage.IsAtEnd())
  {
    mv[0] = itrInputImage.Get();
    for (unsigned int i = 0; i < m_NumberOfClasses; i++)
    {
      membershipPixel[i] = (m_MembershipFunctionContainer->GetElement(i))->Evaluate(mv);
    }
    itrMembershipImage.Set(membershipPixel);
    ++itrInputImage;
    ++itrMembershipImage;
  }
}

template <typename TInputImage, typename TProbabilityPrecisionType>
void
BayesianClassifierInitializationImageFilter<TInputImage, TProbabilityPrecisionType>::SetMembershipFunctions(
  MembershipFunctionContainerType * membershipFunction)
{
  if (m_NumberOfClasses)
  {
    if (membershipFunction->Size() != m_NumberOfClasses)
    {
      itkExceptionMacro(<< "Number of membership functions should be the same as the number of classes");
    }
  }
  else
  {
    m_NumberOfClasses = membershipFunction->Size();
  }

  this->m_MembershipFunctionContainer = membershipFunction;
  m_UserSuppliesMembershipFunctions = true;
  this->Modified();
}

template <typename TInputImage, typename TProbabilityPrecisionType>
void
BayesianClassifierInitializationImageFilter<TInputImage, TProbabilityPrecisionType>::PrintSelf(std::ostream & os,
                                                                                               Indent indent) const
{
  Superclass::PrintSelf(os, indent);
  os << indent << "NumberOfClasses: " << m_NumberOfClasses << std::endl;
  if (m_MembershipFunctionContainer)
  {
    os << indent << "Membership function container:" << m_MembershipFunctionContainer << std::endl;
  }
  if (m_UserSuppliesMembershipFunctions)
  {
    os << indent << "Membership functions provided" << std::endl;
  }
  else
  {
    os << indent << "Membership functions not provided" << std::endl;
  }
}
} // end namespace itk

#endif
