//                                               -*- C++ -*-
/**
 *  @brief Factory for Histogram distribution
 *
 *  Copyright 2005-2024 Airbus-EDF-IMACS-ONERA-Phimeca
 *
 *  This library is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include "openturns/HistogramFactory.hxx"
#include "openturns/DistFunc.hxx"
#include "openturns/SpecFunc.hxx"
#include "openturns/Exception.hxx"
#include "openturns/PersistentObjectFactory.hxx"

BEGIN_NAMESPACE_OPENTURNS

CLASSNAMEINIT(HistogramFactory)

static const Factory<HistogramFactory> Factory_HistogramFactory;

/* Default constructor */
HistogramFactory::HistogramFactory()
  : DistributionFactoryImplementation()
{
  // Nothing to do
}

/* Virtual constructor */
HistogramFactory * HistogramFactory::clone() const
{
  return new HistogramFactory(*this);
}


/* Here is the interface that all derived class must implement */

Distribution HistogramFactory::build(const Sample & sample) const
{
  return buildAsHistogram(sample).clone();
}

Distribution HistogramFactory::build(const Sample & sample,
                                     const Scalar bandwidth) const
{
  return buildAsHistogram(sample, bandwidth).clone();
}

Distribution HistogramFactory::build(const Sample & sample,
                                     const UnsignedInteger binNumber) const
{
  return buildAsHistogram(sample, binNumber).clone();
}

Distribution HistogramFactory::build(const Sample & sample,
                                     const Scalar first,
                                     const Point & width) const
{
  return buildAsHistogram(sample, first, width).clone();
}

Distribution HistogramFactory::build() const
{
  return buildAsHistogram().clone();
}

Histogram HistogramFactory::buildAsHistogram(const Sample & sample) const
{
  return buildAsHistogram(sample, computeBandwidth(sample));
}

