/* Implementation of Gauss's hypergeometric function for complex values.
 *
 * This implementation is based on the Fortran implementation by Shanjie Zhang and
 * Jianming Jin included in specfun.f [1]_.  Computation of Gauss's hypergeometric
 * function involves handling a patchwork of special cases. By default the Zhang and
 * Jin implementation has been followed as closely as possible except for situations where
 * an improvement was obvious. We've attempted to document the reasons behind decisions
 * made by Zhang and Jin and to document the reasons for deviating from their implementation
 * when this has been done. References to the NIST Digital Library of Mathematical
 * Functions [2]_ have been added where they are appropriate. The review paper by
 * Pearson et al [3]_ is an excellent resource for best practices for numerical
 * computation of hypergeometric functions. We have followed this review paper
 * when making improvements to and correcting defects in Zhang and Jin's
 * implementation. When Pearson et al propose several competing alternatives for a
 * given case, we've used our best judgment to decide on the method to use.
 *
 * Author: Albert Steppi
 *
 * Distributed under the same license as Scipy.
 *
 * References
 * ----------
 * .. [1] S. Zhang and J.M. Jin, "Computation of Special Functions", Wiley 1996
 * .. [2] NIST Digital Library of Mathematical Functions. http://dlmf.nist.gov/,
 *        Release 1.1.1 of 2021-03-15. F. W. J. Olver, A. B. Olde Daalhuis,
 *        D. W. Lozier, B. I. Schneider, R. F. Boisvert, C. W. Clark, B. R. Miller,
 *        B. V. Saunders, H. S. Cohl, and M. A. McClain, eds.
 * .. [3] Pearson, J.W., Olver, S. & Porter, M.A.
 *        "Numerical methods for the computation of the confluent and Gauss
 *        hypergeometric functions."
 *        Numer Algor 74, 821-866 (2017). https://doi.org/10.1007/s11075-016-0173-0
 * .. [4] Raimundas Vidunas, "Degenerate Gauss Hypergeometric Functions",
 *        Kyushu Journal of Mathematics, 2007, Volume 61, Issue 1, Pages 109-135,
 * .. [5] López, J.L., Temme, N.M. New series expansions of the Gauss hypergeometric
 *        function. Adv Comput Math 39, 349-365 (2013).
 *        https://doi.org/10.1007/s10444-012-9283-y
 * """
 */

#pragma once

#include "config.h"
#include "error.h"
#include "tools.h"

#include "binom.h"
#include "cephes/gamma.h"
#include "cephes/lanczos.h"
#include "cephes/poch.h"
#include "cephes/hyp2f1.h"
#include "digamma.h"

namespace special {
namespace detail {
    constexpr double hyp2f1_EPS = 1e-15;
    /* The original implementation in SciPy from Zhang and Jin used 1500 for the
     * maximum number of series iterations in some cases and 500 in others.
     * Through the empirical results on the test cases in
     * scipy/special/_precompute/hyp2f1_data.py, it was determined that these values
     * can lead to early termination of series which would have eventually converged
     * at a reasonable level of accuracy. We've bumped the iteration limit to 3000,
     * and may adjust it again based on further analysis. */
    constexpr std::uint64_t hyp2f1_MAXITER = 3000;

