//                                               -*- C++ -*-
/**
 *  @file  GammaFunctions.cxx
 *  @brief Efficient implementation of the computation of the incomplete
 *         regularized gamma function and related functions
 *
 *  Copyright (C) 2005-2014 Airbus-EDF-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-07-16 12:24:33 +0200 (Mon, 16 Jul 2012)
 */
#include <cmath>

#include "GammaFunctions.hxx"
#include "Exception.hxx"
#include "Log.hxx"
#include "SpecFunc.hxx"
#include "incgam.hxx"
#ifdef HAVE_BOOST
#include <boost/math/special_functions/gamma.hpp>
#endif

BEGIN_NAMESPACE_OPENTURNS

namespace GammaFunctions
{

  NumericalScalar IncompleteGamma(const NumericalScalar a,
                                  const NumericalScalar x,
                                  const Bool tail)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    return RegularizedIncompleteGamma(a, x, tail) * SpecFunc::Gamma(a);
  }

  NumericalScalar IncompleteGammaInverse(const NumericalScalar a,
                                         const NumericalScalar x,
                                         const Bool tail)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    return RegularizedIncompleteGammaInverse(a, x / SpecFunc::Gamma(a), tail);
  }

  NumericalScalar IncompleteGammaP(const NumericalScalar a,
                                   const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x < 0.0) return 0.0;
    if (x < a + 1.0) return IncompleteGammaSeries(a, x);
    return SpecFunc::Gamma(a) - IncompleteGammaContinuedFraction(a, x);
  }

  NumericalScalar IncompleteGammaQ(const NumericalScalar a,
                                   const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x < 0.0) return SpecFunc::Gamma(a);
    if (x < a + 1.0) return SpecFunc::Gamma(a) - IncompleteGammaSeries(a, x);
    return IncompleteGammaContinuedFraction(a, x);
  }

  // Based on [8.7] in the NIST Digital Library of Numerical Functions
  NumericalScalar IncompleteGammaSeries(const NumericalScalar a,
                                        const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x <= 0.0) return 0.0;
    NumericalScalar ap(a);
    NumericalScalar coefficient(1.0 / a);
    NumericalScalar value(coefficient);
    for (UnsignedLong k = 0; k < SpecFunc::MaximumIteration; ++k)
      {
        if (coefficient <= value * SpecFunc::Precision) break;
        ++ap;
        coefficient *= x / ap;
        value += coefficient;
      }
    value *= exp(-x + a * log(x));
    return value;
  }

  // Based on [8.9] in the NIST Digital Library of Numerical Functions
  // Implementation using the modified Lentz method as in Boost and the Numerical Recipes Ed.3
  NumericalScalar IncompleteGammaContinuedFraction(const NumericalScalar a,
                                                   const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x <= 0.0) return 1.0;
    const NumericalScalar epsilon(SpecFunc::Precision * SpecFunc::Precision);
    NumericalScalar b(x + 1.0 - a);
    NumericalScalar c(1.0 / epsilon);
    NumericalScalar d(b);
    NumericalScalar value(1.0 / d);
    for (UnsignedLong k = 1; k <= SpecFunc::MaximumIteration; ++k)
      {
        const NumericalScalar aK(k * (k - a));
        b += 2.0;
        d = b - aK / d;
        if (fabs(d) < SpecFunc::Precision) d = SpecFunc::Precision;
        c = b - aK / c;
        if (fabs(c) < SpecFunc::Precision) c = SpecFunc::Precision;
        const NumericalScalar delta(c / d);
        value *= delta;
        if (fabs(delta - 1.0) <= SpecFunc::Precision) break;
      }
    value *= exp(-x + a * log(x));
    return value;
  }

  NumericalScalar RegularizedIncompleteGamma(const NumericalScalar a,
                                             const NumericalScalar x,
                                             const Bool tail)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x <= 0.0) return (tail ? 1.0 : 0.0);
#ifdef HAVE_BOOST
    return (tail ? boost::math::gamma_q(a, x) : boost::math::gamma_p(a, x));
#else
    NumericalScalar p;
    NumericalScalar q;
    SignedInteger ierr;
    incgam(a, x, p, q, ierr);
    if (ierr == 1) throw InternalException(HERE) << "Error: underflow or overflow in RegularizedIncompleteGamma for a=" << a << ", x=" << x;
    return (tail ? q : p);
#endif
  }

  NumericalScalar RegularizedIncompleteGammaInverse(const NumericalScalar a,
                                                    const NumericalScalar x,
                                                    const Bool tail)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
#ifdef HAVE_BOOST
    return (tail ? boost::math::gamma_q_inv(a, x) : boost::math::gamma_p_inv(a, x));
