/*
    Copyright (C) 2018 Fredrik Johansson

    This file is part of Arb.

    Arb is free software: you can redistribute it and/or modify it under
    the terms of the GNU Lesser General Public License (LGPL) as published
    by the Free Software Foundation; either version 2.1 of the License, or
    (at your option) any later version.  See <http://www.gnu.org/licenses/>.
*/

#include <math.h>
#include "flint/double_extras.h"
#include "acb_dirichlet.h"
#include "acb_calc.h"

/* Bound the quadratic Taylor error term. */
static void
stieltjes_bound_quadratic_term(arb_t B, const acb_t z,
        const fmpz_t n1, const acb_t alpha, slong prec)
{
    acb_t t, log_t;
    mag_t b, c, d;

    acb_init(t);
    acb_init(log_t);
    mag_init(b);
    mag_init(c);
    mag_init(d);

    /* g''(z) = (n+1)(1+1/log(t)) / (t^2 log(t)) */
    /* t = alpha + iz, log_t = log(t) */
    acb_mul_onei(t, z);
    acb_add(t, t, alpha, prec);
    acb_log(log_t, t, prec);

    acb_get_mag_lower(b, t);
    acb_get_mag_lower(c, log_t);

    /* d = 1+1/log(t) */
    mag_inv(d, c);
    mag_add_ui(d, d, 1);

    /* divide by t^2 log(t) */
    mag_div(d, d, b);
    mag_div(d, d, b);
    mag_div(d, d, c);

    mag_mul_fmpz(d, d, n1);

    /* For Taylor remainder: 1/2 rad(z)^2 */
    mag_hypot(b, arb_radref(acb_realref(z)), arb_radref(acb_imagref(z)));
    mag_mul(b, b, b);
    mag_mul_2exp_si(b, b, -1);

    mag_mul(d, d, b);

    arf_set_mag(arb_midref(B), d);
    mag_zero(arb_radref(B));

    acb_clear(t);
    acb_clear(log_t);
    mag_clear(b);
    mag_clear(c);
    mag_clear(d);
}

static void
stieltjes_bound_large3(arb_t B, const acb_t x, const fmpz_t n1,
        const acb_t alpha, slong prec)
{
    acb_t m, t, log_t, u, v;
    acb_t g0, g1, g2;
    mag_t C, D;

    acb_init(m);
    acb_init(t);
    acb_init(log_t);
    acb_init(u);
    acb_init(v);

    acb_init(g0);
    acb_init(g1);
    acb_init(g2);

    mag_init(C);
    mag_init(D);

    /* m = mid(x) */
    acb_set(m, x);
    mag_zero(arb_radref(acb_realref(m)));
    mag_zero(arb_radref(acb_imagref(m)));

    /* t = alpha + im */
    acb_mul_onei(t, m);
    acb_add(t, t, alpha, prec);

    /* log_t = log(alpha+im) */
    acb_log(log_t, t, prec);

    /* u = x - m */
    acb_sub(u, x, m, prec);
    acb_get_mag(D, u);

    /* g0 = g(m) = (n+1) log(log(alpha+im)) - 2 pi m */
    acb_log(g0, log_t, prec);
    acb_mul_fmpz(g0, g0, n1, prec);
    acb_const_pi(v, prec);
    acb_mul(v, v, m, prec);
    acb_mul_2exp_si(v, v, 1);
    acb_sub(g0, g0, v, prec);

    /* g1 = g'(m) (x-m); g'(m) = i(n+1)/((alpha+im) log(alpha+im)) - 2 pi */
    acb_mul(g1, t, log_t, prec);
    acb_inv(g1, g1, prec);
    acb_mul_fmpz(g1, g1, n1, prec);
    acb_mul_onei(g1, g1);
    acb_const_pi(t, prec);  /* recycle t */
    acb_mul_2exp_si(t, t, 1);
    acb_sub(g1, g1, t, prec);
    acb_mul(g1, g1, u, prec);
    /* absolute value -- optional (does not seem to affect speed much) */
    acb_abs(acb_realref(g1), g1, prec);
    arb_zero(acb_imagref(g1));

    stieltjes_bound_quadratic_term(acb_realref(g2), x, n1, alpha, prec);

    acb_exp(g0, g0, prec);
    acb_exp(g1, g1, prec);
    acb_exp(g2, g2, prec);

    acb_get_mag(C, g0);
    acb_get_mag(D, g1);
    mag_mul(C, C, D);
    acb_get_mag(D, g2);
    mag_mul(C, C, D);
    mag_mul_ui(C, C, 5);

    arb_zero(B);
    arf_set_mag(arb_midref(B), C);

    acb_clear(m);
    acb_clear(t);
    acb_clear(log_t);
    acb_clear(u);
    acb_clear(v);
    mag_clear(C);
    mag_clear(D);

    acb_clear(g0);
    acb_clear(g1);
    acb_clear(g2);
}

