/*
    Copyright (C) 2017 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 "arb.h"

/* Computes sin(x) or cos(x) using Taylor series truncated at x^N exclusive.
   Computes error bound automatically. Does not allow aliasing of s and x.  */
void
arb_sin_cos_taylor_sum_rs(arb_t s, const arb_t x, slong N, int cosine, slong prec)
{
    mag_t err;
    mag_init(err);

    arb_get_mag(err, x);
    mag_exp_tail(err, err, N);

    if (N == 0 || (!cosine && N == 1))
    {
        arb_zero(s);
    }
    else if (cosine && N <= 2)
    {
        arb_one(s);
    }
    else if (!cosine && N <= 3) /* x */
    {
        arb_set_round(s, x, prec);
    }
    else if (cosine && N <= 4)  /* 1 - x^2/2 */
    {
        arb_mul(s, x, x, prec / 2 + 4);
        arb_mul_2exp_si(s, s, -1);
        arb_sub_ui(s, s, 1, prec);
        arb_neg(s, s);
    }
    else if (!cosine && N <= 5) /* x - x^3/6 */
    {
        arb_mul(s, x, x, prec / 2 + 4);
        arb_div_ui(s, s, 6, prec / 2 + 4);
        arb_mul(s, s, x, prec / 2 + 4);
        arb_sub(s, x, s, prec);
    }
    else
    {
        arb_ptr tpow;
        slong j, k, m, M, tp, xmag;
        mp_limb_t c, d, chi, clo;

        xmag = arf_abs_bound_lt_2exp_si(arb_midref(x));

        /* Convert to order as a series in x^2. */
        if (cosine)
            M = (N + 1) / 2;
        else
            M = N / 2;

        m = n_sqrt(M);

        /* not intended (and not 32-bit safe...) */
        if (M > 30000)
        {
            flint_abort();
        }

        tpow = _arb_vec_init(m + 1);

        arb_mul(s, x, x, prec);
        _arb_vec_set_powers(tpow, s, m + 1, prec);
        arb_zero(s);

        c = 1;
        j = (M - 1) % m;

        for (k = M - 1; k >= 0; k--)
        {
            tp = prec - 2 * k * (-xmag) + 10;
            tp = FLINT_MAX(tp, 2);
            tp = FLINT_MIN(tp, prec);

            if (cosine)
                d = (2 * k - 1) * (2 * k);
            else
                d = (2 * k) * (2 * k + 1);

            if (k != 0)
            {
                umul_ppmm(chi, clo, c, d);

                if (chi != 0)
                {
                    arb_div_ui(s, s, c, tp);
                    c = 1;
                }
            }

            if (k % 2 == 0)
                arb_addmul_ui(s, tpow + j, c, tp);
            else
                arb_submul_ui(s, tpow + j, c, tp);

            if (k != 0)
            {
                c *= d;

                if (j == 0)
                {
                    arb_mul(s, s, tpow + m, tp);
                    j = m - 1;
                }
                else
                {
                    j--;
                }
            }
        }

        arb_div_ui(s, s, c, prec);
        if (!cosine)
            arb_mul(s, s, x, prec);

        _arb_vec_clear(tpow, m + 1);
    }

    arb_add_error_mag(s, err);
    mag_clear(err);
}

void
arb_sin_cos_arf_rs_generic(arb_t res_sin, arb_t res_cos, const arf_t x, slong prec)
{
    slong q, xmag, wp, k, N;
    arb_t s, t;
    int negate;

    if (arf_is_zero(x))
    {
        if (res_sin != NULL)
            arb_zero(res_sin);
        if (res_cos != NULL)
            arb_one(res_cos);
        return;
    }

    xmag = arf_abs_bound_lt_2exp_si(x);

    /* x + O(x^3), 1 + O(x^2) */
    if (xmag < -(prec / 2) - 4)
    {
        arb_init(t);
        arf_set(arb_midref(t), x);
        if (res_sin != NULL)
            arb_sin_cos_taylor_sum_rs(res_sin, t, 3, 0, prec);
        if (res_cos != NULL)
            arb_sin_cos_taylor_sum_rs(res_cos, t, 2, 1, prec);
        arb_clear(t);
        return;
    }

    xmag = FLINT_MAX(xmag, -prec);

    /* could include sanity test, but we assume that the function
       gets called with a valid |x| < pi/2
    if (xmag > 0)
    {
        if (res_sin != NULL)
            arb_indeterminate(res_sin);
        if (res_cos != NULL)
            arb_indeterminate(res_cos);
        return;
    }
     */

    arb_init(s);
    arb_init(t);

    negate = arf_sgn(x) < 0;

    /* generic tuning value */
    q = 4.5 * pow(prec, 0.2);
    q = FLINT_MAX(q, 6);
    /* adjust to magnitude */
    q = FLINT_MAX(0, xmag + q);
    /* don't do a redundant square root */
    if (q <= 2)
        q = 0;

    wp = prec + 10 + 2 * FLINT_BIT_COUNT(prec);

    /* t = x/2^q */
    arf_mul_2exp_si(arb_midref(t), x, -q);

    if (q == 0 && res_sin != NULL)
    {
        /* compute cos from sin since the square root has less cancellation */
        wp += (-xmag);
        N = _arb_exp_taylor_bound(xmag, wp);
        arb_sin_cos_taylor_sum_rs(s, t, N, 0, wp);

        if (res_sin != NULL)
            arb_set_round(res_sin, s, prec);

        if (res_cos != NULL)
        {
            arb_mul(t, s, s, wp);
            arb_sub_ui(t, t, 1, wp);
            arb_neg(t, t);
            arb_sqrt(res_cos, t, prec);
        }
    }
    else
    {
        /* compute sin from cos */
        wp = prec + 10 + 2 * FLINT_BIT_COUNT(prec);
        wp += 2 * (q - xmag);  /* todo: too much when only computing cos? */

        N = _arb_exp_taylor_bound(xmag - q, wp);

        arb_sin_cos_taylor_sum_rs(s, t, N, 1, wp);

        for (k = 0; k < q; k++)
        {
            arb_mul(s, s, s, wp);
            arb_mul_2exp_si(s, s, 1);
            arb_sub_ui(s, s, 1, wp);
        }

        if (res_cos != NULL)
            arb_set_round(res_cos, s, prec);

        if (res_sin != NULL)
        {
            arb_mul(s, s, s, wp);
            arb_sub_ui(s, s, 1, wp);
            arb_neg(s, s);
            arb_sqrtpos(res_sin, s, prec);
            if (negate)
                arb_neg(res_sin, res_sin);
        }
    }

    arb_clear(s);
    arb_clear(t);
}