Histogram HistogramFactory::buildAsHistogram(const Sample & sample,
    const Scalar bandwidth) const
{
  const UnsignedInteger size = sample.getSize();
  if (size < 2) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution from a sample of size < 2";
  if (sample.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: can build an Histogram only if dimension equals 1, here dimension=" << sample.getDimension();
  if (!(bandwidth > 0.0)) throw InvalidArgumentException(HERE) << "Error: expected a positive bandwidth, got bandwidth=" << bandwidth;
  // Construct the histogram
  const Scalar mean = sample.computeMean()[0];
  if (!SpecFunc::IsNormal(mean)) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution if data contains NaN or Inf";
  // It will extends from min to max.
  const Scalar min = sample.getMin()[0];
  const Scalar max = sample.getMax()[0];
  if (max == min) throw InvalidArgumentException(HERE) << "Error: cannot estimate an Histogram distribution from a constant sample.";
  const UnsignedInteger binNumber = static_cast<UnsignedInteger>(ceil((max - min) / bandwidth + 0.5));
  return buildAsHistogram(sample, binNumber);
}

Histogram HistogramFactory::buildAsHistogram(const Sample & sample,
    const UnsignedInteger binNumber) const
{
  const UnsignedInteger size = sample.getSize();
  if (size < 2) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution from a sample of size < 2";
  if (sample.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: can build an Histogram only if dimension equals 1, here dimension=" << sample.getDimension();
  if (binNumber == 0) throw InvalidArgumentException(HERE) << "Error: expected a positive number of bin, got 0.";
  // Construct the histogram
  const Scalar mean = sample.computeMean()[0];
  if (!SpecFunc::IsNormal(mean)) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution if data contains NaN or Inf";
  // It will extends from min to max.
  const Scalar min = sample.getMin()[0];
  const Scalar max = sample.getMax()[0];
  if (max == min) throw InvalidArgumentException(HERE) << "Error: cannot estimate an Histogram distribution from a constant sample.";
  // Adjust the bin with in order to match the bin number. Add a small adjustment in order to have bins defined as [x_k, x_k+1[ intervals
  const Scalar delta = ResourceMap::GetAsScalar("Distribution-DefaultQuantileEpsilon") * (max - min);
  const Scalar hOpt = ((max - min) + delta) / binNumber;
  Point heights(binNumber, 0.0);
  const Scalar step = 1.0 / hOpt;
  // Aggregate the realizations into the bins
  for(UnsignedInteger i = 0; i < size; ++i)
  {
    // The index takes values in [[0, binNumber-1]] because min <= sample(i, 0) <= max and step < binNumber / (max - min)
    const UnsignedInteger index = static_cast<UnsignedInteger>(floor((sample(i, 0) - min) * step));
    heights[index] += 1.0;
  }
  const Scalar inverseArea = 1.0 / (hOpt * size);
  Histogram result(min, Point(binNumber, hOpt), heights * inverseArea);
  result.setDescription(sample.getDescription());
  return result;
}

Histogram HistogramFactory::buildAsHistogram(const Sample & sample,
    const Scalar first,
    const Point & width) const
{
  const UnsignedInteger size = sample.getSize();
  if (size < 2) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution from a sample of size < 2";
  if (sample.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: can build an Histogram only if dimension equals 1, here dimension=" << sample.getDimension();
  const UnsignedInteger binNumber = width.getSize();
  if (binNumber == 0) throw InvalidArgumentException(HERE) << "Error: expected a positive number of bin, got 0.";
  // Construct the histogram
  Point heights(binNumber);
  const Scalar mean = sample.computeMean()[0];
  if (!SpecFunc::IsNormal(mean)) throw InvalidArgumentException(HERE) << "Error: cannot build an Histogram distribution if data contains NaN or Inf";
  // It will extends from min to max.
  const Scalar xMin = sample.getMin()[0];
  const Scalar xMax = sample.getMax()[0];
  if (xMin == xMax) throw InvalidArgumentException(HERE) << "Error: cannot estimate an Histogram distribution from a constant sample.";
  if (xMin < first) throw InvalidArgumentException(HERE) << "Error: the minimum of the sample is smaller than first";
  // Compute the right bound of the histogram = first + the sum of the widths
  Scalar rightBound = first;
  for(UnsignedInteger j = 0; j < binNumber; ++j)
  {
    if (width[j] <= 0.) throw InvalidArgumentException(HERE) << "Error: at least one width is nonpositive";
    rightBound += width[j];
  }
  if (xMax >= rightBound) throw InvalidArgumentException(HERE) << "Error: the maximum of the sample is greater than the right boundary of the histogram";
  // Count the number of points in each class with a naive loop
  Scalar left = first;
  Scalar right;
  for(UnsignedInteger j = 0; j < binNumber; ++j) // Loop over the bins
  {
    right = left + width[j];
    for(UnsignedInteger i = 0; i < size; ++i)    // Loop over the values
    {
      if ((sample(i, 0) >= left) && (sample(i, 0) < right))
      {
        heights[j] += 1.0;
      }
    }
    left = right;
  }
  // Compute the area
  Scalar area = 0.;
  for(UnsignedInteger j = 0; j < binNumber; ++j) // Loop over the bins
  {
    area += width[j] * heights[j];
  }
  const Scalar inverseArea = 1.0 / area;
  Histogram result(first, width, heights * inverseArea);
  result.setDescription(sample.getDescription());
  return result;
}

Histogram HistogramFactory::buildAsHistogram() const
{
  return Histogram();
}

/* Compute the bandwidth according to Scott or Freedman and Diaconis rule */
Scalar HistogramFactory::computeBandwidth(const Sample & sample,
    const Bool useQuantile) const
{
  const UnsignedInteger size = sample.getSize();
  if (size < 2)
    throw InvalidArgumentException(HERE) << "Cannot build an Histogram distribution from a sample of size < 2";

  const Scalar xMin = sample.getMin()[0];
  const Scalar xMax = sample.getMax()[0];
  if (xMin == xMax)
    throw InvalidArgumentException(HERE) << "Cannot estimate an Histogram distribution from a constant sample.";

  Scalar hOpt = 0.0;
  UnsignedInteger binNumber = 0;

  if (useQuantile)
  {
    // We use the robust estimation of dispersion based on inter-quartile
    hOpt = (sample.computeQuantilePerComponent(0.75)[0] - sample.computeQuantilePerComponent(0.25)[0]) * std::pow(24.0 * std::sqrt(M_PI) / size, 1.0 / 3.0) / (2.0 * DistFunc::qNormal(0.75));

    // If the resulting bandwidth is zero it is because a majority of values are repeated in the sample
    if (hOpt == 0.0)
    {
      LOGWARN(OSS() << "The first and third quartiles are equal, which means that many values are repeated in the given sample. Switch to the standard deviation-based bandwidth.");
    }
    else
      binNumber = static_cast<UnsignedInteger>(ceil((xMax - xMin) / hOpt + 0.5));
  }

  // Here hOpt == 0.0 either because we asked for the standard deviation based bandwidth or because the quantile based bandwidth is zero
  if (hOpt == 0.0)
  {
    // We use the standard deviation
    const Scalar sigma = sample.computeStandardDeviation()[0];
    hOpt = sigma * std::pow(24.0 * std::sqrt(M_PI) / size, 1.0 / 3.0);
    binNumber = static_cast<UnsignedInteger>(ceil((xMax - xMin) / hOpt + 0.5));
  }

  // cap the bin number
  const UnsignedInteger maximumBinNumber = ResourceMap::GetAsUnsignedInteger("HistogramFactory-MaximumBinNumber");
  if (binNumber > maximumBinNumber)
    hOpt = (xMax - xMin) / maximumBinNumber;

  return hOpt;
}

Histogram HistogramFactory::buildFromQuantiles(const Scalar lowerBound, const Point & probabilities, const Point & quantiles) const
{
  const UnsignedInteger size = quantiles.getSize();
  if (probabilities.getSize() != size)
    throw InvalidArgumentException(HERE) << "Fractiles size (" << probabilities.getSize() << ") should match quantile size (" << size << ").";
  Point width(size);
  Point height(size);
  for (UnsignedInteger i = 0; i < size; ++ i)
  {
    width[i] = quantiles[i] - (i > 0 ? quantiles[i - 1] : lowerBound);
    if (!(width[i] > 0.0)) // Avoid a division by zero. Histogram will make the required extra checks.
      throw InvalidArgumentException(HERE) << "Invalid histogram quantile at index " << i;
    height[i] = (probabilities[i] - (i > 0 ? probabilities[i - 1] : 0.0)) / width[i];
  }
  return Histogram(lowerBound, width, height);
}


END_NAMESPACE_OPENTURNS