static void
stieltjes_bound_large(acb_t res, const acb_t x,
    const fmpz_t n1, const acb_t alpha, slong prec)
{
    arb_t B;
    mag_t t;

    arb_init(B);
    mag_init(t);

    prec = FLINT_MIN(prec, 30 + fmpz_bits(n1));

    stieltjes_bound_large3(B, x, n1, alpha, prec);
    arb_get_mag(t, B);
    acb_zero(res);
    arb_add_error_mag(acb_realref(res), t);
    arb_add_error_mag(acb_imagref(res), t);

    arb_clear(B);
    mag_clear(t);
}

static void
stieltjes_integrand(acb_t res, const acb_t x, const fmpz_t n1,
                            const acb_t alpha, int analytic, slong prec)
{
    acb_t t, u;

    acb_init(t);
    acb_init(u);

    acb_mul_onei(t, x);
    acb_add(t, t, alpha, prec);

    acb_set_ui(u, 5);

    /* check branch cut of logarithm */
    if (arb_contains_zero(acb_imagref(t)) && !arb_is_positive(acb_realref(t)))
    {
        acb_indeterminate(res);
    }
    else if ((analytic || acb_rel_accuracy_bits(t) < prec - 10)
        && arb_gt(acb_realref(x), acb_realref(u)))
    {
        /* note: requires re(x) > 1 */
        stieltjes_bound_large(res, x, n1, alpha, prec);
    }
    else
    {
        acb_const_pi(u, prec);
        acb_mul(u, u, x, prec);
        acb_sech(u, u, prec);

        if (acb_is_finite(u))
        {
            acb_mul(u, u, u, prec);
            acb_log(t, t, prec);
            acb_pow_fmpz(t, t, n1, prec);
            acb_mul(res, t, u, prec);
        }
        else
        {
            acb_indeterminate(res);
        }
    }

    acb_clear(t);
    acb_clear(u);
}

typedef struct
{
    const fmpz * n1;
    const acb_struct * alpha;
}
_stieltjes_param;

/* Compute the approximate saddle point. */
static void
stieltjes_omega(acb_t omega, const fmpz_t n1, const acb_t alpha, slong prec)
{
    acb_t t, u;
    fmpz_t k;

    acb_init(t);
    acb_init(u);
    fmpz_init(k);

    /* u = (n+1)i / (2pi) */
    arb_set_fmpz(acb_imagref(t), n1);
    acb_const_pi(u, prec);
    acb_mul_2exp_si(u, u, 1);
    acb_div(u, t, u, prec);

    /* u / W(u) */
    acb_lambertw(t, u, k, 0, prec);
    acb_div(t, u, t, prec);

    acb_sub(t, alpha, t, prec);
    acb_mul_onei(t, t);

    acb_set(omega, t);

    acb_clear(t);
    acb_clear(u);
    fmpz_clear(k);
}

/* Compute an approximation of the magnitude of gamma_n. */
static void
stieltjes_mag_approx(arb_t C, mag_t tol, const fmpz_t n1, const acb_t alpha)
{
    slong prec;
    acb_t w, v, q;

    prec = 32 + 2 * fmpz_bits(n1);

    acb_init(w);
    acb_init(v);
    acb_init(q);

    stieltjes_omega(w, n1, alpha, prec);
    stieltjes_integrand(v, w, n1, alpha, 0, prec);

    acb_set_fmpz(q, n1);
    acb_sqrt(q, q, prec);
    acb_mul(v, v, q, prec);

    acb_get_mag(tol, v);

    arb_set(C, acb_imagref(w));
    mag_zero(arb_radref(C));

    acb_clear(w);
    acb_clear(v);
    acb_clear(q);
}

int
_f_stieltjes(acb_ptr res, const acb_t x, void * param, slong order, slong prec)
{
    const fmpz * n1;
    const acb_struct * alpha;

    if (order > 1)
        flint_abort();  /* Would be needed for Taylor method. */

    n1 = ((const _stieltjes_param *) param)->n1;
    alpha = ((const _stieltjes_param *) param)->alpha;

    stieltjes_integrand(res, x, n1, alpha, order != 0, prec);

    return 0;
}

