//                                               -*- C++ -*-
/**
 *  @file  ProductDistribution.cxx
 *  @brief The ProductDistribution distribution
 *
 *  Copyright 2005-2015 Airbus-EDF-IMACS-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
 *  along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  @author schueller
 *  @date   2012-02-17 19:35:43 +0100 (Fri, 17 Feb 2012)
 */
#include <cstdlib>
#include <cmath>

#include "ProductDistribution.hxx"
#include "GaussKronrod.hxx"
#include "PersistentObjectFactory.hxx"
#include "MethodBoundNumericalMathEvaluationImplementation.hxx"
#include "Uniform.hxx"

BEGIN_NAMESPACE_OPENTURNS

CLASSNAMEINIT(ProductDistribution);

static Factory<ProductDistribution> RegisteredFactory("ProductDistribution");

/* Default constructor */
ProductDistribution::ProductDistribution()
  : ContinuousDistribution()
  , left_(Uniform(0.0, 1.0))
  , right_(Uniform(0.0, 1.0))
{
  setName("ProductDistribution");
  setDimension(1);
  // Adjust the truncation interval and the distribution range
  computeRange();
}

/* Parameters constructor to use when the two bounds are finite */
ProductDistribution::ProductDistribution(const Distribution & left,
    const Distribution & right)
  : ContinuousDistribution()
  , left_()
  , right_()
{
  setName("ProductDistribution");
  setLeft(left);
  setRight(right);
  computeRange();
}

/* Comparison operator */
Bool ProductDistribution::operator ==(const ProductDistribution & other) const
{
  if (this == &other) return true;
  return (left_ == other.getLeft()) && (right_ == other.getRight());
}

/* String converter */
String ProductDistribution::__repr__() const
{
  OSS oss;
  oss << "class=" << ProductDistribution::GetClassName()
      << " name=" << getName()
      << " left=" << left_
      << " right=" << right_;
  return oss;
}

String ProductDistribution::__str__(const String & offset) const
{
  OSS oss;
  oss << offset << getClassName() << "(" << left_.__str__() << " * " << right_.__str__() << ")";
  return oss;
}

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

/* Compute the numerical range of the distribution given the parameters values */
void ProductDistribution::computeRange()
{
  const NumericalScalar a(left_.getRange().getLowerBound()[0]);
  const NumericalScalar b(left_.getRange().getUpperBound()[0]);
  const NumericalScalar c(right_.getRange().getLowerBound()[0]);
  const NumericalScalar d(right_.getRange().getUpperBound()[0]);
  const NumericalScalar ac(a * c);
  const NumericalScalar ad(a * d);
  const NumericalScalar bc(b * c);
  const NumericalScalar bd(b * d);
  const NumericalScalar mu(getMean()[0]);
  const NumericalScalar sigma(std::sqrt(getCovariance()(0, 0)));
  setRange(Interval(std::max(mu - 10.0 * sigma, std::min(std::min(ac, ad), std::min(bc, bd))), std::min(mu + 10.0 * sigma, std::max(std::max(ac, ad), std::max(bc, bd)))));
}

/* Get one realization of the distribution */
NumericalPoint ProductDistribution::getRealization() const
{
  return NumericalPoint(1, left_.getRealization()[0] * right_.getRealization()[0]);
}

