/*
 * // Copyright (c) Radzivon Bartoshyk 8/2025. All rights reserved.
 * //
 * // Redistribution and use in source and binary forms, with or without modification,
 * // are permitted provided that the following conditions are met:
 * //
 * // 1.  Redistributions of source code must retain the above copyright notice, this
 * // list of conditions and the following disclaimer.
 * //
 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
 * // this list of conditions and the following disclaimer in the documentation
 * // and/or other materials provided with the distribution.
 * //
 * // 3.  Neither the name of the copyright holder nor the names of its
 * // contributors may be used to endorse or promote products derived from
 * // this software without specific prior written permission.
 * //
 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
use crate::common::{f_fmla, is_integer};
use crate::f_exp;
use crate::logs::simple_fast_log;
use crate::polyeval::{f_polyeval5, f_polyeval18};
use crate::rounding::CpuFloor;
use crate::sin_cosf::fast_sinpif;

/// True gamma function
///
/// ulp 0.5
pub fn f_tgammaf(x: f32) -> f32 {
    let xb = x.to_bits();
    // filter out exceptional cases
    if xb >= 0xffu32 << 23 || xb == 0 {
        if xb.wrapping_shl(1) == 0 {
            return f32::INFINITY;
        }

        if x.is_nan() {
            return x + x;
        }

        if x.is_infinite() {
            if x.is_sign_negative() {
                return f32::NAN;
            }
            return x;
        }
    }

    let x_a = f32::from_bits(x.to_bits() & 0x7fff_ffff);
    if x.is_sign_positive() && x_a.to_bits() >= 0x420c2910u32 {
        // x >= 35.0401
        return f32::INFINITY;
    } else if x.is_sign_negative() && x_a.to_bits() >= 0x421a67dau32 {
        // x <= -38.601418
        if x == x.cpu_floor() {
            // integer where x < 0
            return f32::NAN;
        }
        return -0.0;
    }

    if x_a < 2e-8 {
        // x is tiny then Gamma(x) = 1/x - euler
        const EULER: f64 = f64::from_bits(0x3fe2788cfc6fb619);
        let dx = x as f64;
        let p = 1. / dx - EULER;
        return p as f32;
    } else if x_a < 2e-6 {
        // x is small then Gamma(x) = 1/x - euler + a2*x
        // a2 = 1/12 * (6 * euler^2 + pi^2)
        const EULER: f64 = f64::from_bits(0x3fe2788cfc6fb619);
        const A2: f64 = f64::from_bits(0x3fefa658c23b1578);
        let dx = x as f64;
        let p = 1. / dx + f_fmla(dx, A2, -EULER);
        return p as f32;
    }

    let mut fact = 1.0f64;
    let mut parity = 1.0;
    const PI: f64 = f64::from_bits(0x400921fb54442d18);
    let mut dy = x as f64;
    let mut result;

    // reflection
    if dy < 0. {
        if is_integer(dy) {
            return f32::NAN;
        }
        dy = f64::from_bits(dy.to_bits() & 0x7fff_ffff_ffff_ffff);
        // x < ulp(x)
        if x_a < f32::EPSILON {
            return (1. / x as f64) as f32;
        }
        let y1 = x_a.cpu_floor();
        let fraction = x_a - y1;
        if fraction != 0.0
        // is it an integer?
        {
            // is it odd or even?
            if y1 != (y1 * 0.5).cpu_floor() * 2.0 {
                parity = -1.0;
            }
            fact = -PI / fast_sinpif(fraction);
            dy += 1.0;
        }
    }

    if dy < 12.0 {
        let y1 = dy;
        let z;
        let mut n = 0;
        // x is in (eps, 1.0).
        if dy < 1.0 {
            z = dy;
            dy += 1.0;
        } else
        // x is in [1.0, max].
        {
            n = dy as i32 - 1;
            dy -= n as f64;
            z = dy - 1.0;
        }

        // Gamma(x+1) on [1;2] generated by Wolfram Mathematica:
        // <<FunctionApproximations`
        // ClearAll["Global`*"]
        // f[x_]:=Gamma[x+1]
        // {err0, approx}=MiniMaxApproximation[f[z],{z,{0,1},17,0},WorkingPrecision->90]
        // num=Numerator[approx][[1]];
        // den=Denominator[approx][[1]];
        // poly=num;
        // coeffs=CoefficientList[poly,z];
        // TableForm[Table[Row[{"'",NumberForm[coeffs[[i+1]], {50,50}, ExponentFunction->(Null&)],"',"}],{i,0,Length[coeffs]-1}]]
        result = f_polyeval18(
            z,
            f64::from_bits(0x3fefffffffffff19),
            f64::from_bits(0xbfe2788cfc6d59a6),
            f64::from_bits(0x3fefa658c133abd2),
            f64::from_bits(0xbfed0a116184e1d0),
            f64::from_bits(0x3fef6a4cd2befb54),
            f64::from_bits(0xbfef6c4479fe9aca),
            f64::from_bits(0x3fefc59bfa97e9bf),
            f64::from_bits(0xbfefcfde03bfb0d9),
            f64::from_bits(0x3fefa3ee5eab6681),
            f64::from_bits(0xbfeed8130a67dd46),
            f64::from_bits(0x3fecb6e603fdb5ed),
            f64::from_bits(0xbfe8801e901b4c10),
            f64::from_bits(0x3fe2356219082d3e),
            f64::from_bits(0xbfd64ad556b6062a),
            f64::from_bits(0x3fc5219e235288f4),
            f64::from_bits(0xbfacad444cb0f4ec),
            f64::from_bits(0x3f888ea3bfa9155a),
            f64::from_bits(0xbf53d1776abc9eaa),
        );
        if y1 < dy {
            result /= y1;
        } else if y1 > dy {
            for _ in 0..n {
                result *= dy;
                dy += 1.0;
            }
        }
    } else {
        // Stirling's approximation of Log(Gamma) and then Exp[Log[Gamma]]
        let y_recip = 1. / dy;
        let y_sqr = y_recip * y_recip;
        // Bernoulli coefficients generated by SageMath:
        // var('x')
        // def bernoulli_terms(x, N):
        //     S = 0
        //     for k in range(1, N+1):
        //         B = bernoulli(2*k)
        //         term = (B / (2*k*(2*k-1))) * x**((2*k-1))
        //         S += term
        //     return S
        //
        // terms = bernoulli_terms(x, 5)
        let bernoulli_poly = f_polyeval5(
            y_sqr,
            f64::from_bits(0x3fb5555555555555),
            f64::from_bits(0xbf66c16c16c16c17),
            f64::from_bits(0x3f4a01a01a01a01a),
            f64::from_bits(0xbf43813813813814),
            f64::from_bits(0x3f4b951e2b18ff23),
        );
        // Log[Gamma(x)] = x*log(x) - x + 1/2*Log(2*PI/x) + bernoulli_terms
        const LOG2_PI_OVER_2: f64 = f64::from_bits(0x3fed67f1c864beb5);
        let mut log_gamma = f_fmla(bernoulli_poly, y_recip, -dy) + LOG2_PI_OVER_2;
        log_gamma = f_fmla(simple_fast_log(dy), dy - 0.5, log_gamma);
        result = f_exp(log_gamma);
    }

    result *= parity;
    if fact != 1.0 {
        result = fact / result;
    }

    result as f32
}

#[cfg(test)]
mod tests {
    use crate::f_tgammaf;

    #[test]
    fn test_gammaf() {
        assert!(f_tgammaf(-3.).is_nan());
        assert_eq!(f_tgammaf(12.58038769), 167149000.0);
        assert_eq!(f_tgammaf(4.566854e-7), 2189690.8);
        assert_eq!(f_tgammaf(4.9587783e-8), 20166258.0);
        assert_eq!(f_tgammaf(-46.1837), -0.0);
        assert_eq!(f_tgammaf(1.1502532e-19), 8.6937384e18);
        assert_eq!(f_tgammaf(0.04038769), 24.221333);
        assert_eq!(f_tgammaf(2.58038769), 1.4088479);
        assert_eq!(f_tgammaf(-1.3559477), 2.892815);
        assert_eq!(f_tgammaf(-2.3559477), -1.2278775);
        assert_eq!(f_tgammaf(0.0), f32::INFINITY);
        assert_eq!(f_tgammaf(-0.0), f32::INFINITY);
        assert_eq!(f_tgammaf(f32::INFINITY), f32::INFINITY);
        assert!(f_tgammaf(f32::NEG_INFINITY).is_nan());
        assert!(f_tgammaf(f32::NAN).is_nan());
    }
}