    SPECFUN_HOST_DEVICE inline double four_gammas_lanczos(double u, double v, double w, double x) {
        /* Compute ratio of gamma functions using lanczos approximation.
         *
         * Computes gamma(u)*gamma(v)/(gamma(w)*gamma(x))
         *
         * It is assumed that x = u + v - w, but it is left to the user to
         * ensure this.
         *
         * The lanczos approximation takes the form
         *
         * gamma(x) = factor(x) * lanczos_sum_expg_scaled(x)
         *
         * where factor(x) = ((x + lanczos_g - 0.5)/e)**(x - 0.5).
         *
         * The formula above is only valid for x >= 0.5, but can be extended to
         * x < 0.5 with the reflection principle.
         *
         * Using the lanczos approximation when computing this ratio of gamma functions
         * allows factors to be combined analytically to avoid underflow and overflow
         * and produce a more accurate result. The condition x = u + v - w makes it
         * possible to cancel the factors in the expression
         *
         * factor(u) * factor(v) / (factor(w) * factor(x))
         *
         * by taking one factor and absorbing it into the others. Currently, this
         * implementation takes the factor corresponding to the argument with largest
         * absolute value and absorbs it into the others.
         *
         * Since this is only called internally by four_gammas. It is assumed that
         * |u| >= |v| and |w| >= |x|.
         */

        /* The below implementation may incorrectly return finite results
         * at poles of the gamma function. Handle these cases explicitly. */
        if ((u == std::trunc(u) && u <= 0) || (v == std::trunc(v) && v <= 0)) {
            /* Return nan if numerator has pole. Diverges to +- infinity
             * depending on direction so value is undefined. */
            return std::numeric_limits<double>::quiet_NaN();
        }
        if ((w == std::trunc(w) && w <= 0) || (x == std::trunc(x) && x <= 0)) {
            // Return 0 if denominator has pole but not numerator.
            return 0.0;
        }

        double result = 1.0;
        double ugh, vgh, wgh, xgh, u_prime, v_prime, w_prime, x_prime;

        if (u >= 0.5) {
            result *= cephes::lanczos_sum_expg_scaled(u);
            ugh = u + cephes::lanczos_g - 0.5;
            u_prime = u;
        } else {
            result /= cephes::lanczos_sum_expg_scaled(1 - u) * std::sin(M_PI * u) * M_1_PI;
            ugh = 0.5 - u + cephes::lanczos_g;
            u_prime = 1 - u;
        }

        if (v >= 0.5) {
            result *= cephes::lanczos_sum_expg_scaled(v);
            vgh = v + cephes::lanczos_g - 0.5;
            v_prime = v;
        } else {
            result /= cephes::lanczos_sum_expg_scaled(1 - v) * std::sin(M_PI * v) * M_1_PI;
            vgh = 0.5 - v + cephes::lanczos_g;
            v_prime = 1 - v;
        }

        if (w >= 0.5) {
            result /= cephes::lanczos_sum_expg_scaled(w);
            wgh = w + cephes::lanczos_g - 0.5;
            w_prime = w;
        } else {
            result *= cephes::lanczos_sum_expg_scaled(1 - w) * std::sin(M_PI * w) * M_1_PI;
            wgh = 0.5 - w + cephes::lanczos_g;
            w_prime = 1 - w;
        }

        if (x >= 0.5) {
            result /= cephes::lanczos_sum_expg_scaled(x);
            xgh = x + cephes::lanczos_g - 0.5;
            x_prime = x;
        } else {
            result *= cephes::lanczos_sum_expg_scaled(1 - x) * std::sin(M_PI * x) * M_1_PI;
            xgh = 0.5 - x + cephes::lanczos_g;
            x_prime = 1 - x;
        }

        if (std::abs(u) >= std::abs(w)) {
            // u has greatest absolute value. Absorb ugh into the others.
            if (std::abs((v_prime - u_prime) * (v - 0.5)) < 100 * ugh and v > 100) {
                /* Special case where base is close to 1. Condition taken from
                 * Boost's beta function implementation. */
                result *= std::exp((v - 0.5) * std::log1p((v_prime - u_prime) / ugh));
            } else {
                result *= std::pow(vgh / ugh, v - 0.5);
            }

            if (std::abs((u_prime - w_prime) * (w - 0.5)) < 100 * wgh and u > 100) {
                result *= std::exp((w - 0.5) * std::log1p((u_prime - w_prime) / wgh));
            } else {
                result *= std::pow(ugh / wgh, w - 0.5);
            }

            if (std::abs((u_prime - x_prime) * (x - 0.5)) < 100 * xgh and u > 100) {
                result *= std::exp((x - 0.5) * std::log1p((u_prime - x_prime) / xgh));
            } else {
                result *= std::pow(ugh / xgh, x - 0.5);
            }
        } else {
            // w has greatest absolute value. Absorb wgh into the others.
            if (std::abs((u_prime - w_prime) * (u - 0.5)) < 100 * wgh and u > 100) {
                result *= std::exp((u - 0.5) * std::log1p((u_prime - w_prime) / wgh));
            } else {
                result *= pow(ugh / wgh, u - 0.5);
            }
            if (std::abs((v_prime - w_prime) * (v - 0.5)) < 100 * wgh and v > 100) {
                result *= std::exp((v - 0.5) * std::log1p((v_prime - w_prime) / wgh));
            } else {
                result *= std::pow(vgh / wgh, v - 0.5);
            }
            if (std::abs((w_prime - x_prime) * (x - 0.5)) < 100 * xgh and x > 100) {
                result *= std::exp((x - 0.5) * std::log1p((w_prime - x_prime) / xgh));
            } else {
                result *= std::pow(wgh / xgh, x - 0.5);
            }
        }
        // This exhausts all cases because we assume |u| >= |v| and |w| >= |x|.

        return result;
    }