/* Get the PDF of the distribution: PDF(x) = \int_R PDF_left(u) * PDF_right(x / u) * du / |u| */
NumericalScalar ProductDistribution::computePDF(const NumericalPoint & point) const
{
  if (point.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: the given point must have dimension=1, here dimension=" << point.getDimension();

  const NumericalScalar x(point[0]);
  const NumericalScalar a(getRange().getLowerBound()[0]);
  const NumericalScalar b(getRange().getUpperBound()[0]);
  if ((x < a) || (x > b)) return 0.0;
  const NumericalScalar aLeft(left_.getRange().getLowerBound()[0]);
  const NumericalScalar bLeft(left_.getRange().getUpperBound()[0]);
  //const NumericalScalar aRight(right_.getRange().getLowerBound()[0]);
  //const NumericalScalar bRight(right_.getRange().getUpperBound()[0]);
  const NumericalScalar mean(getMean()[0]);
  GaussKronrod algo;
  const PDFKernelWrapper pdfKernelWrapper(left_, right_, x);
  const NumericalMathFunction pdfKernel(bindMethod<PDFKernelWrapper, NumericalPoint, NumericalPoint>(pdfKernelWrapper, &PDFKernelWrapper::eval, 1, 1));
  NumericalScalar negativeError;
  NumericalScalar negativePart(algo.integrate(pdfKernel, Interval(aLeft, mean), negativeError)[0]);
  NumericalScalar positiveError(0.0);
  const NumericalScalar positivePart(algo.integrate(pdfKernel, Interval(mean, bLeft), positiveError)[0]);
  pdfEpsilon_ = negativeError + positiveError;
  return negativePart + positivePart;
}

/* Get the characteristic function of the distribution, i.e. phi(u) = E(exp(I*u*X)) */
NumericalComplex ProductDistribution::computeCharacteristicFunction(const NumericalScalar x) const
{
  if (std::abs(x) < SpecFunc::NumericalScalarEpsilon) return 1.0;
  if (std::abs(x) > ResourceMap::GetAsNumericalScalar("ProductDistribution-LargeCharacteristicFunctionArgument")) return ContinuousDistribution::computeCharacteristicFunction(x);
  NumericalComplex result(0.0);
  const NumericalScalar a(getRange().getLowerBound()[0]);
  const NumericalScalar b(getRange().getUpperBound()[0]);
  const NumericalScalar mean(getMean()[0]);
  GaussKronrod algo;
  const CFKernelWrapper cfKernelWrapper(left_, right_, x);
  const NumericalMathFunction cfKernel(bindMethod<CFKernelWrapper, NumericalPoint, NumericalPoint>(cfKernelWrapper, &CFKernelWrapper::eval, 1, 2));
  NumericalScalar negativeError(0.0);
  const NumericalPoint negativePart(algo.integrate(cfKernel, Interval(a, mean), negativeError));
  NumericalScalar positiveError(0.0);
  const NumericalPoint positivePart(algo.integrate(cfKernel, Interval(mean, b), positiveError));
  pdfEpsilon_ = negativeError + positiveError;
  return NumericalComplex(negativePart[0] + positivePart[0], negativePart[1] + positivePart[1]);
}

/* Compute the mean of the distribution */
void ProductDistribution::computeMean() const
{
  mean_ = NumericalPoint(1, left_.getMean()[0] * right_.getMean()[0]);
  isAlreadyComputedMean_ = true;
}

/* Compute the covariance of the distribution */
void ProductDistribution::computeCovariance() const
{
  covariance_ = CovarianceMatrix(1);
  const NumericalScalar meanLeft(left_.getMean()[0]);
  const NumericalScalar meanRight(right_.getMean()[0]);
  const NumericalScalar varLeft(left_.getCovariance()(0, 0));
  const NumericalScalar varRight(right_.getCovariance()(0, 0));
  covariance_(0, 0) = meanLeft * meanLeft * varRight + meanRight * meanRight * varLeft + varLeft * varRight;
  isAlreadyComputedCovariance_ = true;
}

/* Parameters value and description accessor */
ProductDistribution::NumericalPointWithDescriptionCollection ProductDistribution::getParametersCollection() const
{
  NumericalPointWithDescriptionCollection parameters(1);
  NumericalPointWithDescription point(left_.getParametersCollection()[0]);
  point.add(right_.getParametersCollection()[0]);
  Description description(left_.getDescription());
  description.add(right_.getDescription());
  point.setDescription(description);
  point.setName(getName());
  parameters[0] = point;
  return parameters;
}

/* Left accessor */
void ProductDistribution::setLeft(const Distribution & left)
{
  if (left.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: can multiply only distribution with dimension=1, here dimension=" << left.getDimension();
  if (!left.isContinuous()) throw InvalidArgumentException(HERE) << "Error: can multiply only continuous distributions";
  left_ = left;
  isAlreadyComputedMean_ = false;
  isAlreadyComputedCovariance_ = false;
  isAlreadyCreatedGeneratingFunction_ = false;
  isParallel_ = left_.getImplementation()->isParallel();
  computeRange();
}

Distribution ProductDistribution::getLeft() const
{
  return left_;
}

/* Right accessor */
void ProductDistribution::setRight(const Distribution & right)
{
  if (right.getDimension() != 1) throw InvalidArgumentException(HERE) << "Error: can multiply only distribution with dimension=1, here dimension=" << right.getDimension();
  if (!right.isContinuous()) throw InvalidArgumentException(HERE) << "Error: can multiply only continuous distributions";
  right_ = right;
  isAlreadyComputedMean_ = false;
  isAlreadyComputedCovariance_ = false;
  isAlreadyCreatedGeneratingFunction_ = false;
  isParallel_ = right_.getImplementation()->isParallel();
  computeRange();
}

Distribution ProductDistribution::getRight() const
{
  return right_;
}

Bool ProductDistribution::isContinuous() const
{
  return left_.isContinuous() && right_.isContinuous();
}

/* Tell if the distribution is integer valued */
Bool ProductDistribution::isDiscrete() const
{
  return left_.isDiscrete() && right_.isDiscrete();
}

/* Tell if the distribution is integer valued */
Bool ProductDistribution::isIntegral() const
{
  return left_.isIntegral() && right_.isIntegral();
}

/* Method save() stores the object through the StorageManager */
void ProductDistribution::save(Advocate & adv) const
{
  ContinuousDistribution::save(adv);
  adv.saveAttribute( "left_", left_ );
  adv.saveAttribute( "right_", right_ );
}

/* Method load() reloads the object from the StorageManager */
void ProductDistribution::load(Advocate & adv)
{
  ContinuousDistribution::load(adv);
  adv.loadAttribute( "left_", left_ );
  adv.loadAttribute( "right_", right_ );
  computeRange();
}

END_NAMESPACE_OPENTURNS