/* Estimate log2(|gamma_n|), for fast cancellation estimate when n is small. */
static double
stieltjes_mag(double n)
{
    double u, v, A, B;
    double va, vb, t;
    double pi = 3.141592653589793;
    int i;

    if (n <= 1.0)
        return 0.0;

    va = 1e-6;
    vb = 0.5 * pi - 1e-6;

    for (i = 0; i < 53; i++)  /* bisection */
    {
        v = (va + vb) * 0.5;
        t = 2 * pi * exp(v * tan(v)) - n * cos(v) / v;
        if (t < 0.0)
            va = v;
        else
            vb = v;
    }

    v = va;
    u = v * tan(v);
    A = 0.5 * log(u * u + v * v) - u / (u * u + v * v);
    B = 2 * sqrt(2 * pi) * sqrt(u * u + v * v) * pow((u + 1) * (u + 1) + v * v, -0.25);
    t = (log(B) + n * A - 0.5 * log(n)) / log(2);
    return t;
}

static double _d_approx_hypot(double x, double y) { return sqrt(x * x + y * y); }

/* log2 magnitude of integrand at z = x+yi; alpha = a+bi */
static double
integrand_mag(double n, double x, double y, double a, double b)
{
    double t, u;
    t = log(_d_approx_hypot(a - y, b + x));
    u = atan2(b + x, a - y);
    t = log(_d_approx_hypot(t,u)) * (n+1) - 2.0 * 3.1415926535897932 * x;
    return t * 1.44269504088896341;
}

static double
find_x_maximizing_mag(double n, double y)
{
    double xa, xb, xma, xmb, ma, mb;
    int i;

    xa = 1.0;
    xb = n;

    for (i = 0; i < 80; i++)
    {
        xma = xa + (xb - xa) / 3;
        xmb = xb - (xb - xa) / 3;

        ma = integrand_mag(n, xma, y, 0.5, 0.0);
        mb = integrand_mag(n, xmb, y, 0.5, 0.0);

        if (ma < mb)
            xa = xma;
        else
            xb = xmb;
    }

    return xa;
}

static void
stieltjes_choose_N(arb_t N, const fmpz_t n1, const acb_t alpha, slong prec)
{
    if (fmpz_bits(n1) < 30)
    {
        double nn, NN, aa, bb;

        nn = fmpz_get_d(n1) - 1.0;
        NN = FLINT_MAX(nn, 4);

        aa = arf_get_d(arb_midref(acb_realref(alpha)), ARF_RND_DOWN);
        bb = arf_get_d(arb_midref(acb_imagref(alpha)), ARF_RND_DOWN);

        while (integrand_mag(nn, NN, 0.0, aa, bb) > -prec - 20)
        {
            NN *= 2.0;

            if (NN > 1e30)
                break;
        }

        arb_set_d(N, NN);
    }
    else
    {
        /* todo: account for huge alpha? */
        arb_set_fmpz(N, n1);
    }
}

static void
stieltjes_tail_bound(mag_t bound, const arb_t N, const fmpz_t n1, const acb_t alpha)
{
    slong prec;
    arb_t x, y, D;
    acb_t aNi, logaNi;
    mag_t t, u;

    prec = MAG_BITS + fmpz_bits(n1);

    arb_init(x);
    arb_init(y);
    arb_init(D);
    acb_init(aNi);
    acb_init(logaNi);
    mag_init(t);
    mag_init(u);

    /* alpha + Ni */
    acb_set(aNi, alpha);
    arb_add(acb_imagref(aNi), acb_imagref(aNi), N, prec);

    /* log(alpha + Ni) */
    acb_log(logaNi, aNi, prec);

    /* check (n+1)/(|alpha+Ni| log(alpha + Ni)|) < 2 */
    acb_get_mag_lower(t, aNi);
    acb_get_mag_lower(u, logaNi);
    mag_mul_lower(t, t, u);
    mag_inv(t, t);
    mag_mul_fmpz(t, t, n1);

    /* also check N >= |im(alpha)| + 2 */
    arb_abs(x, acb_imagref(alpha));
    arb_add_ui(x, x, 2, prec);

    if (mag_cmp_2exp_si(t, 1) >= 0 || !arb_ge(N, x))
    {
        mag_inf(bound);
    }
    else
    {
        /* exp(-2 pi N) */
        arb_set(x, N);
        arb_mul_2exp_si(x, x, 1);
        arb_const_pi(y, prec);
        arb_mul(y, y, x, prec);
        arb_neg(y, y);
        arb_exp(y, y, prec);

        /* |log|^(n+1) */
        acb_get_mag(t, logaNi);
        arf_set_mag(arb_midref(x), t);
        mag_zero(arb_radref(x));
        arb_pow_fmpz(x, x, n1, prec);
        arb_mul(x, x, y, prec);

        arb_get_mag(bound, x);
    }

    arb_clear(x);
    arb_clear(y);
    arb_clear(D);
    acb_clear(aNi);
    acb_clear(logaNi);
    mag_clear(t);
    mag_clear(u);
}