    SPECFUN_HOST_DEVICE inline double four_gammas(double u, double v, double w, double x) {
        double result;

        // Without loss of generality, assume |u| >= |v| and |w| >= |x|.
        if (std::abs(u) > std::abs(v)) {
            std::swap(u, v);
        }
        if (std::abs(x) > std::abs(w)) {
            std::swap(x, w);
        }
        /* Direct ratio tends to be more accurate for arguments in this range. Range
         * chosen empirically based on the relevant benchmarks in
         * scipy/special/_precompute/hyp2f1_data.py */
        if (std::abs(u) <= 100 && std::abs(v) <= 100 && std::abs(w) <= 100 && std::abs(x) <= 100) {
            result = cephes::Gamma(u) * cephes::Gamma(v) / (cephes::Gamma(w) * cephes::Gamma(x));
            if (std::isfinite(result) && result != 0.0) {
                return result;
            }
        }
        result = four_gammas_lanczos(u, v, w, x);
        if (std::isfinite(result) && result != 0.0) {
            return result;
        }
        // If overflow or underflow, try again with logs.
        result = std::exp(cephes::lgam(v) - cephes::lgam(x) + cephes::lgam(u) - cephes::lgam(w));
        result *= cephes::gammasgn(u) * cephes::gammasgn(w) * cephes::gammasgn(v) * cephes::gammasgn(x);
        return result;
    }

    class HypergeometricSeriesGenerator {
        /* Maclaurin series for hyp2f1.
         *
         * Series is convergent for |z| < 1 but is only practical for numerical
         * computation when |z| < 0.9.
         */
      public:
        SPECFUN_HOST_DEVICE HypergeometricSeriesGenerator(double a, double b, double c, std::complex<double> z)
            : a_(a), b_(b), c_(c), z_(z), term_(1.0), k_(0) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            std::complex<double> output = term_;
            term_ = term_ * (a_ + k_) * (b_ + k_) / ((k_ + 1) * (c_ + k_)) * z_;
            ++k_;
            return output;
        }

      private:
        double a_, b_, c_;
        std::complex<double> z_, term_;
        std::uint64_t k_;
    };

    class Hyp2f1Transform1Generator {
        /* 1 -z transformation of standard series.*/
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform1Generator(double a, double b, double c, std::complex<double> z)
            : factor1_(four_gammas(c, c - a - b, c - a, c - b)),
              factor2_(four_gammas(c, a + b - c, a, b) * std::pow(1.0 - z, c - a - b)),
              generator1_(HypergeometricSeriesGenerator(a, b, a + b - c + 1, 1.0 - z)),
              generator2_(HypergeometricSeriesGenerator(c - a, c - b, c - a - b + 1, 1.0 - z)) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            return factor1_ * generator1_() + factor2_ * generator2_();
        }

