/*
 * // 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;
use crate::logs::{fast_logf, simple_fast_log};
use crate::polyeval::{f_polyeval3, f_polyeval5, f_polyeval7, f_polyeval8};
use crate::sin_cosf::fast_sinpif;

/// Computes log(gamma(x))
///
/// ulp 0.5
pub fn f_lgammaf(x: f32) -> f32 {
    if (x.to_bits() & 0x0007_ffff) == 0 {
        if x.is_infinite() {
            return f32::INFINITY;
        }
        if x.is_nan() {
            return f32::NAN;
        }
        if x == 0. {
            return 1. / x;
        }
    }

    if x.floor() == x {
        if x == 2. || x == 1. {
            return 0.;
        }
        if x.is_sign_negative() {
            return f32::INFINITY;
        }
    }

    let ax = f32::from_bits(x.to_bits() & 0x7fff_ffff);
    let dx = ax as f64;

    let is_positive = x.is_sign_positive();
    let mut sum_parity = 1f64;

    let mut f_res = 0f64;

    // For negative x, since (G is gamma function)
    // -x*G(-x)*G(x) = pi/sin(pi*x),
    // we have
    // G(x) = pi/(sin(pi*x)*(-x)*G(-x))
    // since G(-x) is positive, sign(G(x)) = sign(sin(pi*x)) for x<0
    // Hence, for x<0, signgam = sign(sin(pi*x)) and
    // lgamma(x) = log(|Gamma(x)|)
    //  = log(pi/(|x*sin(pi*x)|)) - lgamma(-x);
    if !is_positive {
        let y1 = ax.floor();
        let fraction = ax - y1; // excess over the boundary

        let a = fast_sinpif(fraction);

        sum_parity = -1.;
        const LOG_PI: f64 = f64::from_bits(0x3ff250d048e7a1bd);
        f_res = LOG_PI - simple_fast_log(a * dx);
    }

    if ax <= 0.007 {
        // Log[Gamma(x)] = -x.log() - euler * x + (zeta(2)/2)*x^2 - (zeta(3)/3)*x^3
        let fp = f_polyeval3(
            dx,
            f64::from_bits(0xbfe2788cfc6fb619),
            f64::from_bits(0x3fea51a6625307d3),
            f64::from_bits(0xbfd9a4d55beab2d7),
        );
        let d_log = simple_fast_log(dx);
        let log_gamma = f_fmla(fp, dx, -d_log);
        f_res = f_fmla(log_gamma, sum_parity, f_res);
    } else if ax < 1. {
        // Poly for loggamma(x + 1) - log(x) generated by Wolfram Mathematica
        // <<FunctionApproximations`
        // ClearAll["Global`*"]
        // f[x_]:=LogGamma[x+1]
        // {err0, approx}=MiniMaxApproximation[f[z],{z,{0.06,0.9999999},6,6},WorkingPrecision->90]
        // num=Numerator[approx][[1]];
        // den=Denominator[approx][[1]];
        // poly=den;
        // coeffs=CoefficientList[poly,z];
        // TableForm[Table[Row[{"'",NumberForm[coeffs[[i+1]],{50,50}, ExponentFunction->(Null&)],"',"}],{i,0,Length[coeffs]-1}]]
        let num = f_polyeval7(
            dx,
            f64::from_bits(0xbd24e4cf78c8818a),
            f64::from_bits(0xbfe2788cfc6f64e6),
            f64::from_bits(0xbfe1ea632fe853b2),
            f64::from_bits(0x3fd988d2daad3806),
            f64::from_bits(0x3fe1f4870eaafdf4),
            f64::from_bits(0x3fc51e12d5b97330),
            f64::from_bits(0x3f889ebeb9f0c8ff),
        );
        let den = f_polyeval7(
            dx,
            f64::from_bits(0x3ff0000000000000),
            f64::from_bits(0x4003289872066f33),
            f64::from_bits(0x4000373d88c32fe5),
            f64::from_bits(0x3fe71e95bc874164),
            f64::from_bits(0x3fb9936a0e008be7),
            f64::from_bits(0x3f6cba20a1988a60),
            f64::from_bits(0xbef3ca884ba4237a),
        );
        f_res = f_fmla(num / den - fast_logf(ax), sum_parity, f_res);
    } else if ax <= 4. {
        // Poly for loggamma(x + 1) - log(x) generated by Wolfram Mathematica
        // <<FunctionApproximations`
        // ClearAll["Global`*"]
        // f[x_]:=LogGamma[x+1]
        // {err0, approx}=MiniMaxApproximation[f[z],{z,{1.0000001,4},7,7},WorkingPrecision->90]
        // num=Numerator[approx][[1]];
        // den=Denominator[approx][[1]];
        // poly=den;
        // coeffs=CoefficientList[poly,z];
        // TableForm[Table[Row[{"'",NumberForm[coeffs[[i+1]],{50,50}, ExponentFunction->(Null&)],"',"}],{i,0,Length[coeffs]-1}]]
        let num = f_polyeval8(
            dx,
            f64::from_bits(0xbe630847302a205f),
            f64::from_bits(0xbfe2788c24a7deff),
            f64::from_bits(0xbfdd0f61b8171907),
            f64::from_bits(0x3fdab4ec27801e42),
            f64::from_bits(0x3fde11fd49427a3c),
            f64::from_bits(0x3fc0dbce39c660da),
            f64::from_bits(0x3f88e0b4cd3e97f7),
            f64::from_bits(0x3f328fcd9e9ee94e),
        );
        let den = f_polyeval8(
            dx,
            f64::from_bits(0x3ff0000000000000),
            f64::from_bits(0x4001b135a31bffae),
            f64::from_bits(0x3ffbbecb5e39f4ea),
            f64::from_bits(0x3fe2e4da095d10bd),
            f64::from_bits(0x3fb63cb7548d0d30),
            f64::from_bits(0x3f73a67613163399),
            f64::from_bits(0x3f10edd4e2b80e6f),
            f64::from_bits(0xbe77f4e4068f9d32),
        );
        f_res = f_fmla(num / den - fast_logf(ax), sum_parity, f_res);
    } else if ax <= 12. {
        // Poly for loggamma(x) generated by Wolfram Mathematica
        // <<FunctionApproximations`
        // ClearAll["Global`*"]
        // f[x_]:=LogGamma[x]
        // {err0, approx}=MiniMaxApproximation[f[z],{z,{4,12},7,7},WorkingPrecision->90]
        // num=Numerator[approx][[1]];
        // den=Denominator[approx][[1]];
        // poly=den;
        // coeffs=CoefficientList[poly,z];
        // TableForm[Table[Row[{"'",NumberForm[coeffs[[i+1]],{50,50}, ExponentFunction->(Null&)],"',"}],{i,0,Length[coeffs]-1}]]
        let num = f_polyeval8(
            dx,
            f64::from_bits(0x40086069fcc21690),
            f64::from_bits(0x4008c8ef8602e78d),
            f64::from_bits(0xc0186323149757ae),
            f64::from_bits(0xbff6a9ded42d3f31),
            f64::from_bits(0x3ff1adb5566cd807),
            f64::from_bits(0x3fcff023390dead6),
            f64::from_bits(0x3f8977a5454ec393),
            f64::from_bits(0x3f21361088f694dc),
        );
        let den = f_polyeval8(
            dx,
            f64::from_bits(0x3ff0000000000000),
            f64::from_bits(0x4016096b397dbe73),
            f64::from_bits(0x401478926593a395),
            f64::from_bits(0x3ff6a931f9e31511),
            f64::from_bits(0x3fc0fbe57bf263a4),
            f64::from_bits(0x3f7023f51ba39a98),
            f64::from_bits(0x3efabd75ebfbde40),
            f64::from_bits(0xbe4b2c9e2e4e7daa),
        );
        f_res = f_fmla(num / den, sum_parity, f_res);
    } else {
        // Stirling's approximation of Log(Gamma) and then Exp[Log[Gamma]]
        let y_recip = 1. / dx;
        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, -dx) + LOG2_PI_OVER_2;
        log_gamma = f_fmla(fast_logf(ax), dx - 0.5, log_gamma);
        f_res = f_fmla(log_gamma, sum_parity, f_res);
    }

    f_res as f32
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_lgammaf() {
        assert_eq!(
            f_lgammaf(0.000000000000000000000000000000000000000015425),
            93.97255
        );
        assert_eq!(f_lgammaf(1.7506484), -0.0842405);
        assert_eq!(f_lgammaf(7.095007e-8), 16.461288);
        assert!(f_lgammaf(-12.).is_infinite());
        assert_eq!(f_lgammaf(2.), 0.);
        assert_eq!(f_lgammaf(1.), 0.);
        assert_eq!(f_lgammaf(0.53), 0.5156078);
        assert_eq!(f_lgammaf(1.53), -0.11927056);
        assert_eq!(f_lgammaf(4.53), 2.4955146);
        assert_eq!(f_lgammaf(11.77), 16.94281);
        assert_eq!(f_lgammaf(22.77), 47.756233);
        assert_eq!(f_lgammaf(-0.53), 1.2684484);
        assert_eq!(f_lgammaf(-1.53), 0.84318066);
        assert_eq!(f_lgammaf(-4.53), -2.8570588);
        assert_eq!(f_lgammaf(-11.77), -17.850103);
        assert_eq!(f_lgammaf(-22.77), -49.323418);
        assert_eq!(f_lgammaf(f32::NEG_INFINITY), f32::INFINITY);
    }
}