void
arb_sin_cos_arf_generic(arb_t res_sin, arb_t res_cos, const arf_t x, slong prec)
{
    arb_t pi4, t, u, v;
    fmpz_t q;
    slong wp, mag;
    int octant, swapsincos, sinnegative, cosnegative, negative;

    mag = arf_abs_bound_lt_2exp_si(x);

    if (mag > FLINT_MAX(65536, 4 * prec))
    {
        if (res_sin != NULL)
            arb_zero_pm_one(res_sin);
        if (res_cos != NULL)
            arb_zero_pm_one(res_cos);
    }
    else if (mag <= 0)  /* todo: compare with pi/4-eps instead? */
    {
        if (prec < 90000 || mag < -prec / 16 ||
            /* rs is faster for even smaller prec/N than this but has high memory usage */
            (prec < 100000000 && mag < -prec / 128))
            arb_sin_cos_arf_rs_generic(res_sin, res_cos, x, prec);
        else
            arb_sin_cos_arf_bb(res_sin, res_cos, x, prec);
    }
    else
    {
        arb_init(pi4);
        arb_init(t);
        arb_init(u);
        arb_init(v);
        fmpz_init(q);

        wp = prec + mag + 10;
        negative = arf_sgn(x) < 0;

        arb_const_pi(pi4, wp);
        arb_mul_2exp_si(pi4, pi4, -2);
        arb_set_arf(t, x);
        arb_abs(t, t);

        arb_set_round(v, t, mag + 10);
        arb_set_round(u, pi4, mag + 10);
        arb_div(u, v, u, mag + 10);

        arf_get_fmpz(q, arb_midref(u), ARF_RND_DOWN);
        arb_submul_fmpz(t, pi4, q, wp);
        octant = fmpz_fdiv_ui(q, 8);
        if (octant & 1)
            arb_sub(t, pi4, t, wp);

        arb_clear(pi4);
        arb_clear(u);
        arb_clear(v);

        sinnegative = (octant >= 4) ^ negative;
        cosnegative = (octant >= 2 && octant <= 5);
        swapsincos = (octant == 1 || octant == 2 || octant == 5 || octant == 6);

        /* guard against infinite recursion */
        if (arf_cmpabs_2exp_si(arb_midref(t), 0) > 0)
        {
            flint_printf("mod pi/4 reduction unexpectedly failed!\n");
            flint_abort();
        }

        /* todo: allow NULL in arb_sin_cos and simplify here */
        if (swapsincos)
        {
            if (res_sin != NULL && res_cos != NULL)
                arb_sin_cos(res_cos, res_sin, t, prec);
            else if (res_sin != NULL)
                arb_cos(res_sin, t, prec);
            else
                arb_sin(res_cos, t, prec);
        }
        else
        {
            if (res_sin != NULL && res_cos != NULL)
                arb_sin_cos(res_sin, res_cos, t, prec);
            else if (res_sin != NULL)
                arb_sin(res_sin, t, prec);
            else
                arb_cos(res_cos, t, prec);
        }

        if (sinnegative && res_sin != NULL)
            arb_neg(res_sin, res_sin);
        if (cosnegative && res_cos != NULL)
            arb_neg(res_cos, res_cos);

        arb_clear(t);
        fmpz_clear(q);
    }
}