void
_acb_dirichlet_stieltjes_integral2(acb_t res, const fmpz_t n, const acb_t alpha, slong prec)
{
    double gamma_mag, max_mag, cancellation, xa;
    fmpz_t n1;
    acb_t a, b, v, w;
    slong wp;
    mag_t tol, bound;
    acb_calc_integrate_opt_t opt;
    _stieltjes_param param;
    /* integration points */
    arb_t M, N, C;

    /* required for the integral representation to be valid */
    if (!arb_is_positive(acb_realref(alpha)))
    {
        acb_indeterminate(res);
        return;
    }

    fmpz_init(n1);
    arb_init(M);
    arb_init(N);
    arb_init(C);
    acb_init(a);
    acb_init(b);
    acb_init(v);
    acb_init(w);
    mag_init(tol);
    mag_init(bound);

    fmpz_add_ui(n1, n, 1);

    param.n1 = n1;
    param.alpha = alpha;

    arb_set_ui(M, 10);

    stieltjes_choose_N(N, n1, alpha, prec);
    stieltjes_tail_bound(bound, N, n1, alpha);

    if (acb_is_real(alpha) &&
        arf_cmpabs_2exp_si(arb_midref(acb_realref(alpha)), 2) < 0 &&
        fmpz_cmp_ui(n1, 5000) < 0)
    {
        double nn = fmpz_get_ui(n1) - 1;
        gamma_mag = stieltjes_mag(nn);
        xa = find_x_maximizing_mag(nn, 0.0);
        max_mag = integrand_mag(nn, xa, 0.0, 0.5, 0.0);
        cancellation = FLINT_MAX(0, max_mag - gamma_mag);

        if (cancellation < 10 + 0.1 * prec)
        {
            arb_zero(C);
            mag_one(tol);
            mag_mul_2exp_si(tol, tol, gamma_mag);
        }
        else
        {
            stieltjes_mag_approx(C, tol, n1, alpha);
            cancellation = 0;
        }
    }
    else
    {
        stieltjes_mag_approx(C, tol, n1, alpha);
        cancellation = 0;
    }

    mag_mul_2exp_si(tol, tol, -prec - 5);

    /* todo: 1 * fmpz_bits(n1) should be enough, but acb powering is
       inaccurate */
    wp = prec + 2 * fmpz_bits(n1) + cancellation + 10;

    acb_calc_integrate_opt_init(opt);

    /* opt->verbose = 1; */
    opt->deg_limit = 100 + 1.2 * prec;   /* Small speedup. */

    if (arb_is_zero(C))
    {
        acb_zero(a);
        acb_set_arb(b, N);
        acb_calc_integrate(w, _f_stieltjes, &param, a, b, wp, tol, opt, wp);
        acb_add(v, v, w, wp);
    }
    else
    {
        acb_zero(a);                 /* a = 0 */
        acb_set_arb(b, M);           /* b = M */
        acb_calc_integrate(w, _f_stieltjes, &param, a, b, wp, tol, opt, wp);
        acb_add(v, v, w, wp);

        acb_set(a, b);
        acb_set_arb(b, M);
        arb_set(acb_imagref(b), C);  /* b = M + i C */
        acb_calc_integrate(w, _f_stieltjes, &param, a, b, wp, tol, opt, wp);
        acb_add(v, v, w, wp);

        acb_set(a, b);
        arb_set(acb_realref(b), N);  /* b = N + i C */
        acb_calc_integrate(w, _f_stieltjes, &param, a, b, wp, tol, opt, wp);
        acb_add(v, v, w, wp);

        acb_set(a, b);
        arb_zero(acb_imagref(b));    /* b = N */
        acb_calc_integrate(w, _f_stieltjes, &param, a, b, wp, tol, opt, wp);
        acb_add(v, v, w, wp);
    }

    acb_add_error_mag(v, bound);

    acb_const_pi(b, wp);
    acb_mul(v, v, b, wp);
    acb_div_fmpz(v, v, n1, wp);
    acb_neg(v, v);

    if (acb_is_real(alpha))
        arb_zero(acb_imagref(v));

    acb_set_round(res, v, prec);

    fmpz_clear(n1);
    acb_clear(a);
    acb_clear(b);
    acb_clear(v);
    acb_clear(w);
    mag_clear(tol);
    mag_clear(bound);
    arb_clear(M);
    arb_clear(N);
    arb_clear(C);
}

