// This library is part of PLINK 2.00, copyright (C) 2005-2021 Shaun Purcell,
// Christopher Chang.
//
// 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 "plink2_stats.h"

#ifdef __cplusplus
namespace plink2 {
#endif

// Thread-unsafe portions of plink_stats.c have been replaced, mostly by code
// derived from boost/math/special_functions/gamma.hpp and
// boost/math/special_functions/detail/igamma_inverse.hpp in Boost 1.60
// (Maddock et al.).  The derived portions are subject to the following
// license:
//
// *****
// Boost Software License - Version 1.0 - August 17th, 2003
//
// Permission is hereby granted, free of charge, to any person or organization
// obtaining a copy of the software and accompanying documentation covered by
// this license (the "Software") to use, reproduce, display, distribute,
// execute, and transmit the Software, and to prepare derivative works of the
// Software, and to permit third-parties to whom the Software is furnished to
// do so, all subject to the following:
//
// The copyright notices in the Software and this entire statement, including
// the above license grant, this restriction and the following disclaimer,
// must be included in all copies of the Software, in whole or in part, and
// all derivative works of the Software, unless such copies or derivative
// works are solely in the form of machine-executable object code generated by
// a source language processor.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// *****

// ***** thread-safe ChisqToP *****
// port of Boost 1.60 implementation, float precision

static const double kLogMinValue = -708.0;
static const double kLogMaxValue = 709.0;

static const double kLentzFpmin = 1.0e-30;

/*
static const double kFactorials[30] = {
  1.0,
  1.0,
  2.0,
  6.0,
  24.0,
  120.0,
  720.0,
  5040.0,
  40320.0,
  362880.0,
  3628800.0,
  39916800.0,
  479001600.0,
  6227020800.0,
  87178291200.0,
  1307674368000.0,
  20922789888000.0,
  355687428096000.0,
  6402373705728000.0,
  121645100408832000.0,
  0.243290200817664e19,
  0.5109094217170944e20,
  0.112400072777760768e22,
  0.2585201673888497664e23,
  0.62044840173323943936e24,
  0.15511210043330985984e26,
  0.403291461126605635584e27,
  0.10888869450418352160768e29,
  0.304888344611713860501504e30,
  0.8841761993739701954543616e31
};
*/

static const double kFactorialRecips[34] = {
  1.0,
  1.0,
  0.5,
  0.16666666666666666,
  0.041666666666666664,
  0.008333333333333333,
  0.001388888888888889,
  0.0001984126984126984,
  2.48015873015873e-05,
  2.7557319223985893e-06,
  2.755731922398589e-07,
  2.505210838544172e-08,
  2.08767569878681e-09,
  1.6059043836821613e-10,
  1.1470745597729725e-11,
  7.647163731819816e-13,
  4.779477332387385e-14,
  2.8114572543455206e-15,
  1.5619206968586225e-16,
  8.22063524662433e-18,
  4.110317623312165e-19,
  1.9572941063391263e-20,
  8.896791392450574e-22,
  3.8681701706306835e-23,
  1.6117375710961184e-24,
  6.446950284384474e-26,
  2.4795962632247972e-27,
  9.183689863795546e-29,
  3.279889237069838e-30,
  1.1309962886447718e-31,
  3.7699876288159054e-33,
  1.2161250415535181e-34,
  3.800390754854744e-36,
  1.151633562077195e-37
};

// this may move to a more central location
static const double kSmallRecips[30] = {
  0.0,  // could make this nan, though that's annoying for C++03
  1.0 / 1,
  1.0 / 2,
  1.0 / 3,
  1.0 / 4,
  1.0 / 5,
  1.0 / 6,
  1.0 / 7,
  1.0 / 8,
  1.0 / 9,
  1.0 / 10,
  1.0 / 11,
  1.0 / 12,
  1.0 / 13,
  1.0 / 14,
  1.0 / 15,
  1.0 / 16,
  1.0 / 17,
  1.0 / 18,
  1.0 / 19,
  1.0 / 20,
  1.0 / 21,
  1.0 / 22,
  1.0 / 23,
  1.0 / 24,
  1.0 / 25,
  1.0 / 26,
  1.0 / 27,
  1.0 / 28,
  1.0 / 29
};

double finite_gamma_q(uint32_t aa, double xx, double* p_derivative) {
  // a is a positive integer < 30; max(0.6, a-1) < x < kLogMaxValue
  // (e^{-x})(1 + x + x^2/2 + x^3/3! + x^4/4! + ... + x^{a-1}/(a-1)!)
  const double ee = exp(-xx);
  if (ee == 0.0) {
    return 0;
  }
  double sum = ee;
  double term = sum;
  for (uint32_t nn = 1; nn != aa; ++nn) {
    term *= xx * kSmallRecips[nn];
    sum += term;
  }
  if (p_derivative) {
    *p_derivative = ee * pow(xx, u31tod(aa)) * kFactorialRecips[aa - 1];
  }
  return sum;
}

static const double kSqrtPi = 1.7724538509055159;

double lower_gamma_series(double aa, double zz, double init_value) {
  // evaluate init_value + 1 + (z/(a+1)) + (z^2 / ((a+1)(a+2))) +
  //   (z^3 / ((a+1)(a+2)(a+3))) + ...
  // z shouldn't be larger than (a+170), otherwise overflow is possible
  double result = 1;
  double total = init_value + 1;
  do {
    aa += 1.0;
    result *= zz / aa;
    total += result;
  } while (fabs(result) > (kBigEpsilon * kBigEpsilon));
  return total;
}

// ~6 digits of precision is appropriate for p-value computations
static const double kContinuedFractionEpsilon = 3.0e-7;

double upper_gamma_fraction(double a1, double z1) {
  // No overflow issues, this tends to be close to 1.

  // evaluate a_1 / (b_1 + (a_2 / (b_2 + (a_3 / (b_3 + ...)))))
  // see Boost continued_fraction_a(), upper_incomplete_gamma_fract
  double cur_b = z1 - a1 + 3;

  double hh = cur_b;
  const double a0 = a1 - 1.0;
  if (fabs(hh) < kLentzFpmin) {
    hh = kLentzFpmin;
  }
  double cc = hh;
  double dd = 0.0;
  for (double kk = 2.0; kk <= 100.0; kk += 1.0) {
    const double cur_a = kk * (a1 - kk);
    cur_b += 2.0;
    dd = cur_b + cur_a * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = cur_b + cur_a / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    const double delta = cc * dd;
    hh *= delta;
    if (fabs(delta - 1.0) < kContinuedFractionEpsilon) {
      break;
    }
  }
  // const double cont_frac = a0 / hh;
  // return 1 / (z1 - a1 + 1 + cont_frac);
  return hh / (hh * (z1 - a1 + 1) + a0);
}

/*
double small_gamma2_series(double aa, double xx, double init_value) {
  double apn = aa + 1;
  const double negx = -xx;
  double nn = 1;
  double result = negx;
  double total = init_value;
  double rr;
  do {
    rr = result / apn;
    result *= negx;
    nn += 1.0;
    result /= nn;
    apn += 1;
    total += rr;
  } while (fabs(rr) > (kBigEpsilon * kBigEpsilon));
  return total;
}

double tgamma_small_upper_part_df1(double xx, uint32_t invert, double* p_derivative, double* pgam) {
  // x < 1.1
  // df == 1, a == 0.5
  double result = 0.5 * kSqrtPi - 1.0;
  *pgam = (result + 1) * 2;  // make this a compile-time constant?
  double pp = sqrt(xx) - 1.0;  // no point in using powm1() with ^0.5
  result -= pp;
  result *= 2;
  pp += 1;
  if (p_derivative) {
    *p_derivative = pp / ((*pgam) * exp(xx));
  }
  const double init_value = invert? (*pgam) : 0;
  result = -pp * small_gamma2_series(0.5, xx, (init_value - result) / pp);
  if (invert) {
    result = -result;
  }
  return result;
}
*/

// from Numerical Recipes in Fortran 77: The Art of Scientific Computing, via
// Wikipedia
// maximal error of 1.2e-7
double erfc_fast(double zz) {
  const double tt = 1.0 / (1.0 + 0.5 * zz);
  const double tau = tt * exp(((((((((0.17087277 * tt - 0.82215223) * tt + 1.48851587) * tt - 1.13520398) * tt + 0.27886807) * tt - 0.18628806) * tt + 0.09678418) * tt + 0.37409196) * tt + 1.00002368) * tt - 1.26551223 - zz * zz);
  return tau;
}

static const double kSmallHalfRecips[30] = {
  1.0 / 0.5,
  1.0 / 1.5,
  1.0 / 2.5,
  1.0 / 3.5,
  1.0 / 4.5,
  1.0 / 5.5,
  1.0 / 6.5,
  1.0 / 7.5,
  1.0 / 8.5,
  1.0 / 9.5,
  1.0 / 10.5,
  1.0 / 11.5,
  1.0 / 12.5,
  1.0 / 13.5,
  1.0 / 14.5,
  1.0 / 15.5,
  1.0 / 16.5,
  1.0 / 17.5,
  1.0 / 18.5,
  1.0 / 19.5,
  1.0 / 20.5,
  1.0 / 21.5,
  1.0 / 22.5,
  1.0 / 23.5,
  1.0 / 24.5,
  1.0 / 25.5,
  1.0 / 26.5,
  1.0 / 27.5,
  1.0 / 28.5,
  1.0 / 29.5
};

// This can underflow.  Use finite_half_gamma_q2_ln when that's a problem.
double finite_half_gamma_q2(uint32_t a_minus_half, double xx, double* p_derivative) {
  // a is in {0.5, 1.5, ..., 29.5}; max(0.2, a-1) < x < kLogMaxValue
  const double sqrt_x = sqrt(xx);
  double ee = erfc_fast(sqrt_x);
  if ((ee != 0) && a_minus_half) {
    double term = sqrt_x * exp(-xx) * (2.0 / kSqrtPi);
    double sum = term;
    for (uint32_t n_minus_half = 1; n_minus_half != a_minus_half; ++n_minus_half) {
      term *= xx * kSmallHalfRecips[n_minus_half];
      sum += term;
    }
    ee += sum;
    if (p_derivative) {
      *p_derivative = 0;
    }
  } else if (p_derivative) {
    *p_derivative = sqrt_x * exp(-xx) * (1.0 / kSqrtPi);
  }
  return ee;
}

static const double kLanczosSumNumer[6] = {8706.3495925490091, 8523.650341121874, 3338.0292194764235, 653.64249942940087, 63.999518449381870, 2.5066282746310063};
static const double kLanczosSumDenom[6] = {0, 24, 50, 35, 10, 1};
static const double kLanczosSumExpgNumer[6] = {32.812445410297834, 32.123889414443320, 12.580347294552161, 2.4634444783532414, 0.2412010548258800, 0.0094469677045392};

// this depends on the polynomial coefficients above
static const double kLanczosG = 5.581;

double lanczos_sum(double zz) {
  double s1;
  double s2;
  if (zz <= 1) {
    s1 = kLanczosSumNumer[5];
    s2 = kLanczosSumDenom[5];
    for (int32_t ii = 4; ii >= 0; --ii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosSumNumer[S_CAST(uint32_t, ii)];
      s2 += kLanczosSumDenom[S_CAST(uint32_t, ii)];
    }
  } else {
    zz = 1 / zz;
    s1 = kLanczosSumNumer[0];
    s2 = kLanczosSumDenom[0];
    for (uint32_t uii = 1; uii != 6; ++uii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosSumNumer[uii];
      s2 += kLanczosSumDenom[uii];
    }
  }
  return s1 / s2;
}

double lanczos_sum_expg_scaled_imp(double zz, double* s2_ptr) {
  double s1;
  double s2;
  if (zz <= 1) {
    s1 = kLanczosSumExpgNumer[5];
    s2 = kLanczosSumDenom[5];
    for (int32_t ii = 4; ii >= 0; --ii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosSumExpgNumer[S_CAST(uint32_t, ii)];
      s2 += kLanczosSumDenom[S_CAST(uint32_t, ii)];
    }
  } else {
    zz = 1 / zz;
    s1 = kLanczosSumExpgNumer[0];
    s2 = kLanczosSumDenom[0];
    for (uint32_t uii = 1; uii != 6; ++uii) {
      s1 *= zz;
      s2 *= zz;
      s1 += kLanczosSumExpgNumer[uii];
      s2 += kLanczosSumDenom[uii];
    }
  }
  *s2_ptr = s2;
  return s1;
}

double lanczos_sum_expg_scaled(double zz) {
  double s2;
  double s1 = lanczos_sum_expg_scaled_imp(zz, &s2);
  return s1 / s2;
}

double lanczos_sum_expg_scaled_recip(double zz) {
  double s2;
  double s1 = lanczos_sum_expg_scaled_imp(zz, &s2);
  return s2 / s1;
}

double log1pmx(double xx) {
  // log(1+x) - x
  // assumes abs(xx) < 0.95
  const double aa = fabs(xx);
  if (aa < (kBigEpsilon / kSqrt2)) { // 2^{-21.5}
    return -xx * xx * 0.5;
  }
  double kk = 1.0;  // skip first term of usual log(1+x) series
  const double m_mult = -xx;
  double m_prod = xx;
  double total = 0.0;
  double rr;
  do {
    m_prod *= m_mult;
    kk += 1.0;
    rr = m_prod / kk;
    total += rr;
    // todo: tune these epsilons, but let's wait until we know all of the
    // callers of these functions
  } while (fabs(rr) > (kBigEpsilon * kBigEpsilon));
  return total;
}

// compute (z^a)(e^{-z})/tgamma(a)
double regularized_gamma_prefix(double aa, double zz) {
  // assumes a == 0.5 if a < 1.  assumes z > 0.
  // we are fine with float-level precision, so lanczos_n=6, kLanczosG=5.581
  if (aa < 1) {
    return sqrt(zz) * exp(-zz) * (1.0 / kSqrtPi);
  }
  const double agh = aa + kLanczosG - 0.5;
  const double agh_recip = 1.0 / agh;
  const double dd = ((zz - aa) - (kLanczosG - 0.5)) * agh_recip;
  double prefix;
  if ((fabs(dd * dd * aa) <= 100) && (aa > 150)) {
    // abs(dd) < sqrt(2/3) < 0.95
    prefix = aa * log1pmx(dd) + zz * (0.5 - kLanczosG) * agh_recip;
    prefix = exp(prefix);
  } else {
    const double alz = aa * log(zz * agh_recip);
    const double amz = aa - zz;
    const double cur_minv = MINV(alz, amz);
    if ((cur_minv <= kLogMinValue) || (MAXV(alz, amz) >= kLogMaxValue)) {
      const double amza = amz / aa;
      double sq;
      if ((cur_minv > 2 * kLogMinValue) && (MAXV(alz, amz) < 2 * kLogMaxValue)) {
        // need to structure this to avoid overflow
        sq = pow(zz * agh_recip, aa * 0.5) * exp(amz * 0.5);
        prefix = sq * sq;
      } else if ((cur_minv > 4 * kLogMinValue) && (MAXV(alz, amz) < 4 * kLogMaxValue) && (zz > aa)) {
        sq = pow(zz * agh_recip, aa * 0.25) * exp(amz * 0.25);
        prefix = sq * sq;
        prefix *= prefix;
      } else if ((amza > kLogMinValue) && (amza < kLogMaxValue)) {
        prefix = pow((zz * exp(amza)) * agh_recip, aa);
      } else {
        prefix = exp(alz + amz);
      }
    } else {
      prefix = pow(zz * agh_recip, aa) * exp(amz);
    }
  }
  prefix *= sqrt(agh * kRecipE) * lanczos_sum_expg_scaled_recip(aa);
  return prefix;
}

static const double kTemmeC0[7] = {-0.333333333, 0.0833333333, -0.0148148148, 0.00115740741, 0.000352733686, -0.000178755144, 0.391926318e-4};
static const double kTemmeC1[5] = {-0.00185185185, -0.00347222222, 0.00264550265, -0.000990226337, 0.000205761317};
static const double kTemmeC2[3] = {0.00413359788, -0.00268132716, 0.000771604938};

double igamma_temme_large(double aa, double xx) {
  // 24-bit precision is fine
  const double sigma = (xx - aa) / aa;
  // abs(sigma) < 0.4
  const double phi = -log1pmx(sigma);
  const double sqrt_a = sqrt(aa);
  const double sqrt_phi = sqrt(phi);
  const double yy = aa * phi;
  double zz = kSqrt2 * sqrt_phi;
  if (xx < aa) {
    zz = -zz;
  }
  double workspace[3];
  workspace[0] = (((((kTemmeC0[6] * zz + kTemmeC0[5]) * zz + kTemmeC0[4]) * zz + kTemmeC0[3]) * zz + kTemmeC0[2]) * zz + kTemmeC0[1]) * zz + kTemmeC0[0];
  workspace[1] = (((kTemmeC1[4] * zz + kTemmeC1[3]) * zz + kTemmeC1[2]) * zz + kTemmeC1[1]) * zz + kTemmeC1[0];
  workspace[2] = (kTemmeC2[2] * zz + kTemmeC2[1]) * zz + kTemmeC2[0];
  const double a_recip = 1 / aa;
  double result = (workspace[2] * a_recip + workspace[1]) * a_recip + workspace[0];
  result *= exp(-yy) / ((kSqrt2 * kSqrtPi) * sqrt_a);
  if (xx < aa) {
    result = -result;
  }
  result += erfc_fast(sqrt_a * sqrt_phi) * 0.5;
  return result;
}

double gamma_incomplete_imp2(uint32_t df, double xx, uint32_t invert, double* p_derivative) {
  // normalized = true
  assert(df);
  assert(xx >= 0.0);
  const double aa = u31tod(df) * 0.5;
  const uint32_t is_small_a = (df < 60) && (aa <= xx + 1) && (xx < kLogMaxValue);
  uint32_t is_int = 0;
  uint32_t is_half_int = 0;
  if (is_small_a) {
    is_half_int = df % 2;
    is_int = !is_half_int;
  }
  uint32_t eval_method;
  if (is_int && (xx > 0.6)) {
    invert = !invert;
    eval_method = 0;
  } else if (is_half_int && (xx > 0.2)) {
    invert = !invert;
    eval_method = 1;
  } else if (xx < kSmallEpsilon) {
    // avoid computing log(0)
    // don't need more precision here, 6 digits is enough
    // invert always == 1
    assert(!p_derivative);
    return 1.0;
    /*
  } else if (xx < 0.5) {
    // log(x) is negative
    // -0.4 / log(x) >= 0.5 (this is impossible for larger a)
    // -> -0.4 <= 0.5 * log(x)
    // -> -0.8 <= log(x)
    // -> e^{-0.8} <= x
    eval_method = 2 + ((df == 1) && (xx >= 0.44932896411722156));
  } else if (xx < 1.1) {
    // x * 0.75 >= 0.5
    // x >= 2/3
    eval_method = 2 + ((df == 1) && (xx >= (2.0 / 3.0)));
    */
  } else if (xx < 1.1) {
    eval_method = 2;
  } else {
    const double x_minus_a = xx - aa;
    uint32_t use_temme = 0;
    if (aa > 20) {
      // sigma = abs((x - a) / a);
      // igamma_temme_large() assumes abs(sigma) < 0.95
      if (aa > 200) {
        // abs(sigma) < sqrt(20 / a) < 0.316...
        use_temme = (20 * aa > x_minus_a * x_minus_a);
      } else {
        // abs(sigma) < 0.4
        const double sigma_times_a = fabs(x_minus_a);
        use_temme = (sigma_times_a < 0.4 * aa);
      }
    }
    if (use_temme) {
      eval_method = 5;
    } else {
      // x - (1 / (3 * x)) < a
      // x * x - (1/3) < a * x
      // x * x - a * x < 1/3
      // x * (x - a) < 1/3
      if (xx * x_minus_a < (1.0 / 3.0)) {
        eval_method = 2;
      } else {
        eval_method = 4;
        invert = !invert;
      }
    }
  }
  double result;
  switch(eval_method) {
  case 0:
    result = finite_gamma_q(df / 2, xx, p_derivative);
    break;
  case 1:
    // previously used erfc, but that was up to ~3x as slow as dcdflib (e.g.
    // ChisqToP(2.706, 1) case).
    result = finite_half_gamma_q2(df / 2, xx, p_derivative);
    if (p_derivative && (*p_derivative == 0)) {
      *p_derivative = regularized_gamma_prefix(aa, xx);
    }
    break;
  case 2:
    result = regularized_gamma_prefix(aa, xx);
    if (p_derivative) {
      *p_derivative = result;
    }
    if (result != 0) {
      // uint32_t optimized_invert = 0;
      double init_value = 0;
      if (invert) {
        init_value = -aa / result;
        // optimized_invert = 1;
      }
      result *= lower_gamma_series(aa, xx, init_value) / aa;
      // if (optimized_invert) {
      if (invert) {
        invert = 0;
        result = -result;
      }
    }
    break;
    /*
  case 3:
    {
      // only when df=1 and 0.449 < x < 1.1, so no overflow/underflow issues.
      // ...wait a sec, we never reach here at all since eval_method is always
      // 1 there.
      invert = !invert;
      double gg;
      result = tgamma_small_upper_part_df1(xx, invert, p_derivative, &gg);
      invert = 0;
      result /= gg;  // convert this to multiplication by constant?
    }
    break;
    */
  case 4:
    result = regularized_gamma_prefix(aa, xx);
    if (p_derivative) {
      *p_derivative = result;
    }
    if (result != 0) {
      result *= upper_gamma_fraction(aa, xx);
    }
    break;
  case 5:
    result = igamma_temme_large(aa, xx);
    if (xx >= aa) {
      invert = !invert;
    }
    if (p_derivative) {
      *p_derivative = regularized_gamma_prefix(aa, xx);
    }
  }
  if (result > 1) {
    result = 1;
  }
  if (invert) {
    result = 1 - result;
  }
  if (p_derivative) {
    if ((xx < 1) && (DBL_MAX * xx < (*p_derivative))) {
      *p_derivative = DBL_MAX / 2;  // overflow; do we really need this?
    } else {
      *p_derivative /= xx;
    }
  }
  return result;
}

double ChisqToP(double chisq, uint32_t df) {
  // todo: figure out when we were depending on this to return -9, and decide
  // how to handle those situations now
  return gamma_incomplete_imp2(df, chisq * 0.5, 1, nullptr);
}

// ***** end thread-safe ChisqToP *****


// ***** ChisqToLnP *****

/*
static inline long double u31told(uint32_t uii) {
  const int32_t ii = uii;
  return S_CAST(long double, ii);
}
*/

double finite_gamma_q_ln(uint32_t aa, double xx) {
  // a is a positive integer < 30; max(0.6, a-1) < x < 1e10
  // (e^{-x})(1 + x + x^2/2 + x^3/3! + x^4/4! + ... + x^{a-1}/(a-1)!)
  //
  // logarithm:
  // log(1 + x + ... + x^{a-1}/(a-1)!) - x
  // no overflow or underflow danger for main term thanks to bounds
  double sum = 1.0;
  double term = 1.0;
  for (uint32_t nn = 1; nn != aa; ++nn) {
    // division is slow enough that the lookup table speeds up this function by
    // >3x
    term *= xx * kSmallRecips[nn];
    sum += term;
  }
  return log(sum) - xx;
}

double erfc_fast2(double zz, double* tau_ln_plus_z2_ptr) {
  const double tt = 1.0 / (1.0 + 0.5 * zz);
  *tau_ln_plus_z2_ptr = ((((((((0.17087277 * tt - 0.82215223) * tt + 1.48851587) * tt - 1.13520398) * tt + 0.27886807) * tt - 0.18628806) * tt + 0.09678418) * tt + 0.37409196) * tt + 1.00002368) * tt - 1.26551223;
  return tt;
}

double finite_half_gamma_q2_ln(uint32_t a_minus_half, double xx) {
  // a is in {0.5, 1.5, ..., 29.5}; max(0.2, a-1) < x < 1e10
  const double sqrt_x = sqrt(xx);
  double tau_ln_plus_x;
  double tt = erfc_fast2(sqrt_x, &tau_ln_plus_x);
  if (!a_minus_half) {
    return log(tt) + tau_ln_plus_x - xx;
  }
  // pre-multiply by e^x to avoid underflow
  double term = sqrt_x * (2.0 / kSqrtPi);
  double sum = term;
  for (uint32_t n_minus_half = 1; n_minus_half != a_minus_half; ++n_minus_half) {
    term *= xx * kSmallHalfRecips[n_minus_half];
    sum += term;
  }
  // tau_ln + xx guaranteed to be small
  double ee = tt * exp(tau_ln_plus_x) + sum;
  return log(ee) - xx;
}

static const double kLnSqrtPi = 0.5723649429247001;

// compute -log((z^a)(e^{-z})/tgamma(a))
double regularized_gamma_prefix_ln(double aa, double zz) {
  // assumes a == 0.5 if a < 1.  assumes z > 0.
  // we are fine with float-level precision, so lanczos_n=6, kLanczosG=5.581
  if (aa < 1) {
    return -zz + 0.5 * log(zz) - kLnSqrtPi;
  }
  const double agh = aa + kLanczosG - 0.5;
  const double agh_recip = 1.0 / agh;
  const double dd = ((zz - aa) - (kLanczosG - 0.5)) * agh_recip;
  double prefix_ln;
  if ((fabs(dd * dd * aa) <= 100) && (aa > 150)) {
    // abs(dd) < sqrt(2/3) < 0.95
    prefix_ln = aa * log1pmx(dd) + zz * (0.5 - kLanczosG) * agh_recip;
  } else {
    prefix_ln = (aa - zz) + aa * log(zz * agh_recip);
  }
  const double scaled_recip = lanczos_sum_expg_scaled_recip(aa);
  return prefix_ln + 0.5 * log(agh * kRecipE * scaled_recip * scaled_recip);
}

// does not guarantee return value <= 0 for now; caller must do that.
double gamma_incomplete_imp2_ln(uint32_t df, double xx) {
  // normalized = true, invert = false
  assert(df);
  assert(xx >= 0.0);
  const double aa = u31tod(df) * 0.5;
  const uint32_t is_small_a = (df < 60) && (aa <= xx + 1) && (xx < 1e10);
  uint32_t is_int = 0;
  uint32_t is_half_int = 0;
  if (is_small_a) {
    is_half_int = df % 2;
    is_int = !is_half_int;
  }
  uint32_t eval_method;
  if (is_int && (xx > 0.6)) {
    eval_method = 0;
  } else if (is_half_int && (xx > 0.2)) {
    eval_method = 1;
  } else if (xx < kSmallEpsilon) {
    // avoid computing log(0)
    // don't need more precision here, 6 digits is enough
    return 0.0;
  } else if (xx < 1.1) {
    eval_method = 2;
  } else {
    const double x_minus_a = xx - aa;
    uint32_t use_temme = 0;
    if (aa > 20) {
      // sigma = abs((x - a) / a);
      // igamma_temme_large() assumes abs(sigma) < 0.95
      if (aa > 200) {
        // abs(sigma) < sqrt(20 / a) < 0.316...
        use_temme = (20 * aa > x_minus_a * x_minus_a);
      } else {
        // abs(sigma) < 0.4
        const double sigma_times_a = fabs(x_minus_a);
        use_temme = (sigma_times_a < 0.4 * aa);
      }
    }
    if (use_temme) {
      eval_method = 5;
    } else {
      // x - (1 / (3 * x)) < a
      // x * x - (1/3) < a * x
      // x * x - a * x < 1/3
      // x * (x - a) < 1/3
      if (xx * x_minus_a < (1.0 / 3.0)) {
        eval_method = 2;
      } else {
        eval_method = 4;
      }
    }
  }
  switch(eval_method) {
  case 0:
    return finite_gamma_q_ln(df / 2, xx);
  case 1:
    return finite_half_gamma_q2_ln(df / 2, xx);
  case 2:
    {
      const double result_ln = regularized_gamma_prefix_ln(aa, xx);
      if (result_ln < kLogMinValue + 22) {
        // init_value overflows.  Not a big deal, this just ends up getting
        // inverted to pval=1.
        // (+22 since aa could theoretically be as large as 2^31.  Todo: find
        // the smallest result_ln value that could result in a nonzero value
        // being returned.)
        return 0.0;
      }
      const double init_value = -aa * exp(-result_ln);
      const double multiplier = -lower_gamma_series(aa, xx, init_value) / aa;
      return result_ln + log(multiplier);
    }
  case 4:
    {
      const double result1_ln = regularized_gamma_prefix_ln(aa, xx);
      const double result2_ln = log(upper_gamma_fraction(aa, xx));
      return result1_ln + result2_ln;
    }
  case 5:
  default:  // silence compiler warning
    {
      // aa large, fabs(xx - aa) relatively small, so no overflow/underflow
      // issues.
      double result = igamma_temme_large(aa, xx);
      if (xx < aa) {
        result = 1.0 - result;
      }
      return log(result);
    }
  }
}

double ChisqToLnP(double chisq, uint32_t df) {
  return MINV(gamma_incomplete_imp2_ln(df, chisq * 0.5), 0.0);
}
// ***** end ChisqToLnP *****


// ***** thread-safe PToChisq *****
// port of Boost 1.60 implementation

double find_inverse_gamma2(uint32_t df, double pp, double qq, uint32_t* has_10_digits_ptr) {
  // currently assumes *has_10_digits_ptr initialized to zero
  if (df == 2) {
    return -log(qq);
  }
  if (df == 1) {
    // g == tgamma(0.5) == sqrt(pi)
    const double bb = qq * kSqrtPi;
    if (bb >= 0.45) {
      // b * q > 1e-8, q > 1e-5 guaranteed
      // u = pow(p * g * a, 1/a)
      //   = pow(p * g * 0.5, 2)
      //   = p * p * g * g * 0.25
      //   = p * p * pi * 0.25
      const double uu = pp * pp * (0.25 * kPi);
      return (uu / (1 - (uu * (1.0 / 1.5))));
    } else {
      const double yy = -log(bb);
      if (bb > 0.1) {
        const double uu = yy - 0.5 * log(yy);
        if (bb > 0.15) {
          return (yy - 0.5 * log(uu) - log(1 + 0.5 / uu));
        }
        return (yy - 0.5 * log(uu) - log(((uu + 5) * uu + 3.75) / ((uu + 4.5) * uu + 2)));
      } else {
        const double c1 = -0.5 * log(yy);
        const double c1_2 = c1 * c1;
        const double c1_3 = c1_2 * c1;
        const double c1_4 = c1_2 * c1_2;
        // a_2 = 0.25
        // a_3 = 0.125

        const double c2 = -0.5 * (1 + c1);
        const double c3 = 0.25 * c1_2 + 0.75 * c1 + 0.875;
        const double c4 = c1_3 * (-1.0 / 6.0) - 0.875 * c1_2 - 1.875 * c1 - (26.75 / 12.0);
        const double c5 = 0.125 * c1_4 + (5.75 / 6.0) * c1_3 + 3.625 * c1_2 + 7.75 * c1 + (83.0625 / 12.0);

        const double y_recip = 1.0 / yy;
        const double y_recip_2 = y_recip * y_recip;
        const double y_recip_3 = y_recip_2 * y_recip;
        const double y_recip_4 = y_recip_2 * y_recip_2;
        if (bb < 1e-28) {
          *has_10_digits_ptr = 1;
        }
        // er, I'd think this should just use Horner's instead?
        return (yy + c1 + c2 * y_recip + c3 * y_recip_2 + c4 * y_recip_3 + c5 * y_recip_4);
      }
    }
  }
  // not implemented yet
  assert(0);
  exit(1);
  return 0;
}

double gamma_p_inv_imp2(uint32_t df, double qq) {
  assert(df);
  assert(qq > 0.0);
  if (qq >= 1.0 - kSmallEpsilon) {
    return 0;
  }
  double pp = 1.0 - qq;
  uint32_t has_10_digits = 0;
  double guess = find_inverse_gamma2(df, pp, qq, &has_10_digits);
  if (has_10_digits) {
    return guess;
  }
  double min_guess = kSmallEpsilon;
  double max_guess = DBL_MAX;
  if (guess < kSmallEpsilon) {
    guess = kSmallEpsilon;
  }
  // halley iteration, digits == 24, lower == kSmallEpsilon
  // see second_order_root_finder in boost/math/tools/roots.hpp
  const uint32_t invert = (pp > 0.9);
  if (invert) {
    pp = qq;
  }
  const double a_minus_1 = 0.5 * S_CAST(double, S_CAST(int32_t, df) - 2);
  const double factor = 1.1920928955078125e-07;  // 2^{-23}
  double result = guess;
  double delta = 10000000;
  double delta1 = delta;
  uint32_t out_of_bounds_sentry = 0;
  do {
    double delta2 = delta1;
    delta1 = delta;

    // see gamma_p_inverse_func in
    // boost/math/special_functions/detail/igamma_inverse.hpp
    double f1;
    const double ff = gamma_incomplete_imp2(df, result, invert, &f1);
    const double div = (a_minus_1 - result) / result;
    double f2 = f1;
    if ((fabs(div) > 1) && (DBL_MAX / fabs(div) < f2)) {
      // overflow
      f2 = -DBL_MAX / 2;
    } else {
      f2 *= div;
    }
    if (invert) {
      f1 = -f1;
      f2 = -f2;
    }
    const double f0 = ff - pp;
    if (f0 == 0) {
      break;
    }
    assert(f1 != 0);  // shouldn't be possible, function is monotonic
    delta = f0 / f1;
    if (f2 != 0) {
      // delta = Stepper::step(result, f0, f1, f2);
      const double denom = 2 * f0;
      const double numer = 2 * f1 - f0 * (f2 / f1);
      if ((fabs(numer) >= 1) || (fabs(denom) < fabs(numer) * DBL_MAX)) {
        const double halley_step = denom / numer;
        if (halley_step / delta < 0) {
          if (fabs(delta) > 2 * fabs(guess)) {
            delta = ((delta < 0)? -1 : 1) * 2 * fabs(guess);
          }
        } else {
          delta = halley_step;
        }
      }
    }
    double convergence = fabs(delta / delta2);
    if ((convergence > 0.8) && (convergence < 2)) {
      delta = (delta > 0)? (0.5 * (result - min_guess)) : (0.5 * (result - max_guess));
      if (fabs(delta) > result) {
        delta = ((delta > 0)? 1 : -1) * result;
      }
      // delta2 = delta * 3;
    }
    guess = result;
    result -= delta;
    // do we actually need this?
    if (result < min_guess) {
      double diff = ((fabs(min_guess) < 1) && (fabs(result) > 1) && ((DBL_MAX / fabs(result)) < fabs(min_guess)))? 1000 : (result / min_guess);
      if (fabs(diff) < 1) {
        diff = 1 / diff;
      }
      if ((!out_of_bounds_sentry) && (diff > 0) && (diff < 3)) {
        delta = 0.99 * (guess - min_guess);
        result = guess - delta;
        out_of_bounds_sentry = 1;
      } else {
        delta = (guess - min_guess) * 0.5;
        result = guess - delta;
        if ((result == min_guess) || (result == max_guess)) {
          break;
        }
      }
    } else if (result > max_guess) {
      double diff = ((fabs(max_guess) < 1) && (fabs(result) > 1) && ((DBL_MAX / fabs(result)) < fabs(max_guess)))? 1000 : (result / max_guess);
      if (fabs(diff) < 1) {
        diff = 1 / diff;
      }
      if ((!out_of_bounds_sentry) && (diff > 0) && (diff < 3)) {
        delta = 0.99 * (guess - max_guess);
        result = guess - delta;
        out_of_bounds_sentry = 1;
      } else {
        delta = (guess - max_guess) * 0.5;
        result = guess - delta;
        if ((result == min_guess) || (result == max_guess)) {
          break;
        }
      }
    }
    if (delta > 0) {
      max_guess = guess;
    } else {
      min_guess = guess;
    }
  } while (fabs(result * factor) < fabs(delta));
  return result;
}

double PToChisq(double pval, uint32_t df) {
  // only need this to handle df=1, 2, 4 for now
  return gamma_p_inv_imp2(df, pval) * 2;
}

// ***** end thread-safe PToChisq *****

double LnPToChisq(double ln_pval) {
  if (ln_pval > -65.04474754675797) {
    return gamma_p_inv_imp2(1, exp(ln_pval)) * 2;
  }
  // (bb < 1e-28) case in find_inverse_gamma2()
  const double yy = kLnSqrtPi - ln_pval;
  const double c1 = -0.5 * log(yy);
  const double c1_2 = c1 * c1;
  const double c1_3 = c1_2 * c1;
  const double c1_4 = c1_2 * c1_2;
  // a_2 = 0.25
  // a_3 = 0.125

  const double c2 = -0.5 * (1 + c1);
  const double c3 = 0.25 * c1_2 + 0.75 * c1 + 0.875;
  const double c4 = c1_3 * (-1.0 / 6.0) - 0.875 * c1_2 - 1.875 * c1 - (26.75 / 12.0);
  const double c5 = 0.125 * c1_4 + (5.75 / 6.0) * c1_3 + 3.625 * c1_2 + 7.75 * c1 + (83.0625 / 12.0);

  const double y_recip = 1.0 / yy;
  const double y_recip_2 = y_recip * y_recip;
  const double y_recip_3 = y_recip_2 * y_recip;
  const double y_recip_4 = y_recip_2 * y_recip_2;
  // er, I'd think this should just use Horner's instead?
  return 2 * (yy + c1 + c2 * y_recip + c3 * y_recip_2 + c4 * y_recip_3 + c5 * y_recip_4);
}


// ***** thread-safe TstatToP *****
// ...time to get rid of this

// see Numerical Recipes, section 6.4
// no overflow or convergence issues
double betacf_slow(double aa, double bb, double xx) {
  double qab = aa + bb;
  double qap = aa + 1.0;
  double qam = aa - 1.0;
  double cc = 1.0;
  double dd = 1.0 - qab * xx / qap;
  if (fabs(dd) < kLentzFpmin) {
    dd = kLentzFpmin;
  }
  dd = 1.0 / dd;
  double hh = dd;
  // evaluate 1 / (1 + d_1 / (1 + d_2 / (1 + d_3 / (...))))
  // might need more iterations for very large df?
  for (double mm = 1.0; mm <= 100.0; mm += 1.0) {
    double m2 = 2 * mm;

    // d_{2m}
    double tmp_aa = mm * (bb - mm) * xx / ((qam + m2) * (aa + m2));

    dd = 1.0 + tmp_aa * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = 1.0 + tmp_aa / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    hh *= dd * cc;

    // d_{2m+1}
    tmp_aa = -(aa + mm) * (qab + mm) * xx / ((aa + m2) * (qap + m2));

    dd = 1.0 + tmp_aa * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = 1.0 + tmp_aa / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    double del = dd * cc;
    hh *= del;
    if (fabs(del - 1.0) < kContinuedFractionEpsilon) {
      return hh;
    }
  }
  // don't detect failure for now
  return hh;
}

// Returns log(1 - x^y).
// Assumes x in (0, 1), y positive.
double neg_powm1_imp_ln(double xx, double yy) {
  const double ll = yy * log(xx);
  if (ll > -1.0) {
    // For tiny |l|, we may lose all precision when exp(l) evaluates to 1.
    return log(-expm1(ll));
  }
  // For larger |l|, it's the log step that may throw away precision.
  if (ll < kLogMinValue) {
    return 0.0;
  }
  return log1p(-exp(ll));
}

double binomial_ccdf_ln(uint32_t nn, uint32_t kk, double xx, double yy, uint32_t inv) {
  // x^n + (n choose 1)x^{n-1}y + ... + (n choose (n-k-1))x^{k+1}y^{n-k-1}
  // This is currently just designed to work with ibeta_imp2_ln().  So we
  // assume x and y positive, y = 1 - x, (n-k) < 40, (n-k) <= (k+1)(y/x).

  // Thanks to the (n-k) <= (k+1)(y/x) condition, it's always reasonable to
  // start from the rightmost term normalized to 1, and sum leftward; then we
  // multiply by the rightmost term at the end.
  const double n_d = u31tod(nn);
  const double k_plus1_d = u31tod(kk + 1);
  const double x_div_y = xx / yy;
  double multiplier = 1.0;
  double cur_term = 1.0;
  // need to shift to avoid overflow for n > 1 billion, (n-k) close to 40
  double shifted_inv_binom_coeff = 1.0 / kExactTestBias;
  double i_d = n_d - k_plus1_d;
  for (uint32_t uii = nn - kk - 1; uii; --uii) {
    //   (n choose (i - 1)) / (n choose i)
    //   i!(n-i)! / (i-1)!(n-i+1)!
    // = i / (n-i+1)
    if (cur_term < kRecip2m53) {
      // Only need to finish computing shifted_inv_binom_coeff: multiply by
      // i!, divide by n!/(n-i)!.
      // Since we support n up to 2^31, n!/(n-i)! may overflow for i>33.
      for (; uii > 33; --uii) {
        shifted_inv_binom_coeff *= i_d;
        i_d -= 1.0;
        shifted_inv_binom_coeff /= n_d - i_d;
      }
      double denom = kFactorialRecips[uii];
      double n_minus_i = n_d - i_d;
      for (; uii; --uii) {
        n_minus_i += 1.0;
        denom *= n_minus_i;
      }
      shifted_inv_binom_coeff /= denom;
      break;
    }
    double cur_binom_ratio = i_d;
    i_d -= 1.0;
    cur_binom_ratio /= n_d - i_d;
    shifted_inv_binom_coeff *= cur_binom_ratio;
    cur_term *= cur_binom_ratio * x_div_y;
    multiplier += cur_term;
  }
  // 1.0 / kExactTestBias = 2^83
  // tried taking log(multiplier / shifted_inv_binom_coeff), but that was
  // slower on my Mac?
  const double result_ln = log(multiplier) + k_plus1_d * log(xx) + (n_d - k_plus1_d) * log(yy) - log(shifted_inv_binom_coeff) + 83 * kLn2;
  if (!inv) {
    return result_ln;
  }
  // The nightmare scenario of tiny result_ln shouldn't happen thanks to the
  // (n-k) <= (k+1)(y/x) condition.
  return log1p(-exp(result_ln));
}

double ibeta_series_ln(double aa, double bb, double xx, uint32_t inv) {
  // BPSER in DiDonato and Morris.
  // ibeta(a, b, x) ~= gamma(a+b) / (gamma(a) * gamma(b))
  //                 * x^a / a
  //                 * (1 + a\sum_j=1^N [(1-b)(2-b)...(j-b)x^j / (j!(a+j))])
  // Currently always called with x <= 0.7, and either b*x <= 0.7 or b<40, so
  // guaranteed to converge at reasonable speed.

  // normalized always true

  const double cc = aa + bb;

  const double agh = aa + kLanczosG - 0.5;
  const double bgh = bb + kLanczosG - 0.5;
  const double cgh = cc + kLanczosG - 0.5;
  double numer_a;
  double denom_a = lanczos_sum_expg_scaled_imp(aa, &numer_a);
  double numer_b;
  double denom_b = lanczos_sum_expg_scaled_imp(bb, &numer_b);
  double denom_c;
  double numer_c = lanczos_sum_expg_scaled_imp(cc, &denom_c);
  double result = (numer_a * numer_b * numer_c) / (denom_a * denom_b * denom_c);
  double l1 = log(cgh / bgh) * (bb - 0.5);
  double l2 = log(xx * cgh / agh) * aa;
  double result_ln = log(result * result * agh) * 0.5 + l1 + l2 - 0.5;

  double series_sum = 0.0;
  double term = 1.0;
  double apn = aa;
  double poch = 1.0 - bb;
  double nn = 1.0;
  while (1) {
    double rr = term / apn;
    series_sum += rr;
    if (fabs(rr) <= fabs(series_sum * kEpsilon)) {
      result_ln += log(series_sum);
      if (!inv) {
        return result_ln;
      }
      // assume for now that we wouldn't be inverting into the tiny result_ln
      // case.
      return log1p(-exp(result_ln));
    }
    apn += 1.0;
    term *= poch * xx / nn;
    nn += 1.0;
    poch += 1.0;
  }
}

double ln_sum(double aa, double bb) {
  if (aa > bb) {
    const double tmp = aa;
    aa = bb;
    bb = tmp;
  }
  const double diff = bb - aa;
  if ((aa == -DBL_MAX) || (diff >= kLn2 * 53)) {
    return bb;
  }
  return bb + log(1 + exp(-diff));
}

double tgamma_delta_ratio(double zz, double delta) {
  // gamma(z) / gamma(z + delta)
  // We only call this with delta = 0.5 or 1, so no overflow issues.
  // zz >= 15 for now.
  if (delta == 1.0) {
    // Trivial case.
    return 1.0 / zz;
  }
  assert(delta == 0.5);
  // Can skip z < epsilon and z + delta == z branches for now.
  double zgh = zz + kLanczosG - 0.5;
  // Also skip fabs(delta) >= 10 branch for now.
  double result = exp((0.5 - zz) * log1p(delta / zgh));
  result *= lanczos_sum(zz) / lanczos_sum(zz + delta);
  // exploit forced delta == 0.5
  result *= sqrt(kE / (zgh + delta));
  return result;
}

double beta_small_b_large_a_series_ln(double aa, double bb, double xx, double yy, double s0_ln, uint32_t inv) {
  // BGRAT in DiDonato and Morris.

  // normalized always true, mult always 1
  // a >= 15, b == 0.5 or 1, and ibeta_imp was patched to ensure x > 0.7.
  double bm1 = bb - 1.0;
  double tt = aa + bm1 * 0.5;
  double lx;
  if (yy < 0.35) {
    lx = log1p(-yy);
  } else {
    lx = log(xx);
  }
  double uu = -tt * lx;
  double hh_ln = regularized_gamma_prefix_ln(bb, uu);
  double prefix_ln = hh_ln - log(tgamma_delta_ratio(aa, bb)) - bb * log(tt);
  // validated up to this point

  double pp[15]; // ~8-15 digit accuracy
  pp[0] = 1.0;

  double jj;
  {
    double dxx;
    if (bb == 0.5) {
      // bugfix (17 Jun 2019): original expression could underflow
      // jj = finite_half_gamma_q2(0, uu, nullptr);
      dxx = finite_half_gamma_q2_ln(0, uu);
    } else {
      assert(bb == 1.0);
      dxx = -uu;
    }
    // Underflow is harmless here, jj becomes nonzero for the other terms in
    // the series in that case.
    jj = exp(dxx - hh_ln);
  }
  double sum = jj; // patch in s0 and prefix at the end
  uint32_t tnp1 = 1;
  double lx2 = lx * 0.5;
  lx2 *= lx2;
  double lxp = 1.0;
  double t4_recip = 0.25 / (tt * tt);
  double b2n = bb;
  for (uint32_t nn = 1; nn < 15; ++nn) {
    tnp1 += 2;
    double new_pn = 0.0;
    uint32_t tmp1 = 3;
    const double nn_d = u31tod(nn);
    for (uint32_t mm = 1; mm < nn; ++mm) {
      double mbn = u31tod(mm) * bb - nn_d;
      new_pn += mbn * pp[nn - mm] * kFactorialRecips[tmp1];
      tmp1 += 2;
    }
    new_pn /= nn_d;
    new_pn += bm1 * kFactorialRecips[tnp1];
    pp[nn] = new_pn;

    jj = (b2n * (b2n + 1) * jj + (uu + b2n + 1) * lxp) * t4_recip;
    lxp *= lx2;
    b2n += 2;

    double rr = new_pn * jj;
    sum += rr;
    if (rr > 1) {
      if (fabs(rr) < fabs(kEpsilon * sum)) {
        break;
      }
    } else {
      if (fabs(rr * (1.0 / kEpsilon)) < fabs(sum)) {
        break;
      }
    }
  }
  double result_ln = ln_sum(s0_ln, prefix_ln + log(sum));
  if (!inv) {
    return result_ln;
  }
  return log1p(-exp(result_ln));
}

double ibeta_power_terms_ln(double aa, double bb, double xx, double yy) {
  // returns log((x^a)(y^b) / Beta(a,b))
  //
  // normalized always true
  // prefix always 1
  double cc = aa + bb;

  const double agh = aa + kLanczosG - 0.5;
  const double bgh = bb + kLanczosG - 0.5;
  const double cgh = cc + kLanczosG - 0.5;
  double numer_a;
  double denom_a = lanczos_sum_expg_scaled_imp(aa, &numer_a);
  double numer_b;
  double denom_b = lanczos_sum_expg_scaled_imp(bb, &numer_b);
  double denom_c;
  double numer_c = lanczos_sum_expg_scaled_imp(cc, &denom_c);
  double result = (numer_a * numer_b * numer_c) / (denom_a * denom_b * denom_c);
  result *= sqrt(agh * bgh * kRecipE / cgh);
  double result_ln = log(result);
  double l1 = (xx * bb - yy * agh) / agh;
  double l2 = (yy * aa - xx * bgh) / bgh;
  // Since we're returning log(result) rather than the original result (thus,
  // no intermediate overflow/underflow problems), and we only need 6 digits of
  // precision, we shouldn't need any of the numerous cases in the Boost code.
  return result_ln + aa * log1p(l1) + bb * log1p(l2);
}

double ibeta_a_step_ln(double aa, double bb, double xx, double yy, uint32_t kk) {
  double prefix_ln = ibeta_power_terms_ln(aa, bb, xx, yy) - log(aa);
  double sum = 1.0;
  double term = 1.0;
  const double a_plus_b = aa + bb;
  const double k_minus_1pt5 = u31tod(kk) - 1.5;
  for (double i_d = 0.0; i_d < k_minus_1pt5; ) {
    term *= (a_plus_b + i_d) * xx;
    i_d += 1.0;
    term /= aa + i_d;
    sum += term;
  }
  return prefix_ln + log(sum);
}

double ibeta_fraction2_ln(double aa, double bb, double xx, double yy, uint32_t inv) {
  // normalized always true

  // todo: original DiDonato and Morris paper notes that "x must also be a
  // sufficient distance from p when a > 100"; check if we have a problem there

  double result_ln = ibeta_power_terms_ln(aa, bb, xx, yy);

  // see Boost continued_fraction_b()
  const double ay_minus_bx_plus1 = aa * yy - bb * xx + 1.0;
  double cc = (aa * ay_minus_bx_plus1) / (aa + 1.0);
  if (fabs(cc) < kLentzFpmin) {
    cc = kLentzFpmin;
  }
  const double a_plus_b = aa + bb;
  const double x2 = xx * xx;
  result_ln -= log(cc);
  double dd = 0.0;
  double mm = 0.0;
  while (1) {
    double cur_a = (aa + mm) * (a_plus_b + mm);
    mm += 1.0;
    cur_a *= mm * (bb - mm) * x2;
    double denom = aa + 2 * mm - 1.0;
    cur_a /= denom * denom;
    double cur_b = mm;
    cur_b += (mm * (bb - mm) * xx) / (aa + 2 * mm - 1.0);
    cur_b += ((aa + mm) * (ay_minus_bx_plus1 + mm * (2.0 - xx))) / (aa + 2 * mm + 1.0);
    dd = cur_b + cur_a * dd;
    if (fabs(dd) < kLentzFpmin) {
      dd = kLentzFpmin;
    }
    cc = cur_b + cur_a / cc;
    if (fabs(cc) < kLentzFpmin) {
      cc = kLentzFpmin;
    }
    dd = 1.0 / dd;
    const double delta = cc * dd;
    result_ln -= log(delta);
    if (fabs(delta - 1.0) <= kContinuedFractionEpsilon) {
      return inv? log1p(-exp(result_ln)) : result_ln;
    }
  }
}

// the '2' in imp2 refers to df1 and df2 arguments instead of a and b
double ibeta_imp2_ln(uint32_t df1, uint32_t df2, double xx, uint32_t inv) {
  // In addition to Boost beta.hpp and its dependencies, see DiDonato and
  // Morris's original paper at
  // https://apps.dtic.mil/dtic/tr/fulltext/u2/a210118.pdf .

  // normalized always true
  //
  // assume a >= 0, b >= 0, a+b > 0, x in [0, 1], a and b are multiples of 0.5
  // for now
  // in practice, x always <= 0.5 right now
  if (df1 == 0) {
    return inv? -DBL_MAX : 0.0;
  }
  if (df2 == 0) {
    return inv? 0.0 : -DBL_MAX;
  }
  if (xx == 0.0) {
    return inv? 0.0 : -DBL_MAX;
  }
  if (xx == 1.0) {
    return inv? -DBL_MAX : 0.0;
  }
  double yy = 1.0 - xx;
  if ((df1 == 1) && (df2 == 1)) {
    return log(asin(sqrt(inv? yy : xx)) * (2.0 / kPi));
  }
  if (df1 == 2) {
    df1 = df2;
    df2 = 2;

    const double tmp = xx;
    xx = yy;
    yy = tmp;

    inv = !inv;
  }
  double aa = u31tod(df1) * 0.5;
  if (df2 == 2) {
    if (df1 == 2) {
      return log(inv? yy : xx);
    }
    double ln_pp;
    if (yy < 0.5) {
      //       pp = inv? -expm1(aa * log1p(-yy)) : exp(aa * log1p(-yy))
      // -> ln_pp = inv? log(-exp(aa * log1p(-yy)) + 1.0) : (aa * log1p(-yy))
      //          = inv? log1p(-exp(aa * log1p(-yy))) : (aa * log1p(-yy))
      const double inner_term = aa * log1p(-yy);
      ln_pp = inv? log1p(-exp(inner_term)) : inner_term;
    } else {
      //       pp = inv? -powm1(xx, aa) : pow(xx, aa);
      // -> ln_pp = inv? log(-powm1(xx, aa)) : (aa * log(xx))
      ln_pp = inv? neg_powm1_imp_ln(xx, aa) : (aa * log(xx));
    }
    return ln_pp;
  }

  double bb = u31tod(df2) * 0.5;
  if ((df1 == 1) || (df2 == 1)) {
    if (xx > 0.5) {
      const uint32_t tmp1 = df1;
      df1 = df2;
      df2 = tmp1;

      double tmp2 = aa;
      aa = bb;
      bb = tmp2;

      tmp2 = xx;
      xx = yy;
      yy = tmp2;

      inv = !inv;
    }
    // can ignore max(a, b) <= 1 branch
    if ((df2 == 1) || ((xx < 0.1) && (bb * xx <= 0.49))) {
      return ibeta_series_ln(aa, bb, xx, inv);
    }
    // a/b swapped, x/y swapped, inv swapped
    if (xx >= 0.3) {
      return ibeta_series_ln(bb, aa, yy, !inv);
    }
    if (bb >= 15) {
      return beta_small_b_large_a_series_ln(bb, aa, yy, xx, -DBL_MAX, !inv);
    }
    double fract_ln = ibeta_a_step_ln(bb, aa, yy, xx, 20);
    return beta_small_b_large_a_series_ln(bb + 20.0, aa, yy, xx, fract_ln, !inv);
  }
  double lambda;
  if (aa < bb) {
    lambda = aa - (aa + bb) * xx;
  } else {
    lambda = (aa + bb) * yy - bb;
  }
  if (lambda < 0.0) {
    const uint32_t tmp1 = df1;
    df1 = df2;
    df2 = tmp1;

    double tmp2 = aa;
    aa = bb;
    bb = tmp2;

    tmp2 = xx;
    xx = yy;
    yy = tmp2;

    inv = !inv;
  }

  // a > 1, b > 1 guaranteed if we get here.
  if (df2 >= 80) {
    return ibeta_fraction2_ln(aa, bb, xx, yy, inv);
  }
  if ((!(df1 % 2)) && (!(df2 % 2)) && (yy != 1.0)) {
    const uint32_t a_int = df1 / 2;
    const uint32_t b_int = df2 / 2;
    const uint32_t kk = a_int - 1;
    const uint32_t nn = b_int + kk;
    return binomial_ccdf_ln(nn, kk, xx, yy, inv);
  }
  // Changed from b*x <= 0.7 since BGRAT has problems with small x, while BPSER
  // can handle this larger b*x case since b<40 is guaranteed.
  if (xx <= 0.7) {
    return ibeta_series_ln(aa, bb, xx, inv);
  }
  const uint32_t nn = (df2 - 1) / 2;
  const double bbar = bb - u31tod(nn);
  double fract_ln = ibeta_a_step_ln(bbar, aa, yy, xx, nn);
  if (df1 > 30) {
    return beta_small_b_large_a_series_ln(aa, bbar, xx, yy, fract_ln, inv);
  }
  fract_ln = ln_sum(fract_ln, ibeta_a_step_ln(aa, bbar, xx, yy, 20));
  return beta_small_b_large_a_series_ln(aa + 20.0, bbar, xx, yy, fract_ln, inv);
}

double TstatToP2(double tt, double df, double cached_gamma_mult) {
  // assumes cached_mult == exp(lgamma(df * 0.5 + 0.5) - lgamma(df * 0.5) -
  //   lgamma(0.5))
  //         invert_thresh = (df + 2) / (df + 5)
  double tt_sq = tt * tt;
  double denom_recip = 1.0 / (df + tt_sq);
  double xx = df * denom_recip;
  double yy = tt_sq * denom_recip;
  if ((xx < 0.0) || (yy < 0.0)) {
    return -9;
  }
  uint32_t do_invert = (xx * (df + 5.0)) >= (df + 2.0);
  if ((xx == 0.0) || (yy == 0.0)) {
    return u31tod(do_invert);
  }
  double aa = df * 0.5;
  double bt = cached_gamma_mult * pow(xx, aa) * sqrt(yy);
  if (!do_invert) {
    return bt * betacf_slow(aa, 0.5, xx) / aa;
  }
  return 1.0 - bt * 2 * betacf_slow(0.5, aa, yy);
}
// ***** end thread-safe TstatToP calculation *****

// ***** begin TstatToLnP *****

double TstatToLnP(double tt, uint32_t df) {
  const double df_d = u31tod(df);
  const double t2 = tt * tt;
  if (df_d > 2 * t2) {
    const double zz = t2 / (df_d + t2);
    return ibeta_imp2_ln(1, df, zz, 1);
  }
  const double zz = df_d / (df_d + t2);
  return ibeta_imp2_ln(df, 1, zz, 0);
}
// ***** end TstatToLnP *****

// ***** FstatToLnP *****

double FstatToLnP(double ff, uint32_t df1, uint32_t df2) {
  // See cdf() for complemented2_type in boost/math/distributions/fisher_f.hpp.
  const double df1_d = u31tod(df1);
  const double df2_d = u31tod(df2);
  const double v1x = df1_d * ff;
  if (v1x > df2_d) {
    return ibeta_imp2_ln(df2, df1, df2_d / (df2_d + v1x), 0);
  }
  return ibeta_imp2_ln(df1, df2, v1x / (df2_d + v1x), 1);
}

// ***** end FstatToLnP *****


// Inverse normal distribution
// (todo: check if boost implementation is better)

// Lower tail quantile for standard normal distribution function.
//
// This function returns an approximation of the inverse cumulative
// standard normal distribution function.  I.e., given P, it returns
// an approximation to the X satisfying P = Pr{Z <= X} where Z is a
// random variable from the standard normal distribution.
//
// The algorithm uses a minimax approximation by rational functions
// and the result has a relative error whose absolute value is less
// than 1.15e-9.
//
// Author:      Peter J. Acklam
// Time-stamp:  2002-06-09 18:45:44 +0200
// E-mail:      jacklam@math.uio.no
// WWW URL:     http://www.math.uio.no/~jacklam
//
// C implementation adapted from Peter's Perl version

// Coefficients in rational approximations.

static const double kIvnA[] =
  {
    -3.969683028665376e+01,
    2.209460984245205e+02,
    -2.759285104469687e+02,
    1.383577518672690e+02,
    -3.066479806614716e+01,
     2.506628277459239e+00
  };

static const double kIvnB[] =
  {
    -5.447609879822406e+01,
    1.615858368580409e+02,
    -1.556989798598866e+02,
    6.680131188771972e+01,
    -1.328068155288572e+01
  };

static const double kIvnC[] =
  {
    -7.784894002430293e-03,
    -3.223964580411365e-01,
    -2.400758277161838e+00,
    -2.549732539343734e+00,
    4.374664141464968e+00,
     2.938163982698783e+00
  };

static const double kIvnD[] =
  {
    7.784695709041462e-03,
    3.224671290700398e-01,
    2.445134137142996e+00,
    3.754408661907416e+00
  };

static const double kIvnLow = 0.02425;
static const double kIvnHigh = 0.97575;

double QuantileToZscore(double pp) {
  // assumes 0 < pp < 1
  double q, r;

  if (pp < kIvnLow) {
    // Rational approximation for lower region
    q = sqrt(-2*log(pp));
    return (((((kIvnC[0]*q+kIvnC[1])*q+kIvnC[2])*q+kIvnC[3])*q+kIvnC[4])*q+kIvnC[5]) /
      ((((kIvnD[0]*q+kIvnD[1])*q+kIvnD[2])*q+kIvnD[3])*q+1);
  }
  if (pp > kIvnHigh) {
    // Rational approximation for upper region
    q  = sqrt(-2*log(1-pp));
    return -(((((kIvnC[0]*q+kIvnC[1])*q+kIvnC[2])*q+kIvnC[3])*q+kIvnC[4])*q+kIvnC[5]) /
      ((((kIvnD[0]*q+kIvnD[1])*q+kIvnD[2])*q+kIvnD[3])*q+1);
  }
  // Rational approximation for central region
  q = pp - 0.5;
  r = q*q;
  return (((((kIvnA[0]*r+kIvnA[1])*r+kIvnA[2])*r+kIvnA[3])*r+kIvnA[4])*r+kIvnA[5])*q /
    (((((kIvnB[0]*r+kIvnB[1])*r+kIvnB[2])*r+kIvnB[3])*r+kIvnB[4])*r+1);
}


// HweP() and HweXchrP() are now licensed as GPL 2+.
// possible todo: variant which returns -ln(pval), at least up to long double
// range (i.e. normally use faster notlong-double code, but centerp > DBL_MAX
// launches the slower long double code path)
double HweP(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, uint32_t midp) {
  // This function implements an exact SNP test of Hardy-Weinberg
  // Equilibrium as described in Wigginton, JE, Cutler, DJ, and
  // Abecasis, GR (2005) A Note on Exact Tests of Hardy-Weinberg
  // Equilibrium. American Journal of Human Genetics. 76: 887 - 893.
  //
  // The original version was written by Jan Wigginton.
  //
  // This version was written by Christopher Chang.  It contains the following
  // improvements over the original SNPHWE():
  // - Proper handling of >64k genotypes.  Previously, there was a potential
  //   integer overflow.
  // - Detection and efficient handling of floating point overflow and
  //   underflow.  E.g. instead of summing a tail all the way down, the loop
  //   stops once the latest increment underflows the partial sum's 53-bit
  //   precision; this results in a large speedup when max heterozygote count
  //   >1k.
  // - No malloc() call.  It's only necessary to keep track of a few partial
  //   sums.
  // - Support for the mid-p variant of this test.  See Graffelman J, Moreno V
  //   (2013) The mid p-value in exact tests for Hardy-Weinberg equilibrium.
  //
  // Note that the HweThresh() function below is a lot more efficient for
  // testing against a p-value inclusion threshold.  HweP() should only be
  // used if you need the actual p-value.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  const int64_t rare_copies = 2LL * obs_homr + obs_hets;
  const int64_t genotypes2 = (obs_hets + obs_homc + obs_homr) * 2LL;
  if (!genotypes2) {
    if (midp) {
      return 0.5;
    }
    return 1;
  }
  int32_t tie_ct = 1;
  double curr_hets_t2 = obs_hets;
  double curr_homr_t2 = obs_homr;
  double curr_homc_t2 = obs_homc;
  double tailp = (1 - kSmallEpsilon) * kExactTestBias;
  double centerp = 0;
  double lastp2 = tailp;
  double lastp1 = tailp;

  if (obs_hets * genotypes2 > rare_copies * (genotypes2 - rare_copies)) {
    // tail 1 = upper
    while (curr_hets_t2 > 1.5) {
      // het_probs[curr_hets] = 1
      // het_probs[curr_hets - 2] = het_probs[curr_hets] * curr_hets * (curr_hets - 1.0)
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      if (lastp2 < kExactTestBias) {
        tie_ct += (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias);
        tailp += lastp2;
        break;
      }
      centerp += lastp2;
      // doesn't seem to make a difference, but seems best to minimize use of
      // INFINITY
      if (centerp > DBL_MAX) {
        return 0;
      }
    }
    if ((centerp == 0) && (!midp)) {
      return 1;
    }
    while (curr_hets_t2 > 1.5) {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      const double preaddp = tailp;
      tailp += lastp2;
      if (tailp <= preaddp) {
        break;
      }
    }
    double curr_hets_t1 = obs_hets + 2;
    double curr_homr_t1 = obs_homr;
    double curr_homc_t1 = obs_homc;
    while (curr_homr_t1 > 0.5) {
      // het_probs[curr_hets + 2] = het_probs[curr_hets] * 4 * curr_homr * curr_homc / ((curr_hets + 2) * (curr_hets + 1))
      lastp1 *= (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
      const double preaddp = tailp;
      tailp += lastp1;
      if (tailp <= preaddp) {
        break;
      }
      curr_hets_t1 += 2;
      curr_homr_t1 -= 1;
      curr_homc_t1 -= 1;
    }
  } else {
    // tail 1 = lower
    while (curr_homr_t2 > 0.5) {
      curr_hets_t2 += 2;
      lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
      curr_homr_t2 -= 1;
      curr_homc_t2 -= 1;
      if (lastp2 < kExactTestBias) {
        tie_ct += (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias);
        tailp += lastp2;
        break;
      }
      centerp += lastp2;
      if (centerp > DBL_MAX) {
        return 0;
      }
    }
    if ((centerp == 0) && (!midp)) {
      return 1;
    }
    while (curr_homr_t2 > 0.5) {
      curr_hets_t2 += 2;
      lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
      curr_homr_t2 -= 1;
      curr_homc_t2 -= 1;
      const double preaddp = tailp;
      tailp += lastp2;
      if (tailp <= preaddp) {
        break;
      }
    }
    double curr_hets_t1 = obs_hets;
    double curr_homr_t1 = obs_homr;
    double curr_homc_t1 = obs_homc;
    while (curr_hets_t1 > 1.5) {
      curr_homr_t1 += 1;
      curr_homc_t1 += 1;
      lastp1 *= (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
      const double preaddp = tailp;
      tailp += lastp1;
      if (tailp <= preaddp) {
        break;
      }
      curr_hets_t1 -= 2;
    }
  }
  if (!midp) {
    return tailp / (tailp + centerp);
  }
  return (tailp - ((1 - kSmallEpsilon) * kExactTestBias * 0.5) * tie_ct) / (tailp + centerp);
}

// don't see any point to supporting long double range here
uint32_t HweThresh(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, double thresh) {
  // Threshold-test-only version of HweP() which is usually able to exit
  // from the calculation earlier.  Returns 0 if these counts are close enough
  // to Hardy-Weinberg equilibrium, 1 otherwise.
  //
  // Suppose, for definiteness, that the number of observed hets is no less
  // than expectation.  (Same ideas apply for the other case.)  We proceed as
  // follows:
  // - Sum the *relative* likelihoods of more likely smaller het counts.
  // - Determine the minimum tail mass to pass the threshold.
  // - The majority of the time, the tail boundary elements are enough to pass
  //   the threshold; we never need to sum the remainder of the tails.
  // - And in the case of disequilibrium, we will often be able to immediately
  //   determine that the tail sum cannot possibly pass the threshold, just by
  //   looking at the tail boundary elements and using a geometric series to
  //   upper-bound the tail sums.
  // - Only when neither of these conditions hold do we start traveling down
  //   the tails.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  int64_t rare_copies = 2LL * obs_homr + obs_hets;
  int64_t genotypes2 = (obs_hets + obs_homc + obs_homr) * 2LL;
  double curr_hets_t2 = obs_hets;  // tail 2
  double curr_homr_t2 = obs_homr;
  double curr_homc_t2 = obs_homc;

  // Subtract epsilon from initial probability mass, so that we can compare to
  // 1 when determining tail vs. center membership without floating point error
  // biting us in the ass
  double tailp1 = (1 - kSmallEpsilon) * kExactTestBias;
  double centerp = 0;
  double lastp2 = tailp1;
  double tailp2 = 0;
  double tail1_ceil;
  double tail2_ceil;
  double lastp1;
  double curr_hets_t1;
  double curr_homr_t1;
  double curr_homc_t1;

  // Initially, if center sum reaches this, the test can immediately fail.
  // Once center is summed, this is recalculated, and when tail sum has reached
  // this, we've passed.
  double exit_thresh;
  double exit_threshx;
  double ratio;
  double preaddp;
  if (!genotypes2) {
    return 0;
  }

  // Convert thresh into reverse odds ratio.
  thresh = (1 - thresh) / thresh;

  // Expected het count:
  //   2 * rarefreq * (1 - rarefreq) * genotypes
  // = 2 * (rare_copies / (2 * genotypes)) * (1 - rarefreq) * genotypes
  // = rare_copies * (1 - (rare_copies / (2 * genotypes)))
  // = (rare_copies * (2 * genotypes - rare_copies)) / (2 * genotypes)
  //
  // The computational identity is
  //   P(nhets == n) := P(nhets == n+2) * (n+2) * (n+1) /
  //                    (4 * homr(n) * homc(n))
  // where homr() and homc() are the number of homozygous rares/commons needed
  // to maintain the same allele frequencies.
  // This probability is always decreasing when proceeding away from the
  // expected het count.

  if (obs_hets * genotypes2 > rare_copies * (genotypes2 - rare_copies)) {
    // tail 1 = upper
    if (obs_hets < 2) {
      return 0;
    }

    // An initial upper bound on the tail sum is useful, since it lets us
    // report test failure before summing the entire center.  We use the
    // trivial bound of 1 + floor(rare_copies / 2): that's the total number
    // of possible het counts, and the relative probability for each count must
    // be <= 1 if it's in the tail.
    exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);

    // het_probs[curr_hets] = 1
    // het_probs[curr_hets - 2] = het_probs[curr_hets] * curr_hets * (curr_hets - 1) / (4 * (curr_homr + 1) * (curr_homc + 1))
    do {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      if (lastp2 < kExactTestBias) {
        tailp2 = lastp2;
        break;
      }
      centerp += lastp2;
      if (centerp > exit_thresh) {
        return 1;
      }
    } while (curr_hets_t2 > 1.5);
    exit_thresh = centerp / thresh;
    if (tailp1 + tailp2 >= exit_thresh) {
      return 0;
    }
    // c + cr + cr^2 + ... = c/(1-r), which is an upper bound for the tail sum
    ratio = (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * (curr_homr_t2 + 1) * (curr_homc_t2 + 1));
    tail2_ceil = tailp2 / (1 - ratio);
    curr_hets_t1 = obs_hets + 2;
    curr_homr_t1 = obs_homr;
    curr_homc_t1 = obs_homc;
    // ratio for the other tail
    lastp1 = (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
    tail1_ceil = tailp1 / (1 - lastp1);
    if (tail1_ceil + tail2_ceil < exit_thresh) {
      return 1;
    }
    lastp1 *= tailp1;
    tailp1 += lastp1;

    if (obs_homr > 1) {
      // het_probs[curr_hets + 2] = het_probs[curr_hets] * 4 * curr_homr * curr_homc / ((curr_hets + 2) * (curr_hets + 1))
      exit_threshx = exit_thresh - tailp2;
      do {
        curr_hets_t1 += 2;
        curr_homr_t1 -= 1;
        curr_homc_t1 -= 1;
        lastp1 *= (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
        preaddp = tailp1;
        tailp1 += lastp1;
        if (tailp1 > exit_threshx) {
          return 0;
        }
        if (tailp1 <= preaddp) {
          break;
        }
      } while (curr_homr_t1 > 1.5);
    }
    if (tailp1 + tail2_ceil < exit_thresh) {
      return 1;
    }
    exit_threshx = exit_thresh - tailp1;
    while (curr_hets_t2 > 1) {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      preaddp = tailp2;
      tailp2 += lastp2;
      if (tailp2 >= exit_threshx) {
        return 0;
      }
      if (tailp2 <= preaddp) {
        return 1;
      }
      curr_hets_t2 -= 2;
    }
    return 1;
  }
  // tail 1 = lower
  if (!obs_homr) {
    return 0;
  }
  exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
  do {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    if (lastp2 < kExactTestBias) {
      tailp2 = lastp2;
      break;
    }
    centerp += lastp2;
    if (centerp > exit_thresh) {
      return 1;
    }
  } while (curr_homr_t2 > 0.5);
  exit_thresh = centerp / thresh;
  if (tailp1 + tailp2 >= exit_thresh) {
    return 0;
  }
  ratio = (4 * curr_homr_t2 * curr_homc_t2) / ((curr_hets_t2 + 2) * (curr_hets_t2 + 1));
  tail2_ceil = tailp2 / (1 - ratio);
  curr_hets_t1 = obs_hets;
  curr_homr_t1 = obs_homr + 1;
  curr_homc_t1 = obs_homc + 1;
  lastp1 = (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
  tail1_ceil = tailp1 / (1 - lastp1);
  lastp1 *= tailp1;
  tailp1 += lastp1;

  if (tail1_ceil + tail2_ceil < exit_thresh) {
    return 1;
  }
  if (obs_hets >= 4) {
    exit_threshx = exit_thresh - tailp2;
    do {
      curr_hets_t1 -= 2;
      curr_homr_t1 += 1;
      curr_homc_t1 += 1;
      lastp1 *= (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
      preaddp = tailp1;
      tailp1 += lastp1;
      if (tailp1 > exit_threshx) {
        return 0;
      }
      if (tailp1 <= preaddp) {
        break;
      }
    } while (curr_hets_t1 > 3.5);
  }
  if (tailp1 + tail2_ceil < exit_thresh) {
    return 1;
  }
  exit_threshx = exit_thresh - tailp1;
  while (curr_homr_t2 > 0.5) {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    preaddp = tailp2;
    tailp2 += lastp2;
    if (tailp2 >= exit_threshx) {
      return 0;
    }
    if (tailp2 <= preaddp) {
      return 1;
    }
  }
  return 1;
}

uint32_t HweThreshMidp(int32_t obs_hets, int32_t obs_hom1, int32_t obs_hom2, double thresh) {
  // Mid-p version of HweThresh().  (There are enough fiddly differences that I
  // think it's better for this to be a separate function.)  Assumes threshold
  // is smaller than 0.5.
  intptr_t obs_homc;
  intptr_t obs_homr;
  if (obs_hom1 < obs_hom2) {
    obs_homc = obs_hom2;
    obs_homr = obs_hom1;
  } else {
    obs_homc = obs_hom1;
    obs_homr = obs_hom2;
  }
  int64_t rare_copies = 2LL * obs_homr + obs_hets;
  int64_t genotypes2 = (obs_hets + obs_homc + obs_homr) * 2LL;
  double curr_hets_t2 = obs_hets;  // tail 2
  double curr_homr_t2 = obs_homr;
  double curr_homc_t2 = obs_homc;
  double tailp1 = (1 - kSmallEpsilon) * kExactTestBias * 0.5;
  double centerp = tailp1;
  double lastp2 = (1 - kSmallEpsilon) * kExactTestBias;
  double tailp2 = 0;
  double tail1_ceil;
  double tail2_ceil;
  double lastp1;
  double curr_hets_t1;
  double curr_homr_t1;
  double curr_homc_t1;
  double exit_thresh;
  double exit_threshx;
  double ratio;
  double preaddp;
  if (!genotypes2) {
    return 0;
  }
  thresh = (1 - thresh) / thresh;
  if (obs_hets * genotypes2 > rare_copies * (genotypes2 - rare_copies)) {
    if (obs_hets < 2) {
      return 0;
    }
    exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
    do {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      curr_hets_t2 -= 2;
      if (lastp2 < kExactTestBias) {
        if (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias) {
          // tie with original contingency table, apply mid-p correction here
          // too
          tailp2 = tailp1;
          centerp += tailp1;
        } else {
          tailp2 = lastp2;
        }
        break;
      }
      centerp += lastp2;
      if (centerp > exit_thresh) {
        return 1;
      }
    } while (curr_hets_t2 > 1.5);
    exit_thresh = centerp / thresh;
    if (tailp1 + tailp2 >= exit_thresh) {
      return 0;
    }
    ratio = (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * (curr_homr_t2 + 1) * (curr_homc_t2 + 1));
    // this needs to work in both the tie and no-tie cases
    tail2_ceil = tailp2 + lastp2 * ratio / (1 - ratio);
    curr_hets_t1 = obs_hets + 2;
    curr_homr_t1 = obs_homr;
    curr_homc_t1 = obs_homc;
    lastp1 = (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
    // always a tie here
    tail1_ceil = tailp1 * 2 / (1 - lastp1) - tailp1;
    if (tail1_ceil + tail2_ceil < exit_thresh) {
      return 1;
    }
    lastp1 *= tailp1 * 2;
    tailp1 += lastp1;

    if (obs_homr > 1) {
      exit_threshx = exit_thresh - tailp2;
      do {
        curr_hets_t1 += 2;
        curr_homr_t1 -= 1;
        curr_homc_t1 -= 1;
        lastp1 *= (4 * curr_homr_t1 * curr_homc_t1) / (curr_hets_t1 * (curr_hets_t1 - 1));
        preaddp = tailp1;
        tailp1 += lastp1;
        if (tailp1 > exit_threshx) {
          return 0;
        }
        if (tailp1 <= preaddp) {
          break;
        }
      } while (curr_homr_t1 > 1.5);
    }
    if (tailp1 + tail2_ceil < exit_thresh) {
      return 1;
    }
    exit_threshx = exit_thresh - tailp1;
    while (curr_hets_t2 > 1) {
      curr_homr_t2 += 1;
      curr_homc_t2 += 1;
      lastp2 *= (curr_hets_t2 * (curr_hets_t2 - 1)) / (4 * curr_homr_t2 * curr_homc_t2);
      preaddp = tailp2;
      tailp2 += lastp2;
      if (tailp2 >= exit_threshx) {
        return 0;
      }
      if (tailp2 <= preaddp) {
        return 1;
      }
      curr_hets_t2 -= 2;
    }
    return 1;
  }
  if (!obs_homr) {
    return 0;
  }
  exit_thresh = (1 + (rare_copies / 2)) * (thresh * kExactTestBias);
  do {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    if (lastp2 < kExactTestBias) {
      if (lastp2 > (1 - 2 * kSmallEpsilon) * kExactTestBias) {
        tailp2 = tailp1;
        centerp += tailp1;
      } else {
        tailp2 = lastp2;
      }
      break;
    }
    centerp += lastp2;
    if (centerp > exit_thresh) {
      return 1;
    }
  } while (curr_homr_t2 > 0.5);
  exit_thresh = centerp / thresh;
  if (tailp1 + tailp2 >= exit_thresh) {
    return 0;
  }
  ratio = (4 * curr_homr_t2 * curr_homc_t2) / ((curr_hets_t2 + 2) * (curr_hets_t2 + 1));
  tail2_ceil = tailp2 + lastp2 * ratio / (1 - ratio);
  curr_hets_t1 = obs_hets;
  curr_homr_t1 = obs_homr + 1;
  curr_homc_t1 = obs_homc + 1;
  lastp1 = (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
  tail1_ceil = 2 * tailp1 / (1 - lastp1) - tailp1;
  lastp1 *= 2 * tailp1;
  tailp1 += lastp1;

  if (tail1_ceil + tail2_ceil < exit_thresh) {
    return 1;
  }
  if (obs_hets >= 4) {
    exit_threshx = exit_thresh - tailp2;
    do {
      curr_hets_t1 -= 2;
      curr_homr_t1 += 1;
      curr_homc_t1 += 1;
      lastp1 *= (curr_hets_t1 * (curr_hets_t1 - 1)) / (4 * curr_homr_t1 * curr_homc_t1);
      preaddp = tailp1;
      tailp1 += lastp1;
      if (tailp1 > exit_threshx) {
        return 0;
      }
      if (tailp1 <= preaddp) {
        break;
      }
    } while (curr_hets_t1 > 3.5);
  }
  if (tailp1 + tail2_ceil < exit_thresh) {
    return 1;
  }
  exit_threshx = exit_thresh - tailp1;
  while (curr_homr_t2 > 0.5) {
    curr_hets_t2 += 2;
    lastp2 *= (4 * curr_homr_t2 * curr_homc_t2) / (curr_hets_t2 * (curr_hets_t2 - 1));
    curr_homr_t2 -= 1;
    curr_homc_t2 -= 1;
    preaddp = tailp2;
    tailp2 += lastp2;
    if (tailp2 >= exit_threshx) {
      return 0;
    }
    if (tailp2 <= preaddp) {
      return 1;
    }
  }
  return 1;
}

// 2^{-40} for now, since 2^{-44} was too small on real data
static const double kExactTestEpsilon2 = 0.0000000000009094947017729282379150390625;

double FisherExact2x2P(uint32_t m11, uint32_t m12, uint32_t m21, uint32_t m22, uint32_t midp) {
  // Basic 2x2 Fisher exact test p-value calculation.
  double tprob = (1 - kExactTestEpsilon2) * kExactTestBias;
  double cur_prob = tprob;
  double cprob = 0;
  int32_t tie_ct = 1;
  uint32_t uii;
  double cur11;
  double cur12;
  double cur21;
  double cur22;
  double preaddp;
  // Ensure we are left of the distribution center, m11 <= m22, and m12 <= m21.
  if (m12 > m21) {
    uii = m12;
    m12 = m21;
    m21 = uii;
  }
  if (m11 > m22) {
    uii = m11;
    m11 = m22;
    m22 = uii;
  }
  if ((S_CAST(uint64_t, m11) * m22) > (S_CAST(uint64_t, m12) * m21)) {
    uii = m11;
    m11 = m12;
    m12 = uii;
    uii = m21;
    m21 = m22;
    m22 = uii;
  }
  cur11 = m11;
  cur12 = m12;
  cur21 = m21;
  cur22 = m22;
  while (cur12 > 0.5) {
    cur11 += 1;
    cur22 += 1;
    cur_prob *= (cur12 * cur21) / (cur11 * cur22);
    cur12 -= 1;
    cur21 -= 1;
    if (cur_prob > DBL_MAX) {
      return 0;
    }
    if (cur_prob < kExactTestBias) {
      if (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias) {
        tie_ct++;
      }
      tprob += cur_prob;
      break;
    }
    cprob += cur_prob;
  }
  if ((cprob == 0) && (!midp)) {
    return 1;
  }
  while (cur12 > 0.5) {
    cur11 += 1;
    cur22 += 1;
    cur_prob *= (cur12 * cur21) / (cur11 * cur22);
    cur12 -= 1;
    cur21 -= 1;
    preaddp = tprob;
    tprob += cur_prob;
    if (tprob <= preaddp) {
      break;
    }
  }
  if (m11) {
    cur11 = m11;
    cur12 = m12;
    cur21 = m21;
    cur22 = m22;
    cur_prob = (1 - kExactTestEpsilon2) * kExactTestBias;
    do {
      cur12 += 1;
      cur21 += 1;
      cur_prob *= (cur11 * cur22) / (cur12 * cur21);
      cur11 -= 1;
      cur22 -= 1;
      preaddp = tprob;
      tprob += cur_prob;
      if (tprob <= preaddp) {
        if (!midp) {
          return preaddp / (cprob + preaddp);
        }
        return (preaddp - ((1 - kExactTestEpsilon2) * kExactTestBias * 0.5) * tie_ct) / (cprob + preaddp);
      }
    } while (cur11 > 0.5);
  }
  if (!midp) {
    return tprob / (cprob + tprob);
  }
  return (tprob - ((1 - kExactTestEpsilon2) * kExactTestBias * 0.5) * tie_ct) / (cprob + tprob);
}

int32_t HweXchrPTailsum(uint32_t high_het_side, double* base_probp, double* saved_hetsp, double* saved_hom1p, double* saved_hom2p, uint32_t* tie_ctp, double *totalp) {
  // similar to fisher23_tailsum()
  double total = 0;
  double cur_prob = *base_probp;
  double tmp_hets = *saved_hetsp;
  double tmp_hom1 = *saved_hom1p;
  double tmp_hom2 = *saved_hom2p;
  double tmps_hets;
  double tmps_hom1;
  double tmps_hom2;
  // identify beginning of tail
  if (high_het_side) {
    if (cur_prob > kExactTestBias) {
      double prev_prob = tmp_hom1 * tmp_hom2;
      while (prev_prob > 0.5) {
        tmp_hets += 2;
        cur_prob *= (4 * prev_prob) / (tmp_hets * (tmp_hets - 1));
        tmp_hom1 -= 1;
        tmp_hom2 -= 1;
        if (cur_prob <= kExactTestBias) {
          break;
        }
        prev_prob = tmp_hom1 * tmp_hom2;
      }
      *base_probp = cur_prob;
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
    } else {
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
      while (1) {
        const double prev_prob = cur_prob;
        tmp_hom1 += 1;
        tmp_hom2 += 1;
        cur_prob *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_hom1 * tmp_hom2);
        if (cur_prob < prev_prob) {
          // this should never happen, but better to play it safe re: rounding
          // error
          return 1;
        }
        tmp_hets -= 2;
        if (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias) {
          // throw in extra (1 - kSmallEpsilon) multiplier to prevent rounding
          // errors from causing this to keep going when the left-side test
          // stopped
          if (cur_prob > (1 - kSmallEpsilon) * kExactTestBias) {
            break;
          }
          *tie_ctp += 1;
        }
        total += cur_prob;
      }
      const double prev_prob = cur_prob;
      cur_prob = *base_probp;
      *base_probp = prev_prob;
    }
  } else {
    if (cur_prob > kExactTestBias) {
      while (tmp_hets > 1.5) {
        tmp_hom1 += 1;
        tmp_hom2 += 1;
        cur_prob *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_hom1 * tmp_hom2);
        tmp_hets -= 2;
        if (cur_prob <= kExactTestBias) {
          break;
        }
      }
      *base_probp = cur_prob;
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
    } else {
      tmps_hets = tmp_hets;
      tmps_hom1 = tmp_hom1;
      tmps_hom2 = tmp_hom2;
      while (1) {
        const double prev_prob = cur_prob;
        tmp_hets += 2;
        cur_prob *= (4 * tmp_hom1 * tmp_hom2) / (tmp_hets * (tmp_hets - 1));
        if (cur_prob < prev_prob) {
          return 1;
        }
        tmp_hom1 -= 1;
        tmp_hom2 -= 1;
        if (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias) {
          if (cur_prob > kExactTestBias) {
            break;
          }
          *tie_ctp += 1;
        }
        total += cur_prob;
      }
      const double prev_prob = cur_prob;
      cur_prob = *base_probp;
      *base_probp = prev_prob;
    }
  }
  *saved_hetsp = tmp_hets;
  *saved_hom1p = tmp_hom1;
  *saved_hom2p = tmp_hom2;
  if (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias) {
    if (cur_prob > kExactTestBias) {
      // even most extreme table on this side is too probable
      *totalp = 0;
      return 0;
    }
    *tie_ctp += 1;
  }
  // sum tail to floating point precision limit
  if (high_het_side) {
    while (1) {
      const double prev_tot = total;
      total += cur_prob;
      if (total <= prev_tot) {
        break;
      }
      tmps_hets += 2;
      cur_prob *= (4 * tmps_hom1 * tmps_hom2) / (tmps_hets * (tmps_hets - 1));
      tmps_hom1 -= 1;
      tmps_hom2 -= 1;
    }
  } else {
    while (1) {
      const double prev_tot = total;
      total += cur_prob;
      if (total <= prev_tot) {
        break;
      }
      tmps_hom1 += 1;
      tmps_hom2 += 1;
      cur_prob *= (tmps_hets * (tmps_hets - 1)) / (4 * tmps_hom1 * tmps_hom2);
      tmps_hets -= 2;
    }
  }
  *totalp = total;
  return 0;
}

// returns 1 if we should just return p=0
// could make tailp in/out and initialize cur_prob to it
uint32_t HweFirstRow(double hetab, double homa, double homb, double* tailp_ptr, double* centerp_ptr, uint32_t* tie_ct_ptr, double* orig_base_probl_ptr, double* orig_base_probr_ptr, double* orig_saved_lhets_ptr, double* orig_saved_lhoma_ptr, double* orig_saved_lhomb_ptr, double* orig_saved_rhets_ptr, double* orig_saved_rhoma_ptr, double* orig_saved_rhomb_ptr) {
  // todo: test how reliable this epsilon is.
  double cur_prob = (1 - kExactTestEpsilon2) * kExactTestBias;
  double tailp = cur_prob;
  double centerp = 0;
  uint32_t tie_ct = 1;
  double tmp_hets = hetab;
  if (hetab * hetab > 4 * homa * homb) {
    // Incrementing hetab decreases likelihood from this point on.

    // Start by scanning leftwards.
    *orig_base_probr_ptr = cur_prob;
    *orig_saved_rhets_ptr = hetab;
    *orig_saved_rhoma_ptr = homa;
    *orig_saved_rhomb_ptr = homb;
    double tmp_homa = homa;
    double tmp_homb = homb;
    while (tmp_hets > 1.5) {
      tmp_homa += 1;
      tmp_homb += 1;
      cur_prob *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_homa * tmp_homb);
      tmp_hets -= 2;
      if (cur_prob < kExactTestBias) {
        tie_ct += (cur_prob > (1 - 2 * kExactTestEpsilon2) * kExactTestBias);
        tailp += cur_prob;
        break;
      }
      centerp += cur_prob;
      if (centerp > DBL_MAX) {
        return 1;
      }
    }
    // Found the other end of the center region; save it as the left starting
    // point for the next row.  Meanwhile, continue scanning leftwards until
    // the end, or precision limit.
    *orig_saved_lhets_ptr = tmp_hets;
    *orig_saved_lhoma_ptr = tmp_homa;
    *orig_saved_lhomb_ptr = tmp_homb;
    *orig_base_probl_ptr = cur_prob;
    while (tmp_hets > 1.5) {
      tmp_homa += 1;
      tmp_homb += 1;
      cur_prob *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_homa * tmp_homb);
      tmp_hets -= 2;
      const double preaddp = tailp;
      tailp += cur_prob;
      if (tailp <= preaddp) {
        break;
      }
    }
    // Done with left side; now scan rightwards to end/precision limit.
    tmp_hets = hetab;
    tmp_homa = homa;
    tmp_homb = homb;
    cur_prob = *orig_base_probr_ptr;
    // no need for loop condition here, since cur_prob is always zero when
    // tmp_homa or tmp_homb reach zero.
    while (1) {
      tmp_hets += 2;
      cur_prob *= (4 * tmp_homa * tmp_homb) / (tmp_hets * (tmp_hets - 1));
      const double preaddp = tailp;
      tailp += cur_prob;
      if (tailp <= preaddp) {
        break;
      }
      tmp_homa -= 1;
      tmp_homb -= 1;
    }
  } else {
    // Decrementing hetab decreases likelihood from this point on.
    *orig_base_probl_ptr = cur_prob;
    *orig_saved_lhets_ptr = hetab;
    *orig_saved_lhoma_ptr = homa;
    *orig_saved_lhomb_ptr = homb;

    // Start by scanning rightwards.
    double tmp_homa = homa;
    double tmp_homb = homb;
    double quarter_numer;
    while (1) {
      quarter_numer = tmp_homa * tmp_homb;
      if (quarter_numer <= 0.5) {
        break;
      }
      tmp_hets += 2;
      cur_prob *= (4 * quarter_numer) / (tmp_hets * (tmp_hets - 1));
      tmp_homa -= 1;
      tmp_homb -= 1;
      if (cur_prob < kExactTestBias) {
        tie_ct += (cur_prob > (1 - (2 * kExactTestEpsilon2) * kExactTestBias));
        tailp += cur_prob;
        quarter_numer = tmp_homa * tmp_homb;
        break;
      }
      centerp += cur_prob;
      if (centerp > DBL_MAX) {
        return 1;
      }
    }
    *orig_saved_rhets_ptr = tmp_hets;
    *orig_saved_rhoma_ptr = tmp_homa;
    *orig_saved_rhomb_ptr = tmp_homb;
    *orig_base_probr_ptr = cur_prob;
    while (quarter_numer > 0.5) {
      tmp_hets += 2;
      cur_prob *= (4 * quarter_numer) / (tmp_hets * (tmp_hets - 1));
      tmp_homa -= 1;
      tmp_homb -= 1;
      const double preaddp = tailp;
      tailp += cur_prob;
      if (tailp <= preaddp) {
        break;
      }
      quarter_numer = tmp_homa * tmp_homb;
    }
    tmp_hets = hetab;
    tmp_homa = homa;
    tmp_homb = homb;
    cur_prob = *orig_base_probl_ptr;
    while (tmp_hets > 1.5) {
      tmp_homa += 1;
      tmp_homb += 1;
      cur_prob *= (tmp_hets * (tmp_hets - 1)) / (4 * tmp_homa * tmp_homb);
      const double preaddp = tailp;
      tailp += cur_prob;
      if (tailp <= preaddp) {
        break;
      }
      tmp_hets -= 2;
    }
  }
  *tailp_ptr = tailp;
  *centerp_ptr = centerp;
  *tie_ct_ptr = tie_ct;
  return 0;
}

double HweXchrP(int32_t female_hets, int32_t female_hom1, int32_t female_hom2, int32_t male1, int32_t male2, uint32_t midp) {
  // See Graffelman J, Weir BS (2016) Testing for Hardy-Weinberg equilibrium at
  // biallelic genetic markers on the X chromosome.
  // Evaluation strategy is similar to fisher23().
  if ((!male1) && (!male2)) {
    return HweP(female_hets, female_hom1, female_hom2, midp);
  }
  // 1. Determine relative tail vs. center masses for the male1/male2-unchanged
  //    slice.
  double cur_female_hetd = S_CAST(double, female_hets);
  double cur_female_hom1d = S_CAST(double, female_hom1);
  double cur_female_hom2d = S_CAST(double, female_hom2);
  double n1 = cur_female_hetd + 2 * cur_female_hom1d;
  double n2 = cur_female_hetd + 2 * cur_female_hom2d;
  double tailp;
  double centerp;
  uint32_t tie_ct;
  // "left" = low hets side, "right" = high hets side
  double orig_base_probl;
  double orig_base_probr;
  double orig_saved_lhets;
  double orig_saved_lhom1;
  double orig_saved_lhom2;
  double orig_saved_rhets;
  double orig_saved_rhom1;
  double orig_saved_rhom2;
  if (HweFirstRow(cur_female_hetd, cur_female_hom1d, cur_female_hom2d, &tailp, &centerp, &tie_ct, &orig_base_probl, &orig_base_probr, &orig_saved_lhets, &orig_saved_lhom1, &orig_saved_lhom2, &orig_saved_rhets, &orig_saved_rhom1, &orig_saved_rhom2)) {
    return 0.0;
  }
  // a "row" holds male1/male2 constant.
  const double orig_row_prob = tailp + centerp;
  n1 += male1;
  n2 += male2;
  for (uint32_t male1_decreasing = 0; male1_decreasing != 2; ++male1_decreasing) {
    double cur_male1 = male1;
    double cur_male2 = male2;
    double row_prob = orig_row_prob;
    double cur_lhets = orig_saved_lhets;
    double cur_lhom1 = orig_saved_lhom1;
    double cur_lhom2 = orig_saved_lhom2;
    double cur_rhets = orig_saved_rhets;
    double cur_rhom1 = orig_saved_rhom1;
    double cur_rhom2 = orig_saved_rhom2;
    double base_probl = orig_base_probl;
    double base_probr = orig_base_probr;
    uint32_t iter_ct;
    if (male1_decreasing) {
      iter_ct = 2 * female_hom2 + female_hets;
      if (iter_ct > S_CAST(uint32_t, male1)) {
        iter_ct = male1;
      }
    } else {
      iter_ct = 2 * female_hom1 + female_hets;
      if (iter_ct > S_CAST(uint32_t, male2)) {
        iter_ct = male2;
      }
    }
    for (uint32_t iter_idx = 0; iter_idx != iter_ct; ++iter_idx) {
      if (male1_decreasing) {
        const double old_male1 = cur_male1;
        const double old_female2 = n2 - cur_male2;
        cur_male2 += 1;
        cur_male1 -= 1;
        // row likelihood is ((n1 choose male1) * (n2 choose male2)) /
        //   ((n1 + n2) choose (male1 + male2))
        row_prob *= (old_male1 * old_female2) / (cur_male2 * (n1 - cur_male1));
        // bugfix (19 Apr 2017): We cannot move to the right of the mode here.
        // Otherwise, if the mode itself is more probable than our initial
        // table, but the table to the immediate right of the mode is not,
        // we'll fail to count the mode.
        // ("right" = high het count, "left" = low het count.)
        if (cur_lhets != 0.0) {
          cur_lhom1 += 1;
          base_probl *= (old_male1 * cur_lhets) / (2 * cur_male2 * cur_lhom1);
          cur_lhets -= 1;
        } else {
          cur_lhets += 1;
          base_probl *= (2 * old_male1 * cur_lhom2) / (cur_male2 * cur_lhets);
          cur_lhom2 -= 1;
        }
      } else {
        const double old_male2 = cur_male2;
        const double old_female1 = n1 - cur_male1;
        cur_male1 += 1;
        cur_male2 -= 1;
        row_prob *= (old_male2 * old_female1) / (cur_male1 * (n2 - cur_male2));
        if (cur_lhets != 0.0) {
          cur_lhom2 += 1;
          base_probl *= (old_male2 * cur_lhets) / (2 * cur_male1 * cur_lhom2);
          cur_lhets -= 1;
        } else {
          cur_lhets += 1;
          base_probl *= (2 * old_male2 * cur_lhom1) / (cur_male1 * cur_lhets);
          cur_lhom1 -= 1;
        }
      }
      double tail_incr1;
      if (HweXchrPTailsum(0, &base_probl, &cur_lhets, &cur_lhom1, &cur_lhom2, &tie_ct, &tail_incr1)) {
        // all tables in this row, and all subsequent rows, are less probable
        // than the initial table.
        double cur_female1 = n1 - cur_male1;
        double cur_female2 = n2 - cur_male2;
        if (male1_decreasing) {
          while (1) {
            const double preaddp = tailp;
            tailp += row_prob;
            if (tailp == preaddp) {
              break;
            }
            cur_male2 += 1;
            cur_female1 += 1;
            row_prob *= (cur_male1 * cur_female2) / (cur_male2 * cur_female1);
            cur_male1 -= 1;
            cur_female2 -= 1;
          }
        } else {
          while (1) {
            const double preaddp = tailp;
            tailp += row_prob;
            if (tailp == preaddp) {
              break;
            }
            cur_male1 += 1;
            cur_female2 += 1;
            row_prob *= (cur_male2 * cur_female1) / (cur_male1 * cur_female2);
            cur_male2 -= 1;
            cur_female1 -= 1;
          }
        }
        break;
      }
      tailp += tail_incr1;
      if (male1_decreasing) {
        const double old_male1 = cur_male1 + 1;
        if (cur_rhom2 != 0.0) {
          cur_rhets += 1;
          base_probr *= (2 * old_male1 * cur_rhom2) / (cur_male2 * cur_rhets);
          cur_rhom2 -= 1;
        } else {
          cur_rhom1 += 1;
          base_probr *= (old_male1 * cur_rhets) / (2 * cur_male2 * cur_rhom1);
          cur_rhets -= 1;
        }
      } else {
        const double old_male2 = cur_male2 + 1;
        if (cur_rhom1 != 0.0) {
          cur_rhets += 1;
          base_probr *= (2 * old_male2 * cur_rhom1) / (cur_male1 * cur_rhets);
          cur_rhom1 -= 1;
        } else {
          cur_rhom2 += 1;
          base_probr *= (old_male2 * cur_rhets) / (2 * cur_male1 * cur_rhom2);
          cur_rhets -= 1;
        }
      }
      double tail_incr2 = 0.0;  // maybe-uninitialized warning
      HweXchrPTailsum(1, &base_probr, &cur_rhets, &cur_rhom1, &cur_rhom2, &tie_ct, &tail_incr2);
      tailp += tail_incr2;
      centerp += row_prob - tail_incr1 - tail_incr2;
      if (centerp > DBL_MAX) {
        return 0;
      }
    }
  }
  if (!midp) {
    return tailp / (tailp + centerp);
  }
  return (tailp - ((1 - kExactTestEpsilon2) * kExactTestBias * 0.5) * S_CAST(int32_t, tie_ct)) / (tailp + centerp);
}

#ifdef __cplusplus
}
#endif