      private:
        std::complex<double> factor1_, factor2_;
        HypergeometricSeriesGenerator generator1_, generator2_;
    };

    class Hyp2f1Transform1LimitSeriesGenerator {
        /* 1 - z transform in limit as c - a - b approaches an integer m. */
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform1LimitSeriesGenerator(double a, double b, double m, std::complex<double> z)
            : d1_(special::digamma(a)), d2_(special::digamma(b)), d3_(special::digamma(1 + m)),
              d4_(special::digamma(1.0)), a_(a), b_(b), m_(m), z_(z), log_1_z_(std::log(1.0 - z)),
              factor_(1.0 / cephes::Gamma(m + 1)), k_(0) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            std::complex<double> term_ = (d1_ + d2_ - d3_ - d4_ + log_1_z_) * factor_;
            // Use digamma(x + 1) = digamma(x) + 1/x
            d1_ += 1 / (a_ + k_);       // d1 = digamma(a + k)
            d2_ += 1 / (b_ + k_);       // d2 = digamma(b + k)
            d3_ += 1 / (1.0 + m_ + k_); // d3 = digamma(1 + m + k)
            d4_ += 1 / (1.0 + k_);      // d4 = digamma(1 + k)
            factor_ *= (a_ + k_) * (b_ + k_) / ((k_ + 1.0) * (m_ + k_ + 1)) * (1.0 - z_);
            ++k_;
            return term_;
        }

      private:
        double d1_, d2_, d3_, d4_, a_, b_, m_;
        std::complex<double> z_, log_1_z_, factor_;
        int k_;
    };

    class Hyp2f1Transform2Generator {
        /* 1/z transformation of standard series.*/
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform2Generator(double a, double b, double c, std::complex<double> z)
            : factor1_(four_gammas(c, b - a, b, c - a) * std::pow(-z, -a)),
              factor2_(four_gammas(c, a - b, a, c - b) * std::pow(-z, -b)),
              generator1_(HypergeometricSeriesGenerator(a, a - c + 1, a - b + 1, 1.0 / z)),
              generator2_(HypergeometricSeriesGenerator(b, b - c + 1, b - a + 1, 1.0 / z)) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            return factor1_ * generator1_() + factor2_ * generator2_();
        }

      private:
        std::complex<double> factor1_, factor2_;
        HypergeometricSeriesGenerator generator1_, generator2_;
    };

    class Hyp2f1Transform2LimitSeriesGenerator {
        /* 1/z transform in limit as a - b approaches a non-negative integer m. (Can swap a and b to
         * handle the m a negative integer case. */
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform2LimitSeriesGenerator(double a, double b, double c, double m,
                                                                 std::complex<double> z)
            : d1_(special::digamma(1.0)), d2_(special::digamma(1 + m)), d3_(special::digamma(a)),
              d4_(special::digamma(c - a)), a_(a), b_(b), c_(c), m_(m), z_(z), log_neg_z_(std::log(-z)),
              factor_(special::cephes::poch(b, m) * special::cephes::poch(1 - c + b, m) /
                      special::cephes::Gamma(m + 1)),
              k_(0) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            std::complex<double> term = (d1_ + d2_ - d3_ - d4_ + log_neg_z_) * factor_;
            // Use digamma(x + 1) = digamma(x) + 1/x
            d1_ += 1 / (1.0 + k_);         // d1 = digamma(1 + k)
            d2_ += 1 / (1.0 + m_ + k_);    // d2 = digamma(1 + m + k)
            d3_ += 1 / (a_ + k_);          // d3 = digamma(a + k)
            d4_ -= 1 / (c_ - a_ - k_ - 1); // d4 = digamma(c - a - k)
            factor_ *= (b_ + m_ + k_) * (1 - c_ + b_ + m_ + k_) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
            ++k_;
            return term;
        }

      private:
        double d1_, d2_, d3_, d4_, a_, b_, c_, m_;
        std::complex<double> z_, log_neg_z_, factor_;
        std::uint64_t k_;
    };

    class Hyp2f1Transform2LimitSeriesCminusAIntGenerator {
        /* 1/z transform in limit as a - b approaches a non-negative integer m, and c - a approaches
         * a positive integer n. */
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform2LimitSeriesCminusAIntGenerator(double a, double b, double c, double m,
                                                                           double n, std::complex<double> z)
            : d1_(special::digamma(1.0)), d2_(special::digamma(1 + m)), d3_(special::digamma(a)),
              d4_(special::digamma(n)), a_(a), b_(b), c_(c), m_(m), n_(n), z_(z), log_neg_z_(std::log(-z)),
              factor_(special::cephes::poch(b, m) * special::cephes::poch(1 - c + b, m) /
                      special::cephes::Gamma(m + 1)),
              k_(0) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            std::complex<double> term;
            if (k_ < n_) {
                term = (d1_ + d2_ - d3_ - d4_ + log_neg_z_) * factor_;
                // Use digamma(x + 1) = digamma(x) + 1/x
                d1_ += 1 / (1.0 + k_);    // d1 = digamma(1 + k)
                d2_ += 1 / (1 + m_ + k_); // d2 = digamma(1 + m + k)
                d3_ += 1 / (a_ + k_);     // d3 = digamma(a + k)
                d4_ -= 1 / (n_ - k_ - 1); // d4 = digamma(c - a - k)
                factor_ *= (b_ + m_ + k_) * (1 - c_ + b_ + m_ + k_) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
                ++k_;
                return term;
            }
            if (k_ == n_) {
                /* When c - a approaches a positive integer and k_ >= c - a = n then
                 * poch(1 - c + b + m + k) = poch(1 - c + a + k) = approaches zero and
                 * digamma(c - a - k) approaches a pole. However we can use the limit
                 * digamma(-n + epsilon) / gamma(-n + epsilon) -> (-1)**(n + 1) * (n+1)! as epsilon -> 0
                 * to continue the series.
                 *
                 * poch(1 - c + b, m + k) = gamma(1 - c + b + m + k)/gamma(1 - c + b)
                 *
                 * If a - b is an integer and c - a is an integer, then a and b must both be integers, so assume
                 * a and b are integers and take the limit as c approaches an integer.
                 *
                 * gamma(1 - c + epsilon + a + k)/gamma(1 - c - epsilon + b) =
                 * (gamma(c + epsilon - b) / gamma(c + epsilon - a - k)) *
                 * (sin(pi * (c + epsilon - b)) / sin(pi * (c + epsilon - a - k))) (reflection principle)
                 *
                 * In the limit as epsilon goes to zero, the ratio of sines will approach
                 * (-1)**(a - b + k) = (-1)**(m + k)
                 *
                 * We may then replace
                 *
                 * poch(1 - c - epsilon + b, m + k)*digamma(c + epsilon - a - k)
                 *
                 * with
                 *
                 * (-1)**(a - b + k)*gamma(c + epsilon - b) * digamma(c + epsilon - a - k) / gamma(c + epsilon - a - k)
                 *
                 * and taking the limit epsilon -> 0 gives
                 *
                 * (-1)**(a - b + k) * gamma(c - b) * (-1)**(k + a - c + 1)(k + a - c)!
                 * = (-1)**(c - b - 1)*Gamma(k + a - c + 1)
                 */
                factor_ = std::pow(-1, m_ + n_) * special::binom(c_ - 1, b_ - 1) *
                          special::cephes::poch(c_ - a_ + 1, m_ - 1) / std::pow(z_, static_cast<double>(k_));
            }
            term = factor_;
            factor_ *= (b_ + m_ + k_) * (k_ + a_ - c_ + 1) / ((k_ + 1) * (m_ + k_ + 1)) / z_;
            ++k_;
            return term;
        }

      private:
        double d1_, d2_, d3_, d4_, a_, b_, c_, m_, n_;
        std::complex<double> z_, log_neg_z_, factor_;
        std::uint64_t k_;
    };

    class Hyp2f1Transform2LimitFinitePartGenerator {
        /* Initial finite sum in limit as a - b approaches a non-negative integer m. The limiting series
         * for the 1 - z transform also has an initial finite sum, but it is a standard hypergeometric
         * series. */
      public:
        SPECFUN_HOST_DEVICE Hyp2f1Transform2LimitFinitePartGenerator(double b, double c, double m,
                                                                     std::complex<double> z)
            : b_(b), c_(c), m_(m), z_(z), term_(cephes::Gamma(m) / cephes::Gamma(c - b)), k_(0) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            std::complex<double> output = term_;
            term_ = term_ * (b_ + k_) * (c_ - b_ - k_ - 1) / ((k_ + 1) * (m_ - k_ - 1)) / z_;
            ++k_;
            return output;
        }

      private:
        double b_, c_, m_;
        std::complex<double> z_, term_;
        std::uint64_t k_;
    };

    class LopezTemmeSeriesGenerator {
        /* Lopez-Temme Series for Gaussian hypergeometric function [4].
         *
         * Converges for all z with real(z) < 1, including in the regions surrounding
         * the points exp(+- i*pi/3) that are not covered by any of the standard
         * transformations.
         */
      public:
        SPECFUN_HOST_DEVICE LopezTemmeSeriesGenerator(double a, double b, double c, std::complex<double> z)
            : n_(0), a_(a), b_(b), c_(c), phi_previous_(1.0), phi_(1 - 2 * b / c), z_(z), Z_(a * z / (z - 2.0)) {}

        SPECFUN_HOST_DEVICE std::complex<double> operator()() {
            if (n_ == 0) {
                ++n_;
                return 1.0;
            }
            if (n_ > 1) { // Update phi and Z for n>=2
                double new_phi = ((n_ - 1) * phi_previous_ - (2.0 * b_ - c_) * phi_) / (c_ + (n_ - 1));
                phi_previous_ = phi_;
                phi_ = new_phi;
                Z_ = Z_ * z_ / (z_ - 2.0) * ((a_ + (n_ - 1)) / n_);
            }
            ++n_;
            return Z_ * phi_;
        }

      private:
        std::uint64_t n_;
        double a_, b_, c_, phi_previous_, phi_;
        std::complex<double> z_, Z_;
    };

    SPECFUN_HOST_DEVICE std::complex<double> hyp2f1_transform1_limiting_case(double a, double b, double c, double m,
                                                                             std::complex<double> z) {
        /* 1 - z transform in limiting case where c - a - b approaches an integer m. */
        std::complex<double> result = 0.0;
        if (m >= 0) {
            if (m != 0) {
                auto series_generator = HypergeometricSeriesGenerator(a, b, 1 - m, 1.0 - z);
                result += four_gammas(m, c, a + m, b + m) * series_eval_fixed_length(series_generator,
                                                                                     std::complex<double>{0.0, 0.0},
                                                                                     static_cast<std::uint64_t>(m));
            }
            std::complex<double> prefactor = std::pow(-1.0, m + 1) * special::cephes::Gamma(c) /
                                             (special::cephes::Gamma(a) * special::cephes::Gamma(b)) *
                                             std::pow(1.0 - z, m);
            auto series_generator = Hyp2f1Transform1LimitSeriesGenerator(a + m, b + m, m, z);
            result += prefactor * series_eval(series_generator, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
                                              hyp2f1_MAXITER, "hyp2f1");
            return result;
        } else {
            result = four_gammas(-m, c, a, b) * std::pow(1.0 - z, m);
            auto series_generator1 = HypergeometricSeriesGenerator(a + m, b + m, 1 + m, 1.0 - z);
            result *= series_eval_fixed_length(series_generator1, std::complex<double>{0.0, 0.0},
                                               static_cast<std::uint64_t>(-m));
            double prefactor = std::pow(-1.0, m + 1) * special::cephes::Gamma(c) /
                               (special::cephes::Gamma(a + m) * special::cephes::Gamma(b + m));
            auto series_generator2 = Hyp2f1Transform1LimitSeriesGenerator(a, b, -m, z);
            result += prefactor * series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
                                              hyp2f1_MAXITER, "hyp2f1");
            return result;
        }
    }

    SPECFUN_HOST_DEVICE std::complex<double> hyp2f1_transform2_limiting_case(double a, double b, double c, double m,
                                                                             std::complex<double> z) {
        /* 1 / z transform in limiting case where a - b approaches a non-negative integer m. Negative integer case
         * can be handled by swapping a and b. */
        auto series_generator1 = Hyp2f1Transform2LimitFinitePartGenerator(b, c, m, z);
        std::complex<double> result = cephes::Gamma(c) / cephes::Gamma(a) * std::pow(-z, -b);
        result *=
            series_eval_fixed_length(series_generator1, std::complex<double>{0.0, 0.0}, static_cast<std::uint64_t>(m));
        std::complex<double> prefactor = cephes::Gamma(c) / (cephes::Gamma(a) * cephes::Gamma(c - b) * std::pow(-z, a));
        double n = c - a;
        if (abs(n - std::round(n)) < hyp2f1_EPS) {
            auto series_generator2 = Hyp2f1Transform2LimitSeriesCminusAIntGenerator(a, b, c, m, n, z);
            result += prefactor * series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS,
                                              hyp2f1_MAXITER, "hyp2f1");
            return result;
        }
        auto series_generator2 = Hyp2f1Transform2LimitSeriesGenerator(a, b, c, m, z);
        result += prefactor *
                  series_eval(series_generator2, std::complex<double>{0.0, 0.0}, hyp2f1_EPS, hyp2f1_MAXITER, "hyp2f1");
        return result;
    }

} // namespace detail