void
_acb_dirichlet_stieltjes_integral(acb_t res, const fmpz_t n, const acb_t a, slong prec)
{
    acb_t alpha;
    acb_init(alpha);

    /* alpha = a-1/2 */
    acb_set_d(alpha, 0.5);
    acb_sub(alpha, a, alpha, prec);

    if (acb_is_real(a))
    {
        acb_conj(alpha, alpha);
        _acb_dirichlet_stieltjes_integral2(res, n, alpha, prec);
    }
    else
    {
        acb_t r1, r2;
        acb_init(r1);
        acb_init(r2);

        _acb_dirichlet_stieltjes_integral2(r1, n, alpha, prec);
        acb_conj(alpha, alpha);
        _acb_dirichlet_stieltjes_integral2(r2, n, alpha, prec);
        acb_conj(r2, r2);
        acb_add(res, r1, r2, prec);
        acb_mul_2exp_si(res, res, -1);

        acb_clear(r1);
        acb_clear(r2);
    }

    acb_clear(alpha);
}

void
acb_dirichlet_stieltjes_integral(acb_t res, const fmpz_t n, const acb_t a, slong prec)
{
    /* the algorithm only works for re(a) > 1/2 */
    if (arf_cmp_si(arb_midref(acb_realref(a)), 1) < 0)
    {
        slong k, m, wp;
        acb_t ak, t, s;

        if (arf_cmp_si(arb_midref(acb_realref(a)), -prec) < 0)
        {
            acb_indeterminate(res);
            return;
        }

        m = 1 - arf_get_si(arb_midref(acb_realref(a)), ARF_RND_FLOOR);

        acb_init(ak);
        acb_init(t);
        acb_init(s);

        wp = prec + 2 * fmpz_bits(n);

        for (k = 0; k < m; k++)
        {
            acb_add_si(ak, a, k, wp);
            acb_log(t, ak, wp);
            acb_pow_fmpz(t, t, n, wp);
            acb_div(t, t, ak, wp);
            acb_add(s, s, t, wp);
        }

        acb_add_si(ak, a, m, wp);
        _acb_dirichlet_stieltjes_integral(t, n, ak, prec);
        acb_add(res, s, t, prec);

        acb_clear(s);
        acb_clear(t);
        acb_clear(ak);
    }
    else
    {
        _acb_dirichlet_stieltjes_integral(res, n, a, prec);
    }
}

void
acb_dirichlet_stieltjes_em(acb_t res, const fmpz_t n, const acb_t a, slong prec)
{
    if (fmpz_cmp_ui(n, 10000) > 0)
    {
        acb_indeterminate(res);
    }
    else
    {
        slong nn, wp;
        acb_ptr z;
        acb_t s;

        nn = fmpz_get_ui(n);

        acb_init(s);
        z = _acb_vec_init(nn + 1);

        wp = prec * 1.05 + 2.2*nn + 10;  /* todo: bogus */

        /* todo: we don't want to compute all the coefficients */
        acb_one(s);
        _acb_poly_zeta_cpx_series(z, s, a, 1, nn + 1, wp);

        arb_fac_ui(acb_realref(s), nn, prec + 10);
        acb_mul_arb(res, z + nn, acb_realref(s), prec);

        if (fmpz_is_odd(n))
            acb_neg(res, res);

        acb_clear(s);
        _acb_vec_clear(z, nn + 1);
    }
}

void
acb_dirichlet_stieltjes(acb_t res, const fmpz_t n, const acb_t a, slong prec)
{
    slong cutoff;

    if (acb_is_one(a) && fmpz_is_zero(n))
    {
        arb_const_euler(acb_realref(res), prec);
        arb_zero(acb_imagref(res));
        return;
    }

    if (fmpz_sgn(n) < 0)
    {
        flint_printf("stieltjes constants only defined for n >= 0");
        flint_abort();
    }

    /* undefined at a = 0, -1, -2, ... */
    if (acb_contains_int(a) && !arb_is_positive(acb_realref(a)))
    {
        acb_indeterminate(res);
        return;
    }

    cutoff = FLINT_MAX(100, prec / 2);
    cutoff = FLINT_MIN(cutoff, 10000);

    if (fmpz_cmp_ui(n, cutoff) >= 0)
    {
        acb_dirichlet_stieltjes_integral(res, n, a, prec);
    }
    else
    {
        acb_dirichlet_stieltjes_em(res, n, a, prec);
    }
}

