/* SPDX-License-Identifier: BSD-3-Clause
 * Copyright (c) 2026 Susi Lehtola */
#ifndef PRIMES_H
#define PRIMES_H

/*
 * Prime table and Legendre factorial valuation.
 *
 * The tables are precomputed static constants (generated by
 * tools/gen_prime_table.py into src/prime_table.inc).  We hard-code
 * the prime list and the inverse-lookup index into the compiled
 * library rather than building them at run time, so that no
 * caller-side initialisation step is required, the library is safe
 * to use from concurrent threads with no initialisation race, and
 * the tables live in read-only program data.  The default sieve
 * limit (20011) was chosen on a rule-of-thumb that ~50 kB is a
 * reasonable upper bound on hard-coded constant tables: at this
 * limit, g_primes[2263] of int (~9 kB) plus g_pi_table[20012] of
 * short (~40 kB) totals ~49 kB on a typical 64-bit target, while
 * still admitting angular momenta well beyond what most application
 * domains require (j <= 6673 for 3j/6j and j <= 5004 for 9j; see
 * below).  The table can be regenerated for a larger sieve limit at
 * the cost of a proportionally larger compiled-in table.
 *
 * Default-build correctness limit
 * -------------------------------
 * pfrac_mul_factorial(n) is correct only when every prime p <= n is in the
 * table, i.e. p <= PRIME_SIEVE_LIMIT.  MAX_FACTORIAL_ARG is therefore set to
 * (smallest prime > PRIME_SIEVE_LIMIT) - 1.  Both constants are computed from
 * the sieve by tools/gen_prime_table.py and emitted into
 * prime_table_macros.h (#included below); regenerate with
 * `python3 tools/gen_prime_table.py` after changing LIMIT in that script.
 *
 * Callers must never pass a factorial argument exceeding MAX_FACTORIAL_ARG.
 * pfrac_mul_factorial and pfrac_div_factorial abort with a diagnostic message
 * if this limit is exceeded.  pfrac_mul_int aborts if its argument exceeds
 * PRIME_SIEVE_LIMIT.
 *
 * Angular momentum limits implied by MAX_FACTORIAL_ARG
 * -----------------------------------------------------
 * The largest factorial argument in each symbol comes from the triangle-
 * coefficient denominator (j1+j2+j3+1)!, so for the default LIMIT = 20011:
 *
 *   3j, 6j, CG, Racah W, complex Gaunt, real Gaunt:
 *     j1+j2+j3 <= 20019  (equal-j: j <= 6673)
 *
 *   9j (via wigner9j_exact) and Fano X (delegates to the 9j pipeline):
 *     The k-dependent Delta denominators reach (4j+1)! for equal j, so
 *     j <= 5004 when all nine angular momenta are equal.
 *
 * Performance note
 * ----------------
 * The Racah sum has O(j) terms for 3j/6j and O(j^2) total for 9j (outer k
 * loop times inner Racah sums).  Intermediate bigints grow as ~j/ln2 bits,
 * so each elementary bigint operation already costs O(j).  Combined, the
 * end-to-end asymptotic per-symbol cost is:
 *
 *   3j / 6j / CG / Racah W / Gaunt / Gaunt-real :  O(j^2)
 *   9j / Fano X                                 :  O(j^4)
 *
 * Practical performance horizons (order of magnitude, modern hardware):
 *
 *   3j / 6j        :  j ~ 1000  in milliseconds;  j ~ 6000  in seconds
 *   9j / Fano X    :  j ~  100  in milliseconds;  j ~ 1000  in minutes
 */

/* PRIME_SIEVE_LIMIT, MAX_PRIME_COUNT, MAX_FACTORIAL_ARG.  All three are
 * computed from the single LIMIT knob in tools/gen_prime_table.py and
 * emitted into prime_table_macros.h.  Editing the macros here directly
 * would be overwritten on the next regeneration. */
#include "prime_table_macros.h"

/*
 * Cross-DLL export/import annotation for the data tables below.
 *
 * On ELF and Mach-O the table symbols are public-by-default and `extern`
 * alone is sufficient.  Windows DLLs, on the other hand, only expose
 * symbols that are explicitly marked with __declspec(dllexport) -- and
 * CMake's WINDOWS_EXPORT_ALL_SYMBOLS auto-exports functions, not data.
 * The macro alternation below kicks in only when the library is being
 * built or consumed as a DLL: WIGNERNJ_DLL is set as a PUBLIC compile
 * definition on the wignernj target whenever BUILD_SHARED_LIBS is ON,
 * so consumers (the test binaries) see it automatically.
 * WIGNERNJ_BUILDING_DLL is set PRIVATE on wignernj only, so only the
 * library's own translation units take the dllexport branch.  In a
 * static build neither macro is defined and WIGNERNJ_DATA expands to
 * nothing, exactly as on ELF.
 */
#if defined(_WIN32) && defined(WIGNERNJ_DLL)
#  if defined(WIGNERNJ_BUILDING_DLL)
#    define WIGNERNJ_DATA __declspec(dllexport)
#  else
#    define WIGNERNJ_DATA __declspec(dllimport)
#  endif
#else
#  define WIGNERNJ_DATA
#endif

/* Number of primes in the table. */
extern WIGNERNJ_DATA const int g_nprimes;

/* g_primes[i] = i-th prime (0-indexed: g_primes[0]=2, g_primes[1]=3, ...) */
extern WIGNERNJ_DATA const int g_primes[MAX_PRIME_COUNT];

/*
 * g_pi_table[n] = pi(n) = number of primes <= n.  Valid for
 * 0 <= n <= PRIME_SIEVE_LIMIT.  Equal to the smallest i such that
 * g_primes[i] > n.  Used by pfrac_mul_factorial / pfrac_div_factorial
 * to size their per-call vector add over the cached prime
 * decomposition of n!, replacing the per-call binary search of
 * g_primes.  Also doubles as the lookup table for prime_index_of()
 * below, so that the library does not need to ship a separate
 * 40 kB g_prime_index table just for the prime-or-composite test.
 */
extern WIGNERNJ_DATA const short g_pi_table[PRIME_SIEVE_LIMIT + 1];

/*
 * Returns the index i such that g_primes[i] == p, or -1 when p is
 * composite (or p < 2).  Derived from g_pi_table: pi(p) > pi(p-1)
 * exactly when p contributes to the prime count, in which case
 * i = pi(p) - 1.
 */
static inline int prime_index_of(int p)
{
    int hi, lo;
    if (p < 2 || p > PRIME_SIEVE_LIMIT) return -1;
    hi = g_pi_table[p];
    lo = g_pi_table[p - 1];
    return (hi > lo) ? hi - 1 : -1;
}

/*
 * Return v_p(n!) = sum_{k>=1} floor(n / p^k), the p-adic valuation of n!.
 * pi is the index of p in g_primes.  n must satisfy 0 <= n <= MAX_FACTORIAL_ARG.
 */
int legendre_valuation(int n, int pi);

#endif /* PRIMES_H */