SPECFUN_HOST_DEVICE inline std::complex<double> hyp2f1(double a, double b, double c, std::complex<double> z) {
    /* Special Cases
     * -----------------------------------------------------------------------
     * Takes constant value 1 when a = 0 or b = 0, even if c is a non-positive
     * integer. This follows mpmath. */
    if (a == 0 || b == 0) {
        return 1.0;
    }
    double z_abs = std::abs(z);
    // Equals 1 when z i 0, unless c is 0.
    if (z_abs == 0) {
        if (c != 0) {
            return 1.0;
        } else {
            // Returning real part NAN and imaginary part 0 follows mpmath.
            return std::complex<double>{std::numeric_limits<double>::quiet_NaN(), 0};
        }
    }
    bool a_neg_int = a == std::trunc(a) && a < 0;
    bool b_neg_int = b == std::trunc(b) && b < 0;
    bool c_non_pos_int = c == std::trunc(c) and c <= 0;
    /* Diverges when c is a non-positive integer unless a is an integer with
     * c <= a <= 0 or b is an integer with c <= b <= 0, (or z equals 0 with
     * c != 0) Cases z = 0, a = 0, or b = 0 have already been handled. We follow
     * mpmath in handling the degenerate cases where any of a, b, c are
     * non-positive integers. See [3] for a treatment of degenerate cases. */
    if (c_non_pos_int && !((a_neg_int && c <= a && a < 0) || (b_neg_int && c <= b && b < 0))) {
        return std::complex<double>{std::numeric_limits<double>::infinity(), 0};
    }
    /* Reduces to a polynomial when a or b is a negative integer.
     * If a and b are both negative integers, we take care to terminate
     * the series at a or b of smaller magnitude. This is to ensure proper
     * handling of situations like a < c < b <= 0, a, b, c all non-positive
     * integers, where terminating at a would lead to a term of the form 0 / 0. */
    std::uint64_t max_degree;
    if (a_neg_int || b_neg_int) {
        if (a_neg_int && b_neg_int) {
            max_degree = a > b ? std::abs(a) : std::abs(b);
        } else if (a_neg_int) {
            max_degree = std::abs(a);
        } else {
            max_degree = std::abs(b);
        }
        if (max_degree <= UINT64_MAX) {
            auto series_generator = detail::HypergeometricSeriesGenerator(a, b, c, z);
            return detail::series_eval_fixed_length(series_generator, std::complex<double>{0.0, 0.0}, max_degree + 1);
        } else {
            set_error("hyp2f1", SF_ERROR_NO_RESULT, NULL);
            return std::complex<double>{std::numeric_limits<double>::quiet_NaN(),
                                        std::numeric_limits<double>::quiet_NaN()};
        }
    }
    // Kummer's Theorem for z = -1; c = 1 + a - b (DLMF 15.4.26)
    if (std::abs(z + 1.0) < detail::hyp2f1_EPS && std::abs(1 + a - b - c) < detail::hyp2f1_EPS && !c_non_pos_int) {
        return detail::four_gammas(a - b + 1, 0.5 * a + 1, a + 1, 0.5 * a - b + 1);
    }
    std::complex<double> result;
    bool c_minus_a_neg_int = c - a == std::trunc(c - a) && c - a < 0;
    bool c_minus_b_neg_int = c - b == std::trunc(c - b) && c - b < 0;
    /* If one of c - a or c - b is a negative integer, reduces to evaluating
     * a polynomial through an Euler hypergeometric transformation.
     * (DLMF 15.8.1) */
    if (c_minus_a_neg_int || c_minus_b_neg_int) {
        max_degree = c_minus_b_neg_int ? std::abs(c - b) : std::abs(c - a);
        if (max_degree <= UINT64_MAX) {
            result = std::pow(1.0 - z, c - a - b);
            auto series_generator = detail::HypergeometricSeriesGenerator(c - a, c - b, c, z);
            result *=
                detail::series_eval_fixed_length(series_generator, std::complex<double>{0.0, 0.0}, max_degree + 2);
            return result;
        } else {
            set_error("hyp2f1", SF_ERROR_NO_RESULT, NULL);
            return std::complex<double>{std::numeric_limits<double>::quiet_NaN(),
                                        std::numeric_limits<double>::quiet_NaN()};
        }
    }
    /* Diverges as real(z) -> 1 when c <= a + b.
     * Todo: Actually check for overflow instead of using a fixed tolerance for
     * all parameter combinations like in the Fortran original. */
    if (std::abs(1 - z.real()) < detail::hyp2f1_EPS && z.imag() == 0 && c - a - b <= 0 && !c_non_pos_int) {
        return std::complex<double>{std::numeric_limits<double>::infinity(), 0};
    }
    // Gauss's Summation Theorem for z = 1; c - a - b > 0 (DLMF 15.4.20).
    if (z == 1.0 && c - a - b > 0 && !c_non_pos_int) {
        return detail::four_gammas(c, c - a - b, c - a, c - b);
    }
    /* |z| < 0, z.real() >= 0. Use the Maclaurin Series.
     * -----------------------------------------------------------------------
     * Apply Euler Hypergeometric Transformation (DLMF 15.8.1) to reduce
     * size of a and b if possible. We follow Zhang and Jin's
     * implementation [1] although there is very likely a better heuristic
     * to determine when this transformation should be applied. As it
     * stands, this hurts precision in some cases. */
    if (z_abs < 0.9 && z.real() >= 0) {
        if (c - a < a && c - b < b) {
            result = std::pow(1.0 - z, c - a - b);
            auto series_generator = detail::HypergeometricSeriesGenerator(c - a, c - b, c, z);
            result *= detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                                          detail::hyp2f1_MAXITER, "hyp2f1");
            return result;
        }
        auto series_generator = detail::HypergeometricSeriesGenerator(a, b, c, z);
        return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                                   detail::hyp2f1_MAXITER, "hyp2f1");
    }
    /* Points near exp(iπ/3), exp(-iπ/3) not handled by any of the standard
     * transformations. Use series of López and Temme [5]. These regions
     * were not correctly handled by Zhang and Jin's implementation.
     * -------------------------------------------------------------------------*/
    if (0.9 <= z_abs && z_abs < 1.1 && std::abs(1.0 - z) >= 0.9 && z.real() >= 0) {
        /* This condition for applying Euler Transformation (DLMF 15.8.1)
         * was determined empirically to work better for this case than that
         * used in Zhang and Jin's implementation for |z| < 0.9,
         *  real(z) >= 0. */
        if ((c - a <= a && c - b < b) || (c - a < a && c - b <= b)) {
            auto series_generator = detail::LopezTemmeSeriesGenerator(c - a, c - b, c, z);
            result = std::pow(1.0 - 0.5 * z, a - c); // Lopez-Temme prefactor
            result *= detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                                          detail::hyp2f1_MAXITER, "hyp2f1");
            return std::pow(1.0 - z, c - a - b) * result; // Euler transform prefactor.
        }
        auto series_generator = detail::LopezTemmeSeriesGenerator(a, b, c, z);
        result = detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                                     detail::hyp2f1_MAXITER, "hyp2f1");
        return std::pow(1.0 - 0.5 * z, -a) * result; // Lopez-Temme prefactor.
    }
    /* z/(z - 1) transformation (DLMF 15.8.1). Avoids cancellation issues that
     * occur with Maclaurin series for real(z) < 0.
     * -------------------------------------------------------------------------*/
    if (z_abs < 1.1 && z.real() < 0) {
        if (0 < b && b < a && a < c) {
            std::swap(a, b);
        }
        auto series_generator = detail::HypergeometricSeriesGenerator(a, c - b, c, z / (z - 1.0));
        return std::pow(1.0 - z, -a) * detail::series_eval(series_generator, std::complex<double>{0.0, 0.0},
                                                           detail::hyp2f1_EPS, detail::hyp2f1_MAXITER, "hyp2f1");
    }
    /* 1 - z transformation (DLMF 15.8.4). */
    if (0.9 <= z_abs && z_abs < 1.1) {
        if (std::abs(c - a - b - std::round(c - a - b)) < detail::hyp2f1_EPS) {
            // Removable singularity when c - a - b is an integer. Need to use limiting formula.
            double m = std::round(c - a - b);
            return detail::hyp2f1_transform1_limiting_case(a, b, c, m, z);
        }
        auto series_generator = detail::Hyp2f1Transform1Generator(a, b, c, z);
        return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                                   detail::hyp2f1_MAXITER, "hyp2f1");
    }
    /* 1/z transformation (DLMF 15.8.2). */
    if (std::abs(a - b - std::round(a - b)) < detail::hyp2f1_EPS) {
        if (b > a) {
            std::swap(a, b);
        }
        double m = std::round(a - b);
        return detail::hyp2f1_transform2_limiting_case(a, b, c, m, z);
    }
    auto series_generator = detail::Hyp2f1Transform2Generator(a, b, c, z);
    return detail::series_eval(series_generator, std::complex<double>{0.0, 0.0}, detail::hyp2f1_EPS,
                               detail::hyp2f1_MAXITER, "hyp2f1");
}

inline std::complex<float> hyp2f1(float a, float b, float c, std::complex<float> x) {
    return static_cast<std::complex<float>>(hyp2f1(static_cast<double>(a), static_cast<double>(b),
                                                   static_cast<double>(c), static_cast<std::complex<double>>(x)));
}

inline double hyp2f1(double a, double b, double c, double x) { return cephes::hyp2f1(a, b, c, x); }

inline float hyp2f1(float a, float b, float c, float x) {
    return hyp2f1(static_cast<double>(a), static_cast<double>(b), static_cast<double>(c), static_cast<double>(x));
}

} // namespace special
