// Copyright (C) 2026 Kiyotsugu Arai
// SPDX-License-Identifier: LGPL-3.0-or-later

// IntCombinatorics.hpp
// Combinatorial functions (factorial, binomial coefficient, etc.) for Int

#ifndef SANGI_INT_COMBINATORICS_HPP
#define SANGI_INT_COMBINATORICS_HPP

#include "IntBase.hpp"

namespace sangi {

/**
 * @brief Combinatorial functions
 *
 * Provides functions used in combinatorics, such as factorial and binomial
 * coefficient.
 */
class IntCombinatorics {
public:
    /**
     * @brief Factorial n!
     *
     * Computes n! = n * (n-1) * ... * 2 * 1.
     * Based on benchmarks (2026-02-14) the optimal algorithm is selected:
     *   - N <= 100: table lookup (O(1))
     *   - 100 < N <= 228: simple loop (sequential multiplication starting from the table)
     *   - N > 228: sieve method (prime factorization + tournament multiplication)
     * Note: the tournament method is worse than the sieve method across the
     * whole range, so it is not selected automatically.
     * Details: TODO/NOTES_FACTORIAL_BENCHMARK.md
     *
     * @param n Non-negative integer
     * @return The value of n!
     *
     * Special cases:
     *   factorial(0) = 1 (by definition)
     *   factorial(1) = 1
     *   factorial(n < 0) = NaN (factorial of a negative number is undefined)
     *   factorial(NaN) = NaN
     *   factorial(infinity) = infinity
     */
    static Int factorial(const Int& n);

    /**
     * @brief Double factorial n!!
     *
     * Computes n!! = n * (n-2) * (n-4) * ...
     * - n is even: n * (n-2) * ... * 4 * 2
     * - n is odd:  n * (n-2) * ... * 3 * 1
     *
     * @param n Non-negative integer
     * @return The value of n!!
     *
     * Special cases:
     *   doubleFactorial(0) = 1 (by definition)
     *   doubleFactorial(1) = 1
     *   doubleFactorial(-1) = 1 (by definition)
     *   doubleFactorial(n < -1) = NaN
     *   doubleFactorial(NaN) = NaN
     *   doubleFactorial(infinity) = infinity
     *
     * Examples:
     *   6!! = 6 * 4 * 2 = 48
     *   7!! = 7 * 5 * 3 * 1 = 105
     */
    static Int doubleFactorial(const Int& n);

    /**
     * @brief Binomial coefficient C(n, k) = n! / (k! * (n-k)!)
     *
     * Computes the number of combinations "choose k from n".
     *
     * @param n Non-negative integer
     * @param k Non-negative integer (0 <= k <= n)
     * @return The value of C(n, k)
     *
     * Special cases:
     *   binomial(n, 0) = 1 (one way to pick nothing)
     *   binomial(n, n) = 1 (one way to pick everything)
     *   binomial(n, k) = 0 (when k > n)
     *   binomial(n, k < 0) = NaN
     *   binomial(n < 0, k) = NaN
     *   binomial(NaN, k) = NaN
     *
     * Optimizations:
     *   - Use C(n, k) = C(n, n-k) to reduce the amount of computation
     *   - Alternate multiplications and divisions to avoid overflow
     *
     * Examples:
     *   binomial(5, 2) = 5!/(2!*3!) = 10
     *   binomial(10, 3) = 10!/(3!*7!) = 120
     */
    static Int binomial(const Int& n, const Int& k);

    /**
     * @brief Fibonacci number F(n)
     *
     * Computed via fast doubling, using O(log n) multiplications:
     *   F(2k)   = F(k) * (2*F(k+1) - F(k))
     *   F(2k+1) = F(k)^2 + F(k+1)^2
     *
     * @param n Non-negative integer
     * @return F(n)
     *
     * Special cases:
     *   fibonacci(0) = 0, fibonacci(1) = 1
     *   fibonacci(n < 0) = NaN
     */
    static Int fibonacci(const Int& n);

    /**
     * @brief Lucas number L(n)
     *
     * Uses the relation L(n) = F(n-1) + F(n+1) = 2*F(n+1) - F(n).
     *
     * @param n Non-negative integer
     * @return L(n)
     *
     * Special cases:
     *   lucas(0) = 2, lucas(1) = 1
     *   lucas(n < 0) = NaN
     */
    static Int lucas(const Int& n);

    /**
     * @brief Primorial primorial(n) = product of all primes <= n
     *
     * Equivalent to GMP's mpz_primorial_ui.
     * Enumerates primes up to n using the Sieve of Eratosthenes, then
     * multiplies them using a binary-splitting product.
     *
     * @param n Non-negative integer (int)
     * @return Product of all primes <= n. n < 2 -> 1.
     *
     * Example: primorial(10) = 2 * 3 * 5 * 7 = 210
     */
    static Int primorial(int n);

    /**
     * @brief m-step factorial mfac(n, m) = n * (n-m) * (n-2m) * ...
     *
     * Equivalent to GMP's mpz_mfac_uiui.
     * Reduces to n! when m=1, and to n!! (doubleFactorial) when m=2.
     *
     * @param n Non-negative integer
     * @param m Positive integer (step width)
     * @return n * (n-m) * (n-2m) * ... (while each factor is at least 1)
     *
     * Example: mfac(10, 3) = 10 * 7 * 4 * 1 = 280
     */
    static Int mfac(int n, int m);

    // Individual algorithms (exposed for tests and benchmarks)

    /** Table lookup: N <= 100 */
    static const Int& factorialTable(int n);

    /** Simple loop: sequential multiplication starting from the table (100!) */
    static Int factorialSimple(int n);

    /** Tournament method: split out powers of two + balanced edge-x-edge multiplication (for benchmarks; not used by the automatic selection) */
    static Int factorialTournament(int n);

    /** Sieve method: Eratosthenes-style prime factorization + tournament multiplication */
    static Int factorialSieve(int n);

private:
    static constexpr int TABLE_MAX = 100;
    static constexpr int SIMPLE_MAX = 228;
};

} // namespace sangi

#endif // SANGI_INT_COMBINATORICS_HPP