#else
    /*    NumericalScalar xr;
    SignedInteger ierr;
    invincgam(a, x, 0.5 + (0.5 - x), xr, ierr);
    if (ierr == -1) throw InternalException(HERE) << "Error: cannot compute the RegularizedIncompleteGammaInverse funtion for a=" << a << ", x=" << x << ", tail=" << tail << " because of an overflow.";
    if (ierr == -2) LOGWARN(OSS() << "up to 15 Newton iterations have been made to compute the RegularizedIncompleteGammaInverse funtion for a=" << a << ", x=" << x << ", tail=" << tail << ". The accuracy may be reduced.");
    return xr;*/
    // First search for a bracketing interval
    NumericalScalar lower(0.0);
    NumericalScalar gammaLower(tail ? 1.0 : 0.0);
    NumericalScalar step(1.0);
    NumericalScalar upper(step);
    NumericalScalar gammaUpper(RegularizedIncompleteGamma(a, upper, tail));
    const Bool lowerLessThanX(gammaLower < x);
    while ((lowerLessThanX == (gammaUpper < x)) && (step < SpecFunc::MaxNumericalScalar))
      {
        step *= 2.0;
        lower = upper;
        upper += step;
        gammaUpper = RegularizedIncompleteGamma(a, upper, tail);
      }
    if (lowerLessThanX == (gammaUpper < x)) throw InternalException(HERE) << "Error: unable to bracket the RegularizedIncompleteGammaInverse for a=" << a << ", x=" << x << " and tail=" << tail;
    lower = upper - step;
    for (UnsignedLong k = 0; k < SpecFunc::MaximumIteration; ++k)
      {
        if (fabs(upper - lower) <= SpecFunc::Precision * std::max(1.0, fabs(upper))) break;
        const NumericalScalar middle(0.5 * (lower + upper));
        const NumericalScalar gammaMiddle(RegularizedIncompleteGamma(a, middle, tail));
        const Bool middleLessThanX(gammaMiddle < x);
        if (middleLessThanX == lowerLessThanX) lower = middle;
        else upper = middle;
      } // Bisection loop
    return 0.5 * (upper + lower);
#endif
  }

  NumericalScalar RegularizedIncompleteGammaP(const NumericalScalar a,
                                              const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x < 0.0) return 0.0;
    if (x < a + 1.0) return RegularizedIncompleteGammaSeries(a, x);
    return 1.0 - RegularizedIncompleteGammaContinuedFraction(a, x);
  }

  NumericalScalar RegularizedIncompleteGammaQ(const NumericalScalar a,
                                              const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x < 0.0) return 1.0;
    if (x < a + 1.0) return 1.0 - RegularizedIncompleteGammaSeries(a, x);
    return RegularizedIncompleteGammaContinuedFraction(a, x);
  }

  // Based on [8.7] in the NIST Digital Library of Numerical Functions
  NumericalScalar RegularizedIncompleteGammaSeries(const NumericalScalar a,
                                                   const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x <= 0.0) return 0.0;
    NumericalScalar ap(a);
    NumericalScalar coefficient(1.0 / a);
    NumericalScalar value(coefficient);
    for (UnsignedLong k = 0; k < SpecFunc::MaximumIteration; ++k)
      {
        if (coefficient <= value * SpecFunc::Precision) break;
        ++ap;
        coefficient *= x / ap;
        value += coefficient;
      }
    value *= exp(-x + a * log(x) - SpecFunc::LogGamma(a));
    return value;
  }

  // Based on [8.9] in the NIST Digital Library of Numerical Functions
  // Implementation using the modified Lentz method as in Boost and the Numerical Recipes Ed.3
  NumericalScalar RegularizedIncompleteGammaContinuedFraction(const NumericalScalar a,
                                                              const NumericalScalar x)
  {
    if (a <= 0.0) throw InvalidArgumentException(HERE) << "Error: a must be positive, here a=" << a;
    if (x <= 0.0) return 1.0;
    const NumericalScalar epsilon(SpecFunc::Precision * SpecFunc::Precision);
    NumericalScalar b(x + 1.0 - a);
    NumericalScalar c(1.0 / epsilon);
    NumericalScalar d(b);
    NumericalScalar value(1.0 / d);
    for (UnsignedLong k = 1; k <= SpecFunc::MaximumIteration; ++k)
      {
        const NumericalScalar aK(k * (k - a));
        b += 2.0;
        d = b - aK / d;
        if (fabs(d) < SpecFunc::Precision) d = SpecFunc::Precision;
        c = b - aK / c;
        if (fabs(c) < SpecFunc::Precision) c = SpecFunc::Precision;
        const NumericalScalar delta(c / d);
        value *= delta;
        if (fabs(delta - 1.0) <= SpecFunc::Precision) break;
      }
    value *= exp(-x + a * log(x) - SpecFunc::LogGamma(a));
    return value;
  }

} // GammaFunctions

END_NAMESPACE_OPENTURNS
