package Sidef::Types::Number::Number;

use utf8;
use 5.016;

use parent qw(
  Sidef::Object::Object
);

use Sidef::Types::Bool::Bool;

use Math::GMPz qw();
use Math::GMPq qw();
use Math::MPFR qw();
use Math::MPC  qw();

use List::Util   qw();
use Scalar::Util qw();

use Math::Prime::Util::GMP 0.53 qw();

our ($ROUND, $PREC, $USE_PRIMECOUNT, $USE_PRIMESUM, $USE_PARI_GP, $USE_YAFU, $USE_PFGW, $USE_FACTORDB, $VERBOSE, $SPECIAL_FACTORS, $USE_CONJECTURES);

BEGIN {
    $ROUND           = Math::MPFR::MPFR_RNDN();    # rounding mode for floating-point numbers
    $PREC            = 192;                        # precision in bits for floating-point numbers
    $USE_PRIMESUM    = 0;                          # true to use Kim Walisch's primesum for large n
    $USE_PRIMECOUNT  = 0;                          # true to use Kim Walisch's primecount for large n
    $USE_PFGW        = 0;                          # true to use PFGW64 as a primality pretest for large enough n
    $USE_PARI_GP     = 0;                          # true to use PARI/GP in several methods where it excels
    $USE_YAFU        = 0;                          # true to use YAFU for factoring large integers
    $USE_FACTORDB    = 0;                          # true to use factordb.com for factoring large integers
    $VERBOSE         = 0;                          # true to enable verbose/debug mode
    $USE_CONJECTURES = 0;                          # true to use conjectured methods for better performance
    $SPECIAL_FACTORS = 1;                          # true to check for special factors in `_factor(n)`
}

state $MONE = Math::GMPz::Rmpz_init_set_si(-1);
state $ZERO = Math::GMPz::Rmpz_init_set_ui( 0);
state $ONE  = Math::GMPz::Rmpz_init_set_ui( 1);
state $TWO  = Math::GMPz::Rmpz_init_set_ui( 2);
state $TEN  = Math::GMPz::Rmpz_init_set_ui(10);

state $MPC_VERSION = Math::MPC::MPC_VERSION();

state $TRUE  = Sidef::Types::Bool::Bool::TRUE;
state $FALSE = Sidef::Types::Bool::Bool::FALSE;

my %PRIMEPI_LOOKUP;    # lookup table for prime_count()
my @SMALL_PRIMES_20 = (3, 5, 7, 11, 13, 17, 19);

my %NUMBER_CLASSES = (
                      'Sidef::Types::Number::Mod'           => 1,
                      'Sidef::Types::Number::Gauss'         => 1,
                      'Sidef::Types::Number::Fraction'      => 1,
                      'Sidef::Types::Number::Quadratic'     => 1,
                      'Sidef::Types::Number::Quaternion'    => 1,
                      'Sidef::Types::Number::Polynomial'    => 1,
                      'Sidef::Types::Number::PolynomialMod' => 1,
                     );

use constant {

    ONE   => bless(\(my $one   = 1)),
    TWO   => bless(\(my $two   = 2)),
    THREE => bless(\(my $three = 3)),
    FOUR  => bless(\(my $four  = 4)),
    TEN   => bless(\(my $ten   = 10)),
    ZERO  => bless(\(my $zero  = 0)),
    MONE  => bless(\(my $mone  = -1)),

    ULONG_MAX => Math::GMPq::_ulong_max(),
    LONG_MIN  => Math::GMPq::_long_min(),

    # ULONG_MAX => 4294967295,
    # LONG_MIN  => -2147483647,

    FAST_MODE => 1,    # true to use fast paths for native integers

    HAS_PRIME_UTIL => eval {
        require Math::Prime::Util;
        $Math::Prime::Util::VERSION >= 0.74
          and Math::Prime::Util::prime_get_config()->{'xs'};
    } // 0,            # version >= 0.74 + XS required
};

use constant {

    YAFU_MIN               => 49,     # in decimal digits
    FACTORDB_MIN           => 65,     # in decimal digits
    SPECIAL_FACTORS_MIN    => 36,     # in decimal digits (must be greater than SMALL_NUMBER_MAX_BITS)
    SMALL_NUMBER_MAX_BITS  => 110,    # in bits (numbers that can be factorized fast)
    MEDIUM_NUMBER_MAX_BITS => 150,    # in bits (numbers that can be factorized moderately fast)

    # Check if we have a recent enough version of Math::Prime::Util (>= 0.75)
    HAS_NEW_PRIME_UTIL => (HAS_PRIME_UTIL and defined(&Math::Prime::Util::sopfr)) // 0,

    # HAS_NEWER_PRIME_UTIL => (HAS_PRIME_UTIL and defined(&Math::Prime::Util::nth_powerfree))    // 0,

    # Check if we have a recent enough version of Math::Prime::Util::GMP
    # HAS_NEW_PRIME_UTIL_GMP => defined(&Math::Prime::Util::GMP::nth_powerfree) // 0,

    IS_PRIME_CACHE_SIZE   => 1e5,    # how many entries to cache
    PRIMALITY_PRETEST_MIN => 500,    # in decimal digits

    INTSIZE        => List::Util::max(32, CORE::int(CORE::log(ULONG_MAX) / CORE::log(2))),    # size of ULONG_MAX in base 2
    PRIMECOUNT_MIN => List::Util::min(ULONG_MAX,       (HAS_PRIME_UTIL ? 1e10 : 1e7)),        # absolute value
    PRIMESUM_MIN   => List::Util::min(ULONG_MAX >> 14, (HAS_PRIME_UTIL ? 1e8  : 1e5)),        # absolute value

    # Tune parameters for prime_count() function
    SIEVE_INLINE_MAX => 1e7,                                                                  # use GMP::sieve_primes directly up to this diff
    SIEVE_UTIL_MAX   => 1e8,                                                                  # hand off to Math::Prime::Util up to this diff
    NAIVE_LOOP_MAX   => 1000,                                                                 # walk primes one-by-one for deltas this small
};

if (HAS_PRIME_UTIL) {
    Math::Prime::Util::prime_precalc(1e7);
    eval { Math::Prime::Util::prime_set_config(bigint => 'Math::GMPz') };
}

state $round_z = Math::MPFR::MPFR_RNDZ();

my %DIGITS_36;
@DIGITS_36{0 .. 9, 'a' .. 'z'} = (0 .. 35);

my %DIGITS_62;
@DIGITS_62{0 .. 9, 'A' .. 'Z', 'a' .. 'z'} = (0 .. 61);

my @FROM_DIGITS_36 = (0 .. 9, 'a' .. 'z');
my @FROM_DIGITS_62 = (0 .. 9, 'A' .. 'Z', 'a' .. 'z');

state $LUCAS_PQ_LIMIT = CORE::int(CORE::sqrt(ULONG_MAX >> 2));

use overload
  q{bool} => sub { (@_) = (${$_[0]}); goto &__boolify__ },
  q{0+}   => sub { (@_) = (${$_[0]}); goto &__numify__ },
  q{""}   => sub { (@_) = (${$_[0]}); goto &__stringify__ };

sub new {
    my (undef, $num, $base) = @_;

    if (ref($_[0]) eq __PACKAGE__) {
        return $_[0];
    }

    if (defined($base) and ref($base)) {
        if (ref($base) eq __PACKAGE__) {
            $base = _any2ui($$base) // 0;
        }
        else {
            $base = CORE::int($base);
        }
    }

    my $ref = ref($num);

    if ($ref eq 'Sidef::Types::String::String') {
        $num = "$$num";
        $ref = '';
    }

    # Optimization: return faster for base-10 integers
    if (!defined($base) and !$ref and $num =~ /^-?[0-9]++\z/) {
        return _set_int($num);
    }

    if (exists($NUMBER_CLASSES{$ref})) {
        $num = $num->to_n;
    }
    elsif ($ref eq 'Sidef::Types::Bool::Bool') {
        $num = $num->get_value;
    }

    # Number with base
    if (defined($base)) {

        my $int_base = CORE::int($base);

        if ($int_base < 2 or $int_base > 62) {
            die "[ERROR] Number(): base must be between 2 and 62, got $base";
        }

        $num = defined($num) ? "$num" : '0';

        # Remove the leading plus sign (if any)
        $num =~ s/^\+// if substr($num, 0, 1) eq '+';

        if (index($num, '/') != -1) {
            my $r = Math::GMPq::Rmpq_init();
            eval { Math::GMPq::Rmpq_set_str($r, $num, $int_base); 1 } // goto &nan;
            if (Math::GMPq::Rmpq_get_str($r, 10) !~ m{^\s*[-+]?[0-9]+\s*(?>/\s*[-+]?[1-9]+[0-9]*\s*)?\z}) {
                goto &nan;
            }
            Math::GMPq::Rmpq_canonicalize($r);
            return bless \$r;
        }
        elsif (substr($num, 0, 1) eq '(' and substr($num, -1) eq ')') {
            my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
            eval { Math::MPC::Rmpc_set_str($r, $num, $int_base, $ROUND); 1 } // goto &nan;
            return bless \$r;
        }
        elsif (index($num, '.') != -1) {
            my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
            if (Math::MPFR::Rmpfr_set_str($r, $num, $int_base, $ROUND) < 0) {
                goto &nan;
            }
            return bless \$r;
        }
        else {
            state $t = Math::GMPz::Rmpz_init_nobless();
            eval { Math::GMPz::Rmpz_set_str($t, "$num", $int_base) };
            $@ && goto &nan;
            my $r =
                Math::GMPz::Rmpz_fits_ulong_p($t) ? Math::GMPz::Rmpz_get_ui($t)
              : Math::GMPz::Rmpz_fits_slong_p($t) ? Math::GMPz::Rmpz_get_si($t)
              :                                     Math::GMPz::Rmpz_init_set($t);
            return bless \$r;
        }
    }

    # Special string values
    if (!$ref) {
        return bless \_str2obj($num);
    }

    # Already a __PACKAGE__ object
    if ($ref eq __PACKAGE__) {
        return $num;
    }

    # GMPz
    if ($ref eq 'Math::GMPz') {
        return bless \(my $value = Math::GMPz::Rmpz_init_set($num));
    }

    # MPFR
    if ($ref eq 'Math::MPFR') {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set($r, $num, $ROUND);
        return bless \$r;
    }

    # MPC
    if ($ref eq 'Math::MPC') {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set($r, $num, $ROUND);
        return bless \$r;
    }

    # GMPq
    if ($ref eq 'Math::GMPq') {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set($r, $num);
        return bless \$r;
    }

    bless \_str2obj("$num");
}

*call = \&new;

sub with_value {
    my ($self, $value) = @_;
    ref($self)->new($value);
}

sub run { $_[1] }

sub _valid {
    (
     ref($$_) eq __PACKAGE__
       or do {
         my $sub = overload::Method($$_, '0+');

         my $tmp = (
             defined($sub) ? __PACKAGE__->new($sub->($$_)) : do {
                 my (undef, undef, undef, $caller) = caller(1);
                 die "[ERROR] Value <<$$_>> cannot be implicitly converted to a number, inside <<$caller>>!\n";
             }
         );

         if (ref($tmp) ne __PACKAGE__) {    # this should not happen
             my (undef, undef, undef, $caller) = caller(1);
             die "[ERROR] Cannot convert <<$$_>> to a number, inside <<$caller>>! (is method \"to_n\" well-defined?)\n";
         }

         $$_ = $tmp;
       }
    ) for @_;
}

sub _set_int {
    if (ref($_[0]) eq 'Math::GMPz') {

        if (Math::GMPz::Rmpz_fits_slong_p($_[0])) {
            return bless \(my $o = Math::GMPz::Rmpz_get_si($_[0]));
        }
        elsif (Math::GMPz::Rmpz_fits_ulong_p($_[0])) {
            return bless \(my $o = Math::GMPz::Rmpz_get_ui($_[0]));
        }

        return bless \(my $value = Math::GMPz::Rmpz_init_set($_[0]));
    }
    (!ref($_[0]) and $_[0] < ULONG_MAX and $_[0] > LONG_MIN)
      ? (bless \(my $o = 0 + $_[0]))
      : (bless \(my $value = Math::GMPz::Rmpz_init_set_str("$_[0]", 10)));
}

sub _dump {
    my $x = ${$_[0]};

    my $ref = ref($x);

    if (!$ref) {
        ('int', $x);
    }
    elsif ($ref eq 'Math::GMPz') {
        ('int', Math::GMPz::Rmpz_get_str($x, 10));
    }
    elsif ($ref eq 'Math::GMPq') {
        ('rat', Math::GMPq::Rmpq_get_str($x, 10));
    }
    elsif ($ref eq 'Math::MPFR') {
        ('float', Math::MPFR::Rmpfr_get_str($x, 10, 0, $ROUND));
    }
    elsif ($ref eq 'Math::MPC') {
        ('complex', Math::MPC::Rmpc_get_str(10, 0, $x, $ROUND));
    }
    else {
        die "[ERROR] This shouldn't happen: <<$x>> as <<$ref>>";
    }
}

sub _set_str {
    my ($type, $str) = @_;

    if ($type eq 'int') {

        if ($str < ULONG_MAX and $str > LONG_MIN) {
            return (bless \(my $o = 0 + $str));
        }

        bless \(my $value = Math::GMPz::Rmpz_init_set_str("$str", 10));
    }
    elsif ($type eq 'rat') {
        Math::GMPq::Rmpq_set_str((my $r = Math::GMPq::Rmpq_init()), "$str", 10);
        bless \$r;
    }
    elsif ($type eq 'float') {
        Math::MPFR::Rmpfr_set_str((my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC))), "$str", 10, $ROUND);
        bless \$r;
    }
    elsif ($type eq 'complex') {
        Math::MPC::Rmpc_set_str((my $r = Math::MPC::Rmpc_init2(CORE::int($PREC))), "$str", 10, $ROUND);
        bless \$r;
    }
    else {
        die "[ERROR] Number._set_str(): invalid type <<$type>> with content <<$str>>";
    }
}

sub _str2frac {
    my $str = lc($_[0]);

    my $sign = substr($str, 0, 1);
    if ($sign eq '-') {
        substr($str, 0, 1, '');
        $sign = '-';
    }
    else {
        substr($str, 0, 1, '') if ($sign eq '+');
        $sign = '';
    }

    if ((my $i = index($str, 'e')) != -1) {

        my $exp = substr($str, $i + 1);

        my ($before, $after) = split(/\./, substr($str, 0, $i));

        if (!defined($after)) {    # return faster for numbers like "13e2"
            if ($exp >= 0) {
                return ("$sign$before" . ('0' x $exp));
            }
            else {
                $after = '';
            }
        }

        my $numerator = "$sign$before$after";

        if ($exp < 0) {
            return ("$numerator/1" . ('0' x (CORE::abs($exp) + CORE::length($after))));
        }

        my $diff = ($exp - CORE::length($after));

        if ($diff >= 0) {
            return ($numerator . ('0' x $diff));
        }

        my $s = "$before$after";
        substr($s, $exp + CORE::length($before), 0, '.');
        return __SUB__->("$sign$s");
    }

    if ((my $i = index($str, '.')) != -1) {
        my ($before, $after) = (substr($str, 0, $i), substr($str, $i + 1));

        if ($after == 0) {
            return "$sign$before";
        }

        return ("$sign$before$after/1" . ('0' x CORE::length($after)));
    }

    return "$sign$str";
}

#
## Misc internal functions
#

# Convert a given pair (real, imag) into an MPC object
sub _reals2mpc {
    my ($re, $im) = @_;

    my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));

    $re = _str2obj($re);
    $im = _str2obj($im);

    my $sig = join(' ', ref($re), ref($im));

    if ($sig eq q{Math::MPFR Math::MPFR}) {
        Math::MPC::Rmpc_set_fr_fr($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPz Math::GMPz}) {
        Math::MPC::Rmpc_set_z_z($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPz Math::MPFR}) {
        Math::MPC::Rmpc_set_z_fr($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::MPFR Math::GMPz}) {
        Math::MPC::Rmpc_set_fr_z($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPz Math::GMPq}) {
        Math::MPC::Rmpc_set_z_q($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPq Math::GMPz}) {
        Math::MPC::Rmpc_set_q_z($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPq Math::GMPq}) {
        Math::MPC::Rmpc_set_q_q($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::GMPq Math::MPFR}) {
        Math::MPC::Rmpc_set_q_fr($r, $re, $im, $ROUND);
    }
    elsif ($sig eq q{Math::MPFR Math::GMPq}) {
        Math::MPC::Rmpc_set_fr_q($r, $re, $im, $ROUND);
    }
    elsif (ref($re) eq 'Math::MPC') {
        Math::MPC::Rmpc_set($r, _any2mpc($im), $ROUND);
        Math::MPC::Rmpc_mul_i($r, $r, 1, $ROUND);
        Math::MPC::Rmpc_add($r, $r, $re, $ROUND);
    }
    elsif (ref($im) eq 'Math::MPC') {
        Math::MPC::Rmpc_set($r, $im, $ROUND);
        Math::MPC::Rmpc_mul_i($r, $r, 1, $ROUND);
        Math::MPC::Rmpc_add($r, $r, _any2mpc($re), $ROUND);
    }
    else {
        $re = _any2mpfr($re);
        $im = _any2mpfr($im);
        Math::MPC::Rmpc_set_fr_fr($r, $re, $im, $ROUND);
    }

    return $r;
}

# Converts a string into an object
sub _str2obj {
    my ($s) = @_;

    $s || return 0;

    $s = CORE::lc($s);

    # Remove underscores and whitespace
    $s =~ tr/_ \a\b\t\n\f\r\e//d;

    if ($s eq 'inf' or $s eq '+inf') {
        goto &_inf;
    }
    elsif ($s eq '-inf') {
        goto &_ninf;
    }
    elsif ($s eq 'nan') {
        goto &_nan;
    }

    # Performance improvement for Perl integers
    if (FAST_MODE and CORE::int($s) eq $s and $s > LONG_MIN and $s < ULONG_MAX) {
        return $s + 0;

#<<<
        #~ return (
                #~ $s < 0
                #~ ? Math::GMPz::Rmpz_init_set_si($s)
                #~ : Math::GMPz::Rmpz_init_set_ui($s)
               #~ );
#>>>
    }

    # Decimal expansion (parsed as a rational value)
    if ($s =~ tr/e.// and $s =~ /^([+-]?+(?=\.?[0-9])[0-9_]*+(?:\.[0-9_]++)?(?:[Ee](?:[+-]?+[0-9_]+))?)\z/) {
        my $frac = _str2frac($1);

        if (FAST_MODE and CORE::int($frac) eq $frac and $frac > LONG_MIN and $frac < ULONG_MAX) {
            return $frac + 0;
        }

        if (index($frac, '/') != -1) {
            my $q = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_set_str($q, $frac, 10);
            Math::GMPq::Rmpq_canonicalize($q);
            return $q;
        }
        else {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_set_str($z, $frac, 10);
            return $z;
        }
    }

    # Complex number of form: "(3 4)"
    if (substr($s, 0, 1) eq '(' and substr($s, -1) eq ')') {
        my ($re, $im) = split(' ', substr($s, 1, -1));

        if (defined($re) and defined($im)) {
            return _reals2mpc($re, $im);
        }
    }

    # Complex number of form: "3+4i"
    if (substr($s, -1) eq 'i') {

        if ($s eq 'i' or $s eq '+i') {
            my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
            Math::MPC::Rmpc_set_ui_ui($r, 0, 1, $ROUND);
            return $r;
        }
        elsif ($s eq '-i') {
            my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
            Math::MPC::Rmpc_set_si_si($r, 0, -1, $ROUND);
            return $r;
        }

        my ($re, $im);

        state $numeric_re  = qr/[+-]?+(?=\.?[0-9])[0-9]*+(?:\.[0-9]++)?(?:[Ee](?:[+-]?+[0-9]+))?/;
        state $unsigned_re = qr/(?=\.?[0-9])[0-9]*+(?:\.[0-9]++)?(?:[Ee](?:[+-]?+[0-9]+))?/;

        if ($s =~ /^($numeric_re)\s*([-+])\s*($unsigned_re)i\z/o) {
            ($re, $im) = ($1, $3);
            $im = "-$im" if $2 eq '-';
        }
        elsif ($s =~ /^($numeric_re)i\z/o) {
            ($re, $im) = (0, $1);
        }
        elsif ($s =~ /^($numeric_re)\s*([-+])\s*i\z/o) {
            ($re, $im) = ($1, 1);
            $im = -1 if $2 eq '-';
        }

        if (defined($re) and defined($im)) {
            return _reals2mpc($re, $im);
        }
    }

#<<<
    # Decimal expansion (parsed as a floating-point value)
    #~ if ($s =~ tr/e.//) {
        #~ my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        #~ if (Math::MPFR::Rmpfr_set_str($r, $s, 10, $ROUND)) {
            #~ Math::MPFR::Rmpfr_set_nan($r);
        #~ }
        #~ return $r;
    #~ }
#>>>

    # Remove a leading plus sign
    $s =~ s/^\+// if substr($s, 0, 1) eq '+';

    # Fractional value
    if (index($s, '/') != -1 and $s =~ m{^\s*[-+]?[0-9]+\s*/\s*[-+]?[1-9]+[0-9]*\s*\z}) {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_str($r, $s, 10);
        Math::GMPq::Rmpq_canonicalize($r);
        return $r;
    }

    eval { Math::GMPz::Rmpz_init_set_str("$s", 10) } // goto &_nan;
}

#
## MPZ
#
sub _mpz2mpq {
    my $r = Math::GMPq::Rmpq_init();
    Math::GMPq::Rmpq_set_z($r, $_[0]);
    $r;
}

sub _mpz2mpfr {
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_set_z($r, $_[0], $ROUND);
    $r;
}

sub _mpz2mpc {
    my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
    Math::MPC::Rmpc_set_z($r, $_[0], $ROUND);
    $r;
}

#
## MPQ
#
sub _mpq2mpz {
    my $z = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_set_q($z, $_[0]);
    $z;
}

sub _mpq2mpfr {
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_set_q($r, $_[0], $ROUND);
    $r;
}

sub _mpq2mpc {
    my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
    Math::MPC::Rmpc_set_q($r, $_[0], $ROUND);
    $r;
}

#
## MPFR
#
sub _mpfr2mpc {
    my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
    Math::MPC::Rmpc_set_fr($r, $_[0], $ROUND);
    $r;
}

#
## Any to MPC (complex)
#
sub _any2mpc {
    my ($x) = @_;

    if (!ref($x)) {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        if (FAST_MODE and $x < ULONG_MAX and $x > LONG_MIN) {
            ($x < 0)
              ? Math::MPC::Rmpc_set_si($r, $x, $ROUND)
              : Math::MPC::Rmpc_set_ui($r, $x, $ROUND);
        }
        else {
            Math::MPC::Rmpc_set_str($r, $x, 10, $ROUND);
        }
        return $r;
    }

    ref($x) eq 'Math::MPC'  && return $x;
    ref($x) eq 'Math::GMPz' && goto &_mpz2mpc;
    ref($x) eq 'Math::GMPq' && goto &_mpq2mpc;

    goto &_mpfr2mpc;
}

#
## Any to MPFR (floating-point)
#
sub _any2mpfr {
    my ($x) = @_;

    if (!ref($x)) {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        if (FAST_MODE and $x < ULONG_MAX and $x > LONG_MIN) {
            ($x < 0)
              ? Math::MPFR::Rmpfr_set_si($r, $x, $ROUND)
              : Math::MPFR::Rmpfr_set_ui($r, $x, $ROUND);
        }
        else {
            Math::MPFR::Rmpfr_set_str($r, $x, 10, $ROUND);
        }
        return $r;
    }

    ref($x) eq 'Math::MPFR' && return $x;
    ref($x) eq 'Math::GMPz' && goto &_mpz2mpfr;
    ref($x) eq 'Math::GMPq' && goto &_mpq2mpfr;

    my $fr = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPC::RMPC_IM($fr, $x);

    if (Math::MPFR::Rmpfr_zero_p($fr)) {
        Math::MPC::RMPC_RE($fr, $x);
    }
    else {
        Math::MPFR::Rmpfr_set_nan($fr);
    }

    $fr;
}

#
## Any to MPFR or MPC, in this order
#
sub _any2mpfr_mpc {
    my ($x) = @_;

    if (   ref($x) eq 'Math::MPFR'
        or ref($x) eq 'Math::MPC') {
        return $x;
    }

    ref($x) eq 'Math::GMPz' && goto &_mpz2mpfr;
    ref($x) eq 'Math::GMPq' && goto &_mpq2mpfr;
    goto &_any2mpfr;
}

#
## Any to GMPz (integer)
#
sub _any2mpz {
    my ($x, $k) = @_;

    if (!ref($x)) {

        if (defined($k)) {
            state @table;
            my $mpz = ($table[$k] //= Math::GMPz::Rmpz_init());
            (FAST_MODE and $x < ULONG_MAX and $x > LONG_MIN)
              ? (
                 ($x < 0)
                 ? Math::GMPz::Rmpz_set_si($mpz, $x)
                 : Math::GMPz::Rmpz_set_ui($mpz, $x)
                )
              : Math::GMPz::Rmpz_set_str($mpz, $x, 10);
            return $mpz;
        }

        return (
                (FAST_MODE and $x < ULONG_MAX and $x > LONG_MIN)
                ? (
                   ($x < 0)
                   ? Math::GMPz::Rmpz_init_set_si($x)
                   : Math::GMPz::Rmpz_init_set_ui($x)
                  )
                : Math::GMPz::Rmpz_init_set_str($x, 10)
               );
    }

    ref($x) eq 'Math::GMPz' && return $x;
    ref($x) eq 'Math::GMPq' && goto &_mpq2mpz;

    if (ref($x) eq 'Math::MPFR') {
        if (Math::MPFR::Rmpfr_number_p($x)) {
            my $z = Math::GMPz::Rmpz_init();
            Math::MPFR::Rmpfr_get_z($z, $x, $round_z);
            return $z;
        }
        return;
    }

    (@_) = _any2mpfr($x);
    goto &_any2mpz;
}

#
## Any to GMPq (rational)
#
sub _any2mpq {
    my ($x) = @_;

    if (!ref($x)) {
        my $q = Math::GMPq::Rmpq_init();
        if (FAST_MODE and $x < ULONG_MAX and $x > LONG_MIN) {
            ($x < 0)
              ? Math::GMPq::Rmpq_set_si($q, $x, 1)
              : Math::GMPq::Rmpq_set_ui($q, $x, 1);
        }
        else {
            Math::GMPq::Rmpq_set_str($q, $x, 10);
        }
        return $q;
    }

    ref($x) eq 'Math::GMPq' && return $x;
    ref($x) eq 'Math::GMPz' && goto &_mpz2mpq;

    if (ref($x) eq 'Math::MPFR') {
        if (Math::MPFR::Rmpfr_number_p($x)) {
            my $q = Math::GMPq::Rmpq_init();
            Math::MPFR::Rmpfr_get_q($q, $x);
            return $q;
        }
        return;
    }

    (@_) = _any2mpfr($x);
    goto &_any2mpq;
}

sub _fits_ulong {
    (!ref($_[0]))
      ? ($_[0] >= 0)
      : (ref($_[0]) eq 'Math::GMPz' and Math::GMPz::Rmpz_fits_ulong_p($_[0]));
}

sub _fits_slong {
    (!ref($_[0]))
      ? 1
      : (ref($_[0]) eq 'Math::GMPz' and Math::GMPz::Rmpz_fits_slong_p($_[0]));
}

sub _get_ulong {
    (!ref($_[0]))
      ? $_[0]
      : Math::GMPz::Rmpz_get_ui($_[0]);
}

sub _get_slong {
    (!ref($_[0]))
      ? $_[0]
      : Math::GMPz::Rmpz_get_si($_[0]);
}

#
## Anything to an unsigned native integer
#
sub _any2ui {
    my ($x) = @_;
    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        return (($x < 0) ? undef : $x);
    }

  Math_GMPz: {

        if (Math::GMPz::Rmpz_fits_ulong_p($x)) {
            return Math::GMPz::Rmpz_get_ui($x);
        }

        state $t = Math::GMPz::Rmpz_init_set_str_nobless(join('', ~0), 10);

        if (Math::GMPz::Rmpz_sgn($x) >= 0 and Math::GMPz::Rmpz_cmp($x, $t) <= 0) {
            return Math::GMPz::Rmpz_get_str($x, 10);
        }

        return;
    }

  Math_GMPq: {

        if (Math::GMPq::Rmpq_integer_p($x)) {
            @_ = ($x = _mpq2mpz($x));
            goto Math_GMPz;
        }

        my $d = CORE::int(Math::GMPq::Rmpq_get_d($x));
        return (($d < 0 or $d > ULONG_MAX) ? undef : $d);
    }

  Math_MPFR: {

        if (Math::MPFR::Rmpfr_integer_p($x) and Math::MPFR::Rmpfr_fits_ulong_p($x, $ROUND)) {
            return Math::MPFR::Rmpfr_get_ui($x, $ROUND);
        }

        if (Math::MPFR::Rmpfr_number_p($x)) {
            my $d = CORE::int(Math::MPFR::Rmpfr_get_d($x, $ROUND));
            return (($d < 0 or $d > ULONG_MAX) ? undef : $d);
        }

        return;
    }

  Math_MPC: {
        @_ = ($x = _any2mpfr($x));
        goto Math_MPFR;
    }
}

#
## Anything to a signed native integer
#
sub _any2si {
    my ($x) = @_;
    goto((ref($x) || return $x) =~ tr/:/_/rs);

  Math_GMPz: {

        if (Math::GMPz::Rmpz_fits_slong_p($x)) {
            return Math::GMPz::Rmpz_get_si($x);
        }

        if (Math::GMPz::Rmpz_fits_ulong_p($x)) {
            return Math::GMPz::Rmpz_get_ui($x);
        }

        return;
    }

  Math_GMPq: {

        if (Math::GMPq::Rmpq_integer_p($x)) {
            @_ = ($x = _mpq2mpz($x));
            goto Math_GMPz;
        }

        my $d = CORE::int(Math::GMPq::Rmpq_get_d($x));
        return (($d < LONG_MIN or $d > ULONG_MAX) ? undef : $d);
    }

  Math_MPFR: {

        if (Math::MPFR::Rmpfr_integer_p($x)) {
            if (Math::MPFR::Rmpfr_fits_slong_p($x, $ROUND)) {
                return Math::MPFR::Rmpfr_get_si($x, $ROUND);
            }

            if (Math::MPFR::Rmpfr_fits_ulong_p($x, $ROUND)) {
                return Math::MPFR::Rmpfr_get_ui($x, $ROUND);
            }
        }

        if (Math::MPFR::Rmpfr_number_p($x)) {
            my $d = CORE::int(Math::MPFR::Rmpfr_get_d($x, $ROUND));
            return (($d < LONG_MIN or $d > ULONG_MAX) ? undef : $d);
        }

        return;
    }

  Math_MPC: {
        @_ = ($x = _any2mpfr($x));
        goto Math_MPFR;
    }
}

# Big to (signed) integer-string
sub _big2istr {
    my ($x) = @_;

    ref($x) || return $x;

    # die ((caller(1))[3], ' -- ', (caller(0))[2]) if index(ref($x), 'Sidef::') == 0;

    $x = (_any2mpz($x) // return undef) if ref($x) ne 'Math::GMPz';

    Math::GMPz::Rmpz_fits_slong_p($x)
      ? Math::GMPz::Rmpz_get_si($x)
      : Math::GMPz::Rmpz_get_str($x, 10);
}

# Big to unsigned (non-negative) integer-string
sub _big2uistr {
    my ($x) = @_;

    ref($x) || return (($x >= 0) ? $x : undef);

    # die ((caller(1))[3], ' -- ', (caller(0))[2]) if index(ref($x), 'Sidef::') == 0;

    $x = (_any2mpz($x) // return undef) if ref($x) ne 'Math::GMPz';

    Math::GMPz::Rmpz_fits_ulong_p($x)
      ? Math::GMPz::Rmpz_get_ui($x)
      : ((Math::GMPz::Rmpz_sgn($x) >= 0) ? Math::GMPz::Rmpz_get_str($x, 10) : undef);
}

# Big to positive integer-string
sub _big2pistr {
    my ($x) = @_;

    ref($x) || return (($x > 0) ? $x : undef);

    # die ((caller(1))[3], ' -- ', (caller(0))[2]) if index(ref($x), 'Sidef::') == 0;

    $x = (_any2mpz($x) // return undef) if ref($x) ne 'Math::GMPz';

    if (Math::GMPz::Rmpz_fits_ulong_p($x)) {
        my $ui = Math::GMPz::Rmpz_get_ui($x);
        return (($ui > 0) ? $ui : undef);
    }

    (Math::GMPz::Rmpz_sgn($x) > 0)
      ? Math::GMPz::Rmpz_get_str($x, 10)
      : undef;
}

sub _array {
    Sidef::Types::Array::Array->new(@_);
}

sub _sanitize_mpz {
    my ($n, $k) = @_;

    if (ref($n) ne 'Math::GMPz') {
        __is_int__($n) || return undef;
        $n = _any2mpz($n, $k) // return undef;
    }

    $n;
}

sub _normalize_numeric_type {
    my ($n) = @_;
    ref($n) eq 'Math::GMPz'
      ? (
           Math::GMPz::Rmpz_fits_ulong_p($n)
         ? Math::GMPz::Rmpz_get_ui($n)
         : Math::GMPz::Rmpz_get_str($n, 10)
        )
      : $n;
}

sub _execute_pari_gp {
    my ($code) = @_;
    say STDERR ":: Executing PARI/GP with: $code" if $VERBOSE;
    my $res = `$^X -e 'print \$ARGV[0]' \Q$code\E | gp -q -f --default parisizemax=500000000`;
    (defined($res) and $? == 0) or return undef;
    chomp($res);
    $res eq '' and return undef;
    return $res;
}

sub _execute_in_tmpdir {
    my ($cmd) = @_;

    my $cwd = Sidef::Types::Glob::Dir->cwd;

    # The directory is deleted when the object goes out of scope.
    require File::Temp;
    my $tmp = File::Temp->newdir(CLEANUP => 1);

    CORE::chdir($tmp);

    my $output    = `$cmd`;
    my $exit_code = $?;

    $cwd->chdir;

    return ($output, $exit_code);
}

sub _is_prob_prime {
    my ($n, $cache) = @_;

    $n = _normalize_numeric_type($n);

    state %internal_cache;
    state $internal_cache_size = 0;

    if (exists($internal_cache{$n})) {
        ## say "Prime cache hit: $n (entries: $internal_cache_size)";
        return $internal_cache{$n};
    }

    my $r = (HAS_PRIME_UTIL and $n < ULONG_MAX) ? Math::Prime::Util::is_prime($n) : do {
        (
         (CORE::length($n) > PRIMALITY_PRETEST_MIN)
         ? do {
             state $t = Math::GMPz::Rmpz_init();
             Math::GMPz::Rmpz_set_str($t, "$n", 10);
             _primality_pretest($t);
           }
         : 1
        )
          && Math::Prime::Util::GMP::is_prime($n);
    };

    if ($cache) {
        if (++$internal_cache_size > IS_PRIME_CACHE_SIZE) {
            $internal_cache_size = 1;
            undef %internal_cache;
        }
        $internal_cache{$n} = $r;
    }

    $r;
}

# --- Helper Methods ---

sub _sort_gmp_factors {
    return map { $_->[0] }
      sort { Math::GMPz::Rmpz_cmp($a->[1], $b->[1]) }
      map { [$_, Math::GMPz::Rmpz_init_set_str($_, 10)] } @_;
}

sub _factor_via_prime_util {
    my ($n) = @_;
    (HAS_PRIME_UTIL and $n < ULONG_MAX)
      ? Math::Prime::Util::factor($n)
      : Math::Prime::Util::GMP::factor($n);
}

# --- Specific Factorization Strategies ---

sub _factor_special {
    my ($n) = @_;
    local $SPECIAL_FACTORS = 0;
    say STDERR "Looking for special factors: $n" if $VERBOSE;

    my @special_factors;
    my $composite_factors = 0;
    my $limit             = (CORE::length($n) <= YAFU_MIN) ? ZERO : ();

    foreach my $p (@{_set_int($n)->special_factors($limit)}) {
        if (_is_prob_prime($$p, 1)) {
            push @special_factors, "$$p";
        }
        else {
            say STDERR "Recursively factoring: $$p" if $VERBOSE;
            $composite_factors = 1;
            push @special_factors, _factor($$p);
        }
    }

    return () unless @special_factors;
    return $composite_factors ? _sort_gmp_factors(@special_factors) : @special_factors;
}

sub _factor_factordb {
    my ($n) = @_;
    say STDERR "FactorDB: factoring $n" if $VERBOSE;

    require HTTP::Tiny;
    my $http     = HTTP::Tiny->new;
    my $response = $http->get("http://factordb.com/api?query=$n");

    return () unless $response->{success};

    my $data = eval { require JSON::PP; JSON::PP::decode_json($response->{content}) };
    return () unless (defined($data) && ref($data) eq 'HASH' && ref($data->{factors}) eq 'ARRAY');

    my @factordb_factors;
    my @unknown_factors;

    foreach my $pp (@{$data->{factors}}) {
        my ($p, $e) = @$pp;

        if (_is_prob_prime($p, 1)) {
            push @factordb_factors, ($p) x Math::Prime::Util::GMP::valuation($n, $p);
        }
        else {
            say STDERR "FactorDB: composite factor $p" if $VERBOSE;
            local $USE_FACTORDB = 0;
            my @arr = _factor($p);
            push @unknown_factors, @arr;
            push @factordb_factors, (@arr) x Math::Prime::Util::GMP::valuation($n, $p);
        }
    }

    if (@unknown_factors) {
        say STDERR "FactorDB: sending new factors to factordb.com" if $VERBOSE;
        my $resp = $http->post_form("http://factordb.com/report.php", {msub => "$n = " . join(' * ', @unknown_factors)});
        if ($VERBOSE) {
            say STDERR (
                        $resp->{success}
                        ? (
                           $resp->{content} =~ /Thank you/
                           ? "FactorDB: thank you for your contribution!"
                           : "FactorDB: sent factors successfully!"
                          )
                        : "FactorDB: failed to send the new factors..."
                       );
        }
    }

    my $factors_prod = Math::Prime::Util::GMP::vecprod(@factordb_factors);

    if ($factors_prod ne $n and Math::Prime::Util::GMP::modint($n, $factors_prod) eq '0') {
        say STDERR "FactorDB: recursively factoring the remainder." if $VERBOSE;
        my $r = Math::Prime::Util::GMP::divint($n, $factors_prod);
        local $USE_FACTORDB = 0;
        push @factordb_factors, _factor($r);
        $factors_prod = Math::Prime::Util::GMP::mulint($factors_prod, $r);
    }

    if ($factors_prod eq $n) {
        say STDERR "FactorDB: successful factorization." if $VERBOSE;
        return _sort_gmp_factors(@factordb_factors);
    }

    return ();
}

sub _factor_yafu {
    my ($n) = @_;
    say STDERR "YAFU: factoring $n" if $VERBOSE;

    my ($yafu_output, $exit_code) = _execute_in_tmpdir("yafu $n");
    return () unless ($exit_code == 0 and defined($yafu_output));

    my @yafu_factors;
    while ($yafu_output =~ /^([PC])\d+\s*=\s*([0-9]+)/gm) {
        my ($status, $factor) = ($1, $2);
        if ($status eq 'P') {
            push @yafu_factors, $factor;
        }
        else {
            local $USE_YAFU = 0;
            push @yafu_factors, _factor($factor);
        }
    }

    # Ensure all factors are prime, otherwise recurse
    if (grep { !_is_prob_prime($_, 1) } List::Util::uniq(@yafu_factors)) {
        say STDERR "YAFU: not all factors are prime." if $VERBOSE;
        local $USE_YAFU = 0;
        @yafu_factors = map { _factor($_) } @yafu_factors;
    }

    my $factors_prod = Math::Prime::Util::GMP::vecprod(@yafu_factors);

    if ($factors_prod ne $n) {
        say STDERR "YAFU: factors do not multiply to n." if $VERBOSE;
        if (Math::Prime::Util::GMP::modint($n, $factors_prod) eq '0') {
            say STDERR "YAFU: recomputing multiplicities..." if $VERBOSE;

            my @corrected_factors;
            foreach my $p (List::Util::uniq(@yafu_factors)) {
                next if $n eq $p;
                my $v = Math::Prime::Util::GMP::valuation($n, $p);
                push(@corrected_factors, ($p) x $v) if $v > 0;
            }

            @yafu_factors = @corrected_factors;
            $factors_prod = Math::Prime::Util::GMP::vecprod(@yafu_factors);

            if ($factors_prod ne $n and Math::Prime::Util::GMP::modint($n, $factors_prod) eq '0') {
                say STDERR "YAFU: recursively factoring the remainder." if $VERBOSE;
                my $r = Math::Prime::Util::GMP::divint($n, $factors_prod);
                local $USE_YAFU = 0;
                push @yafu_factors, _factor($r);
                $factors_prod = Math::Prime::Util::GMP::mulint($factors_prod, $r);
            }
        }
        else {
            say STDERR "YAFU: product of factors do not divide n." if $VERBOSE;
        }
    }

    if ($factors_prod eq $n) {
        say STDERR "YAFU: successful factorization: @yafu_factors" if $VERBOSE;
        return _sort_gmp_factors(@yafu_factors);
    }

    return ();
}

sub _factor {
    my ($n) = @_;
    my $orig_n = $n;

    $n = _normalize_numeric_type($n);

    # Fast path for small native numbers
    return _factor_via_prime_util($n) if (FAST_MODE and $n < ULONG_MAX);

    my @factors;

    # Adaptive trial factorization for exceptionally large numbers
    if (CORE::length($n) > 500) {
        ($n, @factors) = _adaptive_trial_factor($n);

        if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
            return @factors if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;
            $n = Math::GMPz::Rmpz_get_ui($n);
        }
        else {
            $n = Math::GMPz::Rmpz_get_str($n, 10);
        }
    }

    # Centralized prime check for large remaining composites
    my $len = CORE::length($n);
    if ($len >= SPECIAL_FACTORS_MIN || $len >= FACTORDB_MIN || $len >= YAFU_MIN) {
        if (_is_prob_prime($n, 1)) {
            push @factors, $n;
            return @factors;
        }
    }

    # 1. Special Factors Strategy
    if ($len >= SPECIAL_FACTORS_MIN and $SPECIAL_FACTORS) {

        # Throw away the small factors (if any)
        my @special = _factor_special($orig_n);
        return @special if @special;
    }

    # 2. FactorDB Strategy
    if ($len >= FACTORDB_MIN and $USE_FACTORDB) {
        my @fdb = _factor_factordb($n);
        if (@fdb) {
            push @factors, @fdb;
            return @factors;
        }
    }

    # 3. YAFU Strategy
    if ($len >= YAFU_MIN and $USE_YAFU and -t STDIN) {
        my @yafu = _factor_yafu($n);
        if (@yafu) {
            push @factors, @yafu;
            return @factors;
        }
    }

    # 4. Fallback Strategy
    push @factors, _factor_via_prime_util($n);
    return @factors;
}

# Prime factorization in [p,k] form, where k is the multiplicity of p.
sub _factor_exp {
    my ($n) = @_;

    $n = _normalize_numeric_type($n);

    if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
        return Math::Prime::Util::factor_exp($n);
    }

    my @factors = _factor($n);

    my $prev_value = shift(@factors) // return;
    my @factor_exp = [$prev_value, 1];

    foreach my $curr_value (@factors) {
        if ($curr_value eq $prev_value) {
            ++$factor_exp[-1][1];
        }
        else {
            CORE::push(@factor_exp, [$curr_value, 1]);
        }
        $prev_value = $curr_value;
    }

    @factor_exp;
}

sub _divisors {
    my ($n) = @_;

    $n = _normalize_numeric_type($n);

    if (CORE::length($n) >= SPECIAL_FACTORS_MIN) {
        my $t = _set_int($n);
        my $D = $t->divisors($t);
        return map { (ref($$_) eq 'Math::GMPz') ? Math::GMPz::Rmpz_get_str($$_, 10) : $$_ } @$D;
    }

    (HAS_PRIME_UTIL and $n < ULONG_MAX)
      ? Math::Prime::Util::divisors($n)
      : Math::Prime::Util::GMP::divisors($n);
}

sub _primes {
    my ($lo, $hi) = @_;

    if (HAS_PRIME_UTIL and $hi < ULONG_MAX) {
        return Math::Prime::Util::primes("$lo", "$hi");
    }

    [Math::Prime::Util::GMP::sieve_primes("$lo", "$hi")];
}

sub _cached_pn_primorial {
    my ($k) = @_;
    state @pn_primorial;
    $pn_primorial[$k] //= Math::GMPz::Rmpz_init_set_str_nobless(Math::Prime::Util::GMP::pn_primorial($k), 10);
}

sub _cached_primorial {
    my ($k, $limit) = @_;
    $limit //= 100;

    state %primorial_cache;
    state %access_count;

    if (exists $primorial_cache{$k}) {
        $access_count{$k}++;
        return $primorial_cache{$k};
    }

    if (scalar(keys(%primorial_cache)) >= $limit) {

        # Evict least-used entry
        my $min_key = (
                       sort { $a->[1] <=> $b->[1] }
                       map  { [$_, $access_count{$_}] } keys %primorial_cache
                      )[0][0];

        Math::GMPz::Rmpz_clear($primorial_cache{$min_key});
        delete $primorial_cache{$min_key};
        delete $access_count{$min_key};
    }

    # Compute and cache
    say STDERR "Computing primorial($k)..." if ($k >= 1e6 and $VERBOSE);
    my $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_primorial_ui($t, $k);
    $primorial_cache{$k} = $t;
    $access_count{$k}    = 1;

    return $t;
}

sub _is_squarefree {
    my ($n) = @_;

    $n = _normalize_numeric_type($n);

    if (CORE::length($n) >= SPECIAL_FACTORS_MIN) {
        my @factors = _factor($n);
        my %seen;
        foreach my $f (@factors) {
            if ($seen{$f}++) {
                return 0;
            }
        }
        return 1;
    }

    (HAS_PRIME_UTIL and $n < ULONG_MAX)
      ? Math::Prime::Util::is_square_free($n)
      : (Math::Prime::Util::GMP::moebius($n) != 0);
}

sub _next_prime {
    my ($n) = @_;

    $n = _normalize_numeric_type($n);

    (HAS_PRIME_UTIL and $n < (ULONG_MAX - 2000))
      ? Math::Prime::Util::next_prime($n)
      : Math::Prime::Util::GMP::next_prime($n);
}

sub _prev_prime {
    my ($n) = @_;

    $n = _normalize_numeric_type($n);

    (HAS_PRIME_UTIL and $n < ULONG_MAX)
      ? Math::Prime::Util::prev_prime($n)
      : Math::Prime::Util::GMP::prev_prime($n);
}

sub _random_prime {
    my ($n) = @_;

    (HAS_PRIME_UTIL and $n < ULONG_MAX)
      ? Math::Prime::Util::random_prime($n)
      : Math::Prime::Util::GMP::random_prime($n);
}

sub _primorial_trial_factor {
    my ($n, $k) = @_;

    # n is a positive > 1 Math::GMPz object
    # k is an unsigned integer

    my $B = _cached_primorial($k);

    state $g = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($g, $n, $B);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

        my $r = Math::GMPz::Rmpz_init_set($n);

        local $USE_YAFU        = 0;
        local $USE_FACTORDB    = 0;
        local $SPECIAL_FACTORS = 0;

        my @factors = _factor(Math::GMPz::Rmpz_get_str($g, 10));
        my @prime_factors;

        foreach my $f (@factors) {
            Math::GMPz::Rmpz_set_ui($g, $f);
            push @prime_factors, ($f) x Math::GMPz::Rmpz_remove($r, $r, $g);
        }

        return ($r, @prime_factors);
    }

    return ($n);
}

sub _adaptive_trial_factor {
    my ($n, $F, $L, $R) = @_;

    $F //= 2;
    $L //= 1e6;
    $R //= 2e6;

    if (ref($n) eq 'Math::GMPz') {
        $n = Math::GMPz::Rmpz_init_set($n);    # copy
    }
    else {
        $n = Math::GMPz::Rmpz_init_set_str($n, 10);
    }

    my @factors;

    my $P = _cached_primorial($L);

    state $g = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    while (1) {

        Math::GMPz::Rmpz_gcd($g, $P, $n);

        # Early stop when n seems to no longer have small factors
        if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
            last;
        }

        foreach my $p (@{_primes($F, $L)}) {
            if (Math::GMPz::Rmpz_divisible_ui_p($g, $p)) {

                Math::GMPz::Rmpz_set_ui($t, $p);
                push @factors, ($p) x Math::GMPz::Rmpz_remove($n, $n, $t);

                # Stop the loop early when no more primes divide `g` (optional)
                Math::GMPz::Rmpz_divexact_ui($g, $g, $p);
                last if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0);
            }
        }

        # Early stop when n has been fully factored or the trial range has been exhausted
        if ($L >= $R or Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
            last;
        }

        $F = $L;
        $L <<= 1;
        $P = _cached_primorial($L);
    }

    return ($n, @factors);
}

#
## Binary splitting
#

sub _binsplit {
    my ($arr, $func) = @_;

    while ($#$arr > 0) {
        my $x = CORE::shift(@$arr);
        my $y = CORE::shift(@$arr);
        push(@$arr, $func->($x, $y));
    }

    $arr->[0];
}

#
## Generic each
#

sub _generic_each {
    my ($from, $to, $block, $step_function, $buffer_callback) = @_;

    # `from` and `to` are Math::GMPz objects
    # `block` is a Sidef callback block
    # `step_function` is a Perl subroutine to compute the step given the initial current value
    # `buffer_callback` is a Perl subroutine that returns an ARRAY ref with the values in the given range

    if (Math::GMPz::Rmpz_cmp($from, $to) > 0) {
        return ZERO;
    }

    my @buffer;
    my $done  = 0;
    my $count = 0;

    for (; ;) {

        if (!@buffer) {

            last if $done;

            my $step = $step_function->($from);

            if ($step <= 0) {
                $step = 1e6;
            }

            my $upto = $from + $step;

            if ($upto >= $to) {
                $done = 1;
                $upto = $to;
            }

            #~ say ":: Sieving ($from, $upto) with step = $step";
            @buffer = @{$buffer_callback->($from, $upto) // die "[ERROR] Failed to sieve the range [$from, $upto]"};
            $from   = $upto + 1;
            @buffer || next;
        }

        ++$count;
        $block->run(_set_int(shift(@buffer)));
    }

    _set_int($count);
}

#
## Internal conversion methods
#

sub __boolify__ {
    my ($x) = @_;
    goto((ref($x) || return !!$x) =~ tr/:/_/rs);

  Math_MPFR: {
        return !!Math::MPFR::Rmpfr_sgn($x);
    }

  Math_GMPq: {
        return !!Math::GMPq::Rmpq_sgn($x);
    }

  Math_GMPz: {
        return !!Math::GMPz::Rmpz_sgn($x);
    }

  Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_RE($r, $x);
        Math::MPFR::Rmpfr_sgn($r)   && return 1;
        Math::MPFR::Rmpfr_nan_p($r) && return 0;
        Math::MPC::RMPC_IM($r, $x);
        return !!Math::MPFR::Rmpfr_sgn($r);
    }
}

sub __numify__ {
    my ($x) = @_;
    goto((ref($x) || return $x) =~ tr/:/_/rs);

  Math_GMPz: {

        if (Math::GMPz::Rmpz_fits_slong_p($x)) {
            return Math::GMPz::Rmpz_get_si($x);
        }

        if (Math::GMPz::Rmpz_fits_ulong_p($x)) {
            return Math::GMPz::Rmpz_get_ui($x);
        }

        return Math::GMPz::Rmpz_get_d($x);
    }

  Math_GMPq: {

        if (Math::GMPq::Rmpq_integer_p($x)) {
            $x = _mpq2mpz($x);
            goto Math_GMPz;
        }

        return Math::GMPq::Rmpq_get_d($x);
    }

  Math_MPFR: {
        if (Math::MPFR::Rmpfr_integer_p($x)) {
            if (Math::MPFR::Rmpfr_fits_slong_p($x, $ROUND)) {
                return Math::MPFR::Rmpfr_get_si($x, $ROUND);
            }

            if (Math::MPFR::Rmpfr_fits_ulong_p($x, $ROUND)) {
                return Math::MPFR::Rmpfr_get_ui($x, $ROUND);
            }
        }

        return Math::MPFR::Rmpfr_get_d($x, $ROUND);
    }

  Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_RE($r, $x);
        $x = $r;
        goto Math_MPFR;
    }
}

sub numify {
    (@_) = (${$_[0]});
    goto &__numify__;
}

sub __stringify__ {
    my ($x) = @_;
    goto((ref($x) || return $x) =~ tr/:/_/rs);

  Math_GMPz: {
        return Math::GMPz::Rmpz_get_str($x, 10);
    }

  Math_GMPq: {

        #return Math::GMPq::Rmpq_get_str($x, 10);

        Math::GMPq::Rmpq_integer_p($x)
          && return Math::GMPq::Rmpq_get_str($x, 10);

        $PREC = CORE::int($PREC) if ref($PREC);

        state $z = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set_q($z, $x);

        my $size = Math::GMPz::Rmpz_sizeinbase($z, 10) - 1;

        my $f = Math::MPFR::Rmpfr_init2(CORE::int(($size + $PREC / 4) * CORE::log(10) / CORE::log(2)) + 10);
        Math::MPFR::Rmpfr_set_q($f, $x, $ROUND);

        local $PREC = 4 * $size + $PREC;
        return __SUB__->($f);
    }

  Math_MPFR: {
        Math::MPFR::Rmpfr_number_p($x)
          || return (
                       Math::MPFR::Rmpfr_nan_p($x)   ? 'NaN'
                     : Math::MPFR::Rmpfr_sgn($x) < 0 ? '-Inf'
                     :                                 'Inf'
                    );

        # log(10)/log(2) =~ 3.3219280948873623
        my $digits = CORE::int($PREC) >> 2;
        my ($mantissa, $exponent) = Math::MPFR::Rmpfr_deref2($x, 10, $digits, $ROUND);

        my $sgn = '';
        if (substr($mantissa, 0, 1) eq '-') {
            $sgn = substr($mantissa, 0, 1, '');
        }

        $mantissa == 0 and return '0';

        if (CORE::abs($exponent) < CORE::length($mantissa)) {

            if ($exponent > 0) {
                substr($mantissa, $exponent, 0, '.');
            }
            else {
                substr($mantissa, 0, 0, '0.' . ('0' x CORE::abs($exponent)));
            }

            $mantissa = CORE::reverse($mantissa);
            $mantissa =~ s/^0+//;
            $mantissa =~ s/^\.//;
            $mantissa = CORE::reverse($mantissa);

            return ($sgn . $mantissa);
        }

        if (CORE::length($mantissa) > 1) {
            substr($mantissa, 1, 0, '.');
        }

        return ($sgn . $mantissa . 'e' . ($exponent - 1));
    }

  Math_MPC: {
        my $fr = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($fr, $x);
        my $re = __SUB__->($fr);

        Math::MPC::RMPC_IM($fr, $x);
        my $im = __SUB__->($fr);

        if ($im eq '0' or $im eq '-0') {
            return $re;
        }

        my $sign = '+';

        if (substr($im, 0, 1) eq '-') {
            $sign = '-';
            substr($im, 0, 1, '');
        }

        #$im = '' if $im eq '1';
        return ($re eq '0' ? $sign eq '+' ? "${im}i" : "$sign${im}i" : "$re $sign ${im}i");
    }
}

sub get_value {
    my ($x) = @_;

    $x = $$x;

    ref($x) || return $x;    # native integer

    # Convert fraction to a floating-point value, so it can be passed to Perl modules
    if (ref($x) eq 'Math::GMPq' and !Math::GMPq::Rmpq_integer_p($x)) {
        $x = _mpq2mpfr($x);
    }

    __stringify__($x);
}

#
## Public conversion methods
#

sub int {
    my ($x) = @_;
    (!ref($$x) or ref($$x) eq 'Math::GMPz') ? $x : do {
        my $t = _any2mpz($$x) // (goto &nan);
        $t = Math::GMPz::Rmpz_get_si($t) if Math::GMPz::Rmpz_fits_slong_p($t);
        bless \$t;
    };
}

*trunc  = \&int;
*to_i   = \&int;
*to_int = \&int;

sub rat {
    my ($x) = @_;
    ref($$x) eq 'Math::GMPq' ? $x : bless \(_any2mpq($$x) // (goto &nan));
}

*to_r   = \&rat;
*to_rat = \&rat;

sub float {
    my ($x) = @_;
    (ref($$x) eq 'Math::MPFR' || ref($$x) eq 'Math::MPC') ? $x : bless \_any2mpfr_mpc($$x);
}

*f        = \&float;
*to_f     = \&float;
*to_float = \&float;

sub to_poly {
    my ($x) = @_;
    Sidef::Types::Number::Polynomial->new(0 => $x);
}

sub eval {
    $_[0];
}

sub lift {
    $_[0];
}

sub complex {
    my ($x, $y) = @_;

    if (defined $y) {
        return Sidef::Types::Number::Complex->new($x, $y);
    }

    ref($$x) eq 'Math::MPC' ? $x : bless \_any2mpc($$x);
}

sub rat_approx {
    my ($x_ref, $max_den) = @_;

    # Algorithm from Math::Prime::Util

    if (defined($max_den)) {
        $max_den = _any2mpz($$max_den, 0) // goto &nan;
    }

    my $x = _any2mpfr($$x_ref);
    Math::MPFR::Rmpfr_number_p($x) || goto &nan;

    # 1. Count decimal places
    if (!defined($max_den)) {
        my $input_str      = __stringify__($x);
        my $decimal_places = (length($input_str) - index($input_str, '.'));
        $max_den = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_ui_pow_ui($max_den, 10, $decimal_places >> 1);
    }

    # Extract sign and work strictly with absolute value
    my $sign = Math::MPFR::Rmpfr_sgn($x);
    my $xabs = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_abs($xabs, $x, $ROUND);

    # Initialize previous state (p0, q0)
    state $p0 = Math::GMPz::Rmpz_init_nobless();
    state $q0 = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($p0, 1);
    Math::GMPz::Rmpz_set_ui($q0, 0);

    # Initialize current state (p1, q1)
    state $p1 = Math::GMPz::Rmpz_init_nobless();
    state $q1 = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($q1, 1);

    my $temp_f1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $temp_f2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    # p1 = floor(xabs)
    Math::MPFR::Rmpfr_floor($temp_f1, $xabs);
    Math::MPFR::Rmpfr_get_z($p1, $temp_f1, $ROUND);

    # remainder = xabs - p1
    my $remainder = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_sub_z($remainder, $xabs, $p1, $ROUND);

    # Working variables allocated once outside the loop
    state $m           = Math::GMPz::Rmpz_init_nobless();
    state $candidate_p = Math::GMPz::Rmpz_init_nobless();
    state $candidate_q = Math::GMPz::Rmpz_init_nobless();
    state $t           = Math::GMPz::Rmpz_init_nobless();

    while (Math::MPFR::Rmpfr_cmp_ui($remainder, 0) > 0) {

        # inv_remainder = 1 / remainder
        Math::MPFR::Rmpfr_ui_div($temp_f2, 1, $remainder, $ROUND);

        # m = floor(inv_remainder)
        Math::MPFR::Rmpfr_floor($temp_f1, $temp_f2);
        Math::MPFR::Rmpfr_get_z($m, $temp_f1, $ROUND);

        last if Math::GMPz::Rmpz_sgn($m) == 0;

        # remainder = inv_remainder - a
        Math::MPFR::Rmpfr_sub_z($remainder, $temp_f2, $m, $ROUND);

        # t = (max_den - q0) / q1   (Integer Division)
        Math::GMPz::Rmpz_sub($t, $max_den, $q0);
        Math::GMPz::Rmpz_tdiv_q($t, $t, $q1);

        if (Math::GMPz::Rmpz_cmp($m, $t) > 0) {

            if (Math::GMPz::Rmpz_cmp_ui($t, 1) >= 0) {

                # candidate_p = (t * p1) + p0
                Math::GMPz::Rmpz_mul($candidate_p, $t, $p1);
                Math::GMPz::Rmpz_add($candidate_p, $candidate_p, $p0);

                # candidate_q = (t * q1) + q0
                Math::GMPz::Rmpz_mul($candidate_q, $t, $q1);
                Math::GMPz::Rmpz_add($candidate_q, $candidate_q, $q0);

                # Calculate candidate_error = abs(candidate_p - (xabs * candidate_q)) * q1
                Math::MPFR::Rmpfr_mul_z($temp_f1, $xabs, $candidate_q, $ROUND);       # xabs * candidate_q
                Math::MPFR::Rmpfr_z_sub($temp_f1, $candidate_p, $temp_f1, $ROUND);    # candidate_p - ...
                Math::MPFR::Rmpfr_abs($temp_f1, $temp_f1, $ROUND);                    # abs(...)
                Math::MPFR::Rmpfr_mul_z($temp_f2, $temp_f1, $q1, $ROUND);

                # Calculate current_error = abs(p1 - (xabs * q1)) * candidate_q
                Math::MPFR::Rmpfr_mul_z($temp_f1, $xabs, $q1, $ROUND);                # xabs * q1
                Math::MPFR::Rmpfr_z_sub($temp_f1, $p1, $temp_f1, $ROUND);             # p1 - ...
                Math::MPFR::Rmpfr_abs($temp_f1, $temp_f1, $ROUND);                    # abs(...)
                Math::MPFR::Rmpfr_mul_z($temp_f1, $temp_f1, $candidate_q, $ROUND);

                # if candidate_error < current_error
                if (Math::MPFR::Rmpfr_cmp($temp_f2, $temp_f1) < 0) {
                    Math::GMPz::Rmpz_set($p1, $candidate_p);
                    Math::GMPz::Rmpz_set($q1, $candidate_q);
                }
            }
            last;
        }

        # (m * p1) + p0
        Math::GMPz::Rmpz_mul($t, $m, $p1);
        Math::GMPz::Rmpz_add($t, $t, $p0);
        Math::GMPz::Rmpz_set($p0, $p1);
        Math::GMPz::Rmpz_set($p1, $t);

        # (m * q1) + q0
        Math::GMPz::Rmpz_mul($t, $m, $q1);
        Math::GMPz::Rmpz_add($t, $t, $q0);
        Math::GMPz::Rmpz_set($q0, $q1);
        Math::GMPz::Rmpz_set($q1, $t);
    }

    # Restore negative sign if required
    Math::GMPz::Rmpz_neg($p1, $p1) if $sign < 0;

    my $ans_q = Math::GMPq::Rmpq_init();
    Math::GMPq::Rmpq_set_num($ans_q, $p1);
    Math::GMPq::Rmpq_set_den($ans_q, $q1);
    Math::GMPq::Rmpq_canonicalize($ans_q);

    bless \$ans_q;
}

*best_rational_approx = \&rat_approx;

sub pair {
    my ($x, $y) = @_;
    Sidef::Types::Number::Complex->new($x, $y);
}

sub __norm__ {
    my ($x) = @_;

    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        my $r = $x * $x;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $x = _any2mpz($x);
        goto Math_GMPz;
    }

  Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_norm($r, $x, $ROUND);
        return $r;
    }

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sqr($r, $x, $ROUND);
        return $r;
    }

  Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mul($r, $x, $x);
        return $r;
    }

  Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_mul($r, $x, $x);
        return $r;
    }
}

sub norm {
    my ($x) = @_;
    bless \__norm__($$x);
}

sub conj {
    my ($x) = @_;
    ref($$x) eq 'Math::MPC' or return $x;
    my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
    Math::MPC::Rmpc_conj($r, $$x, $ROUND);
    bless \$r;
}

sub real {
    my ($x) = @_;

    if (ref($$x) eq 'Math::MPC') {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_RE($r, $$x);
        bless \$r;
    }
    else {
        $x;
    }
}

*re = \&real;

sub imag {
    my ($x) = @_;

    if (ref($$x) eq 'Math::MPC') {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_IM($r, $$x);
        bless \$r;
    }
    else {
        ZERO;
    }
}

*im        = \&imag;
*imaginary = \&imag;

sub reals {
    ($_[0]->real, $_[0]->imag);
}

sub parts {
    _array([$_[0]->reals]);
}

#
## CONSTANTS
#

sub pi {

    if (ref($_[0])) {
        goto &prime_count;
    }

    my $pi = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($pi, $ROUND);
    bless \$pi;
}

*π = \&pi;

sub tau {

    if (ref($_[0])) {
        goto &sigma0;
    }

    my $tau = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($tau, $ROUND);
    Math::MPFR::Rmpfr_mul_2ui($tau, $tau, 1, $ROUND);
    bless \$tau;
}

*τ = \&tau;

sub ln2 {
    my $ln2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_log2($ln2, $ROUND);
    bless \$ln2;
}

sub EulerGamma {
    my $euler = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_euler($euler, $ROUND);
    bless \$euler;
}

*γ           = \&EulerGamma;
*y           = \&EulerGamma;
*euler_gamma = \&EulerGamma;

sub CatalanG {
    my $catalan = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_catalan($catalan, $ROUND);
    bless \$catalan;
}

*C = \&CatalanG;

sub i {
    my ($x) = @_;

    my $i = Math::MPC::Rmpc_init2(CORE::int($PREC));
    Math::MPC::Rmpc_set_ui_ui($i, 0, 1, $ROUND);

    if (ref($x)) {
        return bless \__mul__($i, $$x);
    }

    bless \$i;
}

sub e {
    my $e = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_set_ui($e, 1, $ROUND);
    Math::MPFR::Rmpfr_exp($e, $e, $ROUND);
    bless \$e;
}

sub phi {

    if (ref($_[0])) {
        goto &euler_phi;
    }

    my $phi = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_set_ui($phi, 5, $ROUND);
    Math::MPFR::Rmpfr_div_2ui($phi, $phi, 2, $ROUND);    # phi=5/4
    Math::MPFR::Rmpfr_sqrt($phi, $phi, $ROUND);
    Math::MPFR::Rmpfr_add_d($phi, $phi, 0.5, $ROUND);

    bless \$phi;
}

*φ = \&phi;

sub _nan {
    state $nan = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_nan($r);
        $r;
    };
}

sub nan {
    state $nan = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_nan($r);
        bless \$r;
    };
}

*NaN = \&nan;

sub _inf {
    state $inf = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_inf($r, 1);
        $r;
    };
}

sub inf {
    state $inf = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_inf($r, 1);
        bless \$r;
    };
}

*Inf = \&inf;

sub _ninf {
    state $ninf = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_inf($r, -1);
        $r;
    };
}

sub ninf {
    state $ninf = do {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_inf($r, -1);
        bless \$r;
    };
}

*zero = \&ZERO;
*one  = \&ONE;
*mone = \&MONE;

sub __add__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        my $r = $x + $y;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $r =
          ($x < 0)
          ? Math::GMPz::Rmpz_init_set_si($x)
          : Math::GMPz::Rmpz_init_set_ui($x);
        ($y < 0)
          ? Math::GMPz::Rmpz_sub_ui($r, $r, -$y)
          : Math::GMPz::Rmpz_add_ui($r, $r, $y);
        return $r;
    }

  Math_GMPz__Scalar: {
        my $r = Math::GMPz::Rmpz_init();
        ($y < 0)
          ? Math::GMPz::Rmpz_sub_ui($r, $x, -$y)
          : Math::GMPz::Rmpz_add_ui($r, $x, $y);
        ## $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPq__Scalar: {
        my $r = Math::GMPq::Rmpq_init();
        ($y < 0)
          ? Math::GMPq::Rmpq_set_si($r, $y, 1)
          : Math::GMPq::Rmpq_set_ui($r, $y, 1);
        Math::GMPq::Rmpq_add($r, $r, $x);
        return $r;
    }

  Math_MPFR__Scalar: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($y < 0)
          ? Math::MPFR::Rmpfr_sub_ui($r, $x, -$y, $ROUND)
          : Math::MPFR::Rmpfr_add_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Scalar: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        ($y < 0)
          ? Math::MPC::Rmpc_sub_ui($r, $x, -$y, $ROUND)
          : Math::MPC::Rmpc_add_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Scalar__Math_GMPz: {
        ($x, $y) = ($y, $x);
        goto Math_GMPz__Scalar;
    }

  Scalar__Math_GMPq: {
        ($x, $y) = ($y, $x);
        goto Math_GMPq__Scalar;
    }

  Scalar__Math_MPFR: {
        ($x, $y) = ($y, $x);
        goto Math_MPFR__Scalar;
    }

  Scalar__Math_MPC: {
        ($x, $y) = ($y, $x);
        goto Math_MPC__Scalar;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_add($r, $x, $y);
        ## $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPz__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_add_z($r, $y, $x);
        return $r;
    }

  Math_GMPz__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add_z($r, $y, $x, $ROUND);
        return $r;
    }

  Math_GMPz__Math_MPC: {
        my $c = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($c, $x, $ROUND);
        Math::MPC::Rmpc_add($c, $c, $y, $ROUND);
        return $c;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_add($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_GMPz: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_add_z($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add_q($r, $y, $x, $ROUND);
        return $r;
    }

  Math_GMPq__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $x, $ROUND);
        Math::MPC::Rmpc_add($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPq: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add_q($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPz: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_add_fr($r, $y, $x, $ROUND);
        return $r;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_add($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_add_fr($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPz: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $y, $ROUND);
        Math::MPC::Rmpc_add($r, $r, $x, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPq: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $y, $ROUND);
        Math::MPC::Rmpc_add($r, $r, $x, $ROUND);
        return $r;
    }
}

sub add {
    my ($x, $y) = @_;

    my $ref = ref($y);

    if (exists $NUMBER_CLASSES{$ref}) {
        return $y->add($x);
    }

    _valid(\$y);
    bless \__add__($$x, $$y);
}

sub __sub__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

  Scalar__Scalar: {
        my $r = $x - $y;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $r =
          ($x < 0)
          ? Math::GMPz::Rmpz_init_set_si($x)
          : Math::GMPz::Rmpz_init_set_ui($x);
        ($y < 0)
          ? Math::GMPz::Rmpz_add_ui($r, $r, -$y)
          : Math::GMPz::Rmpz_sub_ui($r, $r, $y);
        return $r;
    }

  Math_GMPz__Scalar: {
        state $r = Math::GMPz::Rmpz_init_nobless();
        ($y < 0)
          ? Math::GMPz::Rmpz_add_ui($r, $x, -$y)
          : Math::GMPz::Rmpz_sub_ui($r, $x, $y);
        my $r2 =
            Math::GMPz::Rmpz_fits_slong_p($r)
          ? Math::GMPz::Rmpz_get_si($r)
          : Math::GMPz::Rmpz_init_set($r);
        return $r2;
    }

  Math_GMPq__Scalar: {
        my $r = Math::GMPq::Rmpq_init();
        ($y < 0)
          ? Math::GMPq::Rmpq_set_si($r, $y, 1)
          : Math::GMPq::Rmpq_set_ui($r, $y, 1);
        Math::GMPq::Rmpq_sub($r, $x, $r);
        return $r;
    }

  Math_MPFR__Scalar: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($y < 0)
          ? Math::MPFR::Rmpfr_add_ui($r, $x, -$y, $ROUND)
          : Math::MPFR::Rmpfr_sub_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Scalar: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        ($y < 0)
          ? Math::MPC::Rmpc_add_ui($r, $x, -$y, $ROUND)
          : Math::MPC::Rmpc_sub_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Scalar__Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        ($x < 0)
          ? do {
            Math::GMPz::Rmpz_add_ui($r, $y, -$x);
            Math::GMPz::Rmpz_neg($r, $r);
          }
          : Math::GMPz::Rmpz_ui_sub($r, $x, $y);
        return $r;
    }

  Scalar__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        ($x < 0)
          ? Math::GMPq::Rmpq_set_si($r, $x, 1)
          : Math::GMPq::Rmpq_set_ui($r, $x, 1);
        Math::GMPq::Rmpq_sub($r, $r, $y);
        return $r;
    }

  Scalar__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($x < 0)
          ? Math::MPFR::Rmpfr_si_sub($r, $x, $y, $ROUND)
          : Math::MPFR::Rmpfr_ui_sub($r, $x, $y, $ROUND);
        return $r;
    }

  Scalar__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_set_ui($r, (($x < 0) ? (-$x) : $x), $ROUND);
        Math::MPC::Rmpc_neg($r, $r, $ROUND) if ($x < 0);
        Math::MPC::Rmpc_sub($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_sub($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_GMPz: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_sub_z($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sub_q($r, $y, $x, $ROUND);
        Math::MPFR::Rmpfr_neg($r, $r, $ROUND);
        return $r;
    }

  Math_GMPq__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $x, $ROUND);
        Math::MPC::Rmpc_sub($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_sub($r, $x, $y);
        $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPz__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_z_sub($r, $x, $y);
        return $r;
    }

  Math_GMPz__Math_MPFR: {

#<<<
        state $has_z_sub = (Math::MPFR::MPFR_VERSION_MAJOR() >  3)
                        || (Math::MPFR::MPFR_VERSION_MAJOR() == 3
                        &&  Math::MPFR::MPFR_VERSION_MINOR() >= 1);
#>>>

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        $has_z_sub ? Math::MPFR::Rmpfr_z_sub($r, $x, $y, $ROUND) : do {
            Math::MPFR::Rmpfr_sub_z($r, $y, $x, $ROUND);
            Math::MPFR::Rmpfr_neg($r, $r, $ROUND);
        };

        return $r;
    }

  Math_GMPz__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $x, $ROUND);
        Math::MPC::Rmpc_sub($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sub($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPq: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sub_q($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPz: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sub_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr($r, $x, $ROUND);
        Math::MPC::Rmpc_sub($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sub($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr($r, $y, $ROUND);
        Math::MPC::Rmpc_sub($r, $x, $r, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPz: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $y, $ROUND);
        Math::MPC::Rmpc_sub($r, $x, $r, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPq: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $y, $ROUND);
        Math::MPC::Rmpc_sub($r, $x, $r, $ROUND);
        return $r;
    }
}

sub sub {
    my ($x, $y) = @_;

    my $ref = ref($y);

    if (exists $NUMBER_CLASSES{$ref}) {
        return $y->with_value($x)->sub($y);
    }

    _valid(\$y);
    bless \__sub__($$x, $$y);
}

sub __mul__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        my $r = $x * $y;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $r =
          ($x < 0)
          ? Math::GMPz::Rmpz_init_set_si($x)
          : Math::GMPz::Rmpz_init_set_ui($x);
        ($y < 0)
          ? Math::GMPz::Rmpz_mul_si($r, $r, $y)
          : Math::GMPz::Rmpz_mul_ui($r, $r, $y);
        return $r;
    }

  Math_GMPz__Scalar: {
        $y || return 0;
        my $r = Math::GMPz::Rmpz_init();
        ($y < 0)
          ? Math::GMPz::Rmpz_mul_si($r, $x, $y)
          : Math::GMPz::Rmpz_mul_ui($r, $x, $y);
        ## $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPq__Scalar: {
        $y || return 0;
        my $r = Math::GMPq::Rmpq_init();
        ($y < 0)
          ? Math::GMPq::Rmpq_set_si($r, $y, 1)
          : Math::GMPq::Rmpq_set_ui($r, $y, 1);
        Math::GMPq::Rmpq_mul($r, $r, $x);
        return $r;
    }

  Math_MPFR__Scalar: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($y < 0)
          ? Math::MPFR::Rmpfr_mul_si($r, $x, $y, $ROUND)
          : Math::MPFR::Rmpfr_mul_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Scalar: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        ($y < 0)
          ? Math::MPC::Rmpc_mul_si($r, $x, $y, $ROUND)
          : Math::MPC::Rmpc_mul_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Scalar__Math_GMPz: {
        ($x, $y) = ($y, $x);
        goto Math_GMPz__Scalar;
    }

  Scalar__Math_GMPq: {
        ($x, $y) = ($y, $x);
        goto Math_GMPq__Scalar;
    }

  Scalar__Math_MPFR: {
        ($x, $y) = ($y, $x);
        goto Math_MPFR__Scalar;
    }

  Scalar__Math_MPC: {
        ($x, $y) = ($y, $x);
        goto Math_MPC__Scalar;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_mul($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_GMPz: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_mul_z($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_mul_q($r, $y, $x, $ROUND);
        return $r;
    }

  Math_GMPq__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $x, $ROUND);
        Math::MPC::Rmpc_mul($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mul($r, $x, $y);
        ## $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPz__Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_mul_z($r, $y, $x);
        return $r;
    }

  Math_GMPz__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_mul_z($r, $y, $x, $ROUND);
        return $r;
    }

  Math_GMPz__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $x, $ROUND);
        Math::MPC::Rmpc_mul($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_mul($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPq: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_mul_q($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPz: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_mul_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_mul_fr($r, $y, $x, $ROUND);
        return $r;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_mul($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_mul_fr($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPz: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $y, $ROUND);
        Math::MPC::Rmpc_mul($r, $r, $x, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPq: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $y, $ROUND);
        Math::MPC::Rmpc_mul($r, $r, $x, $ROUND);
        return $r;
    }
}

sub mul {
    my ($x, $y) = @_;

    my $ref = ref($y);
    if (exists $NUMBER_CLASSES{$ref}) {
        return $y->mul($x);
    }

    _valid(\$y);
    bless \__mul__($$x, $$y);
}

sub __div__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        if ($y != 0 and $x % $y == 0) {
            my $r =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divint($x, $y)
              : Math::Prime::Util::GMP::divint($x, $y);
            if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
                return $r;
            }
        }
        $x = _any2mpz($x);
        goto Math_GMPz__Scalar;
    }

  Math_GMPz__Scalar: {

        # Check for division by zero
        $y || do {
            $y = _any2mpz($y);
            goto Math_GMPz__Math_GMPz;
        };

        # Check for exact divisibility
        if (Math::GMPz::Rmpz_divisible_ui_p($x, CORE::abs($y))) {
            state $r = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_divexact_ui($r, $x, CORE::abs($y));
            Math::GMPz::Rmpz_neg($r, $r) if ($y < 0);
            my $r2 =
                Math::GMPz::Rmpz_fits_slong_p($r)
              ? Math::GMPz::Rmpz_get_si($r)
              : Math::GMPz::Rmpz_init_set($r);
            return $r2;
        }

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_ui($r, 1, CORE::abs($y));
        Math::GMPq::Rmpq_set_num($r, $x);
        Math::GMPq::Rmpq_neg($r, $r) if ($y < 0);
        Math::GMPq::Rmpq_canonicalize($r);
        return $r;
    }

  Scalar__Math_GMPz: {

        # Check for division by zero
        Math::GMPz::Rmpz_sgn($y) || do {
            $y = _mpz2mpfr($y);
            goto Scalar__Math_MPFR;
        };

        my $r = Math::GMPq::Rmpq_init();

        if ($x == 1 or $x == -1) {
            Math::GMPq::Rmpq_set_z($r, $y);
            Math::GMPq::Rmpq_inv($r, $r);
            Math::GMPq::Rmpq_neg($r, $r) if ($x < 0);
            return $r;
        }

        ($x < 0)
          ? Math::GMPq::Rmpq_set_si($r, $x, 1)
          : Math::GMPq::Rmpq_set_ui($r, $x, 1);

        Math::GMPq::Rmpq_set_den($r, $y);
        Math::GMPq::Rmpq_canonicalize($r);

        # If the result is an integer, return a GMPz object
        if (Math::GMPq::Rmpq_integer_p($r)) {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPq::Rmpq_get_num($z, $r);
            return $z;
        }

        return $r;
    }

  Math_GMPq__Scalar: {

        # Check for division by zero
        $y || do {
            $y = _any2mpz($y);
            goto Math_GMPq__Math_GMPz;
        };

        my $r = Math::GMPq::Rmpq_init();
        ($y < 0)
          ? Math::GMPq::Rmpq_set_si($r, -1, -$y)
          : Math::GMPq::Rmpq_set_ui($r, 1, $y);
        Math::GMPq::Rmpq_mul($r, $r, $x);
        return $r;
    }

  Scalar__Math_GMPq: {

        # Check for division by zero
        Math::GMPq::Rmpq_sgn($y) || do {
            $y = _mpq2mpfr($y);
            goto Scalar__Math_MPFR;
        };

        my $r = Math::GMPq::Rmpq_init();

        if ($x == 1 or $x == -1) {
            Math::GMPq::Rmpq_inv($r, $y);
            Math::GMPq::Rmpq_neg($r, $r) if ($x < 0);
            return $r;
        }

        ($x < 0)
          ? Math::GMPq::Rmpq_set_si($r, $x, 1)
          : Math::GMPq::Rmpq_set_ui($r, $x, 1);

        Math::GMPq::Rmpq_div($r, $r, $y);

        return $r;
    }

  Math_MPFR__Scalar: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($y < 0)
          ? Math::MPFR::Rmpfr_div_si($r, $x, $y, $ROUND)
          : Math::MPFR::Rmpfr_div_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Scalar__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        ($x < 0)
          ? Math::MPFR::Rmpfr_si_div($r, $x, $y, $ROUND)
          : Math::MPFR::Rmpfr_ui_div($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Scalar: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_set_ui($r, (($y < 0) ? (-$y) : $y), $ROUND);
        Math::MPC::Rmpc_neg($r, $r, $ROUND) if ($y < 0);
        Math::MPC::Rmpc_div($r, $x, $r, $ROUND);
        return $r;
    }

  Scalar__Math_MPC: {

        # XXX: don't use Rmpc_ui_div(), as it is broken in mpc=1.4.0
        my $r = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_set_ui($r, (($x < 0) ? (-$x) : $x), $ROUND);
        Math::MPC::Rmpc_neg($r, $r, $ROUND) if ($x < 0);
        Math::MPC::Rmpc_div($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {

        # Check for division by zero
        Math::GMPq::Rmpq_sgn($y) || do {
            $x = _mpq2mpfr($x);
            goto Math_MPFR__Math_GMPq;
        };

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_div($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_GMPz: {

        # Check for division by zero
        Math::GMPz::Rmpz_sgn($y) || do {
            $x = _mpq2mpfr($x);
            goto Math_MPFR__Math_GMPz;
        };

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_div_z($r, $x, $y);
        return $r;
    }

  Math_GMPq__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_q_div($r, $x, $y, $ROUND);
        return $r;
    }

  Math_GMPq__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $x, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {

        # Check for division by zero
        Math::GMPz::Rmpz_sgn($y) || do {
            $x = _mpz2mpfr($x);
            goto Math_MPFR__Math_GMPz;
        };

        # Check for exact divisibility
        if (Math::GMPz::Rmpz_divisible_p($x, $y)) {
            state $r = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_divexact($r, $x, $y);
            return (
                      Math::GMPz::Rmpz_fits_slong_p($r)
                    ? Math::GMPz::Rmpz_get_si($r)
                    : Math::GMPz::Rmpz_init_set($r)
                   );
        }

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_num($r, $x);
        Math::GMPq::Rmpq_set_den($r, $y);
        Math::GMPq::Rmpq_canonicalize($r);
        return $r;
    }

  Math_GMPz__Math_GMPq: {

        # Check for division by zero
        Math::GMPq::Rmpq_sgn($y) || do {
            $x = _mpz2mpfr($x);
            goto Math_MPFR__Math_GMPq;
        };

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_z_div($r, $x, $y);
        return $r;
    }

  Math_GMPz__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_z_div($r, $x, $y, $ROUND);
        return $r;
    }

  Math_GMPz__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $x, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPq: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div_q($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPz: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {

        # XXX: don't use Rmpc_fr_div(), as it is broken in mpc=1.4.0
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr($r, $x, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $y, $ROUND);
        return $r;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_div($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_div_fr($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPz: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_z($r, $y, $ROUND);
        Math::MPC::Rmpc_div($r, $x, $r, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPq: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_q($r, $y, $ROUND);
        Math::MPC::Rmpc_div($r, $x, $r, $ROUND);
        return $r;
    }
}

sub div {
    my ($x, $y) = @_;

    my $ref = ref($y);
    if (exists $NUMBER_CLASSES{$ref}) {
        return $y->with_value($x)->div($y);
    }

    _valid(\$y);
    bless \__div__($$x, $$y);
}

# Modular operations

sub addmod {
    my ($x, $y, $m) = @_;

    _valid(\$y, \$m);

    $x = $$x;
    $y = $$y;
    $m = $$m;

    if (FAST_MODE and !ref($m) and $m > 0 and !ref($x) and !ref($y)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::addmod($x, $y, $m)
          : Math::Prime::Util::GMP::addmod($x, $y, $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $m = _any2mpz($m, 2) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add($r, $x, $y);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub addmulmod {
    my ($x, $y, $z, $m) = @_;

    # r = (x + y*z) % m

    _valid(\$y, \$z, \$m);

    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and !ref($m) and $m > 0 and !ref($x) and !ref($y) and !ref($z)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::addmod($x, Math::Prime::Util::mulmod($y, $z, $m), $m)
          : Math::Prime::Util::GMP::addmod($x, Math::Prime::Util::GMP::mulmod($y, $z, $m), $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $z = _any2mpz($z, 2) // goto &nan;
    $m = _any2mpz($m, 3) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init_set($x);
    Math::GMPz::Rmpz_addmul($r, $y, $z);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub submulmod {
    my ($x, $y, $z, $m) = @_;

    _valid(\$y, \$z, \$m);

    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($x) and !ref($y) and !ref($z)) {
        my $r = Math::Prime::Util::submod($x, Math::Prime::Util::mulmod($y, $z, $m), $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $z = _any2mpz($z, 2) // goto &nan;
    $m = _any2mpz($m, 3) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init_set($x);
    Math::GMPz::Rmpz_submul($r, $y, $z);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub submod {
    my ($x, $y, $m) = @_;

    _valid(\$y, \$m);

    $x = $$x;
    $y = $$y;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($x) and !ref($y)) {
        my $r = Math::Prime::Util::submod($x, $y, $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $m = _any2mpz($m, 2) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub($r, $x, $y);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub mulmod {
    my ($x, $y, $m) = @_;

    _valid(\$y, \$m);

    $x = $$x;
    $y = $$y;
    $m = $$m;

    if (FAST_MODE and !ref($m) and $m > 0 and !ref($x) and !ref($y)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::mulmod($x, $y, $m)
          : Math::Prime::Util::GMP::mulmod($x, $y, $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $m = _any2mpz($m, 2) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $x, $y);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub muladdmod {
    my ($x, $y, $z, $m) = @_;

    _valid(\$y, \$z, \$m);

    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($x) and !ref($y) and !ref($z)) {
        my $r = Math::Prime::Util::muladdmod($x, $y, $z, $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $z = _any2mpz($z, 2) // goto &nan;
    $m = _any2mpz($m, 3) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init_set($z);
    Math::GMPz::Rmpz_addmul($r, $x, $y);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub mulsubmod {
    my ($x, $y, $z, $m) = @_;

    _valid(\$y, \$z, \$m);

    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($x) and !ref($y) and !ref($z)) {
        my $r = Math::Prime::Util::mulsubmod($x, $y, $z, $m);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;
    $z = _any2mpz($z, 2) // goto &nan;
    $m = _any2mpz($m, 3) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init_set($z);
    Math::GMPz::Rmpz_neg($r, $r);
    Math::GMPz::Rmpz_addmul($r, $x, $y);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub muladdmulmod {
    my ($w, $x, $y, $z, $m) = @_;

    _valid(\$x, \$y, \$z, \$m);

    $w = $$w;
    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($w) and !ref($x) and !ref($y) and !ref($z)) {
        my $r = Math::Prime::Util::muladdmod($w, $x, Math::Prime::Util::mulmod($y, $z, $m), $m);
        return bless \$r;
    }

    $w = _any2mpz($w, 0) // goto &nan;
    $x = _any2mpz($x, 1) // goto &nan;
    $y = _any2mpz($y, 2) // goto &nan;
    $z = _any2mpz($z, 3) // goto &nan;
    $m = _any2mpz($m, 4) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $w, $x);
    Math::GMPz::Rmpz_addmul($r, $y, $z);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

sub mulsubmulmod {
    my ($w, $x, $y, $z, $m) = @_;

    _valid(\$x, \$y, \$z, \$m);

    $w = $$w;
    $x = $$x;
    $y = $$y;
    $z = $$z;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($m) and $m > 0 and !ref($w) and !ref($x) and !ref($y) and !ref($z)) {
        my $r = Math::Prime::Util::mulsubmod($w, $x, Math::Prime::Util::mulmod($y, $z, $m), $m);
        return bless \$r;
    }

    $w = _any2mpz($w, 0) // goto &nan;
    $x = _any2mpz($x, 1) // goto &nan;
    $y = _any2mpz($y, 2) // goto &nan;
    $z = _any2mpz($z, 3) // goto &nan;
    $m = _any2mpz($m, 4) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) == 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $w, $x);
    Math::GMPz::Rmpz_submul($r, $y, $z);
    Math::GMPz::Rmpz_mod($r, $r, $m);
    bless \$r;
}

#
## Integer operations
#

sub iadd {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        return bless \__add__($x, $y);
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add($r, $x, $y);
    bless \$r;
}

*addint = \&iadd;

sub isub {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        return bless \__sub__($x, $y);
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub($r, $x, $y);
    bless \$r;
}

*subint = \&isub;

sub imul {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        return bless \__mul__($x, $y);
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $x, $y);
    bless \$r;
}

*mulint = \&imul;

sub imod {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        return bless \__mod__($x, $y);
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    my $sign_y = Math::GMPz::Rmpz_sgn($y)
      || goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($r, $x, $y);

    if (!Math::GMPz::Rmpz_sgn($r)) {
        ## OK
    }
    elsif ($sign_y < 0) {
        Math::GMPz::Rmpz_add($r, $r, $y);
    }

    bless \$r;
}

*modint = \&imod;

sub idiv {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y) and $x >= 0 and $y > 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divint($x, $y)
          : Math::Prime::Util::GMP::divint($x, $y);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // (goto &nan);

    if (FAST_MODE and !ref($y) and $y > 0) {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_div_ui($r, $x, $y);
        my $r2 =
            Math::GMPz::Rmpz_fits_slong_p($r)
          ? Math::GMPz::Rmpz_get_si($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    $y = _any2mpz($y, 1) // (goto &nan);

    # Detect division by zero
    Math::GMPz::Rmpz_sgn($y) || do {
        my $sign = Math::GMPz::Rmpz_sgn($x);

        if ($sign == 0) {    # 0/0
            goto &nan;
        }
        elsif ($sign > 0) {    # x/0 where: x > 0
            goto &inf;
        }
        else {                 # x/0 where: x < 0
            goto &ninf;
        }
    };

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_div($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*fdiv       = \&idiv;
*fld        = \&idiv;
*idiv_floor = \&idiv;
*divint     = \&idiv;

sub idiv_ceil {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y) and $x >= 0 and $y > 0) {
        my ($q, $r) =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divrem($x, $y)
          : Math::Prime::Util::GMP::divrem($x, $y);
        ++$q if $r;
        return bless \$q;
    }

    $x = _any2mpz($x, 0) // (goto &nan);

    if (FAST_MODE and !ref($y) and $y > 0) {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_cdiv_q_ui($r, $x, $y);
        my $r2 =
            Math::GMPz::Rmpz_fits_slong_p($r)
          ? Math::GMPz::Rmpz_get_si($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    $y = _any2mpz($y, 1) // (goto &nan);

    # Detect division by zero
    Math::GMPz::Rmpz_sgn($y) || do {
        my $sign = Math::GMPz::Rmpz_sgn($x);

        if ($sign == 0) {    # 0/0
            goto &nan;
        }
        elsif ($sign > 0) {    # x/0 where: x > 0
            goto &inf;
        }
        else {                 # x/0 where: x < 0
            goto &ninf;
        }
    };

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_cdiv_q($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*cld = \&idiv_ceil;

sub idiv_trunc {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpz($$x, 0) // (goto &nan);
    $y = _any2mpz($$y, 1) // (goto &nan);

    # Detect division by zero
    Math::GMPz::Rmpz_sgn($y) || do {
        my $sign = Math::GMPz::Rmpz_sgn($x);

        if ($sign == 0) {    # 0/0
            goto &nan;
        }
        elsif ($sign > 0) {    # x/0 where: x > 0
            goto &inf;
        }
        else {                 # x/0 where: x < 0
            goto &ninf;
        }
    };

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_tdiv_q($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*trd = \&idiv_trunc;

sub idiv_round {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpz($$x, 0) // (goto &nan);
    $y = _any2mpz($$y, 1) // (goto &nan);

    # Detect division by zero
    Math::GMPz::Rmpz_sgn($y) || do {
        my $sign = Math::GMPz::Rmpz_sgn($x);

        if ($sign == 0) {    # 0/0
            goto &nan;
        }
        elsif ($sign > 0) {    # x/0 where: x > 0
            goto &inf;
        }
        else {                 # x/0 where: x < 0
            goto &ninf;
        }
    };

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set($r, $y);
    Math::GMPz::Rmpz_addmul_ui($r, $x, 2);
    Math::GMPz::Rmpz_div($r, $r, $y);
    Math::GMPz::Rmpz_div_2exp($r, $r, 1);
    my $r2 =
        Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*rdd = \&idiv_round;

sub __neg__ {
    my ($x) = @_;

    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        my $r = -$x;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $r =
          ($x < 0)
          ? Math::GMPz::Rmpz_init_set_si($x)
          : Math::GMPz::Rmpz_init_set_ui($x);
        Math::GMPz::Rmpz_neg($r, $r);
        return $r;
    }

  Math_GMPz: {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_neg($r, $x);
        return $r;
    }

  Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_neg($r, $x);
        return $r;
    }

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_neg($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_neg($r, $x, $ROUND);
        return $r;
    }
}

sub neg {
    my ($x) = @_;
    bless \__neg__($$x);
}

sub ineg {
    my ($x) = @_;
    $x->to_i->neg;
}

*negint = \&ineg;

sub __abs__ {
    my ($x) = @_;
    goto((ref($x) || return CORE::abs($x)) =~ tr/:/_/rs);

  Math_GMPz: {
        Math::GMPz::Rmpz_sgn($x) >= 0 and return $x;
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_abs($r, $x);
        return $r;
    }

  Math_GMPq: {
        Math::GMPq::Rmpq_sgn($x) >= 0 and return $x;
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_abs($r, $x);
        return $r;
    }

  Math_MPFR: {
        Math::MPFR::Rmpfr_sgn($x) >= 0 and return $x;
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_abs($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($r, $x, $ROUND);
        return $r;
    }
}

sub abs {
    my ($x) = @_;
    bless \__abs__($$x);
}

sub iabs {
    my ($x) = @_;
    $x->to_i->abs;
}

*absint = \&iabs;

sub __inv__ {
    my ($x) = @_;

    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        $x = _any2mpz($x);
        goto Math_GMPz;
    }

  Math_GMPq: {

        # Check for division by zero
        Math::GMPq::Rmpq_sgn($x) || do {
            $x = _mpq2mpfr($x);
            goto Math_MPFR;
        };

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_inv($r, $x);
        return $r;
    }

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        return $r;
    }

  Math_GMPz: {

        # Check for division by zero
        Math::GMPz::Rmpz_sgn($x) || do {
            $x = _mpz2mpfr($x);
            goto Math_MPFR;
        };

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_z($r, $x);
        Math::GMPq::Rmpq_inv($r, $r);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        return $r;
    }
}

sub inv {
    my ($x) = @_;
    bless \__inv__($$x);
}

sub sqr {
    my ($x) = @_;
    bless \__mul__($$x, $$x);
}

*square = \&sqr;

sub cube {
    my ($x) = @_;
    bless \__pow__($$x, 3);
}

sub __sqrt__ {
    my ($x) = @_;

    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Complex for x < 0
        if (Math::MPFR::Rmpfr_sgn($x) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_sqrt($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sqrt($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sqrt($r, $x, $ROUND);
        return $r;
    }
}

sub sqrt {
    my ($x) = @_;
    bless \__sqrt__(_any2mpfr_mpc($$x));
}

sub sqrtQ {
    my ($x) = @_;
    Sidef::Types::Number::Quadratic->new(ZERO, ONE, $x);
}

sub __cbrt__ {
    my ($x) = @_;

    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Complex for x < 0
        if (Math::MPFR::Rmpfr_sgn($x) < 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_cbrt($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $three_inv = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_ui($three_inv, 3, $ROUND);
        Math::MPFR::Rmpfr_ui_div($three_inv, 1, $three_inv, $ROUND);

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_pow_fr($r, $x, $three_inv, $ROUND);
        return $r;
    }
}

sub cbrt {
    my ($x) = @_;
    bless \__cbrt__(_any2mpfr_mpc($$x));
}

sub __iroot__ {
    my ($x, $y) = @_;

    # $x is a Math::GMPz object
    # $y is a signed integer

    if ($y == 0) {
        Math::GMPz::Rmpz_sgn($x) || return $x;    # 0^Inf = 0

        # 1^Inf = 1 ; (-1)^Inf = 1
        if (Math::GMPz::Rmpz_cmpabs_ui($x, 1) == 0) {
            return 1;
        }

        goto &_inf;
    }
    elsif ($y < 0) {
        my $sign = Math::GMPz::Rmpz_sgn($x) || goto &_inf;    # 1 / 0^k = Inf
        Math::GMPz::Rmpz_cmp_ui($x, 1)      || return $x;     # 1 / 1^k = 1

        if ($sign < 0) {
            goto &_nan;
        }

        return 0;
    }
    elsif ($y % 2 == 0 and Math::GMPz::Rmpz_sgn($x) < 0) {
        goto &_nan;
    }

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_root($r, $x, $y);
    Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);
}

sub iroot {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if (FAST_MODE and $y >= 1 and !ref($x) and $x >= 0) {
        if ($y == 1) {
            return bless \$x;
        }
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::rootint($x, $y)
          : Math::Prime::Util::GMP::rootint($x, $y);
        return bless \$r;
    }

    bless \__iroot__(_any2mpz($x, 0) // (goto &nan), $y);
}

*rootint = \&iroot;

sub iroot_ceil {
    my ($n, $k) = @_;

    if ($n->is_power($k)) {
        return $n->iroot($k);
    }

    return $n->iroot($k)->inc;
}

sub isqrt {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x) and $x >= 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::sqrtint($x)
          : Math::Prime::Util::GMP::sqrtint($x);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($x) < 0 and goto &nan;

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_sqrt($r, $x);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*sqrtint = \&isqrt;

sub icbrt {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x) and $x >= 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::rootint($x, 3)
          : Math::Prime::Util::GMP::rootint($x, 3);
        return bless \$r;
    }

    bless \__iroot__(_any2mpz($x, 0) // (goto &nan), 3);
}

*cbrtint = \&icbrt;

sub isqrtrem {
    my ($x) = @_;

    $x = _any2mpz($$x, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($x) < 0
      and return ((nan()) x 2);

    my $r = Math::GMPz::Rmpz_init();
    my $s = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sqrtrem($r, $s, $x);
    ((bless \$r), (bless \$s));
}

sub irootrem {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpz($$x, 0) // (goto &nan);
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if ($y == 0) {
        Math::GMPz::Rmpz_sgn($x) || return (ZERO, MONE);    # 0^Inf = 0

        if (Math::GMPz::Rmpz_cmpabs_ui($x, 1) == 0) {       # 1^Inf = 1 ; (-1)^Inf = 1
            return (ONE, bless \__dec__($x));
        }

        return (inf(), bless \__dec__($x));
    }
    elsif ($y < 0) {
        my $sign = Math::GMPz::Rmpz_sgn($x) || return (inf(), ZERO);    # 1 / 0^k = Inf
        Math::GMPz::Rmpz_cmp_ui($x, 1) == 0 and return (ONE, ZERO);     # 1 / 1^k = 1
        return ($sign < 0 ? (nan(), nan()) : (ZERO, ninf()));
    }
    elsif ($y % 2 == 0 and Math::GMPz::Rmpz_sgn($x) < 0) {
        return (nan(), nan());
    }

    my $r = Math::GMPz::Rmpz_init();
    my $s = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_rootrem($r, $s, $x, $y);
    ((bless \$r), (bless \$s));
}

sub __pow__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {

        $y || return 1;

        if ($x >= 0 and $y > 0) {
            $x || return 0;
            if (CORE::log($x) * $y < CORE::log(ULONG_MAX)) {
                $x == 1 and return 1;
                my $r =
                  HAS_PRIME_UTIL
                  ? Math::Prime::Util::powint($x, $y)
                  : Math::Prime::Util::GMP::powint($x, $y);
                if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
                    return $r;
                }
            }
            my $r = Math::GMPz::Rmpz_init();
            $y < ULONG_MAX or goto &_nan;
            Math::GMPz::Rmpz_ui_pow_ui($r, $x, $y);
            return $r;
        }
        elsif ($x == -1) {
            return (($y % 2 == 0) ? 1 : -1);
        }
        elsif ($x < 0 and $y > 0) {
            if (CORE::log(CORE::abs($x)) * $y < CORE::log(CORE::abs(LONG_MIN))) {
                my $r =
                  HAS_PRIME_UTIL
                  ? Math::Prime::Util::powint($x, $y)
                  : Math::Prime::Util::GMP::powint($x, $y);
                if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
                    return $r;
                }
            }
            my $r = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_ui_pow_ui($r, CORE::abs($x), $y);
            Math::GMPz::Rmpz_neg($r, $r) if ($y & 1);
            return $r;
        }

        $x = _any2mpz($x);
        goto Math_GMPz__Scalar;
    }

  Scalar__Math_GMPz: {
        $y = Math::GMPz::Rmpz_get_d($y);
        goto Scalar__Scalar;
    }

  Scalar__Math_GMPq: {
        $x = _any2mpz($x);
        goto Math_GMPz__Math_GMPq;
    }

  Scalar__Math_MPC: {
        $x = _any2mpz($x);
        goto Math_GMPz__Math_MPC;
    }

  Scalar__Math_MPFR: {

        if ($x >= 0) {
            my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
            Math::MPFR::Rmpfr_ui_pow($r, $x, $y, $ROUND);
            return $r;
        }

        $x = _any2mpfr($x);
        goto Math_MPFR__Math_MPFR;
    }

    #
    ## GMPq
    #
  Math_GMPq__Scalar: {

        $y || return 1;

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_pow_ui($r, $x, CORE::abs($y));

        if ($y < 0) {
            Math::GMPq::Rmpq_sgn($r) || goto &_inf;
            Math::GMPq::Rmpq_inv($r, $r);
        }

        return $r;
    }

  Math_GMPq__Math_GMPq: {

        # Integer power
        if (Math::GMPq::Rmpq_integer_p($y)) {
            $y = Math::GMPq::Rmpq_get_d($y);
            goto Math_GMPq__Scalar;
        }

        # (-x)^(a/b) is a complex number
        elsif (Math::GMPq::Rmpq_sgn($x) < 0) {
            ($x, $y) = (_mpq2mpc($x), _mpq2mpc($y));
            goto Math_MPC__Math_MPC;
        }

        ($x, $y) = (_mpq2mpfr($x), _mpq2mpfr($y));
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPq__Math_GMPz: {
        $y = Math::GMPz::Rmpz_get_d($y);
        goto Math_GMPq__Scalar;
    }

  Math_GMPq__Math_MPFR: {
        $x = _mpq2mpfr($x);
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPq__Math_MPC: {
        $x = _mpq2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## GMPz
    #
  Math_GMPz__Scalar: {

        $y || return 1;

        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($r, $x, CORE::abs($y));

        if ($y < 0) {
            Math::GMPz::Rmpz_sgn($r) || goto &_inf;

            my $q = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_set_z($q, $r);
            Math::GMPq::Rmpq_inv($q, $q);
            return $q;
        }

        ## $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPz__Math_GMPz: {
        $y = Math::GMPz::Rmpz_get_d($y);
        goto Math_GMPz__Scalar;
    }

  Math_GMPz__Math_GMPq: {
        if (Math::GMPq::Rmpq_integer_p($y)) {
            $y = Math::GMPq::Rmpq_get_d($y);
            goto Math_GMPz__Scalar;
        }

        ($x, $y) = (_mpz2mpfr($x), _mpq2mpfr($y));
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPz__Math_MPFR: {
        $x = _mpz2mpfr($x);
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPz__Math_MPC: {
        $x = _mpz2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        if (    Math::MPFR::Rmpfr_sgn($x) < 0
            and !Math::MPFR::Rmpfr_integer_p($y)
            and Math::MPFR::Rmpfr_number_p($y)) {
            $x = _mpfr2mpc($x);
            goto Math_MPC__Math_MPFR;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_pow($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Scalar: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        ($y < 0)
          ? Math::MPFR::Rmpfr_pow_si($r, $x, $y, $ROUND)
          : Math::MPFR::Rmpfr_pow_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_GMPq: {
        $y = _mpq2mpfr($y);
        goto Math_MPFR__Math_MPFR;
    }

  Math_MPFR__Math_GMPz: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_pow_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_pow($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Scalar: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        ($y < 0)
          ? Math::MPC::Rmpc_pow_si($r, $x, $y, $ROUND)
          : Math::MPC::Rmpc_pow_ui($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_pow_fr($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPz: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_pow_z($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_GMPq: {
        $y = _mpq2mpfr($y);
        goto Math_MPC__Math_MPFR;
    }
}

sub root {
    my ($x, $y) = @_;
    _valid(\$y);
    bless \__pow__($$x, __inv__($$y));
}

sub pow {
    my ($x, $y) = @_;
    _valid(\$y);
    bless \__pow__($$x, $$y);
}

sub tetration {
    my ($x, $y) = @_;
    _valid(\$y);

    # Base cases
    return ONE if $y->is_zero;    # a^^0 = 1
    return $x  if $y->is_one;     # a^^1 = a

    # Recursive case: a^^b = a^(a^^(b-1))
    $x->pow($x->tetration($y->dec));
}

sub ipow {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if (FAST_MODE and $y >= 0 and !ref($x) and $x >= 0) {

        if ($x > 0 and CORE::log($x) * $y < CORE::log(ULONG_MAX)) {
            my $r =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::powint($x, $y)
              : Math::Prime::Util::GMP::powint($x, $y);
            return bless \$r;
        }

        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_ui_pow_ui($r, $x, $y);
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    if (FAST_MODE and !ref($x) and $x == -1) {
        return (($y % 2 == 0) ? ONE : MONE);
    }

    $x = _any2mpz($x, 0) // goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_pow_ui($r, $x, CORE::abs($y));

    if ($y < 0) {
        Math::GMPz::Rmpz_sgn($r) || goto &inf;    # 0^(-y) = Inf
        Math::GMPz::Rmpz_div($r, $ONE, $r);
    }

    bless \$r;
}

*powint = \&ipow;

sub ipow2 {
    my ($n) = @_;

    $n = $$n;
    $n = _any2si($n) // (goto &nan) if ref($n);

    ($n == 0) and return ONE;
    ($n == 1) and return TWO;
    ($n < 0)  and return ZERO;

    if (CORE::log(2) * $n < CORE::log(ULONG_MAX)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::powint(2, $n)
          : Math::Prime::Util::GMP::powint(2, $n);
        return bless \$r;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_setbit($r, $n);
    bless \$r;
}

sub ipow10 {
    my ($n) = @_;

    $n = $$n;
    $n = _any2si($n) // (goto &nan) if ref($n);

    ($n == 0) and return ONE;
    ($n == 1) and return TEN;
    ($n < 0)  and return ZERO;

    if (CORE::log(10) * $n < CORE::log(ULONG_MAX)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::powint(10, $n)
          : Math::Prime::Util::GMP::powint(10, $n);
        return bless \$r;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_ui_pow_ui($r, 10, $n);
    bless \$r;
}

sub __log2__ {
    my ($x) = @_;

    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Complex for x < 0
        if (Math::MPFR::Rmpfr_sgn($x) < 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_log2($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));

        if ($MPC_VERSION >= 66560) {    # available only in mpc>=1.4.0
            Math::MPC::Rmpc_log2($r, $x, $ROUND);
        }
        else {
            my $ln2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
            Math::MPFR::Rmpfr_const_log2($ln2, $ROUND);
            Math::MPC::Rmpc_log($r, $x, $ROUND);
            Math::MPC::Rmpc_div_fr($r, $r, $ln2, $ROUND);
        }
        return $r;
    }
}

sub __log10__ {
    my ($x) = @_;

    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Complex for x < 0
        if (Math::MPFR::Rmpfr_sgn($x) < 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_log10($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));

        if ($MPC_VERSION >= 65536) {    # available only in mpc>=1.0.0
            Math::MPC::Rmpc_log10($r, $x, $ROUND);
        }
        else {
            my $ln10 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
            Math::MPFR::Rmpfr_set_ui($ln10, 10, $ROUND);
            Math::MPFR::Rmpfr_log($ln10, $ln10, $ROUND);
            Math::MPC::Rmpc_log($r, $x, $ROUND);
            Math::MPC::Rmpc_div_fr($r, $r, $ln10, $ROUND);
        }

        return $r;
    }
}

sub __log__ {
    my ($x) = @_;

    goto(ref($x) =~ tr/:/_/rs);

    #
    ## MPFR
    #
  Math_MPFR: {

        # Complex for x < 0
        if (Math::MPFR::Rmpfr_sgn($x) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_log($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_log($r, $x, $ROUND);
        return $r;
    }

    #
    ## MPC
    #
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_log($r, $x, $ROUND);
        return $r;
    }
}

sub log {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        bless \__div__(__log__(_any2mpfr_mpc($$x)), __log__(_any2mpfr_mpc($$y)));
    }
    else {
        bless \__log__(_any2mpfr_mpc($$x));
    }
}

sub ln {
    my ($x) = @_;
    bless \__log__(_any2mpfr_mpc($$x));
}

sub log2 {
    my ($x) = @_;
    bless \__log2__(_any2mpfr_mpc($$x));
}

sub log10 {
    my ($x) = @_;
    bless \__log10__(_any2mpfr_mpc($$x));
}

sub __ilog__ {
    my ($x, $y) = @_;

    if (ref($y) eq 'Math::GMPz' and Math::GMPz::Rmpz_fits_ulong_p($y)) {
        $y = Math::GMPz::Rmpz_get_ui($y);
    }

    # ilog(x, y <= 1) = NaN
    $y <= 1 and return;

    # ilog(x <= 0, y) = NaN
    Math::GMPz::Rmpz_sgn($x) <= 0 and return;

    # ilog(x,y) = 0, when y > x
    (ref($y) ? Math::GMPz::Rmpz_cmp($x, $y) : Math::GMPz::Rmpz_cmp_ui($x, $y)) >= 0
      or return 0;

    if (!ref($y)) {

        # Return faster for y = 2
        if ($y == 2) {
            return Math::GMPz::Rmpz_sizeinbase($x, 2) - 1;
        }

        if (FAST_MODE and Math::GMPz::Rmpz_fits_ulong_p($x)) {
            return (
                    HAS_PRIME_UTIL
                    ? Math::Prime::Util::logint(Math::GMPz::Rmpz_get_ui($x), $y)
                    : Math::Prime::Util::GMP::logint(Math::GMPz::Rmpz_get_ui($x), $y)
                   );
        }
    }

    # Return faster for y <= 62
    if ($y <= 62) {

        # NOTE: size is always exact for base = 2.
        my $e = (Math::GMPz::Rmpz_sizeinbase($x, $y) || return) - 1;

        if ($y != 2 and $e > 0) {
            state $t = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_ui_pow_ui($t, $y, $e);
            Math::GMPz::Rmpz_cmp($t, $x) > 0 and --$e;
        }

        return $e;
    }

    # Make sure `y` is a Math::GMPz object
    $y = Math::GMPz::Rmpz_init_set_ui($y) if !ref($y);

    my $e = 0;

    state $t    = Math::GMPz::Rmpz_init_nobless();
    state $logx = Math::MPFR::Rmpfr_init2_nobless(64);
    state $logy = Math::MPFR::Rmpfr_init2_nobless(64);

    Math::MPFR::Rmpfr_set_z($logx, $x, $round_z);
    Math::MPFR::Rmpfr_set_z($logy, $y, $round_z);
    Math::MPFR::Rmpfr_log($logx, $logx, $round_z);
    Math::MPFR::Rmpfr_log($logy, $logy, $round_z);
    Math::MPFR::Rmpfr_div($logx, $logx, $logy, $round_z);

    if (Math::MPFR::Rmpfr_fits_ulong_p($logx, $round_z)) {
        $e = Math::MPFR::Rmpfr_get_ui($logx, $round_z) - 1;
        Math::GMPz::Rmpz_pow_ui($t, $y, $e + 1);
    }
    else {
        Math::GMPz::Rmpz_set($t, $y);
    }

    for (; Math::GMPz::Rmpz_cmp($t, $x) <= 0 ; Math::GMPz::Rmpz_mul($t, $t, $y)) {
        ++$e;
    }

    return $e;
}

sub ilog {
    my ($x, $y) = @_;
    if (defined($y)) {
        _valid(\$y);

        $x = $$x;
        $y = $$y;

        if (FAST_MODE and !ref($x) and !ref($y) and $x > 0 and $y > 1) {

            if ($y > $x) {
                return ZERO;
            }

            my $r =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::logint($x, $y)
              : Math::Prime::Util::GMP::logint($x, $y);
            return bless \$r;
        }

        _set_int(__ilog__((_any2mpz($x, 0) // goto &nan), (_any2mpz($y, 1) // goto &nan)) // goto &nan);
    }
    else {
        bless \(_any2mpz(__log__(_any2mpfr_mpc($$x))) // goto &nan);
    }
}

*logint = \&ilog;

sub ilog2 {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x) and $x > 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::logint($x, 2)
          : Math::Prime::Util::GMP::logint($x, 2);
        return bless \$r;
    }

    _set_int(__ilog__((_any2mpz($x, 0) // goto &nan), 2) // goto &nan);
}

sub ilog10 {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x) and $x > 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::logint($x, 10)
          : Math::Prime::Util::GMP::logint($x, 10);
        return bless \$r;
    }

    _set_int(__ilog__((_any2mpz($x, 0) // goto &nan), 10) // goto &nan);
}

sub msb {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;
    Math::GMPz::Rmpz_sgn($n) || return undef;

    _set_int(Math::GMPz::Rmpz_sizeinbase($n, 2) - 1);
}

sub lsb {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;
    Math::GMPz::Rmpz_sgn($n) || return undef;

    _set_int(Math::GMPz::Rmpz_scan1($n, 0));
}

sub fusc {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) >= 0 or goto &nan;

    if (Math::GMPz::Rmpz_even_p($n)) {
        $n = Math::GMPz::Rmpz_init_set($n);    # copy
        Math::GMPz::Rmpz_remove($n, $n, $TWO);
    }

    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n);

        my ($x, $y) = (1, 0);

        for (; $n > 0 ; $n >>= 1) {
            ($n & 1) ? ($y += $x) : ($x += $y);
        }

        return _set_int($y);
    }

    state $x = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_ui($x, 1);

    my $y = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $i (0 .. Math::GMPz::Rmpz_sizeinbase($n, 2) - 1) {
        if (Math::GMPz::Rmpz_tstbit($n, $i)) {
            Math::GMPz::Rmpz_add($y, $y, $x);
        }
        else {
            Math::GMPz::Rmpz_add($x, $x, $y);
        }
    }

    bless \$y;
}

sub __lgrt__ {
    my ($c) = @_;

    $PREC = CORE::int($PREC) if ref($PREC);

    my $p = Math::MPFR::Rmpfr_init2($PREC);
    Math::MPFR::Rmpfr_set_str($p, '1e-' . CORE::int($PREC >> 2), 10, $ROUND);

    goto(ref($c) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < e^(-1/e)
        if (Math::MPFR::Rmpfr_cmp_d($c, CORE::exp(-1 / CORE::exp(1))) < 0) {
            $c = _mpfr2mpc($c);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2($PREC);
        Math::MPFR::Rmpfr_log($r, $c, $ROUND);

        Math::MPFR::Rmpfr_set_ui((my $x = Math::MPFR::Rmpfr_init2($PREC)), 1, $ROUND);
        Math::MPFR::Rmpfr_set_ui((my $y = Math::MPFR::Rmpfr_init2($PREC)), 0, $ROUND);

        my $count = 0;
        my $tmp   = Math::MPFR::Rmpfr_init2($PREC);

        while (1) {
            Math::MPFR::Rmpfr_sub($tmp, $x, $y, $ROUND);
            Math::MPFR::Rmpfr_cmpabs($tmp, $p) <= 0 and last;

            Math::MPFR::Rmpfr_set($y, $x, $ROUND);

            Math::MPFR::Rmpfr_log($tmp, $x, $ROUND);
            Math::MPFR::Rmpfr_add_ui($tmp, $tmp, 1, $ROUND);

            Math::MPFR::Rmpfr_add($x, $x, $r, $ROUND);
            Math::MPFR::Rmpfr_div($x, $x, $tmp, $ROUND);
            last if ++$count > $PREC;
        }

        return $x;
    }

  Math_MPC: {
        my $d = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_log($d, $c, $ROUND);

        my $x = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_sqrt($x, $c, $ROUND);
        Math::MPC::Rmpc_add_ui($x, $x, 1, $ROUND);
        Math::MPC::Rmpc_log($x, $x, $ROUND);

        my $y = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_set_ui($y, 0, $ROUND);

        my $tmp = Math::MPC::Rmpc_init2($PREC);
        my $abs = Math::MPFR::Rmpfr_init2($PREC);

        my $count = 0;
        while (1) {
            Math::MPC::Rmpc_sub($tmp, $x, $y, $ROUND);

            Math::MPC::Rmpc_abs($abs, $tmp, $ROUND);
            Math::MPFR::Rmpfr_cmp($abs, $p) <= 0 and last;

            Math::MPC::Rmpc_set($y, $x, $ROUND);

            Math::MPC::Rmpc_log($tmp, $x, $ROUND);
            Math::MPC::Rmpc_add_ui($tmp, $tmp, 1, $ROUND);

            Math::MPC::Rmpc_add($x, $x, $d, $ROUND);
            Math::MPC::Rmpc_div($x, $x, $tmp, $ROUND);
            last if ++$count > $PREC;
        }

        return $x;
    }
}

sub lgrt {
    my ($x) = @_;
    bless \__lgrt__(_any2mpfr_mpc($$x));
}

sub __LambertW__ {
    my ($x) = @_;

    $PREC = CORE::int($PREC) if ref($PREC);

    my $p = Math::MPFR::Rmpfr_init2($PREC);
    Math::MPFR::Rmpfr_set_str($p, '1e-' . CORE::int($PREC >> 2), 10, $ROUND);

    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < -1/e
        if (Math::MPFR::Rmpfr_cmp_d($x, -1 / CORE::exp(1)) < 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        Math::MPFR::Rmpfr_set_ui((my $r = Math::MPFR::Rmpfr_init2($PREC)), 1, $ROUND);
        Math::MPFR::Rmpfr_set_ui((my $y = Math::MPFR::Rmpfr_init2($PREC)), 0, $ROUND);

        my $count = 0;
        my $tmp   = Math::MPFR::Rmpfr_init2($PREC);

        while (1) {
            Math::MPFR::Rmpfr_sub($tmp, $r, $y, $ROUND);
            Math::MPFR::Rmpfr_cmpabs($tmp, $p) <= 0 and last;

            Math::MPFR::Rmpfr_set($y, $r, $ROUND);

            Math::MPFR::Rmpfr_log($tmp, $r, $ROUND);
            Math::MPFR::Rmpfr_add_ui($tmp, $tmp, 1, $ROUND);

            Math::MPFR::Rmpfr_add($r, $r, $x, $ROUND);
            Math::MPFR::Rmpfr_div($r, $r, $tmp, $ROUND);
            last if ++$count > $PREC;
        }

        Math::MPFR::Rmpfr_log($r, $r, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_sqrt($r, $x, $ROUND);
        Math::MPC::Rmpc_add_ui($r, $r, 1, $ROUND);

        my $y = Math::MPC::Rmpc_init2($PREC);
        Math::MPC::Rmpc_set_ui($y, 0, $ROUND);

        my $tmp = Math::MPC::Rmpc_init2($PREC);
        my $abs = Math::MPFR::Rmpfr_init2($PREC);

        my $count = 0;
        while (1) {
            Math::MPC::Rmpc_sub($tmp, $r, $y, $ROUND);

            Math::MPC::Rmpc_abs($abs, $tmp, $ROUND);
            Math::MPFR::Rmpfr_cmp($abs, $p) <= 0 and last;

            Math::MPC::Rmpc_set($y, $r, $ROUND);

            Math::MPC::Rmpc_log($tmp, $r, $ROUND);
            Math::MPC::Rmpc_add_ui($tmp, $tmp, 1, $ROUND);

            Math::MPC::Rmpc_add($r, $r, $x, $ROUND);
            Math::MPC::Rmpc_div($r, $r, $tmp, $ROUND);
            last if ++$count > $PREC;
        }

        Math::MPC::Rmpc_log($r, $r, $ROUND);
        return $r;
    }
}

sub lambert_w {
    my ($x) = @_;
    bless \__LambertW__(_any2mpfr_mpc($$x));
}

*LambertW = \&lambert_w;

sub __exp__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_exp($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_exp($r, $x, $ROUND);
        return $r;
    }
}

sub exp {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        return bless \__pow__(_any2mpfr_mpc($$x), _any2mpfr_mpc($$y));
    }

    bless \__exp__(_any2mpfr_mpc($$x));
}

sub exp2 {
    my ($x) = @_;
    bless \__pow__(2, $$x);
}

sub exp10 {
    my ($x) = @_;
    bless \__pow__(10, $$x);
}

#
## sin / sinh / asin / asinh
#

sub __sin__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sin($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sin($r, $x, $ROUND);
        return $r;
    }
}

sub sin {
    my ($x) = @_;
    bless \__sin__(_any2mpfr_mpc($$x));
}

sub __sinh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sinh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sinh($r, $x, $ROUND);
        return $r;
    }
}

sub sinh {
    my ($x) = @_;
    bless \__sinh__(_any2mpfr_mpc($$x));
}

sub __asin__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < -1 or x > 1
        if (   Math::MPFR::Rmpfr_cmp_ui($x, 1) > 0
            or Math::MPFR::Rmpfr_cmp_si($x, -1) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_asin($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_asin($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_asin($r, $x, $ROUND);
        return $r;
    }
}

sub asin {
    my ($x) = @_;
    bless \__asin__(_any2mpfr_mpc($$x));
}

*arcsin = \&asin;

sub __asinh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_asinh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_asinh($r, $x, $ROUND);
        return $r;
    }
}

sub asinh {
    my ($x) = @_;
    bless \__asinh__(_any2mpfr_mpc($$x));
}

*arcsinh = \&asinh;

#
## cos / cosh / acos / acosh
#

sub __cos__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_cos($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_cos($r, $x, $ROUND);
        return $r;
    }
}

sub cos {
    my ($x) = @_;
    bless \__cos__(_any2mpfr_mpc($$x));
}

sub __cosh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_cosh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_cosh($r, $x, $ROUND);
        return $r;
    }
}

sub cosh {
    my ($x) = @_;
    bless \__cosh__(_any2mpfr_mpc($$x));
}

sub __acos__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < -1 or x > 1
        if (   Math::MPFR::Rmpfr_cmp_ui($x, 1) > 0
            or Math::MPFR::Rmpfr_cmp_si($x, -1) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_acos($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_acos($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_acos($r, $x, $ROUND);
        return $r;
    }
}

sub acos {
    my ($x) = @_;
    bless \__acos__(_any2mpfr_mpc($$x));
}

*arccos = \&acos;

sub __acosh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < 1
        if (Math::MPFR::Rmpfr_cmp_ui($x, 1) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_acosh($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_acosh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_acosh($r, $x, $ROUND);
        return $r;
    }
}

sub acosh {
    my ($x) = @_;
    bless \__acosh__(_any2mpfr_mpc($$x));
}

*arccosh = \&acosh;

#
## tan / tanh / atan / atanh
#

sub __tan__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_tan($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_tan($r, $x, $ROUND);
        return $r;
    }
}

sub tan {
    my ($x) = @_;
    bless \__tan__(_any2mpfr_mpc($$x));
}

sub __tanh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_tanh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_tanh($r, $x, $ROUND);
        return $r;
    }
}

sub tanh {
    my ($x) = @_;
    bless \__tanh__(_any2mpfr_mpc($$x));
}

sub __atan__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_atan($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_atan($r, $x, $ROUND);
        return $r;
    }
}

sub atan {
    my ($x) = @_;
    bless \__atan__(_any2mpfr_mpc($$x));
}

*arctan = \&atan;

sub __atanh__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {

        # Return a complex number for x < -1 or x > 1
        if (   Math::MPFR::Rmpfr_cmp_ui($x, +1) > 0
            or Math::MPFR::Rmpfr_cmp_si($x, -1) < 0) {
            my $r = _mpfr2mpc($x);
            Math::MPC::Rmpc_atanh($r, $r, $ROUND);
            return $r;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_atanh($r, $x, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_atanh($r, $x, $ROUND);
        return $r;
    }
}

sub atanh {
    my ($x) = @_;
    bless \__atanh__(_any2mpfr_mpc($$x));
}

*arctanh = \&atanh;

sub __atan2__ {
    my ($x, $y) = @_;
    goto(join('__', ref($x), ref($y)) =~ tr/:/_/rs);

  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_atan2($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }

    #
    ## atan2(x, y) = -i * log((y + x*i) / sqrt(x^2 + y^2))
    #
  Math_MPC__Math_MPC: {
        my $r = Math::MPC::Rmpc_init2($PREC);

        Math::MPC::Rmpc_mul_i($r, $x, 1, $ROUND);
        Math::MPC::Rmpc_add($r, $r, $y, $ROUND);

        my $t1 = Math::MPC::Rmpc_init2($PREC);
        my $t2 = Math::MPC::Rmpc_init2($PREC);

        Math::MPC::Rmpc_sqr($t1, $x, $ROUND);
        Math::MPC::Rmpc_sqr($t2, $y, $ROUND);
        Math::MPC::Rmpc_add($t1, $t1, $t2, $ROUND);
        Math::MPC::Rmpc_sqrt($t1, $t1, $ROUND);

        Math::MPC::Rmpc_div($r, $r, $t1, $ROUND);
        Math::MPC::Rmpc_log($r, $r, $ROUND);
        Math::MPC::Rmpc_mul_i($r, $r, -1, $ROUND);

        return $r;
    }
}

sub atan2 {
    my ($x, $y) = @_;
    _valid(\$y);
    bless \__atan2__(_any2mpfr_mpc($$x), _any2mpfr_mpc($$y));
}

sub arg {
    my ($x) = @_;
    $x->imag->atan2($x->real);
}

*phase = \&arg;
*angle = \&arg;

#
## sec / sech / asec / asech
#

sub __sec__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sec($r, $x, $ROUND);
        return $r;
    }

    # sec(x) = 1/cos(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_cos($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub sec {
    my ($x) = @_;
    bless \__sec__(_any2mpfr_mpc($$x));
}

sub __sech__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sech($r, $x, $ROUND);
        return $r;
    }

    # sech(x) = 1/cosh(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_cosh($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub sech {
    my ($x) = @_;
    bless \__sech__(_any2mpfr_mpc($$x));
}

sub __asec__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # asec(x) = acos(1/x)
  Math_MPFR: {

        # Return a complex number for x > -1 and x < 1
        if (    Math::MPFR::Rmpfr_cmp_ui($x, 1) < 0
            and Math::MPFR::Rmpfr_cmp_si($x, -1) > 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_acos($r, $r, $ROUND);
        return $r;
    }

    # asec(x) = acos(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_acos($r, $r, $ROUND);
        return $r;
    }
}

sub asec {
    my ($x) = @_;
    bless \__asec__(_any2mpfr_mpc($$x));
}

*arcsec = \&asec;

sub __asech__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # asech(x) = acosh(1/x)
  Math_MPFR: {

        # Return a complex number for x < 0 or x > 1
        if (   Math::MPFR::Rmpfr_cmp_ui($x, 1) > 0
            or Math::MPFR::Rmpfr_cmp_ui($x, 0) < 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_acosh($r, $r, $ROUND);
        return $r;
    }

    # asech(x) = acosh(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_acosh($r, $r, $ROUND);
        return $r;
    }
}

sub asech {
    my ($x) = @_;
    bless \__asech__(_any2mpfr_mpc($$x));
}

*arcsech = \&asech;

#
## csc / csch / acsc / acsch
#

sub __csc__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_csc($r, $x, $ROUND);
        return $r;
    }

    # csc(x) = 1/sin(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sin($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub csc {
    my ($x) = @_;
    bless \__csc__(_any2mpfr_mpc($$x));
}

sub __csch__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_csch($r, $x, $ROUND);
        return $r;
    }

    # csch(x) = 1/sinh(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sinh($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub csch {
    my ($x) = @_;
    bless \__csch__(_any2mpfr_mpc($$x));
}

sub __acsc__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # acsc(x) = asin(1/x)
  Math_MPFR: {

        # Return a complex number for x > -1 and x < 1
        if (    Math::MPFR::Rmpfr_cmp_ui($x, 1) < 0
            and Math::MPFR::Rmpfr_cmp_si($x, -1) > 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_asin($r, $r, $ROUND);
        return $r;
    }

    # acsc(x) = asin(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_asin($r, $r, $ROUND);
        return $r;
    }
}

sub acsc {
    my ($x) = @_;
    bless \__acsc__(_any2mpfr_mpc($$x));
}

*arccsc = \&acsc;

sub __acsch__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # acsch(x) = asinh(1/x)
  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_asinh($r, $r, $ROUND);
        return $r;
    }

    # acsch(x) = asinh(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_asinh($r, $r, $ROUND);
        return $r;
    }
}

sub acsch {
    my ($x) = @_;
    bless \__acsch__(_any2mpfr_mpc($$x));
}

*arccsch = \&acsch;

#
## cot / coth / acot / acoth
#

sub __cot__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_cot($r, $x, $ROUND);
        return $r;
    }

    # cot(x) = 1/tan(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_tan($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub cot {
    my ($x) = @_;
    bless \__cot__(_any2mpfr_mpc($$x));
}

sub __coth__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_coth($r, $x, $ROUND);
        return $r;
    }

    # coth(x) = 1/tanh(x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $t = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_tanh($r, $x, $ROUND);
        Math::MPC::Rmpc_set_ui($t, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $t, $r, $ROUND);
        return $r;
    }
}

sub coth {
    my ($x) = @_;
    bless \__coth__(_any2mpfr_mpc($$x));
}

sub __acot__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # acot(x) = atan(1/x)
  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_atan($r, $r, $ROUND);
        return $r;
    }

    # acot(x) = atan(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_atan($r, $r, $ROUND);
        return $r;
    }
}

sub acot {
    my ($x) = @_;
    bless \__acot__(_any2mpfr_mpc($$x));
}

*arccot = \&acot;

sub __acoth__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

    # acoth(x) = atanh(1/x)
  Math_MPFR: {

        # Return a complex number for x > -1 and x < 1
        if (    Math::MPFR::Rmpfr_cmp_ui($x, 1) < 0
            and Math::MPFR::Rmpfr_cmp_si($x, -1) > 0) {
            $x = _mpfr2mpc($x);
            goto Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ui_div($r, 1, $x, $ROUND);
        Math::MPFR::Rmpfr_atanh($r, $r, $ROUND);
        return $r;
    }

    # acoth(x) = atanh(1/x)
  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($r, 1, $ROUND);
        Math::MPC::Rmpc_div($r, $r, $x, $ROUND);
        Math::MPC::Rmpc_atanh($r, $r, $ROUND);
        return $r;
    }
}

sub acoth {
    my ($x) = @_;
    bless \__acoth__(_any2mpfr_mpc($$x));
}

*arccoth = \&acoth;

sub __cis__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui_fr($r, 0, $x, $ROUND);
        Math::MPC::Rmpc_exp($r, $r, $ROUND);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_mul_i($r, $x, 1, $ROUND);
        Math::MPC::Rmpc_exp($r, $r, $ROUND);
        return $r;
    }
}

sub cis {
    my ($x) = @_;
    bless \__cis__(_any2mpfr_mpc($$x));
}

sub roots_of_unity {
    my ($n) = @_;
    $n = _any2ui($$n) || return _array();

    my $one = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_set_ui($one, 1, $ROUND);
    my @roots = (bless \$one);

    my $cos = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $sin = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    my $pi = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($pi, $ROUND);

    foreach my $i (1 .. $n - 1) {

        # r = exp(2*pi / n * i)
        Math::MPFR::Rmpfr_mul_ui($sin, $pi, 2 * $i, $ROUND);
        Math::MPFR::Rmpfr_div_ui($sin, $sin, $n, $ROUND);
        Math::MPFR::Rmpfr_sin_cos($sin, $cos, $sin, $ROUND);

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr_fr($r, $cos, $sin, $ROUND);
        push @roots, bless \$r;
    }

    _array(\@roots);
}

sub __sin_cos__ {
    my ($x) = @_;
    goto(ref($x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $cos = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $sin = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPFR::Rmpfr_sin_cos($sin, $cos, $x, $ROUND);

        return ($sin, $cos);
    }

  Math_MPC: {
        my $cos = Math::MPC::Rmpc_init2(CORE::int($PREC));
        my $sin = Math::MPC::Rmpc_init2(CORE::int($PREC));

        Math::MPC::Rmpc_sin_cos($sin, $cos, $x, $ROUND, $ROUND);

        return ($sin, $cos);
    }
}

sub sin_cos {
    my ($x) = @_;
    my ($sin, $cos) = __sin_cos__(_any2mpfr_mpc($$x));
    ((bless \$sin), (bless \$cos));
}

#
## Special functions
#

sub __agm__ {
    my ($x, $y) = @_;
    goto(join('__', ref($x), ref($y)) =~ tr/:/_/rs);

  Math_MPFR__Math_MPFR: {
        if (   Math::MPFR::Rmpfr_sgn($x) < 0
            or Math::MPFR::Rmpfr_sgn($y) < 0) {
            ($x, $y) = (_mpfr2mpc($x), _mpfr2mpc($y));
            goto Math_MPC__Math_MPC;
        }

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_agm($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPC: {

        # agm(0,  x) = 0
        Math::MPC::Rmpc_cmp_si_si($x, 0, 0) || return $x;

        # agm(x, 0) = 0
        Math::MPC::Rmpc_cmp_si_si($y, 0, 0) || return $y;

        $PREC = CORE::int($PREC) if ref($PREC);

        my $a0 = Math::MPC::Rmpc_init2($PREC);
        my $g0 = Math::MPC::Rmpc_init2($PREC);

        my $a1 = Math::MPC::Rmpc_init2($PREC);
        my $g1 = Math::MPC::Rmpc_init2($PREC);

        my $t = Math::MPC::Rmpc_init2($PREC);

        Math::MPC::Rmpc_set($a0, $x, $ROUND);
        Math::MPC::Rmpc_set($g0, $y, $ROUND);

        my $count = 0;
        {
            Math::MPC::Rmpc_add($a1, $a0, $g0, $ROUND);
            Math::MPC::Rmpc_div_2ui($a1, $a1, 1, $ROUND);

            Math::MPC::Rmpc_mul($g1, $a0, $g0, $ROUND);
            Math::MPC::Rmpc_add($t, $a0, $g0, $ROUND);
            Math::MPC::Rmpc_sqr($t, $t, $ROUND);
            Math::MPC::Rmpc_cmp_si_si($t, 0, 0) || return $t;
            Math::MPC::Rmpc_div($g1, $g1, $t, $ROUND);
            Math::MPC::Rmpc_sqrt($g1, $g1, $ROUND);
            Math::MPC::Rmpc_add($t, $a0, $g0, $ROUND);
            Math::MPC::Rmpc_mul($g1, $g1, $t, $ROUND);

            if (Math::MPC::Rmpc_cmp($a0, $a1) and ++$count < $PREC) {
                Math::MPC::Rmpc_set($a0, $a1, $ROUND);
                Math::MPC::Rmpc_set($g0, $g1, $ROUND);
                redo;
            }
        }

        return $g0;
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }
}

sub agm {
    my ($x, $y) = @_;
    _valid(\$y);
    bless \__agm__(_any2mpfr_mpc($$x), _any2mpfr_mpc($$y));
}

sub __hypot__ {
    my ($x, $y) = @_;

    # hypot(x, y) = sqrt(x^2 + y^2)

    goto(join('__', ref($x), ref($y)) =~ tr/:/_/rs);

  Math_MPFR__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_hypot($r, $x, $y, $ROUND);
        return $r;
    }

  Math_MPFR__Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($r, $y, $ROUND);
        Math::MPFR::Rmpfr_hypot($r, $r, $x, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($r, $x, $ROUND);
        Math::MPFR::Rmpfr_hypot($r, $r, $y, $ROUND);
        return $r;
    }

  Math_MPC__Math_MPC: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($r, $x, $ROUND);
        my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($t, $y, $ROUND);
        Math::MPFR::Rmpfr_hypot($r, $r, $t, $ROUND);
        return $r;
    }
}

sub hypot {
    my ($x, $y) = @_;
    _valid(\$y);
    bless \__hypot__(_any2mpfr_mpc($$x), _any2mpfr_mpc($$y));
}

sub gamma {
    my ($x) = @_;

    ref($_[0]) || goto &EulerGamma;

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_gamma($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*Γ = \&gamma;

sub lngamma {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_lngamma($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*gamma_log = \&lngamma;

sub lgamma {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_lgamma($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*gamma_abs_log = \&lgamma;

sub digamma {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_digamma($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*Ψ = \&digamma;

#
## beta(x, y) = gamma(x)*gamma(y) / gamma(x+y)
#
sub beta {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpfr($$x);
    $y = _any2mpfr($$y);

    state $has_beta = Math::MPFR::MPFR_VERSION_MAJOR() >= 4;

    if ($has_beta) {    # available since mpfr-4.0.0
        my $r = Math::MPFR::Rmpfr_init2($PREC);
        Math::MPFR::Rmpfr_beta($r, $x, $y, $ROUND);
        return bless \$r;
    }

    my $t1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));    # gamma(x+y)
    my $t2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));    # gamma(y)

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_add($t1, $x, $y, $ROUND);
    Math::MPFR::Rmpfr_gamma($t1, $t1, $ROUND);
    Math::MPFR::Rmpfr_gamma($r,  $x,  $ROUND);
    Math::MPFR::Rmpfr_gamma($t2, $y,  $ROUND);
    Math::MPFR::Rmpfr_mul($r, $r, $t2, $ROUND);
    Math::MPFR::Rmpfr_div($r, $r, $t1, $ROUND);

    bless \$r;
}

#
## eta(s) = (1 - 2^(1-s)) * zeta(s)
#
sub eta {
    my ($x) = @_;

    $x = _any2mpfr($$x);

    my $x_is_int = Math::MPFR::Rmpfr_integer_p($x);
    my $r        = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    # Special case for eta(1) = log(2)
    if ($x_is_int and Math::MPFR::Rmpfr_cmp_ui($x, 1) == 0) {
        Math::MPFR::Rmpfr_const_log2($r, $ROUND);
        return bless \$r;
    }

    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_ui_sub($r, 1, $x, $ROUND);
    Math::MPFR::Rmpfr_ui_pow($r, 2, $r, $ROUND);
    Math::MPFR::Rmpfr_ui_sub($r, 1, $r, $ROUND);

    if ($x_is_int and Math::MPFR::Rmpfr_fits_ulong_p($x, $ROUND)) {
        Math::MPFR::Rmpfr_zeta_ui($t, Math::MPFR::Rmpfr_get_ui($x, $ROUND), $ROUND);
    }
    else {
        Math::MPFR::Rmpfr_zeta($t, $x, $ROUND);
    }

    Math::MPFR::Rmpfr_mul($r, $r, $t, $ROUND);

    bless \$r;
}

*η = \&eta;

sub zeta {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    my $f = _any2mpfr($$x);
    if (    Math::MPFR::Rmpfr_integer_p($f)
        and Math::MPFR::Rmpfr_fits_ulong_p($f, $ROUND)) {
        Math::MPFR::Rmpfr_zeta_ui($r, Math::MPFR::Rmpfr_get_ui($f, $ROUND), $ROUND);
    }
    else {
        Math::MPFR::Rmpfr_zeta($r, $f, $ROUND);
    }
    bless \$r;
}

*ζ = \&zeta;

sub _secant_numbers {
    my ($n) = @_;

    state @cache;

    if ($n <= $#cache) {
        return @cache;
    }

    $n <<= 1 if ($n <= 512);

    my @S = (Math::GMPz::Rmpz_init_set_ui(1));

    foreach my $k (1 .. $n) {
        Math::GMPz::Rmpz_mul_ui($S[$k] = Math::GMPz::Rmpz_init(), $S[$k - 1], $k);
    }

    foreach my $k (1 .. $n) {
        foreach my $j ($k + 1 .. $n) {
            Math::GMPz::Rmpz_addmul_ui($S[$j], $S[$j - 1], ($j - $k) * ($j - $k + 2));
        }
    }

    push @cache, @S[@cache .. (@S <= 1024 ? $#S : 1024)];

    return @S;
}

sub _tangent_numbers {
    my ($n) = @_;

    state @cache;

    if ($n <= $#cache) {
        return @cache;
    }

    $n <<= 1 if ($n <= 512);

    my @T = (Math::GMPz::Rmpz_init_set_ui(1));

    foreach my $k (1 .. $n) {
        Math::GMPz::Rmpz_mul_ui($T[$k] = Math::GMPz::Rmpz_init(), $T[$k - 1], $k);
    }

    foreach my $k (1 .. $n) {
        foreach my $j ($k .. $n) {
            Math::GMPz::Rmpz_mul_ui($T[$j], $T[$j], $j - $k + 2);
            Math::GMPz::Rmpz_addmul_ui($T[$j], $T[$j - 1], $j - $k);
        }
    }

    push @cache, @T[@cache .. (@T <= 1024 ? $#T : 1024)];

    return @T;
}

sub _bernoulli_numbers {
    my ($n) = @_;

    $n = ($n >> 1) + 1;

    state @cache;

    if ($n <= $#cache) {
        return @cache;
    }

    my @B;
    my @T = _tangent_numbers($n);

    my $t = Math::GMPz::Rmpz_init();

    foreach my $k (scalar(@cache) .. 2 * @T) {

        $k % 2 == 0 or $k == 1 or next;

        my $q = Math::GMPq::Rmpq_init();

        if ($k == 0) {
            Math::GMPq::Rmpq_set_ui($q, 1, 1);
            $B[$k] = $q;
            next;
        }

        if ($k == 1) {
            Math::GMPq::Rmpq_set_si($q, -1, 2);
            $B[$k] = $q;
            next;
        }

        # T_k
        Math::GMPz::Rmpz_mul_ui($t, $T[($k >> 1) - 1], $k);
        Math::GMPz::Rmpz_neg($t, $t) if ((($k >> 1) - 1) & 1);
        Math::GMPq::Rmpq_set_z($q, $t);

        # (2^k - 1) * 2^k
        Math::GMPz::Rmpz_set_ui($t, 0);
        Math::GMPz::Rmpz_setbit($t, $k);
        Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        Math::GMPz::Rmpz_mul_2exp($t, $t, $k);

        # B_k = q
        Math::GMPq::Rmpq_div_z($q, $q, $t);

        $B[($k >> 1) + 1] = $q;
    }

    push @cache, @B[@cache .. (@B <= 1024 ? $#B : 1024)];

    return (@cache, (@B > @cache ? @B[@cache .. $#B] : ()));
}

sub bernoulli_numbers {
    my ($n) = @_;

    $n = _any2ui($$n) // return _array();

    my @bern = _bernoulli_numbers($n);

    $#bern = ($n >> 1) + 1;

    for (my $i = 3 ; $i <= $n ; $i += 2) {
        splice(@bern, $i, 0, 0);
    }

    $#bern = $n;
    _array([map { bless \$_ } @bern]);
}

sub bernoulli_polynomial {
    my ($n, $x) = @_;

    #
    ## B_n(x) = Sum_{k=0..n} binomial(n, k) * bernoulli(n-k) * x^k
    #

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
        $x = $$x;
    }
    else {
        $polynomial = 1;
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
    }

    $n = _any2ui($$n) // goto &nan;

    if ($polynomial and $n >= 4500 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("bernpol($n)")) {
            return Sidef::Types::Number::Polynomial->new(Sidef::Types::String::String->new($res));
        }
    }

    my @B = _bernoulli_numbers($n);

    my $u = $n + 1;
    my $z = Math::GMPz::Rmpz_init();
    my $q = Math::GMPq::Rmpq_init();

    my @terms;

    foreach my $k (0 .. $n) {

        --$u & 1 and $u > 1 and next;    # B_n = 0 for odd n > 1

        Math::GMPz::Rmpz_bin_uiui($z, $n, $k);
        Math::GMPq::Rmpq_mul_z($q, $u <= 1 ? $B[$u] : $B[($u >> 1) + 1], $z);

        if ($polynomial) {
            push @terms, $x->pow(_set_int($k))->mul(bless \$q);
        }
        else {
            push @terms, __mul__(($k ? __pow__($x, $k) : 1), $q);
        }
    }

    $polynomial
      ? _binsplit([CORE::reverse @terms], \&Sidef::Types::Number::Polynomial::add)
      : (bless \_binsplit([CORE::reverse @terms], \&__add__));
}

sub bernfrac {
    my ($n, $x) = @_;

    defined($x) && goto &bernoulli_polynomial;

    $n = _any2ui($$n) // goto &nan;

    $n == 0 and return ONE;
    $n > 1 and $n % 2 and return ZERO;    # Bn=0 for odd n>1

    if ($n > 1 and $n < 512) {
        return bless \((_bernoulli_numbers($n))[($n >> 1) + 1]);
    }

    if ($n == 1) {
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_ui($q, 1, 2);
        return bless \$q;
    }

    if ($n >= 5000 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("bernfrac($n)")) {
            my $q = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_set_str($q, $res, 10);
            return bless \$q;
        }
    }

    # Using bernfrac() from `Math::Prime::Util::GMP`
    my ($num, $den) = Math::Prime::Util::GMP::bernfrac($n);

    my $q = Math::GMPq::Rmpq_init();
    Math::GMPq::Rmpq_set_str($q, "$num/$den", 10);
    bless \$q;
}

*bern             = \&bernfrac;
*bernoulli        = \&bernfrac;
*Bernoulli        = \&bernfrac;
*BernoulliB       = \&bernfrac;
*bernoulli_number = \&bernfrac;

sub genocchi {
    my ($n) = @_;
    (TWO)->mul(ONE->sub((FOUR)->ipow($n)))->mul($n->add($n)->bernoulli);
}

sub faulhaber_polynomial {
    my ($n, $x) = @_;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
    }

    $n = $n->inc;
    $x = $x->inc;

    $n->bernoulli_polynomial($x)->sub($n->bernfrac)->div($n);
}

sub euler_polynomial {
    my ($n, $x) = @_;

    #
    ## E_n(x) = Sum_{k=0..n} binomial(n, n-k) * euler_number(n-k) / 2^(n-k) * (x - 1/2)^k
    #

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
        $x = $$x;
        $x = __dec__(__add__($x, $x));    # x = 2*x - 1
    }
    else {
        $polynomial = 1;
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
        $x = $x->add($x)->dec;
    }

    $n = _any2ui($$n) // goto &nan;

    if ($polynomial and $n >= 25 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("eulerpol($n)")) {
            return Sidef::Types::Number::Polynomial->new(Sidef::Types::String::String->new($res));
        }
    }

    my @S = _secant_numbers(($n >> 1) + 1);

    my $u = $n + 1;
    my $z = Math::GMPz::Rmpz_init();

    my @terms;

    foreach my $k (0 .. $n) {
        --$u & 1 and next;    # E_n = 0 for all odd n

        Math::GMPz::Rmpz_bin_uiui($z, $n, $u);
        Math::GMPz::Rmpz_mul($z, $z, $S[$u >> 1]);
        Math::GMPz::Rmpz_neg($z, $z) if (($u >> 1) & 1);

        if ($polynomial) {
            push @terms, $x->pow(_set_int($k))->mul(bless \$z);
        }
        else {
            push @terms, ($k ? __mul__(__pow__($x, $k), $z) : Math::GMPz::Rmpz_init_set($z));
        }
    }

    if ($polynomial) {
        my $sum = _binsplit(\@terms, \&Sidef::Types::Number::Polynomial::add);
        Math::GMPz::Rmpz_set_ui($z, 0);
        Math::GMPz::Rmpz_setbit($z, $n);
        return $sum->div(bless \$z);
    }

    my $sum = _binsplit(\@terms, \&__add__);
    Math::GMPz::Rmpz_set_ui($z, 0);
    Math::GMPz::Rmpz_setbit($z, $n);
    bless \__div__($sum, $z);
}

sub euler {
    my ($n, $x) = @_;

    ref($n) || goto &EulerGamma;
    defined($x) && goto &euler_polynomial;

    $n = _any2ui($$n) // goto &nan;

    $n & 1 and return ZERO;    # E_n = 0 for all odd indices

    if ($n >= 1500 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("eulerfrac($n)")) {
            my $z = Math::GMPz::Rmpz_init_set_str($res, 10);
            return bless \$z;
        }
    }

    my $e = Math::GMPz::Rmpz_init_set((_secant_numbers(($n >> 1) + 1))[$n >> 1]);
    Math::GMPz::Rmpz_neg($e, $e) if (($n >> 1) & 1);
    bless \$e;
}

*Euler        = \&euler;
*euler_number = \&euler;

sub euler_numbers {
    my ($n) = @_;

    $n = _any2ui($$n) // return _array();

    my @euler = _secant_numbers(($n >> 1) + 1);

    $#euler = ($n >> 1) + 1;

    for (my $i = 1 ; $i <= $#euler ; $i += 2) {
        my $t = Math::GMPz::Rmpz_init_set($euler[$i]);
        Math::GMPz::Rmpz_neg($t, $t);
        splice(@euler, $i, 1, $t);
    }

    for (my $i = 1 ; $i <= $n ; $i += 2) {
        splice(@euler, $i, 0, 0);
    }

    $#euler = $n;
    _array([map { bless \$_ } @euler]);
}

sub _fubini_numbers {
    my ($n) = @_;

    state $F = [Math::GMPz::Rmpz_init_set_ui(1)];
    state $t = Math::GMPz::Rmpz_init_nobless();

    foreach my $i ($#{$F} + 1 .. $n) {
        my $w = Math::GMPz::Rmpz_init_set_ui(0);
        Math::GMPz::Rmpz_set_ui($t, 1);
        foreach my $k (0 .. $i - 1) {
            Math::GMPz::Rmpz_addmul($w, $F->[$k], $t);
            Math::GMPz::Rmpz_mul_ui($t, $t, $i - $k);
            Math::GMPz::Rmpz_divexact_ui($t, $t, $k + 1);
        }
        $F->[$i] = $w;
    }

    return $F;
}

sub fubini_numbers {    # OEIS: A000670
    my ($n) = @_;

    $n = _any2ui($$n) // return _array();

    my @F = @{_fubini_numbers($n)};
    $#F = $n;
    _array([map { bless \$_ } @F]);
}

sub fubini {    # OEIS: A000670
    my ($n) = @_;
    $n = _any2ui($$n) // goto &nan;
    my $r = _fubini_numbers($n)->[$n];
    bless \$r;
}

*Fubini        = \&fubini;
*fubini_number = \&fubini;

sub secant_number {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my @E = _secant_numbers($n);
    bless \(my $value = Math::GMPz::Rmpz_init_set($E[$n]));
}

sub tangent_number {
    my ($n) = @_;

    #
    ## T_n = 2^(2*n) * (2^(2*n) - 1) * abs(bernoulli(2*n)) / (2*n)
    #

    $n = _any2ui($$n) // goto &nan;
    $n || return ZERO;
    $n <<= 1;

    my ($num, $den) = Math::Prime::Util::GMP::bernfrac($n);

    $num = Math::GMPz::Rmpz_init_set_str("$num", 10);
    $den = Math::GMPz::Rmpz_init_set_str("$den", 10);

    Math::GMPz::Rmpz_abs($num, $num) if !($n & 1);

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_setbit($r, $n);
    Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    Math::GMPz::Rmpz_mul_2exp($r, $r, $n);
    Math::GMPz::Rmpz_mul($r, $r, $num);
    Math::GMPz::Rmpz_mul_ui($den, $den, $n);
    Math::GMPz::Rmpz_divexact($r, $r, $den);
    bless \$r;
}

# TODO: add support for an optional argument and return B_n(x)
sub bernreal {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    # |B(n)| = zeta(n) * n! / 2^(n-1) / pi^n

    $n == 0 and return ONE;
    $n == 1 and return do { state $x = bless(\_str2obj('1/2')) };
    $n % 2  and return ZERO;                                        # Bn = 0 for odd n>1

    #local $PREC = CORE::int($n*CORE::log($n)+1);

    my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $p = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_zeta_ui($f, $n, $ROUND);                      # f = zeta(n)
    Math::MPFR::Rmpfr_set_ui($p, $n + 1, $ROUND);                   # p = n+1
    Math::MPFR::Rmpfr_gamma($p, $p, $ROUND);                        # p = gamma(p)

    Math::MPFR::Rmpfr_mul($f, $f, $p, $ROUND);                      # f = f * p

    Math::MPFR::Rmpfr_const_pi($p, $ROUND);                         # p = PI
    Math::MPFR::Rmpfr_pow_ui($p, $p, $n, $ROUND);                   # p = p^n

    Math::MPFR::Rmpfr_div_2ui($f, $f, $n - 1, $ROUND);              # f = f / 2^(n-1)

    Math::MPFR::Rmpfr_div($f, $f, $p, $ROUND);                      # f = f/p
    Math::MPFR::Rmpfr_neg($f, $f, $ROUND) if $n % 4 == 0;

    bless \$f;
}

# TODO: add support for an optional argument and return log(B_n(x))
sub lnbernreal {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    # log(|B(n)|) = (1 - n)*log(2) - n*log(π) + log(zeta(n)) + log(n!)

    (Math::GMPz::Rmpz_sgn($n) || return ZERO) < 0 and goto &nan;

    my $L = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_log2($L, $ROUND);

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        Math::MPFR::Rmpfr_neg($L, $L, $ROUND);
        return bless \$L;
    }

    Math::GMPz::Rmpz_odd_p($n) && goto &ninf;    # log(Bn) = -Inf for odd n>1

    my $pi = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($pi, $ROUND);     # pi = π

    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_log($t, $pi, $ROUND);         # t = log(π)
    Math::MPFR::Rmpfr_mul_z($t, $t, $n, $ROUND);    # t = n*log(π)

    my $s = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_ui_sub($s, 1, $n);             # s = 1-n

    Math::MPFR::Rmpfr_mul_z($L, $L, $s, $ROUND);    # L = (1 - n)*log(2)
    Math::MPFR::Rmpfr_sub($L, $L, $t, $ROUND);      # L -= n*log(π)

    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {        # n is a native unsigned integer
        Math::MPFR::Rmpfr_zeta_ui($t, Math::GMPz::Rmpz_get_ui($n), $ROUND);
    }
    else {
        Math::MPFR::Rmpfr_set_z($t, $n, $ROUND);    # t = n
        Math::MPFR::Rmpfr_zeta($t, $t, $ROUND);     # t = zeta(n)
    }

    Math::MPFR::Rmpfr_log($t, $t, $ROUND);          # t = log(zeta(n))
    Math::MPFR::Rmpfr_add($L, $L, $t, $ROUND);      # L += log(zeta(n))

    Math::GMPz::Rmpz_add_ui($s, $n, 1);             # s = n+1
    Math::MPFR::Rmpfr_set_z($t, $s, $ROUND);        # t = n+1
    Math::MPFR::Rmpfr_lngamma($t, $t, $ROUND);      # t = log(gamma(n+1)) = log(n!)

    Math::MPFR::Rmpfr_add($L, $L, $t, $ROUND);      # L += log(n!)

    # If 4|n, then B_n is negative; log(-Re(x)) = log(Re(x)) + π*i, for x>0
    if (Math::GMPz::Rmpz_divisible_2exp_p($n, 2)) {
        my $c = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr_fr($c, $L, $pi, $ROUND);
        return bless \$c;
    }

    bless \$L;
}

*lnbern        = \&lnbernreal;
*bern_log      = \&lnbernreal;
*bernoulli_log = \&lnbernreal;

sub harmfrac {
    my ($n, $k) = @_;

    # Formula in terms of the Harmonic numbers, due to Conway and Guy (1996),
    # for computing the Harmonic numbers of the k-th order.
    if (defined($k)) {

        my $km1   = $k->dec;
        my $npkm1 = $n->add($km1);

        return $npkm1->binomial($km1)->mul($npkm1->harmfrac->sub($km1->harmfrac));
    }

    $n = _any2ui($$n) // goto &nan;
    $n || return ZERO();

    # Using harmfrac() from Math::Prime::Util::GMP
    my ($num, $den) = Math::Prime::Util::GMP::harmfrac($n);

    my $q = Math::GMPq::Rmpq_init();
    Math::GMPq::Rmpq_set_str($q, "$num/$den", 10);
    bless \$q;
}

*harm            = \&harmfrac;
*harmonic        = \&harmfrac;
*harmonic_number = \&harmfrac;

sub harmreal {
    my ($n, $k) = @_;

    # Formula in terms of the Harmonic numbers, due to Conway and Guy (1996),
    # for computing the Harmonic numbers of the k-th order.
    if (defined($k)) {

        my $km1   = $k->dec;
        my $npkm1 = $n->add($km1);

        return $npkm1->binomial($km1)->mul($npkm1->harmreal->sub($km1->harmreal));
    }

    $n = _any2mpfr($$n);

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_add_ui($r, $n, 1, $ROUND);
    Math::MPFR::Rmpfr_digamma($r, $r, $ROUND);

    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_euler($t, $ROUND);
    Math::MPFR::Rmpfr_add($r, $r, $t, $ROUND);

    bless \$r;
}

sub erf {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_erf($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

sub erfc {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_erfc($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

sub bessel_j {
    my ($x, $n) = @_;

    $n = defined($n) ? do { _valid(\$n); __numify__($$n) } : 0;

    if ($n < LONG_MIN or $n > ULONG_MAX) {
        return ZERO;
    }

    $x = _any2mpfr($$x);
    $n = CORE::int($n);

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    if ($n == 0) {
        Math::MPFR::Rmpfr_j0($r, $x, $ROUND);
    }
    elsif ($n == 1) {
        Math::MPFR::Rmpfr_j1($r, $x, $ROUND);
    }
    else {
        Math::MPFR::Rmpfr_jn($r, $n, $x, $ROUND);
    }

    bless \$r;
}

*BesselJ = \&bessel_j;

sub bessel_y {
    my ($x, $n) = @_;

    $n = defined($n) ? do { _valid(\$n); __numify__($$n) } : 0;

    if ($n < LONG_MIN or $n > ULONG_MAX) {
        if (__cmp__($$x, 0) < 0) {
            return nan();
        }
        return ($n < 0 ? inf() : ninf());
    }

    $x = _any2mpfr($$x);
    $n = CORE::int($n);

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    if ($n == 0) {
        Math::MPFR::Rmpfr_y0($r, $x, $ROUND);
    }
    elsif ($n == 1) {
        Math::MPFR::Rmpfr_y1($r, $x, $ROUND);
    }
    else {
        Math::MPFR::Rmpfr_yn($r, $n, $x, $ROUND);
    }

    bless \$r;
}

*BesselY = \&bessel_y;

sub eint {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_eint($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*ei = \&eint;
*Ei = \&eint;

sub ai {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_ai($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*airy = \&ai;
*Ai   = \&ai;

sub li {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_log($r, _any2mpfr($$x), $ROUND);
    Math::MPFR::Rmpfr_eint($r, $r, $ROUND);
    bless \$r;
}

*Li = \&li;

sub li2 {
    my ($x) = @_;
    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_li2($r, _any2mpfr($$x), $ROUND);
    bless \$r;
}

*Li2 = \&li2;

#
## Comparison and testing operations
#

sub __eq__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        return ($x == $y);
    }

  Scalar__Math_GMPz: {
        ($x, $y) = ($y, $x);
        goto Math_GMPz__Scalar;
    }

  Scalar__Math_GMPq: {
        ($x, $y) = ($y, $x);
        goto Math_GMPq__Scalar;
    }

  Scalar__Math_MPFR: {
        ($x, $y) = ($y, $x);
        goto Math_MPFR__Scalar;
    }

  Scalar__Math_MPC: {
        ($x, $y) = ($y, $x);
        goto Math_MPC__Scalar;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        return Math::MPFR::Rmpfr_equal_p($x, $y);
    }

  Math_MPFR__Math_GMPz: {
        return (Math::MPFR::Rmpfr_integer_p($x) and Math::MPFR::Rmpfr_cmp_z($x, $y) == 0);
    }

  Math_MPFR__Math_GMPq: {
        return (Math::MPFR::Rmpfr_number_p($x) and Math::MPFR::Rmpfr_cmp_q($x, $y) == 0);
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_MPFR__Scalar: {
        return (
                Math::MPFR::Rmpfr_integer_p($x)
                  and (
                       ($y || return !Math::MPFR::Rmpfr_sgn($x)) < 0
                       ? Math::MPFR::Rmpfr_cmp_si($x, $y)
                       : Math::MPFR::Rmpfr_cmp_ui($x, $y)
                  ) == 0
               );
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        return Math::GMPq::Rmpq_equal($x, $y);
    }

  Math_GMPq__Math_GMPz: {
        return (Math::GMPq::Rmpq_integer_p($x) and Math::GMPq::Rmpq_cmp_z($x, $y) == 0);
    }

  Math_GMPq__Math_MPFR: {
        return (Math::MPFR::Rmpfr_number_p($y) and Math::MPFR::Rmpfr_cmp_q($y, $x) == 0);
    }

  Math_GMPq__Math_MPC: {
        $x = _mpq2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_GMPq__Scalar: {
        return (
                Math::GMPq::Rmpq_integer_p($x)
                  and (
                       ($y || return !Math::GMPq::Rmpq_sgn($x)) < 0
                       ? Math::GMPq::Rmpq_cmp_si($x, $y, 1)
                       : Math::GMPq::Rmpq_cmp_ui($x, $y, 1)
                  ) == 0
               );
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        return (Math::GMPz::Rmpz_cmp($x, $y) == 0);
    }

  Math_GMPz__Math_GMPq: {
        return (Math::GMPq::Rmpq_integer_p($y) and Math::GMPq::Rmpq_cmp_z($y, $x) == 0);
    }

  Math_GMPz__Math_MPFR: {
        return (Math::MPFR::Rmpfr_integer_p($y) and Math::MPFR::Rmpfr_cmp_z($y, $x) == 0);
    }

  Math_GMPz__Math_MPC: {
        $x = _mpz2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_GMPz__Scalar: {
        return (
                (
                 ($y || return !Math::GMPz::Rmpz_sgn($x)) < 0
                 ? Math::GMPz::Rmpz_cmp_si($x, $y)
                 : Math::GMPz::Rmpz_cmp_ui($x, $y)
                ) == 0
               );
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $f1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $f2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($f1, $x);
        Math::MPC::RMPC_RE($f2, $y);

        Math::MPFR::Rmpfr_equal_p($f1, $f2) || return 0;

        Math::MPC::RMPC_IM($f1, $x);
        Math::MPC::RMPC_IM($f2, $y);

        return Math::MPFR::Rmpfr_equal_p($f1, $f2);
    }

  Math_MPC__Math_GMPz: {
        $y = _mpz2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_GMPq: {
        $y = _mpq2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Scalar: {
        my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_IM($f, $x);
        Math::MPFR::Rmpfr_zero_p($f) || return 0;
        Math::MPC::RMPC_RE($f, $x);
        $x = $f;
        goto Math_MPFR__Scalar;
    }
}

sub eq {
    my ($x, $y) = @_;

    ref($y) ne __PACKAGE__
      and return $y->eq($x);

    __eq__($$x, $$y) ? ($TRUE) : ($FALSE);
}

sub __ne__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        return ($x != $y);
    }

  Scalar__Math_GMPz: {
        ($x, $y) = ($y, $x);
        goto Math_GMPz__Scalar;
    }

  Scalar__Math_GMPq: {
        ($x, $y) = ($y, $x);
        goto Math_GMPq__Scalar;
    }

  Scalar__Math_MPFR: {
        ($x, $y) = ($y, $x);
        goto Math_MPFR__Scalar;
    }

  Scalar__Math_MPC: {
        ($x, $y) = ($y, $x);
        goto Math_MPC__Scalar;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        return !Math::MPFR::Rmpfr_equal_p($x, $y);
    }

  Math_MPFR__Math_GMPz: {
        return (!Math::MPFR::Rmpfr_integer_p($x) or Math::MPFR::Rmpfr_cmp_z($x, $y) != 0);
    }

  Math_MPFR__Math_GMPq: {
        return (!Math::MPFR::Rmpfr_number_p($x) or Math::MPFR::Rmpfr_cmp_q($x, $y) != 0);
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_MPFR__Scalar: {
        return (
                !Math::MPFR::Rmpfr_integer_p($x)
                  or (
                      ($y || return !!Math::MPFR::Rmpfr_sgn($x)) < 0
                      ? Math::MPFR::Rmpfr_cmp_si($x, $y)
                      : Math::MPFR::Rmpfr_cmp_ui($x, $y)
                  ) != 0
               );
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        return !Math::GMPq::Rmpq_equal($x, $y);
    }

  Math_GMPq__Math_GMPz: {
        return (!Math::GMPq::Rmpq_integer_p($x) or Math::GMPq::Rmpq_cmp_z($x, $y) != 0);
    }

  Math_GMPq__Math_MPFR: {
        return (!Math::MPFR::Rmpfr_number_p($y) or Math::MPFR::Rmpfr_cmp_q($y, $x) != 0);
    }

  Math_GMPq__Math_MPC: {
        $x = _mpq2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_GMPq__Scalar: {
        return (
                !Math::GMPq::Rmpq_integer_p($x)
                  or (
                      ($y || return !!Math::GMPq::Rmpq_sgn($x)) < 0
                      ? Math::GMPq::Rmpq_cmp_si($x, $y, 1)
                      : Math::GMPq::Rmpq_cmp_ui($x, $y, 1)
                  ) != 0
               );
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        return (Math::GMPz::Rmpz_cmp($x, $y) != 0);
    }

  Math_GMPz__Math_GMPq: {
        return (!Math::GMPq::Rmpq_integer_p($y) or Math::GMPq::Rmpq_cmp_z($y, $x) != 0);
    }

  Math_GMPz__Math_MPFR: {
        return (!Math::MPFR::Rmpfr_integer_p($y) or Math::MPFR::Rmpfr_cmp_z($y, $x) != 0);
    }

  Math_GMPz__Math_MPC: {
        $x = _mpz2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_GMPz__Scalar: {
        return (
                (
                 ($y || return !!Math::GMPz::Rmpz_sgn($x)) < 0
                 ? Math::GMPz::Rmpz_cmp_si($x, $y)
                 : Math::GMPz::Rmpz_cmp_ui($x, $y)
                ) != 0
               );
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {

        my $f1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $f2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($f1, $x);
        Math::MPC::RMPC_RE($f2, $y);

        Math::MPFR::Rmpfr_equal_p($f1, $f2) || return 1;

        Math::MPC::RMPC_IM($f1, $x);
        Math::MPC::RMPC_IM($f2, $y);

        return !Math::MPFR::Rmpfr_equal_p($f1, $f2);
    }

  Math_MPC__Math_GMPz: {
        $y = _mpz2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_GMPq: {
        $y = _mpq2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Scalar: {
        my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_IM($f, $x);
        Math::MPFR::Rmpfr_zero_p($f) || return 1;
        Math::MPC::RMPC_RE($f, $x);
        $x = $f;
        goto Math_MPFR__Scalar;
    }
}

sub ne {
    my ($x, $y) = @_;

    ref($y) ne __PACKAGE__
      and return $y->ne($x);

    __ne__($$x, $$y) ? ($TRUE) : ($FALSE);
}

sub __approx_cmp__ {
    my ($x, $y, $places, $equal) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (defined($places)) {
        _valid(\$places);
        $places = _any2si($$places) // return undef;
    }
    else {
        $places = -((CORE::int($PREC) >> 2) - 1);
    }

    if (   ref($x) eq 'Math::MPFR'
        or ref($y) eq 'Math::MPFR'
        or ref($x) eq 'Math::MPC'
        or ref($y) eq 'Math::MPC') {
        $x = _any2mpfr_mpc($x);
        $y = _any2mpfr_mpc($y);
    }

    $x = __round__($x, $places);
    $y = __round__($y, $places);

    $equal ? __eq__($x, $y) : __cmp__($x, $y);
}

sub approx_cmp {
    my ($x, $y, $places) = @_;
    ((__approx_cmp__($x, $y, $places) // return undef) || return ZERO) > 0 ? ONE : MONE;
}

sub approx_lt {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places) // return undef) < 0 ? $TRUE : $FALSE;
}

sub approx_le {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places) // return undef) <= 0 ? $TRUE : $FALSE;
}

sub approx_gt {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places) // return undef) > 0 ? $TRUE : $FALSE;
}

sub approx_ge {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places) // return undef) >= 0 ? $TRUE : $FALSE;
}

sub approx_eq {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places, 1) // return undef) ? $TRUE : $FALSE;
}

sub approx_ne {
    my ($x, $y, $places) = @_;
    (__approx_cmp__($x, $y, $places, 1) // return undef) ? $FALSE : $TRUE;
}

sub __cmp__ {
    my ($x, $y) = @_;

    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #

  Scalar__Scalar: {
        return ($x <=> $y);
    }

  Math_GMPz__Scalar: {
        return (
                ($y || return Math::GMPz::Rmpz_sgn($x)) < 0
                ? Math::GMPz::Rmpz_cmp_si($x, $y)
                : Math::GMPz::Rmpz_cmp_ui($x, $y)
               );
    }

  Math_GMPq__Scalar: {
        return (
                ($y || return Math::GMPq::Rmpq_sgn($x)) < 0
                ? Math::GMPq::Rmpq_cmp_si($x, $y, 1)
                : Math::GMPq::Rmpq_cmp_ui($x, $y, 1)
               );
    }

  Math_MPFR__Scalar: {
        Math::MPFR::Rmpfr_nan_p($x) && return undef;
        return (
                ($y || return Math::MPFR::Rmpfr_sgn($x)) < 0
                ? Math::MPFR::Rmpfr_cmp_si($x, $y)
                : Math::MPFR::Rmpfr_cmp_ui($x, $y)
               );
    }

  Math_MPC__Scalar: {
        $y = _any2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Scalar__Math_GMPz: {
        my $r = __cmp__($y, $x) // return undef;
        return -$r;
    }

  Scalar__Math_GMPq: {
        my $r = __cmp__($y, $x) // return undef;
        return -$r;
    }

  Scalar__Math_MPFR: {
        my $r = __cmp__($y, $x) // return undef;
        return -$r;
    }

  Scalar__Math_MPC: {
        my $r = __cmp__($y, $x) // return undef;
        return -$r;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        if (   Math::MPFR::Rmpfr_nan_p($x)
            or Math::MPFR::Rmpfr_nan_p($y)) {
            return undef;
        }

        return Math::MPFR::Rmpfr_cmp($x, $y);
    }

  Math_MPFR__Math_GMPz: {
        Math::MPFR::Rmpfr_nan_p($x) && return undef;
        return Math::MPFR::Rmpfr_cmp_z($x, $y);
    }

  Math_MPFR__Math_GMPq: {
        Math::MPFR::Rmpfr_nan_p($x) && return undef;
        return Math::MPFR::Rmpfr_cmp_q($x, $y);
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {
        return Math::GMPq::Rmpq_cmp($x, $y);
    }

  Math_GMPq__Math_GMPz: {
        return Math::GMPq::Rmpq_cmp_z($x, $y);
    }

  Math_GMPq__Math_MPFR: {
        Math::MPFR::Rmpfr_nan_p($y) && return undef;
        return -(Math::MPFR::Rmpfr_cmp_q($y, $x));
    }

  Math_GMPq__Math_MPC: {
        $x = _mpq2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {
        return Math::GMPz::Rmpz_cmp($x, $y);
    }

  Math_GMPz__Math_GMPq: {
        return -(Math::GMPq::Rmpq_cmp_z($y, $x));
    }

  Math_GMPz__Math_MPFR: {
        Math::MPFR::Rmpfr_nan_p($y) && return undef;
        return -(Math::MPFR::Rmpfr_cmp_z($y, $x));
    }

  Math_GMPz__Math_MPC: {
        $x = _mpz2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($f, $x);
        Math::MPFR::Rmpfr_nan_p($f) && return undef;

        Math::MPC::RMPC_RE($f, $y);
        Math::MPFR::Rmpfr_nan_p($f) && return undef;

        Math::MPC::RMPC_IM($f, $x);
        Math::MPFR::Rmpfr_nan_p($f) && return undef;

        Math::MPC::RMPC_IM($f, $y);
        Math::MPFR::Rmpfr_nan_p($f) && return undef;

        my $si     = Math::MPC::Rmpc_cmp($x, $y);
        my $re_cmp = Math::MPC::RMPC_INEX_RE($si);
        $re_cmp == 0 or return $re_cmp;
        return Math::MPC::RMPC_INEX_IM($si);
    }

  Math_MPC__Math_GMPz: {
        $y = _mpz2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_GMPq: {
        $y = _mpq2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }
}

sub icmp {
    my ($x, $y) = @_;
    _valid(\$y);
    $x->to_i->cmp->($y->to_i);
}

*cmpint = \&icmp;

sub cmp {
    my ($x, $y) = @_;

    ref($y) ne __PACKAGE__
      and return do {
        ($y->cmp($x) // return undef)->neg;
      };

    _valid(\$y);
    my $cmp = __cmp__($$x, $$y) // return undef;
    !$cmp ? ZERO : ($cmp > 0) ? ONE : MONE;
}

sub acmp {
    my ($x, $y) = @_;
    _valid(\$y);
    my $cmp = __cmp__(__abs__($$x), __abs__($$y)) // return undef;
    !$cmp ? ZERO : ($cmp > 0) ? ONE : MONE;
}

sub gt {
    my ($x, $y) = @_;
    _valid(\$y);
    ((__cmp__($$x, $$y) // return undef) > 0) ? ($TRUE) : ($FALSE);
}

sub ge {
    my ($x, $y) = @_;
    _valid(\$y);
    ((__cmp__($$x, $$y) // return undef) >= 0) ? ($TRUE) : ($FALSE);
}

sub lt {
    my ($x, $y) = @_;
    _valid(\$y);
    ((__cmp__($$x, $$y) // return undef) < 0) ? ($TRUE) : ($FALSE);
}

sub le {
    my ($x, $y) = @_;
    _valid(\$y);
    ((__cmp__($$x, $$y) // return undef) <= 0) ? ($TRUE) : ($FALSE);
}

sub is_zero {
    my ($x) = @_;
    __eq__($$x, 0) ? ($TRUE) : ($FALSE);
}

sub is_one {
    my ($x) = @_;
    __eq__($$x, 1) ? ($TRUE) : ($FALSE);
}

sub is_mone {
    my ($x) = @_;
    __eq__($$x, -1) ? ($TRUE) : ($FALSE);
}

sub is_positive {
    my ($x) = @_;
    ((__cmp__($$x, 0) // return undef) > 0) ? ($TRUE) : ($FALSE);
}

*is_pos = \&is_positive;

sub is_negative {
    my ($x) = @_;
    ((__cmp__($$x, 0) // return undef) < 0) ? ($TRUE) : ($FALSE);
}

*is_neg = \&is_negative;

sub __sgn__ {
    my ($x) = @_;
    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        return 1  if ($x > 0);
        return -1 if ($x < 0);
        return 0;
    }

  Math_MPFR: {
        return Math::MPFR::Rmpfr_sgn($x);
    }

  Math_GMPq: {
        return Math::GMPq::Rmpq_sgn($x);
    }

  Math_GMPz: {
        return Math::GMPz::Rmpz_sgn($x);
    }

  Math_MPC: {
        my $abs = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::Rmpc_abs($abs, $x, $ROUND);

        if (Math::MPFR::Rmpfr_zero_p($abs)) {    # it's zero
            return 0;
        }

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_div_fr($r, $x, $abs, $ROUND);
        return $r;
    }
}

sub sign {
    my ($x) = @_;
    my $r = __sgn__($$x);
    if (ref($r)) {
        bless \$r;
    }
    else {
        ($r < 0) ? MONE : ($r > 0) ? ONE : ZERO;
    }
}

*sgn = \&sign;

sub popcount {
    my ($x) = @_;
    my $z = _any2mpz($$x, 0) // return undef;

    if (Math::GMPz::Rmpz_sgn($z) < 0) {
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_neg($t, $z);
        $z = $t;
    }

    _set_int(Math::GMPz::Rmpz_popcount($z));
}

*hammingweight = \&popcount;

# Hamming distance
sub hamdist {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // return undef;
    $k = _any2mpz($$k, 1) // return undef;

    _set_int(Math::GMPz::Rmpz_hamdist($n, $k));
}

sub __is_int__ {
    my ($x) = @_;

    ref($x) || return 1;
    ref($x) eq 'Math::GMPz' && return 1;
    ref($x) eq 'Math::GMPq' && return Math::GMPq::Rmpq_integer_p($x);
    ref($x) eq 'Math::MPFR' && return Math::MPFR::Rmpfr_integer_p($x);

    (@_) = _any2mpfr($x);
    goto __SUB__;
}

sub is_int {
    my ($x) = @_;
    __is_int__($$x) ? ($TRUE) : ($FALSE);
}

sub __is_rat__ {
    my ($x) = @_;
    !ref($x) or ref($x) eq 'Math::GMPz' or ref($x) eq 'Math::GMPq';
}

sub is_rat {
    my ($x) = @_;
    __is_rat__($$x) ? ($TRUE) : ($FALSE);
}

sub __is_float__ {
    my ($x) = @_;
    ref($x) eq 'Math::MPFR' or ref($x) eq 'Math::MPC';
}

sub is_float {
    my ($x) = @_;
    __is_float__($$x) ? ($TRUE) : ($FALSE);
}

sub __is_real__ {
    my ($x) = @_;

    ref($x) || return 1;
    ref($x) eq 'Math::GMPz' && return 1;
    ref($x) eq 'Math::GMPq' && return 1;
    ref($x) eq 'Math::MPFR' && return Math::MPFR::Rmpfr_number_p($x);

    (@_) = _any2mpfr($x);
    goto __SUB__;
}

sub is_real {
    my ($x) = @_;
    __is_real__($$x) ? ($TRUE) : ($FALSE);
}

sub __is_imag__ {
    my ($x) = @_;

    ref($x) eq 'Math::MPC' or return 0;

    my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPC::RMPC_RE($f, $x);
    Math::MPFR::Rmpfr_zero_p($f) || return 0;    # is complex
    Math::MPC::RMPC_IM($f, $x);
    !Math::MPFR::Rmpfr_zero_p($f);
}

sub is_imag {
    my ($x) = @_;
    __is_imag__($$x) ? ($TRUE) : ($FALSE);
}

sub __is_complex__ {
    my ($x) = @_;

    ref($x) eq 'Math::MPC' or return 0;

    my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPC::RMPC_IM($f, $x);
    Math::MPFR::Rmpfr_zero_p($f) && return 0;    # is real
    Math::MPC::RMPC_RE($f, $x);
    !Math::MPFR::Rmpfr_zero_p($f);
}

sub is_complex {
    my ($x) = @_;
    __is_complex__($$x) ? ($TRUE) : ($FALSE);
}

sub is_between {
    my ($x, $min, $max) = @_;
    _valid(\$min, \$max);
    (__cmp__($$x, $$min) >= 0 and __cmp__($$x, $$max) <= 0) ? ($TRUE) : ($FALSE);
}

sub is_even {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return (($x % 2 == 0) ? ($TRUE) : ($FALSE));
    }

    (__is_int__($x) && Math::GMPz::Rmpz_even_p(_any2mpz($x, 0) // (return $FALSE))) ? ($TRUE) : ($FALSE);
}

sub is_odd {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return (($x % 2 == 1) ? ($TRUE) : ($FALSE));
    }

    (__is_int__($x) && Math::GMPz::Rmpz_odd_p(_any2mpz($x, 0) // (return $FALSE))) ? ($TRUE) : ($FALSE);
}

sub is_congruent {
    my ($n, $k, $m) = @_;
    _valid(\$k, \$m);

    $n = $$n;
    $k = $$k;
    $m = $$m;

    if (FAST_MODE and !ref($n) and !ref($k) and !ref($m)) {
        $m || return $FALSE;
        return ((($k % $m) == ($n % $m)) ? ($TRUE) : ($FALSE));
    }

    if (ref($n) eq 'Math::GMPz' and ref($k) eq 'Math::GMPz' and ref($m) eq 'Math::GMPz') {
        Math::GMPz::Rmpz_sgn($m) || return $FALSE;
        return (Math::GMPz::Rmpz_congruent_p($n, $k, $m) ? ($TRUE) : ($FALSE));
    }

    __eq__(__mod__($n, $m), __mod__($k, $m)) ? ($TRUE) : ($FALSE);
}

sub is_div {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        $y == 0 and return $FALSE;
        return (($x % $y == 0) ? ($TRUE) : ($FALSE));
    }

    if (ref($x) eq 'Math::GMPz') {

        if (FAST_MODE and !ref($y)) {
            $y == 0 and return $FALSE;
            return (Math::GMPz::Rmpz_divisible_ui_p($x, CORE::abs($y)) ? ($TRUE) : ($FALSE));
        }

        if (ref($y) eq 'Math::GMPz') {
            return ((Math::GMPz::Rmpz_divisible_p($x, $y) && Math::GMPz::Rmpz_sgn($y)) ? ($TRUE) : ($FALSE));
        }
    }

    __eq__(__mod__($x, $y), 0) ? ($TRUE) : ($FALSE);
}

*is_divisible    = \&is_div;
*is_divisible_by = \&is_div;

sub divides {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x)) {
        $x == 0 and return $FALSE;
        if (!ref($y)) {
            return (($y % $x == 0) ? ($TRUE) : ($FALSE));
        }
        elsif (ref($y) eq 'Math::GMPz') {
            return (Math::GMPz::Rmpz_divisible_ui_p($y, CORE::abs($x)) ? ($TRUE) : ($FALSE));
        }
    }

    if (ref($x) eq 'Math::GMPz' and ref($y) eq 'Math::GMPz') {
        return ((Math::GMPz::Rmpz_divisible_p($y, $x) && Math::GMPz::Rmpz_sgn($x)) ? ($TRUE) : ($FALSE));
    }

    __eq__(__mod__($y, $x), 0) ? ($TRUE) : ($FALSE);
}

sub __is_inf__ {
    my ($x) = @_;

    ref($x) || return 0;
    ref($x) eq 'Math::GMPz' && return 0;
    ref($x) eq 'Math::GMPq' && return 0;
    ref($x) eq 'Math::MPFR' && return (Math::MPFR::Rmpfr_inf_p($x) and Math::MPFR::Rmpfr_sgn($x) > 0);

    (@_) = _any2mpfr($x);
    goto __SUB__;
}

sub is_inf {
    my ($x) = @_;
    __is_inf__($$x) ? $TRUE : $FALSE;
}

sub __is_ninf__ {
    my ($x) = @_;

    ref($x) || return 0;
    ref($x) eq 'Math::GMPz' && return 0;
    ref($x) eq 'Math::GMPq' && return 0;
    ref($x) eq 'Math::MPFR' && return (Math::MPFR::Rmpfr_inf_p($x) and Math::MPFR::Rmpfr_sgn($x) < 0);

    (@_) = _any2mpfr($x);
    goto __SUB__;
}

sub is_ninf {
    my ($x) = @_;
    __is_ninf__($$x) ? $TRUE : $FALSE;
}

sub is_nan {
    my ($x) = @_;

    $x = $$x;

    ref($x) || return $FALSE;
    ref($x) eq 'Math::GMPz' && return $FALSE;
    ref($x) eq 'Math::GMPq' && return $FALSE;
    ref($x) eq 'Math::MPFR'
      && return (Math::MPFR::Rmpfr_nan_p($x) ? $TRUE : $FALSE);

    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPC::RMPC_RE($t, $x);
    Math::MPFR::Rmpfr_nan_p($t) && return $TRUE;

    Math::MPC::RMPC_IM($t, $x);
    Math::MPFR::Rmpfr_nan_p($t) && return $TRUE;

    $FALSE;
}

sub sum {
    my (@vals) = @_;

    @vals || return ZERO;

    my @extra;
    my @numbers;

    foreach my $value (@vals) {
        if (ref($value) eq __PACKAGE__) {
            push @numbers, $$value;
        }
        else {
            if (UNIVERSAL::isa($value, 'Sidef::Types::Number::Number')) {
                push @extra, $value;
            }
            else {
                _valid(\$value);
                push @numbers, $$value;
            }
        }
    }

    my @nonmpz;
    my ($native_sum, $new_sum, $sum) = (0, 0, undef);

    foreach my $n (@numbers) {
        if (FAST_MODE and !ref($n)) {
            $new_sum = $native_sum + $n;

            if (FAST_MODE and $new_sum < ULONG_MAX and $new_sum > LONG_MIN) {
                $native_sum = $new_sum;
            }
            else {
                $sum //= Math::GMPz::Rmpz_init_set_ui(0);
                ($native_sum < 0)
                  ? Math::GMPz::Rmpz_sub_ui($sum, $sum, -$native_sum)
                  : Math::GMPz::Rmpz_add_ui($sum, $sum, $native_sum);
                $native_sum = $n;    # reset the sum
            }
        }
        elsif (ref($n) eq 'Math::GMPz') {
            $sum //= Math::GMPz::Rmpz_init_set_ui(0);
            Math::GMPz::Rmpz_add($sum, $sum, $n);
        }
        else {
            push @nonmpz, $n;
        }
    }

    if (!defined($sum)) {
        $sum = $native_sum;
    }
    elsif ($native_sum != 0) {

        ($native_sum < 0)
          ? Math::GMPz::Rmpz_sub_ui($sum, $sum, -$native_sum)
          : Math::GMPz::Rmpz_add_ui($sum, $sum, $native_sum);

        if (Math::GMPz::Rmpz_fits_slong_p($sum)) {
            $sum = Math::GMPz::Rmpz_get_si($sum);
        }
    }

    if (@nonmpz) {
        $sum = __add__($sum, _binsplit(\@nonmpz, \&__add__));
    }

    my $r = bless \$sum;

    if (@extra) {
        $r = $r->add(_binsplit(\@extra, sub { $_[0]->add($_[1]) }));
    }

    $r;
}

*Σ      = \&sum;
*vecsum = \&sum;

sub prod {
    my (@vals) = @_;

    @vals || return ONE;

    my @numbers;
    my @unknown;

    foreach my $value (@vals) {
        if (ref($value) eq __PACKAGE__) {
            push @numbers, $$value;
        }
        else {
            if (UNIVERSAL::isa($value, 'Sidef::Types::Number::Number')) {
                push @unknown, $value;
            }
            else {
                _valid(\$value);
                push @numbers, $$value;
            }
        }
    }

    my $r = (@numbers ? (bless \_binsplit(\@numbers, \&__mul__)) : ONE);

    if (@unknown) {
        $r = $r->mul(_binsplit(\@unknown, sub { $_[0]->mul($_[1]) }));
    }

    return $r;
}

*Π       = \&prod;
*vecprod = \&prod;

sub max {
    my (@vals) = @_;
    _valid(\(@vals));

    my $max = shift(@vals);

    foreach my $curr (@vals) {
        if ((__cmp__($$curr, $$max) // return undef) > 0) {
            $max = $curr;
        }
    }

    $max;
}

*vecmax = \&max;

sub min {
    my (@vals) = @_;
    _valid(\(@vals));

    my $min = shift(@vals);

    foreach my $curr (@vals) {
        if ((__cmp__($$curr, $$min) // return undef) < 0) {
            $min = $curr;
        }
    }

    $min;
}

*vecmin = \&min;

sub as_int {
    my ($x, $y) = @_;

    my $base = 10;
    if (defined($y)) {
        _valid(\$y);
        $base = _any2ui($$y) // 0;
        if ($base < 2 or $base > 62) {
            die "[ERROR] Number.as_int(): base must be between 2 and 62, got $y";
        }
    }

    Sidef::Types::String::String->new(Math::GMPz::Rmpz_get_str((_any2mpz($$x, 0) // return undef), $base));
}

sub __base__ {
    my ($x, $base) = @_;
    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        $x = _any2mpz($x, 0);
        goto Math_GMPz;
    }

  Math_GMPz: {
        return Math::GMPz::Rmpz_get_str($x, $base);
    }

  Math_GMPq: {
        return Math::GMPq::Rmpq_get_str($x, $base);
    }

  Math_MPFR: {
        return Math::MPFR::Rmpfr_get_str($x, $base, 0, $ROUND);
    }

  Math_MPC: {
        my $fr = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPC::RMPC_RE($fr, $x);
        my $real = __base__($fr, $base);
        Math::MPC::RMPC_IM($fr, $x);
        return $real if Math::MPFR::Rmpfr_zero_p($fr);
        my $imag = __base__($fr, $base);
        return "($real $imag)";
    }
}

sub base {
    my ($x, $y) = @_;

    my $base = 10;

    if (defined($y)) {
        _valid(\$y);
        $base = _any2ui($$y) // 0;
        if ($base < 2 or $base > 62) {
            die "[ERROR] Number.base(): base must be between 2 and 62, got $y";
        }
    }

    $x = $$x;

    Sidef::Types::String::String->new(__base__($x, $base));
}

*in_base = \&base;

sub as_rat {
    my ($x, $y) = @_;

    my $base = 10;
    if (defined($y)) {
        _valid(\$y);
        $base = _any2ui($$y) // 0;
        if ($base < 2 or $base > 62) {
            die "[ERROR] base must be between 2 and 62, got $y";
        }
    }

    my $str =
      ref($$x) eq 'Math::GMPz'
      ? Math::GMPz::Rmpz_get_str($$x, $base)
      : Math::GMPq::Rmpq_get_str((_any2mpq($$x) // return undef), $base);

    Sidef::Types::String::String->new($str);
}

sub as_frac {
    my ($x, $y) = @_;

    my $base = 10;
    if (defined($y)) {
        _valid(\$y);
        $base = _any2ui($$y) // 0;
        if ($base < 2 or $base > 62) {
            die "as_frac(): base must be between 2 and 62, got $y";
        }
    }

    my $str =
      ref($$x) eq 'Math::GMPz'
      ? Math::GMPz::Rmpz_get_str($$x, $base)
      : Math::GMPq::Rmpq_get_str((_any2mpq($$x) // return undef), $base);

    $str .= '/1' if (index($str, '/') == -1);

    Sidef::Types::String::String->new($str);
}

sub as_cfrac {
    my ($x, $n) = @_;

    my $p = CORE::int($PREC) >> 1;

    $x = $$x;
    $n = defined($n) ? do { _valid(\$n); _any2ui($$n) // 0 } : ($p >> 1);

    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Math_GMPq: {
        my @cfrac;
        my $q = Math::GMPq::Rmpq_init();

        Math::GMPq::Rmpq_set($q, $x);

        for (1 .. $n) {
            my $z = __floor__($q);
            push @cfrac, bless \$z;
            Math::GMPq::Rmpq_sub_z($q, $q, _any2mpz($z));
            Math::GMPq::Rmpq_sgn($q) || last;
            Math::GMPq::Rmpq_inv($q, $q);
        }

        return _array(\@cfrac);
    }

  Math_MPFR: {
        my @cfrac;
        my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPFR::Rmpfr_set($f, $x, $ROUND);

        for (1 .. $n) {
            my $t = __floor__($f);
            push @cfrac, bless \(_any2mpz($t) // $t);

            Math::MPFR::Rmpfr_eq($f, $t, $p) && last;
            Math::MPFR::Rmpfr_sub($f, $f, $t, $ROUND);
            Math::MPFR::Rmpfr_ui_div($f, 1, $f, $ROUND);
        }

        return _array(\@cfrac);
    }

  Math_MPC: {
        my @cfrac;
        my $c = Math::MPC::Rmpc_init2(CORE::int($PREC));

        my $real_1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $real_2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        my $imag_1 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag_2 = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        my $one = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_ui($one, 1, $ROUND);

        Math::MPC::Rmpc_set($c, $x, $ROUND);

        for (1 .. $n) {
            my $t = __round__($c, 0);
            push @cfrac, bless \(_any2mpz($t) // $t);

            Math::MPC::Rmpc_real($real_1, $c, $ROUND);
            Math::MPC::Rmpc_imag($imag_1, $c, $ROUND);

            if (ref($t) eq 'Math::MPFR') {
                Math::MPFR::Rmpfr_neg($t, $t, $ROUND);
                Math::MPC::Rmpc_add_fr($c, $c, $t, $ROUND);
                Math::MPFR::Rmpfr_neg($t, $t, $ROUND);

                Math::MPFR::Rmpfr_set($real_2, $t, $ROUND);
                Math::MPFR::Rmpfr_set_ui($imag_2, 0, $ROUND);
            }
            else {
                Math::MPC::Rmpc_sub($c, $c, $t, $ROUND);

                Math::MPC::Rmpc_real($real_2, $t, $ROUND);
                Math::MPC::Rmpc_imag($imag_2, $t, $ROUND);
            }

#<<<
               Math::MPFR::Rmpfr_eq($real_1, $real_2, $p)
            && Math::MPFR::Rmpfr_eq($imag_1, $imag_2, $p)
            && last;
#>>>

            Math::MPC::Rmpc_div($c, $one, $c, $ROUND);
        }

        return _array(\@cfrac);
    }

  Math_GMPz: {
        return _array([bless \$x]);
    }

  Scalar: {
        return _array([bless \$x]);
    }
}

*cfrac = \&as_cfrac;

sub as_float {
    my ($x, $prec) = @_;

    if (defined($prec)) {
        _valid(\$prec);
        $prec = (_any2ui($$prec) // 0) << 2;

        state $min_prec = Math::MPFR::RMPFR_PREC_MIN();
        state $max_prec = Math::MPFR::RMPFR_PREC_MAX();

        if ($prec < $min_prec or $prec > $max_prec) {
            die "as_float(): precision must be between $min_prec and $max_prec, got ", $prec >> 2;
        }
    }
    else {
        $prec = CORE::int($PREC);
    }

    local $PREC = $prec;
    Sidef::Types::String::String->new(__stringify__(_any2mpfr_mpc($$x)));
}

*as_dec = \&as_float;

# Utility: Polynomial multiplication modulo P(x) (Fiduccia's algorithm core)
sub _poly_mul_mod {
    my ($A, $B, $ker, $k, $mod) = @_;

    # Pre-allocate result array to avoid dynamic resizing
    my @C = map { Math::GMPz::Rmpz_init_set_ui(0) } (0 .. 2 * $k - 2);

    # Polynomial multiplication (schoolbook)
    for my $i (0 .. $k - 1) {
        my $a = $A->[$i] // next;
        Math::GMPz::Rmpz_sgn($a) || next;

        for my $j (0 .. $k - 1) {
            my $b = $B->[$j] // next;
            Math::GMPz::Rmpz_sgn($b) || next;

            my $idx = $i + $j;
            Math::GMPz::Rmpz_addmul($C[$idx], $a, $b);
        }
    }

    # Apply mod if provided
    if (defined $mod) {
        for my $c (@C) {
            Math::GMPz::Rmpz_mod($c, $c, $mod);
        }
    }

    # Reduce modulo characteristic polynomial P(x)
    for (my $i = $#C ; $i >= $k ; $i--) {
        my $c = $C[$i];
        Math::GMPz::Rmpz_sgn($c) || next;

        # Multiply and subtract: C -= c * ker * x^{i-k}
        for my $j (1 .. $k) {
            my $idx = $i - $j;
            Math::GMPz::Rmpz_addmul($C[$idx], $c, $ker->[$j - 1]);
        }

        # Apply mod if provided (only to updated indices)
        if (defined $mod) {
            for my $j (1 .. $k) {
                my $idx = $i - $j;
                Math::GMPz::Rmpz_mod($C[$idx], $C[$idx], $mod);
            }
        }

        # Zero out the reduced coefficient
        Math::GMPz::Rmpz_set_ui($C[$i], 0);
    }

    # Truncate to degree k-1
    $#C = $k - 1;

    return \@C;
}

# Utility: Polynomial exponentiation modulo P(x)
sub _poly_pow_mod {
    my ($n, $ker, $k, $mod) = @_;

    # Pre-allocate result and base
    my @res = map { Math::GMPz::Rmpz_init_set_ui(0) } (0 .. $k - 1);
    $res[0] //= Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_set_ui($res[0], 1);    # Start with 1

    my @base = map { Math::GMPz::Rmpz_init_set_ui(0) } (0 .. $k - 1);
    $base[1] //= Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_set_ui($base[1], 1);    # x^1

    # Edge case: degree 1 reduction (P(x) = a0 + a1*x)
    if ($k == 1) {
        Math::GMPz::Rmpz_set($base[0], $ker->[0]);
        Math::GMPz::Rmpz_mod($base[0], $base[0], $mod) if defined $mod;
    }

    my $exp = Math::GMPz::Rmpz_init_set($n);

    while (Math::GMPz::Rmpz_sgn($exp) > 0) {
        if (Math::GMPz::Rmpz_odd_p($exp)) {
            @res = @{_poly_mul_mod(\@res, \@base, $ker, $k, $mod)};
        }

        Math::GMPz::Rmpz_fdiv_q_2exp($exp, $exp, 1);

        if (Math::GMPz::Rmpz_sgn($exp) > 0) {
            @base = @{_poly_mul_mod(\@base, \@base, $ker, $k, $mod)};
        }
    }

    return \@res;
}

sub _linear_recurrence_mpz {
    my ($ker, $init, $n_min, $n_max) = @_;

    my $want_array = 1;
    if (!defined($n_max)) {
        $n_max      = $n_min;
        $want_array = 0;
    }

    my $k = scalar(@$ker);
    return $want_array ? _array() : ZERO if $k == 0;

    # Convert kernel and initial terms to GMPz
    my @gmp_ker    = map { _any2mpz($$_) // goto &nan } @$ker;
    my @init_terms = map { _any2mpz($$_) // goto &nan } @$init[0 .. ($#$init > $#$ker ? $#$ker : $#$init)];

    # Pad initial terms with zeros if needed
    while (@init_terms < $k) {
        push @init_terms, $ZERO;
    }

    my $n = _any2mpz($$n_min) // goto &nan;

    # Compute x^n mod P(x)
    my $res = _poly_pow_mod($n, \@gmp_ker, $k, undef);

    # Fast-path: single value
    if (!$want_array) {
        my $val = Math::GMPz::Rmpz_init_set_ui(0);
        for my $i (0 .. $k - 1) {
            next unless defined $res->[$i];
            Math::GMPz::Rmpz_addmul($val, $res->[$i], $init_terms[$i]);
        }
        return bless \$val;
    }

    # Compute the range
    my $count = Math::GMPz::Rmpz_init_set(_any2mpz($$n_max) // return _array());
    Math::GMPz::Rmpz_sub($count, $count, $n);
    return _array() if Math::GMPz::Rmpz_sgn($count) < 0;

    my $total_terms = Math::GMPz::Rmpz_get_ui($count) + 1;
    my @result_seq;
    my @window;

    # Compute initial window: [x^{n_min} * init, x^{n_min+1} * init, ..., x^{n_min+k-1} * init]
    for my $step (0 .. $k - 1) {
        my $val = Math::GMPz::Rmpz_init_set_ui(0);
        for my $i (0 .. $k - 1) {
            Math::GMPz::Rmpz_addmul($val, $res->[$i], $init_terms[$i]) if Math::GMPz::Rmpz_sgn($res->[$i]);
        }
        push @window, $val;

        # Multiply R(x) by x mod P(x) for sequential state steps
        if ($step < $k - 1) {
            my $top     = $res->[$k - 1];
            my $sgn_top = Math::GMPz::Rmpz_sgn($top);
            for (my $i = $k - 1 ; $i >= 1 ; $i--) {
                $res->[$i] = Math::GMPz::Rmpz_init_set($res->[$i - 1]);
                Math::GMPz::Rmpz_addmul($res->[$i], $top, $gmp_ker[$k - 1 - $i]) if $sgn_top;
            }
            Math::GMPz::Rmpz_set_ui($res->[0], 0);
            Math::GMPz::Rmpz_addmul($res->[0], $top, $gmp_ker[$k - 1]) if $sgn_top;
        }
    }

    # LFSR: generate terms from n_min to n_max
    for my $i (0 .. $total_terms - 1) {
        if ($i < $k) {
            my $val = $window[$i];
            push @result_seq, bless \$val;
        }
        else {
            # Compute next term: sum_{j=0}^{k-1} ker[j] * window[k-1-j]
            my $next_val = Math::GMPz::Rmpz_init_set_ui(0);
            for my $j (0 .. $k - 1) {
                Math::GMPz::Rmpz_addmul($next_val, $gmp_ker[$j], $window[$k - 1 - $j]);
            }

            shift @window;
            push @window,     $next_val;
            push @result_seq, bless \$next_val;
        }
    }

    return _array(\@result_seq);
}

sub _linear_recurrence_mod_mpz {
    my ($ker, $init, $n, $m) = @_;

    my $k = scalar(@$ker);
    return Sidef::Types::Number::Number::ZERO if $k == 0;

    my $gmp_m = _any2mpz($$m) // goto &nan;
    Math::GMPz::Rmpz_sgn($gmp_m) || goto &nan;

    # Pre-allocate kernel and initial terms with mod applied
    my @gmp_ker = map {
        my $z = _any2mpz($$_) // goto &nan;
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mod($t, $z, $gmp_m);
        $t;
    } @$ker;

    my @init_terms = map {
        my $z = _any2mpz($$_) // goto &nan;
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mod($t, $z, $gmp_m);
        $t;
    } @$init[0 .. ($#$init > $#$ker ? $#$ker : $#$init)];

    # Pad with zeros
    while (@init_terms < $k) {
        push @init_terms, $ZERO;
    }

    my $gmp_n = _any2mpz($$n) // goto &nan;
    my $res   = _poly_pow_mod($gmp_n, \@gmp_ker, $k, $gmp_m);

    # Compute dot product: res · init_terms
    my $val = Math::GMPz::Rmpz_init_set_ui(0);
    for my $i (0 .. $k - 1) {
        next unless defined $res->[$i];
        Math::GMPz::Rmpz_addmul($val, $res->[$i], $init_terms[$i]);
    }
    Math::GMPz::Rmpz_mod($val, $val, $gmp_m);

    return bless \$val;
}

sub _solve_rec_seq {    # Berlekamp-Massey algorithm
    my (@seq) = @_;

    # Reference:
    #   https://youtube.com/watch?v=NO1_-qptr6c

    _valid(\(@seq));

    my $n = scalar(@seq);
    return _array() if ($n == 0);

    my $from_mpq = sub {
        my ($q) = @_;

        if (Math::GMPq::Rmpq_integer_p($q)) {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPq::Rmpq_get_num($z, $q);
            return bless \$z;
        }

        return bless \$q;
    };

    my @s = map { _any2mpq($$_) // return _array() } @seq;

    my @C = (Math::GMPq->new(1));    # connection polynomial coefficients
    my @B = (Math::GMPq->new(1));    # B from the last length-increase step
    my $L = 0;                       # LFSR length
    my $m = 1;                       # steps since last length increase
    my $b = Math::GMPq->new(1);      # discrepancy at last length increase

    my $tmp   = Math::GMPq::Rmpq_init();
    my $coeff = Math::GMPq::Rmpq_init();

    for my $i (0 .. $n - 1) {

        my $d = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set($d, $s[$i]);

        for my $j (1 .. $L) {
            Math::GMPq::Rmpq_mul($tmp, $C[$j], $s[$i - $j]);
            Math::GMPq::Rmpq_add($d, $d, $tmp);
        }

        # Zero discrepancy — C already explains this term.
        if (Math::GMPq::Rmpq_sgn($d) == 0) {
            ++$m;
            next;
        }

        # δ = d / b
        Math::GMPq::Rmpq_div($coeff, $d, $b);

        my @T = map {
            my $q = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_set($q, $_);
            $q
        } @C;

        # Ensure C has enough slots: highest index touched = ($#B + $m).
        my $need = @B + $m;
        push @C, Math::GMPq::Rmpq_init() while (scalar(@C) < $need);

        # C(x) ← C(x) − δ · xᵐ · B(x)
        for my $j (0 .. $#B) {
            Math::GMPq::Rmpq_mul($tmp, $coeff, $B[$j]);
            Math::GMPq::Rmpq_sub($C[$j + $m], $C[$j + $m], $tmp);
        }

        if (2 * $L <= $i) {
            $L = $i + 1 - $L;
            @B = @T;
            Math::GMPq::Rmpq_set($b, $d);
            $m = 1;
        }
        else {
            ++$m;
        }
    }

    my @result;
    for my $k (1 .. $L) {
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_neg($q, $C[$k]);
        push @result, $from_mpq->($q);
    }

    _array(\@result);
}

sub _solve_seq {    # Newton's forward difference formula
    my ($offset, @sequence) = @_;

    _valid(\$offset);
    _valid(\(@sequence));

    my $N = scalar(@sequence);

    if ($N == 0) {
        return Sidef::Types::Number::Polynomial->new();
    }

    # Extract sequence into raw Math::GMPq objects for speed
    my @S = map { _any2mpq($$_) // return Sidef::Types::Number::Polynomial->new() } @sequence;
    my @c;

    for my $k (0 .. $N - 1) {

        my $all_zero = 1;
        for my $i (0 .. $N - 1 - $k) {
            if (Math::GMPq::Rmpq_sgn($S[$i]) != 0) {
                $all_zero = 0;
                last;
            }
        }

        last if $all_zero;

        # Capture the leading delta coefficient for the current degree
        $c[$k] = Math::GMPq->new($S[0]);

        # Generate next difference tier within the same buffer
        for my $i (0 .. $N - 2 - $k) {
            Math::GMPq::Rmpq_sub($S[$i], $S[$i + 1], $S[$i]);
        }
    }

    # If all terms were zero, return an empty polynomial
    return Sidef::Types::Number::Polynomial->new() if !@c;

    my @master_poly;
    my @P          = (Math::GMPq->new(1));
    my $fact       = Math::GMPz::Rmpz_init_set_ui(1);
    my $offset_mpz = defined($offset) ? _any2mpz($$offset) : Math::GMPz::Rmpz_init_set_ui(0);

    # Pre-allocate mathematical scratchpads
    my $f_k   = Math::GMPq::Rmpq_init();
    my $term  = Math::GMPq::Rmpq_init();
    my $alpha = Math::GMPq::Rmpq_init();
    my $tmp   = Math::GMPq::Rmpq_init();

    for my $k (0 .. $#c) {
        my $ck = $c[$k];

        if (Math::GMPq::Rmpq_sgn($ck) != 0) {

            # f_k = c_k / k!
            Math::GMPq::Rmpq_div_z($f_k, $ck, $fact);

            # Accumulate current basis expansion into master polynomial
            for my $i (0 .. $#P) {
                $master_poly[$i] //= Math::GMPq::Rmpq_init();
                Math::GMPq::Rmpq_mul($term, $f_k, $P[$i]);
                Math::GMPq::Rmpq_add($master_poly[$i], $master_poly[$i], $term);
            }
        }

        # Progressively build the binomial basis: P_{k+1}(x) = P_k(x) * (x - (offset + k))
        if ($k < $#c) {

            # fact = fact * (k + 1)
            Math::GMPz::Rmpz_mul_ui($fact, $fact, $k + 1);

            # alpha = offset + k
            Math::GMPz::Rmpz_add_ui($alpha, $offset_mpz, $k);

            my @next_P;

            # next_P[0] = -alpha * P[0]
            $next_P[0] = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_mul_z($next_P[0], $P[0], $alpha);
            Math::GMPq::Rmpq_neg($next_P[0], $next_P[0]);

            # Linear combinations for intermediate polynomial indices
            for my $i (1 .. $k) {
                $next_P[$i] = $P[$i - 1];
                Math::GMPq::Rmpq_mul_z($tmp, $P[$i], $alpha);
                Math::GMPq::Rmpq_sub($next_P[$i], $next_P[$i], $tmp);
            }

            # Shift the highest degree term up: next_P[k+1] = P[k] * x
            $next_P[$k + 1] = $P[$k];
            @P = @next_P;
        }
    }

    my @poly_args;
    for my $i (0 .. $#master_poly) {
        next if (!defined $master_poly[$i] or Math::GMPq::Rmpq_sgn($master_poly[$i]) == 0);
        push(@poly_args, $i, Sidef::Types::Number::Number->new($master_poly[$i]));
    }

    return Sidef::Types::Number::Polynomial->new(@poly_args);
}

# Solution in integers to `x^2 - d*y^2 = n`
# where `d` and `n` are provided (n=1 by default).

sub solve_pell {
    my ($d, $n) = @_;

    $d = _any2mpz($$d, 0) // return (undef, undef);

    if (defined($n)) {
        _valid(\$n);
        $n = _any2mpz($$n, 1) // return (undef, undef);
    }
    else {
        $n = $ONE;
    }

    # No solutions for d <= 0 or n = 0
    if (   Math::GMPz::Rmpz_sgn($d) <= 0
        or Math::GMPz::Rmpz_sgn($n) == 0) {
        return (undef, undef);
    }

    # No solutions to `x^2 - d*y^2 = n` if `d` is a perfect square
    if (Math::GMPz::Rmpz_perfect_square_p($d)) {
        return (undef, undef);
    }

    my $a0 = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($a0, $d);

    my $P = Math::GMPz::Rmpz_init_set_ui(0);
    my $Q = Math::GMPz::Rmpz_init_set_ui(1);
    my $a = Math::GMPz::Rmpz_init_set($a0);

    my $h2 = Math::GMPz::Rmpz_init_set_ui(0);
    my $h1 = Math::GMPz::Rmpz_init_set_ui(1);
    my $k2 = Math::GMPz::Rmpz_init_set_ui(1);
    my $k1 = Math::GMPz::Rmpz_init_set_ui(0);

    my $h = Math::GMPz::Rmpz_init();
    my $k = Math::GMPz::Rmpz_init();

    my $next_P = Math::GMPz::Rmpz_init();
    my $next_Q = Math::GMPz::Rmpz_init();
    my $val    = Math::GMPz::Rmpz_init();

    my $U = undef;
    my $V = undef;

    # The bound of the square root period is: O(sqrt(d)*log(d))
    my $max = Math::GMPz::Rmpz_get_d($a0);
    $max = CORE::int(4 * $max * CORE::log($max) + 10);

    # Continued Fraction pass for fundamental unit & small n convergents
    for (my $i = 0 ; $i <= $max ; ++$i) {

        # Numerator (h): h = a * h1 + h2
        Math::GMPz::Rmpz_mul($h, $a, $h1);
        Math::GMPz::Rmpz_add($h, $h, $h2);

        # Denominator (k): k = a * k1 + k2
        Math::GMPz::Rmpz_mul($k, $a, $k1);
        Math::GMPz::Rmpz_add($k, $k, $k2);

        # next_P = a * Q - P
        Math::GMPz::Rmpz_mul($next_P, $a, $Q);
        Math::GMPz::Rmpz_sub($next_P, $next_P, $P);

        # next_Q = (d - next_P^2) / Q
        Math::GMPz::Rmpz_mul($next_Q, $next_P, $next_P);
        Math::GMPz::Rmpz_sub($next_Q, $d, $next_Q);
        Math::GMPz::Rmpz_divexact($next_Q, $next_Q, $Q);

        # h^2 - d*k^2 = (-1)^{i+1} * Q_{i+1}
        my $sgn = ($i % 2 == 0) ? -1 : 1;

        # Check if the current convergent satisfies x^2 - d*y^2 = n
        if (!defined $U && Math::GMPz::Rmpz_cmp_ui($n, 1) != 0) {
            Math::GMPz::Rmpz_set($val, $next_Q);
            if ($sgn == -1) {
                Math::GMPz::Rmpz_neg($val, $val);
            }
            if (Math::GMPz::Rmpz_cmp($val, $n) == 0) {
                my $ret_h = Math::GMPz::Rmpz_init_set($h);
                my $ret_k = Math::GMPz::Rmpz_init_set($k);
                return ((bless \$ret_h), (bless \$ret_k));
            }
        }

        # Check if we found the fundamental solution to x^2 - d*y^2 = 1
        if (!defined $U && $sgn == 1 && Math::GMPz::Rmpz_cmp_ui($next_Q, 1) == 0) {
            $U = Math::GMPz::Rmpz_init_set($h);
            $V = Math::GMPz::Rmpz_init_set($k);

            if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
                return ((bless \$U), (bless \$V));
            }
            last;    # Break and proceed to bounded search for arbitrary n
        }

        # Update CF sequences
        Math::GMPz::Rmpz_set($P, $next_P);
        Math::GMPz::Rmpz_set($Q, $next_Q);

        # a_{i+1} = floor((a0 + P_{i+1}) / Q_{i+1})
        Math::GMPz::Rmpz_add($a, $a0, $P);
        Math::GMPz::Rmpz_div($a, $a, $Q);

        Math::GMPz::Rmpz_set($h2, $h1);
        Math::GMPz::Rmpz_set($h1, $h);

        Math::GMPz::Rmpz_set($k2, $k1);
        Math::GMPz::Rmpz_set($k1, $k);
    }

    # Bounded search for Generalized Pell Equation
    if (defined $U && Math::GMPz::Rmpz_cmp_ui($n, 1) != 0) {

        # Max bounds according to Nagell: Y_max = floor( sqrt( |n| * (U -/+ 1) / 2d ) )
        my $Y_max = Math::GMPz::Rmpz_init();
        my $n_abs = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_abs($n_abs, $n);

        my $u_mod = Math::GMPz::Rmpz_init_set($U);
        if (Math::GMPz::Rmpz_sgn($n) > 0) {
            Math::GMPz::Rmpz_sub_ui($u_mod, $u_mod, 1);
        }
        else {
            Math::GMPz::Rmpz_add_ui($u_mod, $u_mod, 1);
        }

        Math::GMPz::Rmpz_mul($Y_max, $n_abs, $u_mod);
        my $twod = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mul_2exp($twod, $d, 1);

        Math::GMPz::Rmpz_tdiv_q($Y_max, $Y_max, $twod);
        Math::GMPz::Rmpz_sqrt($Y_max, $Y_max);

        my $Y     = Math::GMPz::Rmpz_init_set_ui(0);
        my $limit = Math::GMPz::Rmpz_init_set($Y_max);

        my $X_sq = Math::GMPz::Rmpz_init();
        my $X    = Math::GMPz::Rmpz_init();

        # Iterate Y up to the upper limit
        while (Math::GMPz::Rmpz_cmp($Y, $limit) <= 0) {

            # X^2 = n + d * Y^2
            Math::GMPz::Rmpz_mul($X_sq, $Y,    $Y);
            Math::GMPz::Rmpz_mul($X_sq, $X_sq, $d);
            Math::GMPz::Rmpz_add($X_sq, $X_sq, $n);

            if (Math::GMPz::Rmpz_sgn($X_sq) >= 0 && Math::GMPz::Rmpz_perfect_square_p($X_sq)) {
                Math::GMPz::Rmpz_sqrt($X, $X_sq);

                my $ret_x = Math::GMPz::Rmpz_init_set($X);
                my $ret_y = Math::GMPz::Rmpz_init_set($Y);
                return ((bless \$ret_x), (bless \$ret_y));
            }
            Math::GMPz::Rmpz_add_ui($Y, $Y, 1);
        }
    }

    # No solution could be found
    return (undef, undef);
}

sub solve_lcg {
    my ($n, $r, $m) = @_;

    # Solve: n*x == r (mod m)

    _valid(\$r, \$m);

    $n = _any2mpz($$n, 0) // goto &nan;
    $r = _any2mpz($$r, 1) // goto &nan;
    $m = _any2mpz($$m, 2) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    my $g = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_gcd($g, $n, $m);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) != 0) {

        # No solution exists if `r` is NOT divisible by `gcd(n,m)`
        Math::GMPz::Rmpz_divisible_p($r, $g) || goto &nan;

        $n = Math::GMPz::Rmpz_init_set($n);
        $r = Math::GMPz::Rmpz_init_set($r);
        $m = Math::GMPz::Rmpz_init_set($m);

        Math::GMPz::Rmpz_divexact($n, $n, $g);
        Math::GMPz::Rmpz_divexact($r, $r, $g);
        Math::GMPz::Rmpz_divexact($m, $m, $g);
    }

    Math::GMPz::Rmpz_invert($g, $n, $m);
    Math::GMPz::Rmpz_mul($g, $g, $r);
    Math::GMPz::Rmpz_mod($g, $g, $m);

    bless \$g;
}

*solve_linear_congruence = \&solve_lcg;

sub linear_congruence {
    my ($n, $r, $m) = @_;

    if ($n->is_div($m)) {
        if ($r->is_div($m)) {
            return $m->range->to_a;
        }
        else {
            return _array();
        }
    }

    # TODO: rewrite for better performance

    $n = _big2istr($$n) // return _array();
    $r = _big2istr($$r) // return _array();
    $m = _big2istr($$m) // return _array();

    my ($u, $v, $g) = Math::Prime::Util::GMP::gcdext($n, $m);

    if (Math::Prime::Util::GMP::modint($r, $g) ne '0') {
        return _array();
    }

    my $orig_m = $m;

    $r = Math::Prime::Util::GMP::divint($r, $g);
    $m = Math::Prime::Util::GMP::divint($m, $g);

    my $z = Math::Prime::Util::GMP::mulint($u, $r);

    my @solutions;
    for my $t (0 .. $g - 1) {
        push @solutions, _set_int(Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::addint($z, Math::Prime::Util::GMP::mulint($m, $t)), $orig_m));
    }

    return _array(\@solutions)->isort;
}

sub sqrt_cfrac_period_each {
    my ($n, $block, $max) = @_;

    $n   = _any2mpz($$n, 0) // return ZERO;
    $max = defined($max) ? CORE::int($max) : (0 + 'inf');

    Math::GMPz::Rmpz_sgn($n) < 0
      and return ZERO;

    my $x = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($x, $n);

    Math::GMPz::Rmpz_perfect_square_p($n)
      and return ZERO;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {

        $n = Math::GMPz::Rmpz_get_ui($n);
        $x = Math::GMPz::Rmpz_get_ui($x);

        my $y = $x;
        my $z = 1;
        my $r = $x + $x;

        my $count = 0;

        for (; $count < $max ; ++$count) {

            $y = $r * $z - $y;
            $z = CORE::int(($n - $y * $y) / $z);
            $r = CORE::int(($x + $y) / $z);

            $block->run(_set_int($r));

            if ($z == 1) {
                ++$count;
                last;
            }
        }

        return _set_int($count);
    }

    my $y = Math::GMPz::Rmpz_init_set($x);
    my $z = Math::GMPz::Rmpz_init_set_ui(1);
    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_add($r, $x, $x);    # r = x+x

    my $count = 0;
    for (; $count < $max ; ++$count) {

        my $t = Math::GMPz::Rmpz_init();

        # y = (r*z - y)
        Math::GMPz::Rmpz_submul($y, $r, $z);    # y = y - t*z
        Math::GMPz::Rmpz_neg($y, $y);           # y = -y

        # z = ((n - y*y) / z)
        Math::GMPz::Rmpz_mul($t, $y, $y);         # t = y*y
        Math::GMPz::Rmpz_sub($t, $n, $t);         # t = n-t
        Math::GMPz::Rmpz_divexact($z, $t, $z);    # z = t/z

        # t = floor((x + y) / z)
        Math::GMPz::Rmpz_add($t, $x, $y);         # t = x+y
        Math::GMPz::Rmpz_div($t, $t, $z);         # t = floor(t/z)

        $r = $t;
        $block->run(bless \$t);

        if (Math::GMPz::Rmpz_cmp_ui($z, 1) == 0) {
            ++$count;
            last;
        }
    }

    return _set_int($count);
}

sub sqrt_cfrac {
    my ($n, $max) = @_;
    $n->sqrt_cfrac_period($max)->unshift($n->isqrt);
}

sub sqrt_cfrac_period {
    my ($n, $max) = @_;

    my @cfrac;

    $n->sqrt_cfrac_period_each(
        Sidef::Types::Block::Block->new(
            code => sub {
                push @cfrac, $_[0];
            }
        ),
        $max
    );

    _array(\@cfrac);
}

sub sqrt_cfrac_period_len {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    return ZERO if Math::GMPz::Rmpz_perfect_square_p($n);

    goto &nan if Math::GMPz::Rmpz_sgn($n) < 0;

    my $t = Math::GMPz::Rmpz_init();
    my $x = Math::GMPz::Rmpz_init();
    my $z = Math::GMPz::Rmpz_init_set_ui(1);

    Math::GMPz::Rmpz_sqrt($x, $n);

    my $y = Math::GMPz::Rmpz_init_set($x);

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {

        $n = Math::GMPz::Rmpz_get_ui($n);
        $x = Math::GMPz::Rmpz_get_ui($x);

        my $y = $x;
        my $z = 1;
        my $r = $x + $x;

        my $period = 0;

        do {
            $y = $r * $z - $y;
            $z = CORE::int(($n - $y * $y) / $z);
            $r = CORE::int(($x + $y) / $z);
            ++$period;
        } until ($z == 1);

        return _set_int($period);
    }

    my $period = 0;

    do {

        # y = floor((x+y)/z)*z - y
        Math::GMPz::Rmpz_add($t, $x, $y);    # t = x+y
        Math::GMPz::Rmpz_div($t, $t, $z);    # t = floor(t/z)
        Math::GMPz::Rmpz_mul($t, $t, $z);    # t = t*z
        Math::GMPz::Rmpz_sub($y, $t, $y);    # y = t-y

        # z = (n - y*y)/z
        Math::GMPz::Rmpz_mul($t, $y, $y);         # t = y*y
        Math::GMPz::Rmpz_sub($t, $n, $t);         # t = n-t
        Math::GMPz::Rmpz_divexact($z, $t, $z);    # z = t/z

        ++$period;

    } until (Math::GMPz::Rmpz_cmp_ui($z, 1) == 0);

    _set_int($period);
}

sub convergents {
    my ($x, $n) = @_;

    my @cfrac = @{$x->as_cfrac($n)};

    my ($n1, $n2) = (0, 1);
    my ($d1, $d2) = (1, 0);

    my @convergents;
    foreach my $z (map { $$_ } @cfrac) {

        ($n1, $n2) = ($n2, __add__(__mul__($n2, $z), $n1));
        ($d1, $d2) = ($d2, __add__(__mul__($d2, $z), $d1));

        push @convergents, bless \__div__($n2, $d2);
    }

    _array(\@convergents);
}

sub farey {
    my ($n) = @_;

    $n = _any2ui($$n) // return undef;

    my ($A, $B, $C, $D) = (0, 1, 1, $n);

    my @list = (ZERO);

    while ($C >= 0 and $C <= $n) {
        my $k =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divint($n + $B, $D)
          : Math::Prime::Util::GMP::divint($n + $B, $D);
        ($A, $B, $C, $D) = ($C, $D, $k * $C - $A, $k * $D - $B);
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_ui($q, $A, $B);
        push @list, bless \$q;
    }

    _array(\@list);
}

sub farey_neighbors {
    my ($n, $pq) = @_;

    $n  = _any2mpz($$n, 0) // return (undef, undef);
    $pq = _any2mpq($$pq)   // return (undef, undef);

    Math::GMPq::Rmpq_sgn($pq) > 0          or return (undef, undef);
    Math::GMPq::Rmpq_cmp_ui($pq, 1, 1) < 0 or return (undef, undef);

    my $p = Math::GMPz::Rmpz_init();
    my $q = Math::GMPz::Rmpz_init();

    Math::GMPq::Rmpq_get_num($p, $pq);
    Math::GMPq::Rmpq_get_den($q, $pq);

    my $pl = Math::GMPz::Rmpz_init();
    my $ql = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_invert($pl, $q, $p);
    Math::GMPz::Rmpz_neg($pl, $pl);
    Math::GMPz::Rmpz_mod($pl, $pl, $p);
    Math::GMPz::Rmpz_invert($ql, $p, $q);

    my $pr = Math::GMPz::Rmpz_init();
    my $qr = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sub($pr, $p, $pl);
    Math::GMPz::Rmpz_sub($qr, $q, $ql);

    my $fl = Math::GMPz::Rmpz_init();
    my $fr = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sub($fl, $n, $ql);
    Math::GMPz::Rmpz_sub($fr, $n, $qr);
    Math::GMPz::Rmpz_div($fl, $fl, $q);
    Math::GMPz::Rmpz_div($fr, $fr, $q);

    my $q1 = Math::GMPq::Rmpq_init();
    my $q2 = Math::GMPq::Rmpq_init();

    Math::GMPz::Rmpz_addmul($pl, $p, $fl);
    Math::GMPz::Rmpz_addmul($ql, $q, $fl);

    Math::GMPz::Rmpz_addmul($pr, $p, $fr);
    Math::GMPz::Rmpz_addmul($qr, $q, $fr);

    Math::GMPq::Rmpq_set_num($q1, $pl);
    Math::GMPq::Rmpq_set_den($q1, $ql);

    Math::GMPq::Rmpq_set_num($q2, $pr);
    Math::GMPq::Rmpq_set_den($q2, $qr);

    return ((bless \$q1), (bless \$q2));
}

sub egypt_greedy {
    my ($pq) = @_;

    $pq = _any2mpq($$pq) // return undef;

    my $n = Math::GMPz::Rmpz_init();
    my $d = Math::GMPz::Rmpz_init();

    Math::GMPq::Rmpq_get_num($n, $pq);
    Math::GMPq::Rmpq_get_den($d, $pq);

    my @list;

    state $g = Math::GMPz::Rmpz_init_nobless();

    while (Math::GMPz::Rmpz_cmp_ui($n, 1) != 0) {

        my $x = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_div($x, $d, $n);
        Math::GMPz::Rmpz_add_ui($x, $x, 1);

        push @list, $x;

        Math::GMPz::Rmpz_mul($n, $n, $x);
        Math::GMPz::Rmpz_sub($n, $n, $d);
        Math::GMPz::Rmpz_mul($d, $d, $x);
        Math::GMPz::Rmpz_gcd($g, $n, $d);

        if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
            Math::GMPz::Rmpz_divexact($n, $n, $g);
            Math::GMPz::Rmpz_divexact($d, $d, $g);
        }
    }

    push @list, $d;
    _array([map { bless \$_ } @list]);
}

sub dump {
    my ($x) = @_;

    $x = $$x;
    Sidef::Types::String::String->new(
                                       !ref($x)                 ? $x
                                      : ref($x) eq 'Math::GMPz' ? Math::GMPz::Rmpz_get_str($x, 10)
                                      : ref($x) eq 'Math::GMPq' ? Math::GMPq::Rmpq_get_str($x, 10)
                                      :                           __stringify__($x)
                                     );
}

*stringify = \&dump;
*pretty    = \&dump;

sub to_str {
    my ($x) = @_;
    Sidef::Types::String::String->new(__stringify__($$x));
}

*to_s = \&to_str;

sub to_num {
    $_[0];
}

*to_n = \&to_num;

sub as_bin {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return Sidef::Types::String::String->new((($x < 0) ? '-' : '') . CORE::sprintf('%b', CORE::abs($x)));
    }

    Sidef::Types::String::String->new(Math::GMPz::Rmpz_get_str((_any2mpz($x, 0) // return undef), 2));
}

sub as_oct {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return Sidef::Types::String::String->new((($x < 0) ? '-' : '') . CORE::sprintf('%o', CORE::abs($x)));
    }

    Sidef::Types::String::String->new(Math::GMPz::Rmpz_get_str((_any2mpz($x, 0) // return undef), 8));
}

sub as_hex {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return Sidef::Types::String::String->new((($x < 0) ? '-' : '') . CORE::sprintf('%x', CORE::abs($x)));
    }

    Sidef::Types::String::String->new(Math::GMPz::Rmpz_get_str((_any2mpz($x, 0) // return undef), 16));
}

sub bits {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        return _array([map { $_ ? ONE : ZERO } split(//, CORE::sprintf('%b', CORE::abs($x)))]);
    }

    $x = _any2mpz($x, 0) // return _array();
    my $bin = Math::GMPz::Rmpz_get_str($x, 2);
    $bin = substr($bin, 1) if substr($bin, 0, 1) eq '-';
    _array([map { $_ ? ONE : ZERO } split(//, $bin)]);
}

sub digital_root {
    my ($n, $base) = @_;

    # Formula:
    #   digital_root(n,b) = n - (b-1)*floor((n-1)/(b-1))

    $n = _any2mpz($$n, 0) // goto &nan;

    if (defined($base)) {
        _valid(\$base);
        $base = _any2mpz($$base, 1) // goto &nan;
        Math::GMPz::Rmpz_cmp_ui($base, 1) > 0 or goto &nan;
    }
    else {
        $base = $TEN;
    }

    Math::GMPz::Rmpz_sgn($n) || return ZERO;

    state $r = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($r, $n,    1);
    Math::GMPz::Rmpz_sub_ui($t, $base, 1);
    Math::GMPz::Rmpz_mod($r, $r, $t);
    Math::GMPz::Rmpz_add_ui($r, $r, 1);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub expnorm {
    my ($n, $base) = @_;

    # Based on the following identities (where b is the base):
    #   f / exp(log(b)*floor(log(f) / log(b) + 1)) = exp(log(f) - log(b)*floor(log(f) / log(b) + 1))
    #                                              = b^(-1 - floor(log(f^(1/log(b))))) * f
    #                                              = exp(log(b)*(-1 - floor(log(f) / log(b))) + log(f))

    $n = _any2mpfr_mpc($$n) // goto &nan;

    my $log;

    if (defined($base)) {
        _valid(\$base);
        $base = _any2mpfr_mpc($$base) // goto &nan;
        $log  = __log__($base);
    }
    else {
        state $ten = _any2mpfr(10);
        $log = __log__($ten);
    }

    if (ref($log) eq 'Math::MPC' and ref($n) eq 'Math::MPFR') {
        $n = _mpfr2mpc($n);
    }

    if (ref($n) eq 'Math::MPFR' and ref($log) eq 'Math::MPFR') {

        my $exp = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPFR::Rmpfr_div($exp, $n, $log, $ROUND);
        Math::MPFR::Rmpfr_floor($exp, $exp);
        Math::MPFR::Rmpfr_add_ui($exp, $exp, 1, $ROUND);
        Math::MPFR::Rmpfr_mul($exp, $exp, $log, $ROUND);
        Math::MPFR::Rmpfr_sub($exp, $n, $exp, $ROUND);
        Math::MPFR::Rmpfr_exp($exp, $exp, $ROUND);

        return bless \$exp;
    }

    my $exp = Math::MPC::Rmpc_init2(CORE::int($PREC));

    ref($log) eq 'Math::MPFR'
      ? Math::MPC::Rmpc_div_fr($exp, $n, $log, $ROUND)
      : Math::MPC::Rmpc_div($exp, $n, $log, $ROUND);

    my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPC::RMPC_RE($real, $exp);
    Math::MPC::RMPC_IM($imag, $exp);

    Math::MPFR::Rmpfr_floor($real, $real);
    Math::MPFR::Rmpfr_floor($imag, $imag);

    Math::MPC::Rmpc_set_fr_fr($exp, $real, $imag, $ROUND);

    Math::MPC::Rmpc_add_ui($exp, $exp, 1, $ROUND);

    ref($log) eq 'Math::MPFR'
      ? Math::MPC::Rmpc_mul_fr($exp, $exp, $log, $ROUND)
      : Math::MPC::Rmpc_mul($exp, $exp, $log, $ROUND);

    Math::MPC::Rmpc_sub($exp, $n, $exp, $ROUND);
    Math::MPC::Rmpc_exp($exp, $exp, $ROUND);

    bless \$exp;
}

sub digits {
    my ($n, $k) = @_;

    $n = $$n;

    if (FAST_MODE and !defined($k) and !ref($n)) {
        $n == 0 and return _array([ZERO]);
        my @digits = CORE::reverse(
                                   HAS_PRIME_UTIL
                                   ? Math::Prime::Util::todigits(CORE::abs($n))
                                   : split(//, CORE::abs($n))
                                  );
        return _array([map { bless \$_ } @digits]);
    }

    if (defined($k)) {
        _valid(\$k);

        $k = $$k;

        if (FAST_MODE and !ref($k) and !ref($n)) {

            $n = CORE::abs($n);

            $k >= 2 or return _array();
            $n == 0 and return _array([ZERO]);
            $n < $k and return _array([bless \$n]);

            my @digits;

            if (HAS_PRIME_UTIL and $k < 2147483647) {
                @digits = Math::Prime::Util::todigits($n, $k);
            }
            elsif ($k == 2 or $k == 8) {
                @digits = split(//, sprintf(($k == 2 ? '%b' : '%o'), $n));
            }
            elsif ($k == 10) {
                @digits = split(//, $n);
            }
            elsif ($k == 16) {
                @digits = @DIGITS_36{split(//, sprintf('%x', $n))};
            }

            if (@digits) {
                return _array([map { bless \$_ } CORE::reverse(@digits)]);
            }
        }

        $k = _any2mpz($k, 0) // return _array();

        # Not defined for k <= 1
        if (Math::GMPz::Rmpz_cmp_ui($k, 1) <= 0) {
            return _array();
        }
    }

    $n = _any2mpz($n, 1) // return _array();

    my $sgn = Math::GMPz::Rmpz_sgn($n);

    if ($sgn == 0) {
        return _array([ZERO]);
    }
    elsif ($sgn < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);
    }

#<<<
    if (!defined($k) or Math::GMPz::Rmpz_cmp_ui($k, 62) <= 0) {
        $k = defined($k) ? Math::GMPz::Rmpz_get_ui($k) : 10;
        return _array([
           map { bless \$_ }
            ($k <= 36)
                ? @DIGITS_36{split(//, scalar CORE::reverse scalar Math::GMPz::Rmpz_get_str($n, $k))}
                : @DIGITS_62{split(//, scalar CORE::reverse scalar Math::GMPz::Rmpz_get_str($n, $k))}
        ]);
    }
#>>>

    # Subquadratic Algorithm 1.26 FastIntegerOutput from "Modern Computer Arithmetic v0.5.9"
    if (Math::GMPz::Rmpz_fits_ulong_p($k)) {

        my $A = $n;
        my $B = Math::GMPz::Rmpz_get_ui($k);

        # When B < 2^31, use Math::Prime::Util::GMP::todigits().
        if ($B < 2147483647) {
            return _array([map { bless \$_ } CORE::reverse(Math::Prime::Util::GMP::todigits(Math::GMPz::Rmpz_get_str($n, 10), $B))]);
        }

        # Find r such that B^(2r - 2) <= A < B^(2r)
        my $r = (__ilog__($A, $B) >> 1) + 1;

        state $Q = Math::GMPz::Rmpz_init_nobless();
        state $R = Math::GMPz::Rmpz_init_nobless();

        my @digits = map { ($_ < ULONG_MAX) ? (bless \$_) : _set_int($_) } sub {
            my ($A, $r) = @_;

            # Cut the recursion early
            if (Math::GMPz::Rmpz_fits_ulong_p($A)) {
                my $v = Math::GMPz::Rmpz_get_ui($A);
                my ($m, @digits);
                while ($v) {
                    ($v, $m) =
                      HAS_PRIME_UTIL
                      ? Math::Prime::Util::divrem($v, $B)
                      : Math::Prime::Util::GMP::divrem($v, $B);
                    push @digits, $m;
                }
                return @digits;
            }

            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_ui_pow_ui($t, $B, 2 * ($r - 1));    # can this be optimized away?

            if (Math::GMPz::Rmpz_cmp($t, $A) > 0) {
                --$r;
            }

            Math::GMPz::Rmpz_ui_pow_ui($t, $B, $r);
            Math::GMPz::Rmpz_divmod($Q, $R, $A, $t);

            my $w = ($r + 1) >> 1;
            Math::GMPz::Rmpz_set($t, $Q);

            my @right = __SUB__->($R, $w);
            my @left  = __SUB__->($t, $w);

            (@right, (0) x ($r - scalar(@right)), @left);
          }
          ->($A, $r);

        return _array(\@digits);
    }

    # This algorithm will be used only when base > ULONG_MAX
    my @digits;

    $n = Math::GMPz::Rmpz_init_set($n);    # copy

    while (Math::GMPz::Rmpz_sgn($n) > 0) {
        my $m = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_divmod($n, $m, $n, $k);
        push @digits, bless \$m;
    }

    _array(\@digits);
}

sub __digits2num__ {
    my ($base, $digits) = @_;

    # $base is a native integer >= 2
    # $digits is a non-empty array of native integers < base (msd first)

    if (HAS_PRIME_UTIL and $base < 2147483647 and CORE::log($base) * scalar(@$digits) < CORE::log(ULONG_MAX)) {
        return Math::Prime::Util::fromdigits($digits, $base);
    }

    if ($base <= 10) {
        return Math::GMPz::Rmpz_init_set_str(join('', @$digits), $base);
    }

    if ($base <= 36) {
        return Math::GMPz::Rmpz_init_set_str(join('', @FROM_DIGITS_36[@$digits]), $base);
    }

    if ($base <= 62) {
        return Math::GMPz::Rmpz_init_set_str(join('', @FROM_DIGITS_62[@$digits]), $base);
    }

    my @D   = CORE::reverse(@$digits);
    my $len = scalar(@D);

    if (CORE::log($base) * $len < CORE::log(ULONG_MAX)) {
        my $r = 0;
        my $B = 1;

        foreach my $d (@D) {
            $r += $B * $d;
            $B *= $base;
        }

        return $r;
    }

    my @d = map { Math::GMPz::Rmpz_init_set_ui($_) } @D;
    my $B = Math::GMPz::Rmpz_init_set_ui($base);
    my $L = \@d;

    for (my $k = $len ; $k > 1 ; $k = ($k >> 1) + ($k & 1)) {

        my @T;
        for (0 .. ($k >> 1) - 1) {
            my $t = $L->[2 * $_];
            Math::GMPz::Rmpz_addmul($t, $L->[2 * $_ + 1], $B);
            push(@T, $t);
        }

        push(@T, $L->[-1]) if ($k & 1);
        $L = \@T;
        Math::GMPz::Rmpz_mul($B, $B, $B);
    }

    $L->[0];
}

sub digits2num {
    my ($base, $D) = @_;

    my @digits = @$D;
    @digits || return ZERO;

    _valid(\$base);
    _valid(\(@digits));

    $base   = $$base;
    @digits = map { $$_ } @digits;

    my $all_native = (!ref($base) and $base >= 2);

    if ($all_native) {
        foreach my $digit (@digits) {
            if (FAST_MODE and !ref($digit) and $digit >= 0 and $digit < $base) {
                ## ok
            }
            else {
                $all_native = 0;
                last;
            }
        }
    }

    if ($all_native) {
        return _set_int(__digits2num__($base, [CORE::reverse(@digits)]));
    }

    my $all_mpz = ref($base) eq 'Math::GMPz';

    if ($all_mpz) {
        foreach my $digit (@digits) {
            if (ref($digit) ne 'Math::GMPz') {
                $all_mpz = 0;
                last;
            }
        }
    }

    my $L = \@digits;

    if ($all_mpz and Math::GMPz::Rmpz_cmp_ui($base, 2) >= 0) {

        if (Math::GMPz::Rmpz_cmp_ui($base, 62) <= 0) {    # return faster for base in 2..62

            my $str      = '';
            my $optimize = 1;
            my $B        = Math::GMPz::Rmpz_get_ui($base);

            foreach my $digit (CORE::reverse(@digits)) {
                if (Math::GMPz::Rmpz_cmp_ui($digit, $B) < 0 and Math::GMPz::Rmpz_sgn($digit) >= 0) {
                    $str .= Math::GMPz::Rmpz_get_str($digit, $B);
                }
                else {
                    $optimize = 0;
                    last;
                }
            }

            if ($optimize) {
                return bless \(my $value = Math::GMPz::Rmpz_init_set_str($str, $B));
            }
        }

        my $B = Math::GMPz::Rmpz_init_set($base);

        # Subquadratic Algorithm 1.25 FastIntegerInput from "Modern Computer Arithmetic v0.5.9"
        for (my $k = scalar(@digits) ; $k > 1 ; $k = ($k >> 1) + ($k & 1)) {

            my @T;
            for (0 .. ($k >> 1) - 1) {
                my $t = Math::GMPz::Rmpz_init_set($L->[2 * $_]);
                Math::GMPz::Rmpz_addmul($t, $L->[2 * $_ + 1], $B);
                push(@T, $t);
            }

            push(@T, $L->[-1]) if ($k & 1);
            $L = \@T;
            Math::GMPz::Rmpz_mul($B, $B, $B);
        }

        return bless \($L->[0]);
    }

    my $B = $base;

    # Subquadratic Algorithm 1.25 FastIntegerInput from "Modern Computer Arithmetic v0.5.9"
    for (my $k = scalar(@digits) ; $k > 1 ; $k = ($k >> 1) + ($k & 1)) {

        my @T;
        for (0 .. ($k >> 1) - 1) {
            push(@T, __add__($L->[2 * $_], __mul__($B, $L->[2 * $_ + 1])));
        }

        push(@T, $L->[-1]) if ($k & 1);
        $L = \@T;
        $B = __mul__($B, $B);
    }

    bless \($L->[0]);
}

*from_digits = \&digits2num;

sub digit {
    my ($n, $i, $k) = @_;

    _valid(\$i);

    $n = _any2mpz($$n, 0) // (return undef);
    $i = $$i;

    $i = _any2si($i) // (return undef) if ref($i);

    if (defined($k)) {
        _valid(\$k);

        $k = _any2mpz($$k, 1) // return undef;

        # Not defined for k <= 1
        if (Math::GMPz::Rmpz_cmp_ui($k, 1) <= 0) {
            return undef;
        }
    }
    else {
        $k = $TEN;
    }

    my $t = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init_set($n);

    my $sgn = Math::GMPz::Rmpz_sgn($u);

    if ($sgn == 0) {
        return ZERO;
    }
    elsif ($sgn < 0) {
        Math::GMPz::Rmpz_abs($u, $u);
    }

    if ($i < 0) {
        $i += __ilog__($u, $k) + 1;
        return undef if ($i < 0);
    }

    Math::GMPz::Rmpz_pow_ui($t, $k, $i);
    Math::GMPz::Rmpz_div($u, $u, $t);
    Math::GMPz::Rmpz_mod($u, $u, $k);

    bless \$u;
}

sub sumdigits {
    my ($n, $k) = @_;

    $n = $$n;

    if (FAST_MODE and !defined($k) and !ref($n)) {
        $n == 0 and return ZERO;
        return
          bless \(
                  my $value = List::Util::sum(
                                              HAS_PRIME_UTIL
                                              ? Math::Prime::Util::todigits(CORE::abs($n))
                                              : split(//, CORE::abs($n))
                                             )
                 );
    }

    if (defined($k)) {
        _valid(\$k);

        $k = $$k;

        if (FAST_MODE and !ref($k) and !ref($n)) {

            $n = CORE::abs($n);

            $k >= 2 or return undef;
            $n == 0 and return ZERO;
            $n < $k and return _set_int($n);

            if (HAS_PRIME_UTIL and $k < 2147483647) {
                if ($k == 2) {
                    return bless \(my $value = Math::Prime::Util::hammingweight($n));
                }
                return bless \(my $value = List::Util::sum(Math::Prime::Util::todigits($n, $k)));
            }
            elsif ($k == 2 or $k == 8) {
                return bless \(my $value = List::Util::sum(split(//, sprintf(($k == 2 ? '%b' : '%o'), $n))));
            }
            elsif ($k == 10) {
                return bless \(my $value = List::Util::sum(split(//, $n)));
            }
            elsif ($k == 16) {
                return bless \(my $value = List::Util::sum(@DIGITS_36{split(//, sprintf('%x', $n))}));
            }
        }

        $k = _any2mpz($k, 0) // return undef;

        # Not defined for k <= 1
        if (Math::GMPz::Rmpz_cmp_ui($k, 1) <= 0) {
            return undef;
        }
    }

    $n = _any2mpz($n, 1) // return undef;

    my $sgn = Math::GMPz::Rmpz_sgn($n);

    if ($sgn == 0) {
        return ZERO;
    }
    elsif ($sgn < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);
    }

    if (!defined($k) or Math::GMPz::Rmpz_cmp_ui($k, 62) <= 0) {
        $k = defined($k) ? Math::GMPz::Rmpz_get_ui($k) : 10;

        return _set_int(scalar Math::GMPz::Rmpz_popcount($n)) if ($k == 2);

        if (Math::GMPz::Rmpz_sizeinbase($n, $k) <= 1e6) {
            if ($k <= 36) {
                return _set_int(List::Util::sum(@DIGITS_36{split(//, Math::GMPz::Rmpz_get_str($n, $k))}));
            }
            else {
                return _set_int(List::Util::sum(@DIGITS_62{split(//, Math::GMPz::Rmpz_get_str($n, $k))}));
            }
        }
        else {
            $k = Math::GMPz::Rmpz_init_set_ui($k);
        }
    }

    # Subquadratic Algorithm 1.26 FastIntegerOutput from "Modern Computer Arithmetic v0.5.9"
    if (Math::GMPz::Rmpz_fits_ulong_p($k)) {

        my $A = $n;
        my $B = Math::GMPz::Rmpz_get_ui($k);

        # When B < 2^31, use Math::Prime::Util::GMP::todigits().
        if ($B < 2147483647 and Math::GMPz::Rmpz_sizeinbase($n, 62) <= 1e6) {
            return _set_int(List::Util::sum(Math::Prime::Util::GMP::todigits(Math::GMPz::Rmpz_get_str($n, 10), $B)));
        }

        # Find r such that B^(2r - 2) <= A < B^(2r)
        my $r = (__ilog__($A, $B) >> 1) + 1;

        state $Q = Math::GMPz::Rmpz_init_nobless();
        state $R = Math::GMPz::Rmpz_init_nobless();

        my $total = sub {
            my ($A, $r) = @_;

            # Cut the recursion early
            if (Math::GMPz::Rmpz_fits_ulong_p($A)) {
                my $v = Math::GMPz::Rmpz_get_ui($A);
                my ($sum, $m) = (0);
                while ($v) {
                    ($v, $m) =
                      HAS_PRIME_UTIL
                      ? Math::Prime::Util::divrem($v, $B)
                      : Math::Prime::Util::GMP::divrem($v, $B);
                    $sum += $m;
                }
                return $sum;
            }

            my $w = ($r + 1) >> 1;
            my $t = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_ui_pow_ui($t, $B, $r);
            Math::GMPz::Rmpz_divmod($Q, $R, $A, $t);
            Math::GMPz::Rmpz_set($t, $Q);

            __SUB__->($R, $w) + __SUB__->($t, $w);
          }
          ->($A, $r);

        ($total < ULONG_MAX)
          && return _set_int($total);
    }

    # This algorithm will be used only for very large bases,
    # base > ULONG_MAX, or when the sum of digits exceeds ULONG_MAX.
    my $m   = Math::GMPz::Rmpz_init();
    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    $n = Math::GMPz::Rmpz_init_set($n);    # copy

    while (Math::GMPz::Rmpz_sgn($n) > 0) {
        Math::GMPz::Rmpz_divmod($n, $m, $n, $k);
        Math::GMPz::Rmpz_add($sum, $sum, $m);
    }

    bless \$sum;
}

*digits_sum = \&sumdigits;
*sum_digits = \&sumdigits;

sub is_pandigital {
    my ($n, $base) = @_;

    if (defined($base)) {
        _valid(\$base);
    }
    else {
        $base = TEN;
    }

    # If a(n) is the smallest pandigital number to base n, then:
    # a(n) = (n^n-n)/(n-1)^2 + n^(n-2)*(n-1) - 1
    # https://en.wikipedia.org/wiki/Pandigital_number

    my $t = $base->numify;

    if ($n->ilog2->numify < ($t - 2) * (CORE::log($t) / CORE::log(2))) {
        return $FALSE;
    }

    # my $smallest = $base->ipow($base)->sub($base)->idiv($base->dec->sqr)->add($base->ipow($base->sub(TWO))->mul($base->dec))->dec;

    # if ($n->lt($smallest)) {
    #    return $FALSE;
    # }

    my %hash;
    @hash{@{$n->digits($base)}} = ();

    scalar(keys %hash) == $t
      or return $FALSE;

    return $TRUE;
}

sub factorial_power {
    my ($n, $p) = @_;

    _valid(\$p);

    $n = $$n;
    $p = $$p;

    # Generalized method for composite p
    # Formula from SymPy:
    #   https://github.com/sympy/sympy/blob/d7938cbc2f703801413075c147f54db350d29bff/sympy/ntheory/factor_.py#L227

    if (!_is_prob_prime($p)) {
        my %seen;
        my @list;
        foreach my $pp (grep { !$seen{$_->[1]}++ } CORE::reverse(_factor_exp($p))) {
            push @list,
              _set_int(
                       Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::subint($n, ${$_[0]->sumdigits(_set_int($pp->[0])) // return ZERO}),
                                                      Math::Prime::Util::GMP::mulint(Math::Prime::Util::GMP::subint($pp->[0], 1), $pp->[1]))
                      );
        }
        return (_array(\@list)->imin() // ZERO);
    }

    # Formula for prime p:
    #   (n - sumdigits(n,p)) / (p-1)

    if (FAST_MODE and !ref($n) and !ref($p) and $p < 2147483647 and $n > 0 and $p > 1) {

        my $sum =
          HAS_PRIME_UTIL
          ? List::Util::sum(Math::Prime::Util::todigits($n, $p))
          : List::Util::sum(Math::Prime::Util::GMP::todigits($n, $p));

        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divint($n - $sum, $p - 1)
          : Math::Prime::Util::GMP::divint($n - $sum, $p - 1);

        return bless \$r;
    }

    my $sum = ${$_[0]->sumdigits($_[1]) // return ZERO};
    bless \__div__(__sub__($n, $sum), __dec__($p));
}

*factorial_valuation = \&factorial_power;

sub length {
    my ($x, $y) = @_;

    $x = $$x;

    if (FAST_MODE and !defined($y) and !ref($x)) {
        my $len = CORE::length($x) - (($x < 0) ? 1 : 0);
        return bless \$len;
    }

    $x = _any2mpz($x, 0) // return undef;

    my $neg = ((Math::GMPz::Rmpz_sgn($x) || return ONE) < 0) ? 1 : 0;

    $y = defined($y) ? do { _valid(\$y); _any2mpz($$y, 1) // return undef } : 10;

    if ($neg) {
        $x = Math::GMPz::Rmpz_init_set($x);
        Math::GMPz::Rmpz_abs($x, $x);
    }

    _set_int(1 + (__ilog__($x, $y) // return ZERO));
}

*len  = \&length;
*size = \&length;

sub __floor__ {
    my ($x) = @_;
    goto((ref($x) || return $x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_floor($r, $x);
        return $r;
    }

  Math_GMPq: {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set_q($r, $x);
        if (Math::GMPq::Rmpq_integer_p($x)) {
            return (
                      Math::GMPz::Rmpz_fits_slong_p($r)
                    ? Math::GMPz::Rmpz_get_si($r)
                    : Math::GMPz::Rmpz_init_set($r)
                   );
        }
        Math::GMPz::Rmpz_sub_ui($r, $r, 1) if Math::GMPq::Rmpq_sgn($x) < 0;
        return (
                  Math::GMPz::Rmpz_fits_slong_p($r)
                ? Math::GMPz::Rmpz_get_si($r)
                : Math::GMPz::Rmpz_init_set($r)
               );
    }

  Math_MPC: {
        my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($real, $x);
        Math::MPC::RMPC_IM($imag, $x);

        Math::MPFR::Rmpfr_floor($real, $real);
        Math::MPFR::Rmpfr_floor($imag, $imag);

        if (Math::MPFR::Rmpfr_zero_p($imag)) {
            return $real;
        }

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr_fr($r, $real, $imag, $ROUND);
        return $r;
    }
}

sub floor {
    my ($x) = @_;
    (!ref($$x) or ref($$x) eq 'Math::GMPz') and return $x;    # already an integer
    bless \__floor__($$x);
}

sub __ceil__ {
    my ($x) = @_;
    goto((ref($x) || return $x) =~ tr/:/_/rs);

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_ceil($r, $x);
        return $r;
    }

  Math_GMPq: {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set_q($r, $x);
        if (Math::GMPq::Rmpq_integer_p($x)) {
            return (
                      Math::GMPz::Rmpz_fits_slong_p($r)
                    ? Math::GMPz::Rmpz_get_si($r)
                    : Math::GMPz::Rmpz_init_set($r)
                   );
        }
        Math::GMPz::Rmpz_add_ui($r, $r, 1) if Math::GMPq::Rmpq_sgn($x) > 0;
        return (
                  Math::GMPz::Rmpz_fits_slong_p($r)
                ? Math::GMPz::Rmpz_get_si($r)
                : Math::GMPz::Rmpz_init_set($r)
               );
    }

  Math_MPC: {
        my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($real, $x);
        Math::MPC::RMPC_IM($imag, $x);

        Math::MPFR::Rmpfr_ceil($real, $real);
        Math::MPFR::Rmpfr_ceil($imag, $imag);

        if (Math::MPFR::Rmpfr_zero_p($imag)) {
            return $real;
        }

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr_fr($r, $real, $imag, $ROUND);
        return $r;
    }
}

sub ceil {
    my ($x) = @_;
    (!ref($$x) or ref($$x) eq 'Math::GMPz') and return $x;    # already an integer
    bless \__ceil__($$x);
}

*ceiling = \&ceil;

sub __inc__ {
    my ($x) = @_;
    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        my $r = $x + 1;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $x = _any2mpz($x);
        goto Math_GMPz;
    }

  Math_GMPz: {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_add_ui($r, $x, 1);
        return (
                  Math::GMPz::Rmpz_fits_slong_p($r)
                ? Math::GMPz::Rmpz_get_si($r)
                : Math::GMPz::Rmpz_init_set($r)
               );
    }

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_add_ui($r, $x, 1, $ROUND);
        return $r;
    }

  Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_add_z($r, $x, $ONE);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_add_ui($r, $x, 1, $ROUND);
        return $r;
    }
}

sub inc {
    my ($x) = @_;
    bless \__inc__($$x);
}

sub __dec__ {
    my ($x) = @_;
    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        my $r = $x - 1;
        if (FAST_MODE and $r < ULONG_MAX and $r > LONG_MIN) {
            return $r;
        }
        $x = _any2mpz($x);
        goto Math_GMPz;
    }

  Math_GMPz: {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_sub_ui($r, $x, 1);
        return (
                  Math::GMPz::Rmpz_fits_slong_p($r)
                ? Math::GMPz::Rmpz_get_si($r)
                : Math::GMPz::Rmpz_init_set($r)
               );
    }

  Math_MPFR: {
        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_sub_ui($r, $x, 1, $ROUND);
        return $r;
    }

  Math_GMPq: {
        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_sub_z($r, $x, $ONE);
        return $r;
    }

  Math_MPC: {
        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_sub_ui($r, $x, 1, $ROUND);
        return $r;
    }
}

sub dec {
    my ($x) = @_;
    bless \__dec__($$x);
}

sub __mod__ {
    my ($x, $y) = @_;
    goto(join('__', ref($x) || 'Scalar', ref($y) || 'Scalar') =~ tr/:/_/rs);

    #
    ## Scalar
    #
  Scalar__Scalar: {
        $y || goto &_nan;
        return $x % $y;
    }

  Scalar__Math_GMPz: {

        # y is a native integer
        if (Math::GMPz::Rmpz_fits_ulong_p($y)) {
            $y = Math::GMPz::Rmpz_get_ui($y) || goto &_nan;
            return $x % $y;
        }

        # x is positive and y > x
        if ($x > 0 and Math::GMPz::Rmpz_cmp_ui($y, $x) > 0) {
            return $x;
        }

        $x = _any2mpz($x);
        goto Math_GMPz__Math_GMPz;
    }

  Scalar__Math_GMPq: {
        $x = _any2mpz($x);
        goto Math_GMPz__Math_GMPq;
    }

  Scalar__Math_MPFR: {
        $x = _any2mpz($x);
        goto Math_GMPz__Math_MPFR;
    }

  Scalar__Math_MPC: {
        $x = _any2mpz($x);
        goto Math_GMPz__Math_MPC;
    }

    #
    ## GMPq
    #
  Math_GMPq__Math_GMPq: {

        if (Math::GMPq::Rmpq_integer_p($y)) {
            $y = _mpq2mpz($y);
            goto Math_GMPq__Math_GMPz;
        }

        Math::GMPq::Rmpq_sgn($y) || goto &_nan;

        my $quo = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_div($quo, $x, $y);

        # Floor
        Math::GMPq::Rmpq_integer_p($quo) || do {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_set_q($z, $quo);
            Math::GMPz::Rmpz_sub_ui($z, $z, 1) if Math::GMPq::Rmpq_sgn($quo) < 0;
            Math::GMPq::Rmpq_set_z($quo, $z);
        };

        Math::GMPq::Rmpq_mul($quo, $quo, $y);
        Math::GMPq::Rmpq_sub($quo, $x, $quo);

        return $quo;
    }

  Math_GMPq__Math_GMPz: {
        my $sgn_y = Math::GMPz::Rmpz_sgn($y) || goto &_nan;

        my $r = _modular_rational($x, $y) // do {

            my $quo = Math::GMPq::Rmpq_init();
            Math::GMPq::Rmpq_div_z($quo, $x, $y);

            # Floor
            Math::GMPq::Rmpq_integer_p($quo) || do {
                my $z = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_set_q($z, $quo);
                Math::GMPz::Rmpz_sub_ui($z, $z, 1) if Math::GMPq::Rmpq_sgn($quo) < 0;
                Math::GMPq::Rmpq_set_z($quo, $z);
            };

            Math::GMPq::Rmpq_mul_z($quo, $quo, $y);
            Math::GMPq::Rmpq_sub($quo, $x, $quo);

            return $quo;
        };

        Math::GMPz::Rmpz_mod($r, $r, $y);

        if (!Math::GMPz::Rmpz_sgn($r)) {
            ## ok
        }
        elsif ($sgn_y < 0) {
            Math::GMPz::Rmpz_add($r, $r, $y);
        }

        $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPq__Math_MPFR: {
        $x = _mpq2mpfr($x);
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPq__Math_MPC: {
        $x = _mpq2mpc($x);
        goto Math_MPC__Math_MPC;
    }

  Math_GMPq__Scalar: {
        $y = _any2mpz($y);
        goto Math_GMPq__Math_GMPz;
    }

    #
    ## GMPz
    #
  Math_GMPz__Math_GMPz: {

        if (Math::GMPz::Rmpz_fits_ulong_p($y)) {
            state $t = Math::GMPz::Rmpz_init_nobless();
            return Math::GMPz::Rmpz_mod_ui($t, $x, Math::GMPz::Rmpz_get_ui($y) || goto &_nan);
        }

        my $sgn_y = Math::GMPz::Rmpz_sgn($y) || goto &_nan;

        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mod($r, $x, $y);

        if (!Math::GMPz::Rmpz_sgn($r)) {
            ## ok
        }
        elsif ($sgn_y < 0) {
            Math::GMPz::Rmpz_add($r, $r, $y);
        }

        $r = Math::GMPz::Rmpz_get_si($r) if Math::GMPz::Rmpz_fits_slong_p($r);
        return $r;
    }

  Math_GMPz__Scalar: {

        if ($y < 0) {
            $y = _any2mpz($y);
            goto Math_GMPz__Math_GMPz;
        }

        $y || goto &_nan;

        state $t = Math::GMPz::Rmpz_init_nobless();
        return Math::GMPz::Rmpz_mod_ui($t, $x, $y);
    }

  Math_GMPz__Math_GMPq: {
        $x = _mpz2mpq($x);
        goto Math_GMPq__Math_GMPq;
    }

  Math_GMPz__Math_MPFR: {
        $x = _mpz2mpfr($x);
        goto Math_MPFR__Math_MPFR;
    }

  Math_GMPz__Math_MPC: {
        $x = _mpz2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## MPFR
    #
  Math_MPFR__Math_MPFR: {
        my $quo = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div($quo, $x, $y, $ROUND);
        Math::MPFR::Rmpfr_floor($quo, $quo);
        Math::MPFR::Rmpfr_mul($quo, $quo, $y, $ROUND);
        Math::MPFR::Rmpfr_sub($quo, $x, $quo, $ROUND);
        return $quo;
    }

  Math_MPFR__Scalar: {

        if ($y < 0) {
            $y = _any2mpz($y);
            goto Math_MPFR__Math_GMPz;
        }

        my $quo = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div_ui($quo, $x, $y, $ROUND);
        Math::MPFR::Rmpfr_floor($quo, $quo);
        Math::MPFR::Rmpfr_mul_ui($quo, $quo, $y, $ROUND);
        Math::MPFR::Rmpfr_sub($quo, $x, $quo, $ROUND);
        return $quo;
    }

  Math_MPFR__Math_GMPq: {
        my $quo = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div_q($quo, $x, $y, $ROUND);
        Math::MPFR::Rmpfr_floor($quo, $quo);
        Math::MPFR::Rmpfr_mul_q($quo, $quo, $y, $ROUND);
        Math::MPFR::Rmpfr_sub($quo, $x, $quo, $ROUND);
        return $quo;
    }

  Math_MPFR__Math_GMPz: {
        my $quo = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_div_z($quo, $x, $y, $ROUND);
        Math::MPFR::Rmpfr_floor($quo, $quo);
        Math::MPFR::Rmpfr_mul_z($quo, $quo, $y, $ROUND);
        Math::MPFR::Rmpfr_sub($quo, $x, $quo, $ROUND);
        return $quo;
    }

  Math_MPFR__Math_MPC: {
        $x = _mpfr2mpc($x);
        goto Math_MPC__Math_MPC;
    }

    #
    ## MPC
    #
  Math_MPC__Math_MPC: {
        my $quo = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_div($quo, $x, $y, $ROUND);

        my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($real, $quo);
        Math::MPC::RMPC_IM($imag, $quo);

        Math::MPFR::Rmpfr_floor($real, $real);
        Math::MPFR::Rmpfr_floor($imag, $imag);

        Math::MPC::Rmpc_set_fr_fr($quo, $real, $imag, $ROUND);

        Math::MPC::Rmpc_mul($quo, $quo, $y, $ROUND);
        Math::MPC::Rmpc_sub($quo, $x, $quo, $ROUND);

        return $quo;
    }

  Math_MPC__Scalar: {

        if ($y < 0) {
            $y = _any2mpz($y);
            goto Math_MPC__Math_GMPz;
        }

        my $quo = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_div_ui($quo, $x, $y, $ROUND);

        my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($real, $quo);
        Math::MPC::RMPC_IM($imag, $quo);

        Math::MPFR::Rmpfr_floor($real, $real);
        Math::MPFR::Rmpfr_floor($imag, $imag);

        Math::MPC::Rmpc_set_fr_fr($quo, $real, $imag, $ROUND);

        Math::MPC::Rmpc_mul_ui($quo, $quo, $y, $ROUND);
        Math::MPC::Rmpc_sub($quo, $x, $quo, $ROUND);

        return $quo;
    }

  Math_MPC__Math_MPFR: {
        $y = _mpfr2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_GMPz: {
        $y = _mpz2mpc($y);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_GMPq: {
        $y = _mpq2mpc($y);
        goto Math_MPC__Math_MPC;
    }
}

sub mod {
    my ($x, $y) = @_;

    my $ref = ref($y);
    if (exists $NUMBER_CLASSES{$ref}) {
        return $y->with_value($x)->mod($y);
    }

    _valid(\$y);
    bless \__mod__($$x, $$y);
}

sub smod {
    my ($x, $m) = @_;

    my $r = $x->mod($m);
    if ($r->add($r)->le($m)) {
        return $r;
    }

    return $r->sub($m);
}

sub polymod {
    my ($x, @m) = @_;

    _valid(map { \$_ } @m);

    $x = $$x;
    @m = map { $$_ } @m;

    my @r;
    foreach my $m (@m) {
        my $mod = __mod__($x, $m);

        $x = __sub__($x, $mod);
        $x = __div__($x, $m);

        push @r, $mod;
    }

    push @r, $x;
    map { bless \$_ } @r;
}

sub quadratic_nonresidue {
    my ($n) = @_;

    # Least quadratic non-residue of n. (OEIS: A020649)
    # Inspired by Dana Jacobsen's code from Math::Prime::Util::PP.

    $n = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_cmp_ui($n, 2) <= 0) {
        return _set_int($n);
    }

    if (Math::GMPz::Rmpz_ui_kronecker(2, $n) == -1) {
        return TWO;
    }

    if (_is_prob_prime($n)) {
        for (my $k = 3 ; ; $k = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($k) : Math::Prime::Util::GMP::next_prime($k))) {
            if (Math::GMPz::Rmpz_ui_kronecker($k, $n) == -1) {
                return _set_int($k);
            }
        }
    }

    if (Math::GMPz::Rmpz_even_p($n)) {
        return TWO if Math::GMPz::Rmpz_scan1($n, 0) >= 2;
    }

    foreach my $k (3, 5, 11, 13, 19) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $k)) {
            return TWO;
        }
    }

    my @factors = _factor_exp($n);

#<<<
    @factors = map {
        ($_ < ULONG_MAX)
            ? Math::GMPz::Rmpz_init_set_ui($_)
            : Math::GMPz::Rmpz_init_set_str("$_", 10)
    } map { $_->[0] } @factors;
#>>>

    for (my $k = 2 ; ; $k = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($k) : Math::Prime::Util::GMP::next_prime($k))) {
        foreach my $p (@factors) {
            if (Math::GMPz::Rmpz_cmp_ui($p, $k) > 0 and Math::GMPz::Rmpz_ui_kronecker($k, $p) == -1) {
                return _set_int($k);
            }
        }
    }
}

*qnr = \&quadratic_nonresidue;

sub sqrtmod {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (0 and HAS_PRIME_UTIL and !ref($y) and !ref($x)) {
        if (defined(my $r = Math::Prime::Util::sqrtmod($x, $y))) {
            return bless \$r;
        }
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;

    my $y_sgn = Math::GMPz::Rmpz_sgn($y) || goto &nan;

    if ($y_sgn < 0) {
        $y = Math::GMPz::Rmpz_init_set($y);
        Math::GMPz::Rmpz_abs($y, $y);
    }

    my $n = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($n, $x, $y);

    if (Math::GMPz::Rmpz_sgn($n) == 0) {
        return ZERO;
    }

    if (0 and HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($y)) {
        if (defined(my $r = Math::Prime::Util::sqrtmod(Math::GMPz::Rmpz_get_ui($n), Math::GMPz::Rmpz_get_ui($y)))) {
            return bless \$r;
        }
    }

    my $nstr = Math::GMPz::Rmpz_get_str($n, 10);
    my $ystr = Math::GMPz::Rmpz_get_str($y, 10);

    if (_is_prob_prime($ystr)) {
        return _set_int(Math::Prime::Util::GMP::sqrtmod($nstr, $ystr) // goto &nan);
    }

    if (0 and HAS_PRIME_UTIL) {
        if (defined(my $r = Math::Prime::Util::sqrtmod($nstr, $ystr))) {
            return _set_int("$r");
        }
    }

    # Fallback to rootmod(a,2,m)
    return _set_int($n)->rootmod(TWO, _set_int($y));
}

sub sqrtmod_all {
    my ($A, $N) = @_;
    return $A->rootmod_all(TWO, $N);
}

#----------------------------------------------------------
# Tonelli-Shanks algorithm for k-th roots modulo a prime
#----------------------------------------------------------
sub _tonelli_shanks {
    my ($a, $k, $p) = @_;

    my $exp = 0;
    my $q   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($q, $p, 1);

    while (Math::GMPz::Rmpz_divisible_p($q, $k)) {
        $exp++;
        Math::GMPz::Rmpz_divexact($q, $q, $k);
    }

    my $k_exp = Math::GMPz::Rmpz_init();
    my $tmp   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($tmp, $p, 1);
    Math::GMPz::Rmpz_divexact($k_exp, $tmp, $q);

    my $inv_k   = Math::GMPz::Rmpz_init();
    my $k_mod_q = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($k_mod_q, $k, $q);
    Math::GMPz::Rmpz_invert($inv_k, $k_mod_q, $q);

    my $root = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_powm($root, $a, $inv_k, $p);

    my $root_k = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_powm($root_k, $root, $k, $p);

    my $inv_a = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_invert($inv_a, $a, $p);

    my $b = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($b, $root_k, $inv_a);
    Math::GMPz::Rmpz_mod($b, $b, $p);

    # Find a generator of the k-th roots of unity
    my $candidate   = Math::GMPz::Rmpz_init_set_ui(2);
    my $zeta        = Math::GMPz::Rmpz_init_set_ui(1);
    my $gen         = Math::GMPz::Rmpz_init();
    my $k_exp_div_k = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_divexact($k_exp_div_k, $k_exp, $k);

    while (Math::GMPz::Rmpz_cmp_ui($zeta, 1) == 0) {
        Math::GMPz::Rmpz_powm($gen,  $candidate, $q,           $p);
        Math::GMPz::Rmpz_powm($zeta, $gen,       $k_exp_div_k, $p);
        Math::GMPz::Rmpz_add_ui($candidate, $candidate, 1);
    }

    # Iteratively refine the root
    my $new_gen           = Math::GMPz::Rmpz_init();
    my $k_exp_div_k_inner = Math::GMPz::Rmpz_init();
    my $test              = Math::GMPz::Rmpz_init();

    while (Math::GMPz::Rmpz_cmp($k_exp, $k) != 0) {
        Math::GMPz::Rmpz_divexact($k_exp, $k_exp, $k);

        Math::GMPz::Rmpz_powm($new_gen, $gen, $k, $p);
        Math::GMPz::Rmpz_set($candidate, $gen);
        Math::GMPz::Rmpz_set($gen,       $new_gen);

        Math::GMPz::Rmpz_divexact($k_exp_div_k_inner, $k_exp, $k);
        Math::GMPz::Rmpz_powm($test, $b, $k_exp_div_k_inner, $p);

        while (Math::GMPz::Rmpz_cmp_ui($test, 1) != 0) {
            Math::GMPz::Rmpz_mul($root, $root, $candidate);
            Math::GMPz::Rmpz_mod($root, $root, $p);

            Math::GMPz::Rmpz_mul($b, $b, $gen);
            Math::GMPz::Rmpz_mod($b, $b, $p);

            Math::GMPz::Rmpz_mul($test, $test, $zeta);
            Math::GMPz::Rmpz_mod($test, $test, $p);
        }
    }

    return ($root, $gen);    # return both root and zeta (gen)
}

#----------------------------------------------------------
# Chinese Remainder Theorem:   combine roots from two moduli
#----------------------------------------------------------
sub _crt_combine {
    my ($roots_a, $mod_a, $roots_b, $mod_b, $single_root) = @_;

    state $mod = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($mod, $mod_a, $mod_b);

    state $inv = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_invert($inv, $mod_a, $mod_b)
      or die "CRT: undefined inverse";

    my @roots;
    state $diff   = Math::GMPz::Rmpz_init();
    state $result = Math::GMPz::Rmpz_init();

    for my $ra (@$roots_a) {
        for my $rb (@$roots_b) {
            Math::GMPz::Rmpz_sub($diff, $rb, $ra);
            Math::GMPz::Rmpz_mul($diff, $diff, $inv);
            Math::GMPz::Rmpz_mod($diff, $diff, $mod_b);

            Math::GMPz::Rmpz_mul($result, $mod_a, $diff);
            Math::GMPz::Rmpz_add($result, $result, $ra);
            Math::GMPz::Rmpz_mod($result, $result, $mod);

            push @roots, Math::GMPz::Rmpz_init_set($result);
            return \@roots if $single_root;
        }
    }

    return \@roots;
}

#----------------------------------------------------------
# All k-th roots of a modulo prime p
#----------------------------------------------------------
sub _roots_mod_prime {
    my ($a, $k, $p, $single_root) = @_;

    state $a_mod = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($a_mod, $a, $p);

    if (Math::GMPz::Rmpz_cmp_ui($p, 2) == 0 || Math::GMPz::Rmpz_cmp_ui($a_mod, 0) == 0) {
        return [Math::GMPz::Rmpz_init_set($a_mod)];
    }

    state $phi = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($phi, $p, 1);

    state $g = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_gcd($g, $k, $phi);

    # Unique root when gcd(k, p-1) = 1
    if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
        my $k_mod_phi = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_mod($k_mod_phi, $k, $phi);
        my $inv = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_invert($inv, $k_mod_phi, $phi);
        my $root = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_powm($root, $a_mod, $inv, $p);
        return [$root];
    }

    # No roots if a is not a k-th power residue
    state $phi_div_g = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_divexact($phi_div_g, $phi, $g);
    state $test = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_powm($test, $a_mod, $phi_div_g, $p);
    return [] if (Math::GMPz::Rmpz_cmp_ui($test, 1) != 0);

    if (Math::GMPz::Rmpz_cmp_ui($p, 3) == 0) {
        return [Math::GMPz::Rmpz_init_set_ui(1)] if $single_root;
        return [Math::GMPz::Rmpz_init_set_ui(1), Math::GMPz::Rmpz_init_set_ui(2)];
    }

    # Find one root and generate all others using roots of unity
    my ($root, $zeta) = _tonelli_shanks($a_mod, $k, $p);

    if (Math::GMPz::Rmpz_cmp_ui($zeta, 0) == 0) {
        die "Failed to find root";
    }
    state $root_k = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_powm($root_k, $root, $k, $p);
    if (Math::GMPz::Rmpz_cmp($root_k, $a_mod) != 0) {
        die "Failed to find root";
    }

    my @roots = (Math::GMPz::Rmpz_init_set($root));

    return \@roots if $single_root;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $root, $zeta);
    Math::GMPz::Rmpz_mod($r, $r, $p);

    my $k_ui = Math::GMPz::Rmpz_get_ui($k);

    while (Math::GMPz::Rmpz_cmp($r, $root) != 0 && scalar(@roots) < $k_ui) {
        push @roots, Math::GMPz::Rmpz_init_set($r);
        Math::GMPz::Rmpz_mul($r, $r, $zeta);
        Math::GMPz::Rmpz_mod($r, $r, $p);
    }

    return \@roots;
}

#----------------------------------------------------------
# Hensel lifting helpers
#----------------------------------------------------------
sub _hensel_lift_standard {
    my ($roots, $A, $k, $mod, $single_root) = @_;

    my @result;

    state $k_minus_1 = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($k_minus_1, $k, 1);

    state $s_pow     = Math::GMPz::Rmpz_init();
    state $deriv     = Math::GMPz::Rmpz_init();
    state $s_k       = Math::GMPz::Rmpz_init();
    state $residue   = Math::GMPz::Rmpz_init();
    state $common    = Math::GMPz::Rmpz_init();
    state $res_div   = Math::GMPz::Rmpz_init();
    state $deriv_div = Math::GMPz::Rmpz_init();
    state $inv_deriv = Math::GMPz::Rmpz_init();
    state $quot      = Math::GMPz::Rmpz_init();
    state $new_s     = Math::GMPz::Rmpz_init();

    for my $s (@$roots) {
        Math::GMPz::Rmpz_powm($s_pow, $s, $k_minus_1, $mod);

        Math::GMPz::Rmpz_mul($deriv, $k, $s_pow);
        Math::GMPz::Rmpz_mod($deriv, $deriv, $mod);

        Math::GMPz::Rmpz_powm($s_k, $s, $k, $mod);

        Math::GMPz::Rmpz_sub($residue, $A, $s_k);
        Math::GMPz::Rmpz_mod($residue, $residue, $mod);
        Math::GMPz::Rmpz_gcd($common, $residue, $deriv);

        Math::GMPz::Rmpz_divexact($res_div,   $residue, $common);
        Math::GMPz::Rmpz_divexact($deriv_div, $deriv,   $common);

        Math::GMPz::Rmpz_invert($inv_deriv, $deriv_div, $mod);

        Math::GMPz::Rmpz_mul($quot, $res_div, $inv_deriv);
        Math::GMPz::Rmpz_mod($quot, $quot, $mod);

        Math::GMPz::Rmpz_add($new_s, $s, $quot);
        Math::GMPz::Rmpz_mod($new_s, $new_s, $mod);

        push @result, Math::GMPz::Rmpz_init_set($new_s);
        last if $single_root;
    }
    return \@result;
}

sub _hensel_lift_singular {
    my ($roots, $A, $k, $p, $mod, $single_root) = @_;

    state $ext_mod = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($ext_mod, $mod, $p);

    state $submod_val = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_divexact($submod_val, $mod, $p);

    my %seen;

    state $k_minus_1 = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($k_minus_1, $k, 1);

    state $s_pow     = Math::GMPz::Rmpz_init();
    state $deriv     = Math::GMPz::Rmpz_init();
    state $s_k       = Math::GMPz::Rmpz_init();
    state $residue   = Math::GMPz::Rmpz_init();
    state $common    = Math::GMPz::Rmpz_init();
    state $res_div   = Math::GMPz::Rmpz_init();
    state $deriv_div = Math::GMPz::Rmpz_init();
    state $inv_deriv = Math::GMPz::Rmpz_init();
    state $quot      = Math::GMPz::Rmpz_init();
    state $r         = Math::GMPz::Rmpz_init();
    state $r_k       = Math::GMPz::Rmpz_init();
    state $A_mod     = Math::GMPz::Rmpz_init();
    state $i_val     = Math::GMPz::Rmpz_init();
    state $new_r     = Math::GMPz::Rmpz_init();

    my $k_ui = Math::GMPz::Rmpz_get_ui($k);

    for my $s (@$roots) {
        Math::GMPz::Rmpz_powm($s_pow, $s, $k_minus_1, $ext_mod);

        Math::GMPz::Rmpz_mul($deriv, $k, $s_pow);
        Math::GMPz::Rmpz_mod($deriv, $deriv, $ext_mod);
        Math::GMPz::Rmpz_powm($s_k, $s, $k, $ext_mod);

        Math::GMPz::Rmpz_sub($residue, $A, $s_k);
        Math::GMPz::Rmpz_mod($residue, $residue, $ext_mod);
        Math::GMPz::Rmpz_gcd($common, $residue, $deriv);

        Math::GMPz::Rmpz_divexact($res_div,   $residue, $common);
        Math::GMPz::Rmpz_divexact($deriv_div, $deriv,   $common);

        Math::GMPz::Rmpz_invert($inv_deriv, $deriv_div, $mod);

        Math::GMPz::Rmpz_mul($quot, $res_div, $inv_deriv);
        Math::GMPz::Rmpz_mod($quot, $quot, $mod);

        Math::GMPz::Rmpz_add($r, $s, $quot);
        Math::GMPz::Rmpz_mod($r, $r, $mod);

        Math::GMPz::Rmpz_powm($r_k, $r, $k, $mod);

        Math::GMPz::Rmpz_mod($A_mod, $A, $mod);
        next if (Math::GMPz::Rmpz_cmp($r_k, $A_mod) != 0);

        for my $i (0 .. $k_ui - 1) {
            Math::GMPz::Rmpz_mul_ui($i_val, $submod_val, $i);
            Math::GMPz::Rmpz_mod($i_val, $i_val, $mod);
            Math::GMPz::Rmpz_add_ui($i_val, $i_val, 1);
            Math::GMPz::Rmpz_mod($i_val, $i_val, $mod);

            Math::GMPz::Rmpz_mul($new_r, $r, $i_val);
            Math::GMPz::Rmpz_mod($new_r, $new_r, $mod);

            $seen{Math::GMPz::Rmpz_get_str($new_r, 10)} = Math::GMPz::Rmpz_init_set($new_r);
            return [values %seen] if $single_root;
        }
    }
    return [values %seen];
}

#----------------------------------------------------------
# All k-th roots of r modulo prime power p^e
#----------------------------------------------------------
sub _roots_mod_prime_power {
    my ($r, $k, $p, $e, $single_root) = @_;

    return _roots_mod_prime($r, $k, $p, $single_root) if ($e == 1);

    my $mod = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_pow_ui($mod, $p, $e);

    my $k_ui = Math::GMPz::Rmpz_get_ui($k);
    my $pk   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_pow_ui($pk, $p, $k_ui);

    # Special case:   a ≡ 0 (mod p^e)
    my $r_mod = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($r_mod, $r, $mod);
    if (Math::GMPz::Rmpz_cmp_ui($r_mod, 0) == 0) {
        my $t  = CORE::int(($e - 1) / $k_ui) + 1;
        my $pt = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($pt, $p, $t);
        my $cnt = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($cnt, $p, $e - $t);
        my $cnt_ui = Math::GMPz::Rmpz_get_ui($cnt);

        my @result;
        my $val = Math::GMPz::Rmpz_init();
        for my $i (0 .. $cnt_ui - 1) {
            Math::GMPz::Rmpz_mul_ui($val, $pt, $i);
            Math::GMPz::Rmpz_mod($val, $val, $mod);
            push @result, Math::GMPz::Rmpz_init_set($val);
            last if $single_root;
        }
        return \@result;
    }

    # Special case:  a ≡ 0 (mod p^k) but a ≢ 0 (mod p^e)
    my $r_mod_pk = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($r_mod_pk, $r, $pk);
    if (Math::GMPz::Rmpz_cmp_ui($r_mod_pk, 0) == 0) {

        my $factor = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($factor, $p, ($e - $k_ui) + 1);

        my $count = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($count, $p, $k_ui - 1);

        my $count_ui = Math::GMPz::Rmpz_get_ui($count);
        my $r_div_pk = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_divexact($r_div_pk, $r, $pk);

        my $sub = _roots_mod_prime_power($r_div_pk, $k, $p, $e - $k_ui, $single_root);

        return [] if !@$sub;

        my @result;
        my $base = Math::GMPz::Rmpz_init();
        my $val  = Math::GMPz::Rmpz_init();

        for my $s (@$sub) {
            Math::GMPz::Rmpz_mul($base, $s, $p);
            Math::GMPz::Rmpz_mod($base, $base, $mod);

            for my $i (0 .. $count_ui - 1) {
                Math::GMPz::Rmpz_mul_ui($val, $factor, $i);
                Math::GMPz::Rmpz_add($val, $val, $base);
                Math::GMPz::Rmpz_mod($val, $val, $mod);
                push @result, Math::GMPz::Rmpz_init_set($val);
                return \@result if $single_root;
            }
        }
        return \@result;
    }

    # No roots if p | a but p^k ∤ a
    my $r_mod_p = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mod($r_mod_p, $r, $p);
    return [] if (Math::GMPz::Rmpz_cmp_ui($r_mod_p, 0) == 0);

    # Hensel lifting from smaller exponent
    my $half =
      (Math::GMPz::Rmpz_cmp_ui($p, 2) > 0 || $e < 5)
      ? CORE::int(($e + 1) / 2)
      : CORE::int(($e + 3) / 2);

    my $sub = _roots_mod_prime_power($r, $k, $p, $half, $single_root);

    if (Math::GMPz::Rmpz_cmp($k, $p) != 0) {
        return _hensel_lift_standard($sub, $r, $k, $mod, $single_root);
    }
    else {
        return _hensel_lift_singular($sub, $r, $k, $p, $mod, $single_root);
    }
}

#----------------------------------------------------------
# All k-th roots of r modulo n (with factorization)
#----------------------------------------------------------
sub _roots_mod_composite {
    my ($r, $k, $factors, $single_root) = @_;

    my $mod   = Math::GMPz::Rmpz_init_set_ui(1);
    my $roots = [];
    my $pe    = Math::GMPz::Rmpz_init();

    for my $factor (@$factors) {
        my ($p, $e) = @$factor;

        my $sub = _roots_mod_prime_power($r, $k, $p, $e, $single_root);
        return [] if (!@$sub);

        Math::GMPz::Rmpz_pow_ui($pe, $p, $e);

        if (@$roots) {
            $roots = _crt_combine($roots, $mod, $sub, $pe, $single_root);
        }
        else {
            $roots = $sub;
        }
        Math::GMPz::Rmpz_mul($mod, $mod, $pe);
    }
    return $roots;
}

#----------------------------------------------------------
# Main entry point:   all k-th roots of A modulo n
#----------------------------------------------------------
sub rootmod_all {
    my ($A, $k, $n, $single_root) = @_;

    $A = _any2mpz($$A, 0) // return _array();
    $k = _any2mpz($$k, 1) // return _array();
    $n = _any2mpz($$n, 2) // return _array();

    if (HAS_PRIME_UTIL and !$single_root and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $A = Math::GMPz::Rmpz_get_str($A, 10);
        $k = Math::GMPz::Rmpz_get_str($k, 10);
        $n = Math::GMPz::Rmpz_get_ui($n);
        my @roots = Math::Prime::Util::allrootmod($A, $k, $n);
        return _array([map { bless \$_ } @roots]);
    }

    Math::GMPz::Rmpz_abs($n, $n);
    return _array() if (Math::GMPz::Rmpz_cmp_ui($n, 0) == 0);

    Math::GMPz::Rmpz_mod($A, $A, $n);

    if (Math::GMPz::Rmpz_cmp_ui($k, 0) <= 0 && Math::GMPz::Rmpz_cmp_ui($A, 0) == 0) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($k) < 0) {
        my $inv = Math::GMPz::Rmpz_init();
        if (!Math::GMPz::Rmpz_invert($inv, $A, $n)) {
            return _array();
        }
        my $g = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_gcd($g, $inv, $n);
        return _array() if (Math::GMPz::Rmpz_cmp_ui($g, 1) != 0);
        Math::GMPz::Rmpz_set($A, $inv);
        Math::GMPz::Rmpz_neg($k, $k);
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, 2) <= 0 || Math::GMPz::Rmpz_cmp_ui($k, 1) == 0) {
        return _array([_set_int($A)]);
    }

    if (Math::GMPz::Rmpz_cmp_ui($k, 0) == 0) {
        if (Math::GMPz::Rmpz_cmp_ui($A, 1) == 0) {
            return _array([ZERO]) if $single_root;
            my $n_ui = Math::GMPz::Rmpz_get_ui($n);
            return _array([map { bless \$_ } (0 .. $n_ui - 1)]);
        }
        return _array();
    }

    my @factors = map { [Math::GMPz::Rmpz_init_set_str($_->[0], 10), $_->[1]] } _factor_exp($n);

    my $roots     = [Math::GMPz::Rmpz_init_set($A)];
    my @k_factors = map { Math::GMPz::Rmpz_init_set_str($_, 10) } _factor($k);

    for my $prime_factor (@k_factors) {
        my @new_roots;
        for my $r (@$roots) {
            my $sub = _roots_mod_composite($r, $prime_factor, \@factors, $single_root);
            push @new_roots, @$sub;
        }
        $roots = \@new_roots;
    }

    _array([map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } @$roots]);
}

sub rootmod {
    my ($A, $k, $n) = @_;
    $A->rootmod_all($k, $n, 1)->first // goto &nan;
}

sub difference_of_squares {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();

    # No solutions when n == 2 (mod 4). See: A016825.
    if (Math::GMPz::Rmpz_congruent_ui_p($n, 2, 4)) {
        return _array();
    }

    my $D = (bless \$n)->divisors($_[0]->isqrt);

    my $t = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init();

    my @solutions;

    foreach my $d (@$D) {

        if (ref($$d)) {
            Math::GMPz::Rmpz_divexact($t, $n, $$d);
            Math::GMPz::Rmpz_add($u, $t, $$d);
        }
        else {
            Math::GMPz::Rmpz_divexact_ui($t, $n, $$d);
            Math::GMPz::Rmpz_add_ui($u, $t, $$d);
        }

        Math::GMPz::Rmpz_even_p($u) || next;

        my $x = Math::GMPz::Rmpz_init();
        my $y = Math::GMPz::Rmpz_init();

        ref($$d)
          ? Math::GMPz::Rmpz_sub($y, $t, $$d)
          : Math::GMPz::Rmpz_sub_ui($y, $t, $$d);

        Math::GMPz::Rmpz_div_2exp($x, $u, 1);
        Math::GMPz::Rmpz_div_2exp($y, $y, 1);

        unshift @solutions, _array([(bless \$x), (bless \$y)]);
    }

    _array(\@solutions);
}

*diff_of_squares = \&difference_of_squares;

sub _primitive_sum_of_two_squares {
    my ($p) = @_;

    # Trivial case: 2 = 1² + 1²
    if (Math::GMPz::Rmpz_cmp_ui($p, 2) == 0) {
        return (1, 1);
    }

    state %sos_cache;
    undef %sos_cache if scalar(keys(%sos_cache)) > 1e5;

    my $key = Math::GMPz::Rmpz_get_str($p, 10);

    if (exists $sos_cache{$key}) {
        return @{$sos_cache{$key}};
    }

    # Step 1: s ← sqrt(-1) mod p
    my $s = Math::GMPz::Rmpz_init_set_str((Math::Prime::Util::GMP::sqrtmod(-1, Math::GMPz::Rmpz_get_str($p, 10)) // die "error"), 10);
    my $q = Math::GMPz::Rmpz_init_set($p);

    state $t = Math::GMPz::Rmpz_init_nobless();

    # Step 2: Euclidean reduction — stop when s² ≤ p
    while (1) {
        Math::GMPz::Rmpz_mul($t, $s, $s);             # t = s²
        last if Math::GMPz::Rmpz_cmp($t, $p) <= 0;    # s² ≤ p → done

        # One Euclidean step: (q, s) ← (s, q mod s)
        Math::GMPz::Rmpz_set($t, $s);
        Math::GMPz::Rmpz_mod($s, $q, $s);
        Math::GMPz::Rmpz_set($q, $t);
    }

    # Step 3: b = (previous remainder) mod a
    Math::GMPz::Rmpz_mod($q, $q, $s);

    $sos_cache{$key} = [$q, $s];
    return ($q, $s);
}

sub _combine_pairs {
    my ($A,  $B,  $C,  $D)  = @_;
    my ($AC, $BD, $AD, $BC) = ($A * $C, $B * $D, $A * $D, $B * $C);
    return ([$AC - $BD, $AD + $BC], [$AC + $BD, $AD - $BC]);
}

sub _multiply_sets {
    my ($A, $B) = @_;
    my (@result, %seen);

    for my $p (@$A) {
        for my $q (@$B) {
            for my $r (_combine_pairs(@$p, @$q)) {
                my ($x, $y) = @$r;

                # Canonicalise: ensure non-negative and x ≤ y
                $x = -$x if $x < 0;
                $y = -$y if $y < 0;

                ($x, $y) = ($y, $x) if $x > $y;

                if (!$seen{$x}{$y}++) {
                    push @result, [$x, $y];
                }
            }
        }
    }

    return \@result;
}

sub sum_of_squares {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) >= 0 or return _array();

    if (Math::GMPz::Rmpz_sgn($n) == 0) {
        return _array(_array([ZERO, ZERO]));
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        return _array(_array([ZERO, ONE]));
    }

    my @factor_exp = _factor_exp($n);

    # Feasibility check (Legendre's three-square theorem)
    # A prime factor p ≡ 3 (mod 4) must appear to an even power;
    # if any such prime appears to an odd power, no representation exists.
    for my $pp (@factor_exp) {
        my ($p, $k) = @$pp;
        next if ($k % 2 == 0);
        my $mod4 = ($p < ULONG_MAX) ? ($p % 4) : Math::Prime::Util::GMP::modint($p, 4);
        return _array() if ($mod4 == 3);
    }

    # Build representations prime-by-prime
    # Start from the trivial identity 1 = 0² + 1², then fold in each factor.
    my $reps         = [[0, 1]];
    my $square_scale = Math::GMPz::Rmpz_init_set_ui(1);

    state $p = Math::GMPz::Rmpz_init_nobless();

    for my $pp (@factor_exp) {
        my ($p_raw, $k) = @$pp;

        # Convert p to GMPz for all subsequent arithmetic
        (FAST_MODE and $p_raw < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($p, $p_raw)
          : Math::GMPz::Rmpz_set_str($p, "$p_raw", 10);

        # Primes p ≡ 3 (mod 4) appear only to even powers (guaranteed above).
        # p^(2t) = (p^t)² is a perfect square; it scales both coordinates
        # uniformly by p^(k/2) without changing the number of representations.
        if (Math::GMPz::Rmpz_congruent_ui_p($p, 3, 4)) {
            Math::GMPz::Rmpz_pow_ui($p, $p, $k >> 1);    # p = p^(k/2)
            Math::GMPz::Rmpz_mul($square_scale, $square_scale, $p);
            next;
        }

        # p = 2 or p ≡ 1 (mod 4): obtain the primitive representation p = a² + b²
        my ($x, $y) = _primitive_sum_of_two_squares($p);

        # Lift p → p^k via binary exponentiation over representation sets.
        # @acc accumulates the running product starting from 1 = 0² + 1²;
        # @base holds the current power-of-two of p's representation.
        my $acc  = [[0, 1]];
        my $base = [[$x, $y]];
        my $exp  = $k;

        while ($exp > 0) {
            $acc = _multiply_sets($acc, $base) if $exp & 1;
            $exp >>= 1;
            $base = _multiply_sets($base, $base) if $exp > 0;    # skip final squaring
        }

        $reps = _multiply_sets($reps, $acc);
    }

    # Apply the p ≡ 3 (mod 4) scale factor
    if (Math::GMPz::Rmpz_cmp_ui($square_scale, 1) > 0) {
        @$reps = map { [$_->[0] * $square_scale, $_->[1] * $square_scale] } @$reps;
    }

    # Sort into canonical order (x ascending) and wrap for return
    @$reps = sort { $a->[0] <=> $b->[0] } @$reps;

    _array(
        [
         map {
             _array([map { _set_int($_) } @$_])
           } @$reps
        ]
    );
}

# Sum of two k-gonal numbers
sub sum_of_polygonals {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // return _array();
    $k = _any2mpz($$k, 1) // return _array();

    Math::GMPz::Rmpz_sgn($n) >= 0 or return _array();

    # Defined only for k >= 3
    if (Math::GMPz::Rmpz_cmp_ui($k, 3) < 0) {
        return _array();
    }

    if (Math::GMPz::Rmpz_cmp_ui($k, 4) == 0) {
        return ((bless \$n)->sum_of_squares);
    }

    my $k_minus_2 = Math::GMPz::Rmpz_init();
    my $k_minus_4 = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sub_ui($k_minus_2, $k, 2);
    Math::GMPz::Rmpz_sub_ui($k_minus_4, $k, 4);

    my $x = Math::GMPz::Rmpz_init();
    my $y = Math::GMPz::Rmpz_init();

    # Calculate N = 8 * (k - 2) * n + 2 * (k - 4)^2
    my $N = Math::GMPz::Rmpz_init_set($n);
    Math::GMPz::Rmpz_mul_2exp($N, $N, 3);
    Math::GMPz::Rmpz_mul($N, $N,         $k_minus_2);
    Math::GMPz::Rmpz_mul($x, $k_minus_4, $k_minus_4);
    Math::GMPz::Rmpz_addmul_ui($N, $x, 2);

    # Retrieve all base square representations
    my @sq_sols = @{(bless \$N)->sum_of_squares};

    my @results;
    my %seen;

    my $den = Math::GMPz::Rmpz_init_set($k_minus_2);
    Math::GMPz::Rmpz_mul_2exp($den, $den, 1);

    for my $sol (@sq_sols) {

        my $X = _any2mpz(${$sol->[0]}, 2);
        my $Y = _any2mpz(${$sol->[1]}, 3);

        my @cand_X = ($X);
        push @cand_X, -$X if Math::GMPz::Rmpz_sgn($X) != 0;

        my @cand_Y = ($Y);
        push @cand_Y, -$Y if Math::GMPz::Rmpz_sgn($Y) != 0;

        my @pairs;
        for my $cx (@cand_X) {
            for my $cy (@cand_Y) {
                push @pairs, [$cx, $cy];
                push @pairs, [$cy, $cx] if Math::GMPz::Rmpz_cmp($X, $Y) != 0;
            }
        }

        # Validate configurations that map back to valid integers
        for my $pair (@pairs) {
            my ($A, $B) = @$pair;

            Math::GMPz::Rmpz_add($x, $A, $k_minus_4);
            Math::GMPz::Rmpz_add($y, $B, $k_minus_4);

            if (    Math::GMPz::Rmpz_divisible_p($x, $den)
                and Math::GMPz::Rmpz_divisible_p($y, $den)) {
                Math::GMPz::Rmpz_divexact($x, $x, $den);
                Math::GMPz::Rmpz_divexact($y, $y, $den);

                if (Math::GMPz::Rmpz_sgn($x) >= 0 and Math::GMPz::Rmpz_sgn($y) >= 0) {

                    if (Math::GMPz::Rmpz_cmp($y, $x) < 0) {
                        ($x, $y) = ($y, $x);
                    }

                    my $key = join(';', Math::GMPz::Rmpz_get_str($x, 10), Math::GMPz::Rmpz_get_str($y, 10),);

                    if (!$seen{$key}++) {
                        push @results, [Math::GMPz::Rmpz_init_set($x), Math::GMPz::Rmpz_init_set($y)];
                    }
                }
            }
        }
    }

    @results = sort { $a->[0] <=> $b->[0] } @results;

    _array(
        [
         map {
             _array([map { _set_int($_) } @$_])
           } @results
        ]
    );
}

sub _modular_rational {
    my ($n, $m) = @_;

    if (ref($n) ne 'Math::GMPq') {
        $n = _any2mpq($n) // return;
    }

    state $z = Math::GMPz::Rmpz_init_nobless();

    my $t = Math::GMPz::Rmpz_init();
    Math::GMPq::Rmpq_get_den($z, $n);
    Math::GMPz::Rmpz_invert($t, $z, $m) or return;
    Math::GMPq::Rmpq_get_num($z, $n);
    Math::GMPz::Rmpz_mul($t, $t, $z);

    return $t;
}

sub powmod {
    my ($n, $k, $m) = @_;

    _valid(\$k, \$m);

    $n = $$n;
    $k = $$k;
    $m = $$m;

    if (FAST_MODE and !ref($m) and !ref($n) and !ref($k) and $m > 0) {
        my $r = (
                 HAS_PRIME_UTIL
                 ? Math::Prime::Util::powmod($n, $k, $m)
                 : Math::Prime::Util::GMP::powmod($n, $k, $m)
                ) // goto &nan;
        return bless \$r;
    }

    $k = _any2mpz($k, 0) // goto &nan;
    $m = _any2mpz($m, 1) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    if (ref($n) ne 'Math::GMPz') {
        if (__is_int__($n)) {
            $n = _any2mpz($n, 2) // goto &nan;
        }
        else {
            $n = _modular_rational($n, $m) // goto &nan;
        }
    }

    state $r = Math::GMPz::Rmpz_init_nobless();

    if (Math::GMPz::Rmpz_sgn($k) < 0) {
        Math::GMPz::Rmpz_invert($r, $n, $m) or goto &nan;
    }

    Math::GMPz::Rmpz_fits_ulong_p($k)
      ? Math::GMPz::Rmpz_powm_ui($r, $n, Math::GMPz::Rmpz_get_ui($k), $m)
      : Math::GMPz::Rmpz_powm($r, $n, $k, $m);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);

    bless \$r2;
}

*expmod = \&powmod;

sub complex_cmp {
    my ($x_re, $x_im, $y_re, $y_im) = @_;

    $x_im //= ZERO;
    $y_re //= ZERO;
    $y_im //= ZERO;

    _valid(\$x_im, \$y_re, \$y_im);

#<<<
    my $cmp = (
           (__cmp__($$x_re, $$y_re) // return undef)
        || (__cmp__($$x_im, $$y_im) // return undef)
    );
#>>>

    ($cmp ? ($cmp == 1 ? ONE : MONE) : ZERO);
}

sub complex_mod {
    my ($x, $y, $m) = @_;
    _valid(\$y, \$m);
    ((bless \__mod__($$x, $$m)), (bless \__mod__($$y, $$m)));
}

*cmod = \&complex_mod;

sub complex_add {
    my ($x_re, $x_im, $y_re, $y_im) = @_;

    $x_im //= ZERO;
    $y_re //= ZERO;
    $y_im //= ZERO;

    _valid(\$x_im, \$y_re, \$y_im);
    ((bless \__add__($$x_re, $$y_re)), (bless \__add__($$x_im, $$y_im)));
}

*cadd = \&complex_add;

sub complex_sub {
    my ($x_re, $x_im, $y_re, $y_im) = @_;

    $x_im //= ZERO;
    $y_re //= ZERO;
    $y_im //= ZERO;

    _valid(\$x_im, \$y_re, \$y_im);
    ((bless \__sub__($$x_re, $$y_re)), (bless \__sub__($$x_im, $$y_im)));
}

*csub = \&complex_sub;

sub complex_mul {
    my ($x_re, $x_im, $y_re, $y_im) = @_;

    # (a + b*i) * (x + y*i) = (a*x - b*y) + (a*y + b*x)*i

    $x_im //= ZERO;
    $y_re //= ZERO;
    $y_im //= ZERO;

    _valid(\$x_im, \$y_re, \$y_im);

#<<<
    (
        (bless \__sub__(__mul__($$x_re, $$y_re), __mul__($$x_im, $$y_im))),
        (bless \__add__(__mul__($$x_re, $$y_im), __mul__($$x_im, $$y_re))),
    );
#>>>
}

*cmul = \&complex_mul;

sub complex_div {
    my ($x_re, $x_im, $y_re, $y_im) = @_;

    if (!defined($y_re) and !defined($y_im)) {    # ceil division
        return $x_re->idiv_ceil($x_im);
    }

    # (a + b*i) / (x + y*i) = (a*x + b*y)/(x^2 + y^2) + (b*x - a*y)/(x^2 + y^2)*i

    $x_im //= ZERO;
    $y_re //= ZERO;
    $y_im //= ZERO;

    _valid(\$x_im, \$y_re, \$y_im);

    my $den = __add__(__mul__($$y_re, $$y_re), __mul__($$y_im, $$y_im));

#<<<
    (
        (bless \__div__(__add__(__mul__($$x_re, $$y_re), __mul__($$x_im, $$y_im)), $den)),
        (bless \__div__(__sub__(__mul__($$x_im, $$y_re), __mul__($$x_re, $$y_im)), $den)),
    );
#>>>
}

*cdiv = \&complex_div;

sub complex_inv {
    my ($re, $im) = @_;

    $im //= ZERO;
    _valid(\$im);

    my $den = __add__(__mul__($$re, $$re), __mul__($$im, $$im));

#<<<
    (
        (bless \__div__(        $$re,  $den)),
        (bless \__div__(__neg__($$im), $den)),
    );
#>>>
}

*cinv = \&complex_inv;

sub complex_invmod {
    my ($x, $y, $m) = @_;

    _valid(\$y, \$m);

    $x = _any2mpz($$x, 0) // return (nan(), nan());
    $y = _any2mpz($$y, 1) // return (nan(), nan());
    $m = _any2mpz($$m, 2) // return (nan(), nan());

    my $t = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_mul($t, $x, $x);
    Math::GMPz::Rmpz_addmul($t, $y, $y);

    if (Math::GMPz::Rmpz_invert($t, $t, $m)) {

        my $c0 = Math::GMPz::Rmpz_init();
        my $c1 = Math::GMPz::Rmpz_init();

        Math::GMPz::Rmpz_mul($c0, $x, $t);
        Math::GMPz::Rmpz_mul($c1, $y, $t);
        Math::GMPz::Rmpz_neg($c1, $c1);
        Math::GMPz::Rmpz_mod($c0, $c0, $m);
        Math::GMPz::Rmpz_mod($c1, $c1, $m);

        return ((bless \$c0), (bless \$c1));
    }

    return (nan(), nan());    # no inverse
}

*cinvmod = \&complex_invmod;

sub complex_ipow {
    my ($x, $y, $n) = @_;

    _valid(\$y, \$n);

    $x = _any2mpz($$x, 0) // return (nan(), nan());
    $y = _any2mpz($$y, 1) // return (nan(), nan());
    $n = _any2mpz($$n, 2) // return (nan(), nan());

    my $c0 = Math::GMPz::Rmpz_init_set_ui(1);
    my $c1 = Math::GMPz::Rmpz_init_set_ui(0);

    $x = Math::GMPz::Rmpz_init_set($x);
    $y = Math::GMPz::Rmpz_init_set($y);

    my $neg = 0;
    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);
        $neg = 1;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    foreach my $k (0 .. Math::GMPz::Rmpz_sizeinbase($n, 2) - 1) {

        if (Math::GMPz::Rmpz_tstbit($n, $k)) {
            Math::GMPz::Rmpz_set($t, $c0);

            Math::GMPz::Rmpz_mul($c0, $c0, $x);
            Math::GMPz::Rmpz_submul($c0, $c1, $y);

            Math::GMPz::Rmpz_mul($c1, $c1, $x);
            Math::GMPz::Rmpz_addmul($c1, $t, $y);
        }

        Math::GMPz::Rmpz_mul($t, $x, $y);
        Math::GMPz::Rmpz_mul_2exp($t, $t, 1);

        Math::GMPz::Rmpz_mul($x, $x, $x);
        Math::GMPz::Rmpz_submul($x, $y, $y);
        Math::GMPz::Rmpz_set($y, $t);
    }

    my ($r1, $r2) = ((bless \$c0), (bless \$c1));
    ($r1, $r2) = complex_inv($r1, $r2) if $neg;
    ($r1, $r2);
}

sub complex_pow {
    my ($x, $y, $n) = @_;

    _valid(\$y, \$n);

    if (__is_int__($$x) and __is_int__($$y)) {
        return complex_ipow($x, $y, $n);
    }

    $x = $$x;
    $y = $$y;
    $n = _any2mpz($$n, 0) // return (nan(), nan());

    my $neg = 0;
    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);
        $neg = 1;
    }

    my $c0 = 1;
    my $c1 = 0;

#<<<
    foreach my $k (0 .. Math::GMPz::Rmpz_sizeinbase($n, 2) - 1) {

        if (Math::GMPz::Rmpz_tstbit($n, $k)) {
            ($c0, $c1) = (
                __sub__(__mul__($c0, $x), __mul__($c1, $y)),
                __add__(__mul__($c0, $y), __mul__($c1, $x)),
            );
        }

        ($x, $y) = (
            __sub__(__mul__($x, $x), __mul__($y, $y)),
            __mul__(__mul__($x, $y), 2),
        );
    }
#>>>

    my ($r1, $r2) = ((bless \$c0), (bless \$c1));
    ($r1, $r2) = complex_inv($r1, $r2) if $neg;
    ($r1, $r2);
}

*cpow = \&complex_pow;

sub complex_powmod {
    my ($x, $y, $n, $m) = @_;

    _valid(\$y, \$n, \$m);

    $x = $$x;
    $y = $$y;

    $n = _any2mpz($$n, 0) // return (nan(), nan());
    $m = _any2mpz($$m, 1) // return (nan(), nan());

    Math::GMPz::Rmpz_sgn($m) || return (nan(), nan());

    # Identities for fractional x = a/b, y = c/d:
    #   ((a/b) + (c/d)*i)^n = ((a*d + b*c*i) / (b*d))^n
    #                       = (a*d + b*c*i)^n * (b*d)^(-n)

    # We use:
    #   ((a/b) + (c/d)*i)^n mod m = (a*invmod(b,m) + c*invmod(d, m)*i)^n mod m

    if (ref($x) ne 'Math::GMPz') {
        if (__is_int__($x)) {
            $x = _any2mpz($x, 2) // return (nan(), nan());
        }
        else {
            $x = _modular_rational($x, $m) // return (nan(), nan());
        }
    }

    if (ref($y) ne 'Math::GMPz') {
        if (__is_int__($y)) {
            $y = _any2mpz($y, 3) // return (nan(), nan());
        }
        else {
            $y = _modular_rational($y, $m) // return (nan(), nan());
        }
    }

    $x = _any2mpz($x, 2) // return (nan(), nan());
    $y = _any2mpz($y, 3) // return (nan(), nan());

    my $c0 = Math::GMPz::Rmpz_init_set_ui(1);
    my $c1 = Math::GMPz::Rmpz_init_set_ui(0);

    $x = Math::GMPz::Rmpz_init_set($x);
    $y = Math::GMPz::Rmpz_init_set($y);

    state $t = Math::GMPz::Rmpz_init_nobless();

    # Handle negative exponent
    if (Math::GMPz::Rmpz_sgn($n) < 0) {

        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);

        my $t = Math::GMPz::Rmpz_init();

        Math::GMPz::Rmpz_mul($t, $x, $x);
        Math::GMPz::Rmpz_addmul($t, $y, $y);

        if (Math::GMPz::Rmpz_invert($t, $t, $m)) {

            Math::GMPz::Rmpz_mul($c0, $x, $t);
            Math::GMPz::Rmpz_mul($c1, $y, $t);
            Math::GMPz::Rmpz_neg($c1, $c1);
            Math::GMPz::Rmpz_mod($c0, $c0, $m);
            Math::GMPz::Rmpz_mod($c1, $c1, $m);

            Math::GMPz::Rmpz_set($x, $c0);
            Math::GMPz::Rmpz_set($y, $c1);

            Math::GMPz::Rmpz_set_ui($c0, 1);
            Math::GMPz::Rmpz_set_ui($c1, 0);
        }
        else {    # no inverse exists
            return (nan(), nan());
        }
    }

    foreach my $k (0 .. Math::GMPz::Rmpz_sizeinbase($n, 2) - 1) {

        if (Math::GMPz::Rmpz_tstbit($n, $k)) {
            Math::GMPz::Rmpz_set($t, $c0);

            Math::GMPz::Rmpz_mul($c0, $c0, $x);
            Math::GMPz::Rmpz_submul($c0, $c1, $y);

            Math::GMPz::Rmpz_mul($c1, $c1, $x);
            Math::GMPz::Rmpz_addmul($c1, $t, $y);

            Math::GMPz::Rmpz_mod($c0, $c0, $m);
            Math::GMPz::Rmpz_mod($c1, $c1, $m);
        }

        Math::GMPz::Rmpz_mul($t, $x, $y);
        Math::GMPz::Rmpz_mul_2exp($t, $t, 1);

        Math::GMPz::Rmpz_powm_ui($x, $x, 2, $m);
        Math::GMPz::Rmpz_powm_ui($y, $y, 2, $m);

        Math::GMPz::Rmpz_sub($x, $x, $y);
        Math::GMPz::Rmpz_mod($y, $t, $m);
    }

    ((bless \$c0), (bless \$c1));
}

*cpowmod = \&complex_powmod;

sub invmod {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($y) and !ref($x) and $x != 0) {
        my $r = (
                 HAS_PRIME_UTIL
                 ? Math::Prime::Util::invmod($x, $y)
                 : Math::Prime::Util::GMP::invmod($x, $y)
                ) // goto &nan;
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_invert($r, $x, $y) || (goto &nan);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub divmod {
    my ($x, $y, $m) = @_;

    if (defined($m)) {    # modular division

        _valid(\$y, \$m);

        $x = $$x;
        $y = $$y;
        $m = $$m;

        if (FAST_MODE and !ref($m) and $m > 0 and !ref($y) and $y > 1 and !ref($x)) {
            my $r =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divmod($x, $y, $m)
              : Math::Prime::Util::GMP::divmod($x, $y, $m);
            if (defined($r)) {
                return bless \$r;
            }
        }

        $x = _any2mpz($x, 0) // goto &nan;
        $y = _any2mpz($y, 1) // goto &nan;
        $m = _any2mpz($m, 2) // goto &nan;

        my $r = Math::GMPz::Rmpz_init();

        if (Math::GMPz::Rmpz_invert($r, $y, $m)) {
            Math::GMPz::Rmpz_mul($r, $r, $x);
            Math::GMPz::Rmpz_mod($r, $r, $m);
        }
        else {
            goto &nan;
        }

        return bless \$r;
    }

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($y) and $y > 0 and !ref($x)) {
        my ($q, $r) = (
                       HAS_PRIME_UTIL
                       ? Math::Prime::Util::divrem($x, $y)
                       : Math::Prime::Util::GMP::divrem($x, $y)
                      );
        return (_set_int($q), _set_int($r));
    }

    $x = _any2mpz($x, 0) // return (nan(), nan());
    $y = _any2mpz($y, 1) // return (nan(), nan());

    Math::GMPz::Rmpz_sgn($y)
      || return (nan(), nan());

    my $q = Math::GMPz::Rmpz_init();
    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_divmod($q, $r, $x, $y);
    ((bless \$q), (bless \$r));
}

*divrem = \&divmod;

sub and {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and (!ref($x) && $x >= 0) and (!ref($y) && $y >= 0)) {
        my $r = $x & $y;
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_and($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub or {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and (!ref($x) && $x >= 0) and (!ref($y) && $y >= 0)) {
        my $r = $x | $y;
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_ior($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub xor {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and (!ref($x) && $x >= 0) and (!ref($y) && $y >= 0)) {
        my $r = $x ^ $y;
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // (goto &nan);
    $y = _any2mpz($y, 1) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_xor($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub not {
    my ($x) = @_;

    $x = _any2mpz($$x, 0) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_com($r, $x);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub bit {
    my ($x, $k) = @_;

    _valid(\$k);

    $x = _any2mpz($$x, 0) // return undef;
    $k = _any2ui($$k)     // return undef;

    Math::GMPz::Rmpz_tstbit($x, $k) ? ONE : ZERO;
}

*getbit  = \&bit;
*testbit = \&bit;

sub setbit {
    my ($x, $k) = @_;

    _valid(\$k);

    $x = _any2mpz($$x, 0) // return undef;
    $k = _any2ui($$k)     // return undef;

    my $r = Math::GMPz::Rmpz_init_set($x);
    Math::GMPz::Rmpz_setbit($r, $k);
    bless \$r;
}

sub flipbit {
    my ($x, $k) = @_;

    _valid(\$k);

    $x = _any2mpz($$x, 0) // return undef;
    $k = _any2ui($$k)     // return undef;

    my $r = Math::GMPz::Rmpz_init_set($x);

    Math::GMPz::Rmpz_tstbit($r, $k)
      ? Math::GMPz::Rmpz_clrbit($r, $k)
      : Math::GMPz::Rmpz_setbit($r, $k);

    bless \$r;
}

sub clearbit {
    my ($x, $k) = @_;

    _valid(\$k);

    $x = _any2mpz($$x, 0) // return undef;
    $k = _any2ui($$k)     // return undef;

    my $r = Math::GMPz::Rmpz_init_set($x);
    Math::GMPz::Rmpz_clrbit($r, $k);
    bless \$r;
}

sub bit_scan0 {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return undef } : 0;
    $n = _any2mpz($$n, 0) // return undef;

    _set_int(Math::GMPz::Rmpz_scan0($n, $k));
}

sub bit_scan1 {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return undef } : 0;
    $n = _any2mpz($$n, 0) // return undef;

    _set_int(Math::GMPz::Rmpz_scan1($n, $k));
}

sub ramanujan_tau {
    my ($n) = @_;
    _set_int(Math::Prime::Util::GMP::ramanujan_tau(_big2uistr($$n) // goto &nan));
}

*RamanujanTau = \&ramanujan_tau;

sub ramanujan_sum {
    my ($n, $k) = @_;

    #
    ## c_k(n) = μ(k/gcd(n, k)) * φ(k) / φ(k/gcd(n, k))
    #

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    # Make `k` positive if it is negative
    if (Math::GMPz::Rmpz_sgn($k) < 0) {
        $k = Math::GMPz::Rmpz_init_set($k);
        Math::GMPz::Rmpz_neg($k, $k);
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_gcd($r, $n, $k);
    Math::GMPz::Rmpz_divexact($r, $k, $r) if Math::GMPz::Rmpz_sgn($r);

    my $r_str = Math::GMPz::Rmpz_get_str($r, 10);
    my $mu    = Math::Prime::Util::GMP::moebius($r_str) || return ZERO;

    if (Math::GMPz::Rmpz_cmp($r, $k) == 0) {
        return ($mu == 1 ? ONE : MONE);
    }

    my $k_str = Math::GMPz::Rmpz_get_str($k, 10);
    my $phi_k = Math::Prime::Util::GMP::totient($k_str);
    my $phi_r = Math::Prime::Util::GMP::totient($r_str);

    (FAST_MODE and $phi_k < ULONG_MAX)
      ? Math::GMPz::Rmpz_set_ui($r, $phi_k)
      : Math::GMPz::Rmpz_set_str($r, $phi_k, 10);

    if (FAST_MODE and $phi_r < ULONG_MAX) {
        Math::GMPz::Rmpz_divexact_ui($r, $r, $phi_r);
    }
    else {
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_set_str($t, $phi_r, 10);
        Math::GMPz::Rmpz_divexact($r, $r, $t);
    }

    Math::GMPz::Rmpz_neg($r, $r) if ($mu == -1);
    bless \$r;
}

*RamanujanSum = \&ramanujan_sum;

sub subfactorial {    # OEIS: A000166
    my ($x, $y) = @_;

    my $m = _any2ui($$x) // goto &nan;
    my $k = defined($y) ? do { _valid(\$y); _any2si($$y) // goto &nan } : 0;

    my $n = $m - $k;

    return ZERO if ($k < 0);
    return ONE  if ($n == 0);
    return ZERO if ($n < 0);

    my $z = Math::GMPz::Rmpz_init();

    if ($n >= 30000) {

        state $logtau = CORE::log(6.28318530717958647692528676655900576839433879875);

        my $logn = CORE::log($n);
        my $prec = 4 + CORE::int(($n * $logn + ($logn + $logtau) / 2 - $n) / CORE::log(2));

        Math::GMPz::Rmpz_fac_ui($z, $n);

        my $f = Math::MPFR::Rmpfr_init2($prec);
        Math::MPFR::Rmpfr_set_ui($f, 1, $round_z);
        Math::MPFR::Rmpfr_exp($f, $f, $round_z);
        Math::MPFR::Rmpfr_z_div($f, $z, $f, $round_z);
        Math::MPFR::Rmpfr_add_d($f, $f, 0.5, $round_z);
        Math::MPFR::Rmpfr_floor($f, $f);
        Math::MPFR::Rmpfr_get_z($z, $f, $round_z);
    }
    else {
        Math::GMPz::Rmpz_set_str($z, Math::Prime::Util::GMP::subfactorial($n), 10);
    }

    if ($k != 0) {
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_bin_uiui($t, $m, $k);
        Math::GMPz::Rmpz_mul($z, $z, $t);
    }

    bless \$z;
}

*rencontres = \&subfactorial;

sub factorial_sum {
    my ($n) = @_;
    $n = _any2ui($$n) // goto &nan;
    _set_int(Math::Prime::Util::GMP::factorial_sum($n));
}

*left_factorial = \&factorial_sum;

sub superprimorial {    # A006939
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    $n || return ONE;

    my @terms;
    my $k = 1;

    foreach my $p (@{_primes(2, ${$_[0]->nth_prime})}) {
        my $z = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_ui_pow_ui($z, $p, $n - $k + 1);
        push @terms, $z;
        ++$k;
    }

    bless \_binsplit(\@terms, \&__mul__);
}

sub lnsuperprimorial {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_set_ui($r, 0, $ROUND);

    my $k = 1;
    foreach my $p (@{_primes(2, ${$_[0]->nth_prime})}) {
        Math::MPFR::Rmpfr_set_ui($t, $p, $ROUND);
        Math::MPFR::Rmpfr_log($t, $t, $ROUND);
        Math::MPFR::Rmpfr_mul_ui($t, $t, $n - $k + 1, $ROUND);
        Math::MPFR::Rmpfr_add($r, $r, $t, $ROUND);
        ++$k;
    }

    bless \$r;
}

*superprimorial_ln  = \&lnsuperprimorial;
*superprimorial_log = \&lnsuperprimorial;

sub superfactorial {    # A000178
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my @terms;
    foreach my $k (2 .. $n) {
        my $z = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_ui_pow_ui($z, $k, $n - $k + 1);
        push @terms, $z;
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub lnsuperfactorial {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_set_ui($r, 0, $ROUND);

    foreach my $k (2 .. $n) {
        Math::MPFR::Rmpfr_set_ui($t, $k, $ROUND);
        Math::MPFR::Rmpfr_log($t, $t, $ROUND);
        Math::MPFR::Rmpfr_mul_ui($t, $t, $n - $k + 1, $ROUND);
        Math::MPFR::Rmpfr_add($r, $r, $t, $ROUND);
    }

    bless \$r;
}

*superfactorial_ln  = \&lnsuperfactorial;
*superfactorial_log = \&lnsuperfactorial;

sub hyperfactorial {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my @terms;
    foreach my $k (2 .. $n) {
        my $z = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_ui_pow_ui($z, $k, $k);
        push @terms, $z;
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub lnhyperfactorial {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    my $t = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

    Math::MPFR::Rmpfr_set_ui($r, 0, $ROUND);

    foreach my $k (2 .. $n) {
        Math::MPFR::Rmpfr_set_ui($t, $k, $ROUND);
        Math::MPFR::Rmpfr_log($t, $t, $ROUND);
        Math::MPFR::Rmpfr_mul_ui($t, $t, $k, $ROUND);
        Math::MPFR::Rmpfr_add($r, $r, $t, $ROUND);
    }

    bless \$r;
}

*hyperfactorial_ln  = \&lnhyperfactorial;
*hyperfactorial_log = \&lnhyperfactorial;

sub factorial {
    my ($n) = @_;
    $n = _any2ui($$n) // goto &nan;
    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_fac_ui($r, $n);
    $r = Math::GMPz::Rmpz_get_ui($r) if ($n <= ((INTSIZE <= 32) ? 12 : 20));
    bless \$r;
}

*fac = \&factorial;

sub kempner {
    my ($n) = @_;

    my $v = _big2pistr($$n) // return ONE;

    # The Smarandache/Kempner function is defined for positive integers.
    # For n <= 1, S(n) = 1.
    if ($v eq '1') {
        return ONE;
    }

    # Identity: S(p) = p for prime numbers
    if (_is_prob_prime($v)) {
        return _set_int($v);
    }

    my @factors = _factor_exp($v);

    # Track the maximum S(p^k) across all prime factors
    my $max_m = Math::GMPz::Rmpz_init_set_ui(0);

    state $p = Math::GMPz::Rmpz_init_nobless();
    state $m = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    foreach my $pp (@factors) {
        my $k = $pp->[1];
        Math::GMPz::Rmpz_set_str($p, $pp->[0], 10);

        # Build the Kempner expansion coefficients array: a_j = (p^j - 1)/(p - 1)
        # and store the corresponding powers of p to avoid redundant exponentiations.
        my @a     = (1);
        my @p_pow = ($p);

        if (Math::GMPz::Rmpz_cmp_ui($p, $k) < 0) {

            my $p_ui = Math::GMPz::Rmpz_get_ui($p);

            while (1) {

                my $next_a = $a[-1] * $p_ui + 1;

                if ($next_a > $k) {
                    last;
                }

                push @a, $next_a;
                my $next_p = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul($next_p, $p_pow[-1], $p);
                push @p_pow, $next_p;
            }
        }

        # Greedy decomposition of the exponent k
        my $rem_k = $k;
        Math::GMPz::Rmpz_set_ui($m, 0);

        for (my $i = $#a ; $i >= 0 ; $i--) {
            my $ai = $a[$i];
            my $c  = CORE::int($rem_k / $ai);

            if ($c > 0) {
                $rem_k -= $c * $ai;
                Math::GMPz::Rmpz_mul_ui($t, $p_pow[$i], $c);
                Math::GMPz::Rmpz_add($m, $m, $t);
            }
        }

        # Maximize: S(n) = max( S(p_i^e_i) )
        if (Math::GMPz::Rmpz_cmp($m, $max_m) > 0) {
            Math::GMPz::Rmpz_set($max_m, $m);
        }
    }

    bless \$max_m;
}

*smarandache = \&kempner;

sub factorialmod {
    my ($n, $m) = @_;
    _valid(\$m);

    $n = $$n;
    $m = $$m;

    if (HAS_PRIME_UTIL and _fits_ulong($m) and _fits_ulong($n)) {
        my $r = Math::Prime::Util::factorialmod(_get_ulong($n), _get_ulong($m)) // goto &nan;
        return bless \$r;
    }

    $n = _any2mpz($n, 0) // goto &nan;
    $m = _any2mpz($m, 1) // goto &nan;

    my $r;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($m) and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $r = Math::Prime::Util::factorialmod(Math::GMPz::Rmpz_get_ui($n), Math::GMPz::Rmpz_get_ui($m)) // goto &nan;
    }
    else {
        $r = Math::Prime::Util::GMP::factorialmod(_big2uistr($n) // (goto &nan), _big2uistr($m) // (goto &nan)) // goto &nan;
    }

    _set_int($r);
}

sub double_factorial {
    my ($x) = @_;
    my $ui  = _any2ui($$x) // (goto &nan);
    my $z   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_2fac_ui($z, $ui);
    bless \$z;
}

*dfac       = \&double_factorial;
*dfactorial = \&double_factorial;

sub multi_factorial {
    my ($x, $y) = @_;
    _valid(\$y);
    my $ui1 = _any2ui($$x) // (goto &nan);
    my $ui2 = _any2ui($$y) // (goto &nan);
    my $z   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mfac_uiui($z, $ui1, $ui2);
    bless \$z;
}

*mfac       = \&multi_factorial;
*mfactorial = \&multi_factorial;

#
## falling_factorial(x, +y) = binomial(x, y) * y!
## falling_factorial(x, -y) = 1/falling_factorial(x + y, y)
#

sub falling_factorial {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = _any2mpz($$x, 0) // (goto &nan);
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    my $r = Math::GMPz::Rmpz_init_set($x);

    if ($y < 0) {
        Math::GMPz::Rmpz_add_ui($r, $r, CORE::abs($y));
    }

    Math::GMPz::Rmpz_bin_ui($r, $r, CORE::abs($y));

    Math::GMPz::Rmpz_sgn($r) || do {
        ($y < 0) ? (goto &nan) : (return ZERO);
    };

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_fac_ui($t, CORE::abs($y));
    Math::GMPz::Rmpz_mul($r, $r, $t);

    if ($y < 0) {
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_z($q, $r);
        Math::GMPq::Rmpq_inv($q, $q);
        return bless \$q;
    }

    bless \$r;
}

#
## rising_factorial(x, +y) = binomial(x + y - 1, y) * y!
## rising_factorial(x, -y) = 1/rising_factorial(x - y, y)
#

sub rising_factorial {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = _any2mpz($$x, 0) // (goto &nan);
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    my $r = Math::GMPz::Rmpz_init_set($x);
    Math::GMPz::Rmpz_add_ui($r, $r, CORE::abs($y));
    Math::GMPz::Rmpz_sub_ui($r, $r, 1);

    if ($y < 0) {
        Math::GMPz::Rmpz_sub_ui($r, $r, CORE::abs($y));
    }

    Math::GMPz::Rmpz_bin_ui($r, $r, CORE::abs($y));

    Math::GMPz::Rmpz_sgn($r) || do {
        ($y < 0) ? (goto &nan) : (return ZERO);
    };

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_fac_ui($t, CORE::abs($y));
    Math::GMPz::Rmpz_mul($r, $r, $t);

    if ($y < 0) {
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_z($q, $r);
        Math::GMPq::Rmpq_inv($q, $q);
        return bless \$q;
    }

    bless \$r;
}

sub primorial {
    my ($x) = @_;
    my $ui  = _any2ui($$x) // goto &nan;
    my $z   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_primorial_ui($z, $ui);
    bless \$z;
}

sub primorial_inflation {    # A108951(n)
    my ($n) = @_;

    $n = $$n;

    if (ref($n) eq 'Math::GMPq') {    # handle rational inputs (not very efficient)

        my $num = Math::GMPz::Rmpz_init();
        my $den = Math::GMPz::Rmpz_init();

        Math::GMPq::Rmpq_get_num($num, $n);
        Math::GMPq::Rmpq_get_den($den, $n);

        my $t1 = (bless \$num)->primorial_inflation;
        my $t2 = (bless \$den)->primorial_inflation;

        return $t1->div($t2);
    }

    $n = _big2uistr($n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;
        ($p < ULONG_MAX) || goto &nan;

        my $primorial = ($p <= 1e5) ? _cached_primorial($p, 1e4) : do {
            Math::GMPz::Rmpz_primorial_ui($t, $p);
            $t;
        };

        if ($e > 1) {
            Math::GMPz::Rmpz_pow_ui($t, $primorial, $e);
            push @terms, Math::GMPz::Rmpz_init_set($t);
        }
        else {
            push @terms, Math::GMPz::Rmpz_init_set($primorial);
        }
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub primorial_deflation {    # A319626(n) / A319627(n)
    my ($n) = @_;

    $n = _big2uistr($$n) // goto &nan;

    my @terms;

    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        my $q = ($p <= 2) ? 1 : _prev_prime($p);
        my $t = Math::GMPq::Rmpq_init();

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPq::Rmpq_set_ui($t, $p, $q);
        }
        else {
            Math::GMPq::Rmpq_set_str($t, "$p/$q", 10);
        }

        if ($e > 1) {
            Math::GMPq::Rmpq_pow_ui($t, $t, $e);
        }

        push @terms, $t;
    }

    @terms || return ONE;
    my $prod = _binsplit(\@terms, \&__mul__);

    if (ref($prod) eq 'Math::GMPq' and Math::GMPq::Rmpq_integer_p($prod)) {
        $prod = _mpq2mpz($prod);
    }

    bless \$prod;
}

sub pn_primorial {
    my ($x) = @_;
    _set_int(Math::Prime::Util::GMP::pn_primorial(_any2ui($$x) // goto &nan));
}

sub lucas {
    my ($x) = @_;
    my $ui  = _any2ui($$x) // (goto &nan);
    my $z   = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_lucnum_ui($z, $ui);
    bless \$z;
}

*Lucas = \&lucas;

sub lucasu {
    my ($p, $q, $n) = @_;

    _valid(\$q, \$n);

    $p = _big2istr($$p)  // goto &nan;
    $q = _big2istr($$q)  // goto &nan;
    $n = _big2uistr($$n) // goto &nan;

    _set_int(Math::Prime::Util::GMP::lucasu($p, $q, $n));
}

*lucasU = \&lucasu;
*LucasU = \&lucasu;

sub lucasv {
    my ($p, $q, $n) = @_;

    _valid(\$q, \$n);

    $p = _big2istr($$p)  // goto &nan;
    $q = _big2istr($$q)  // goto &nan;
    $n = _big2uistr($$n) // goto &nan;

    _set_int(Math::Prime::Util::GMP::lucasv($p, $q, $n));
}

*lucasV = \&lucasv;
*LucasV = \&lucasv;

sub pell {
    my ($n) = @_;
    Sidef::Types::Number::Number::lucasu(TWO, MONE, $n);
}

sub pell_lucas {
    my ($n) = @_;
    Sidef::Types::Number::Number::lucasv(TWO, MONE, $n);
}

sub jacobsthal {
    my ($n) = @_;
    state $TWO_NEG = (TWO)->neg;
    Sidef::Types::Number::Number::lucasu(ONE, $TWO_NEG, $n);
}

sub jacobsthal_lucas {
    my ($n) = @_;
    state $TWO_NEG = (TWO)->neg;
    Sidef::Types::Number::Number::lucasv(ONE, $TWO_NEG, $n);
}

sub padovan {
    my ($n) = @_;
    Sidef::Math::Math->linear_rec(_array([ZERO, ONE, ONE]), _array([ONE, ZERO, ZERO]), $n);
}

sub padovanmod {
    my ($n, $m) = @_;
    _valid(\$m);
    Sidef::Math::Math->linear_recmod(_array([ZERO, ONE, ONE]), _array([ONE, ZERO, ZERO]), $n, $m);
}

sub perrin {
    my ($n) = @_;
    Sidef::Math::Math->linear_rec(_array([ZERO, ONE, ONE]), _array([THREE, ZERO, TWO]), $n);
}

sub perrinmod {
    my ($n, $m) = @_;
    _valid(\$m);
    Sidef::Math::Math->linear_recmod(_array([ZERO, ONE, ONE]), _array([THREE, ZERO, TWO]), $n, $m);
}

sub __lucasUVmod__ {
    my ($P, $Q, $n, $m) = @_;

    my $U1 = Math::GMPz::Rmpz_init_set_ui(1);

    my ($V1, $V2) = (Math::GMPz::Rmpz_init_set_ui(2), Math::GMPz::Rmpz_init_set($P));
    my ($Q1, $Q2) = (Math::GMPz::Rmpz_init_set_ui(1), Math::GMPz::Rmpz_init_set_ui(1));

    Math::GMPz::Rmpz_sgn($n) == 0
      and return (Math::GMPz::Rmpz_init_set_ui(0), Math::GMPz::Rmpz_init_set_ui(2));

    my $t = Math::GMPz::Rmpz_init();
    my $s = Math::GMPz::Rmpz_scan1($n, 0);

    Math::GMPz::Rmpz_div_2exp($t, $n, $s + 1);

    foreach my $bit (split(//, Math::GMPz::Rmpz_get_str($t, 2))) {

        Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);
        Math::GMPz::Rmpz_mod($Q1, $Q1, $m);

        if ($bit) {
            Math::GMPz::Rmpz_mul($Q2, $Q1, $Q);
            Math::GMPz::Rmpz_mul($U1, $U1, $V2);
            Math::GMPz::Rmpz_mul($V1, $V1, $V2);

            Math::GMPz::Rmpz_powm_ui($V2, $V2, 2, $m);
            Math::GMPz::Rmpz_submul($V1, $Q1, $P);
            Math::GMPz::Rmpz_submul_ui($V2, $Q2, 2);

            Math::GMPz::Rmpz_mod($V1, $V1, $m);
            Math::GMPz::Rmpz_mod($U1, $U1, $m);
        }
        else {
            Math::GMPz::Rmpz_set($Q2, $Q1);
            Math::GMPz::Rmpz_mul($U1, $U1, $V1);
            Math::GMPz::Rmpz_mul($V2, $V2, $V1);
            Math::GMPz::Rmpz_sub($U1, $U1, $Q1);

            Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $m);
            Math::GMPz::Rmpz_submul($V2, $Q1, $P);
            Math::GMPz::Rmpz_submul_ui($V1, $Q2, 2);

            Math::GMPz::Rmpz_mod($V2, $V2, $m);
            Math::GMPz::Rmpz_mod($U1, $U1, $m);
        }
    }

    Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);
    Math::GMPz::Rmpz_mul($Q2, $Q1, $Q);
    Math::GMPz::Rmpz_mul($U1, $U1, $V1);
    Math::GMPz::Rmpz_mul($V1, $V1, $V2);
    Math::GMPz::Rmpz_sub($U1, $U1, $Q1);
    Math::GMPz::Rmpz_submul($V1, $Q1, $P);
    Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);

    for (1 .. $s) {
        Math::GMPz::Rmpz_mul($U1, $U1, $V1);
        Math::GMPz::Rmpz_mod($U1, $U1, $m);
        Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $m);
        Math::GMPz::Rmpz_submul_ui($V1, $Q1, 2);
        Math::GMPz::Rmpz_powm_ui($Q1, $Q1, 2, $m);
    }

    Math::GMPz::Rmpz_mod($U1, $U1, $m);
    Math::GMPz::Rmpz_mod($V1, $V1, $m);

    return ($U1, $V1);
}

sub __lucasVmod__ {
    my ($P, $Q, $n, $m) = @_;

    my ($V1, $V2) = (Math::GMPz::Rmpz_init_set_ui(2), Math::GMPz::Rmpz_init_set($P));
    my ($Q1, $Q2) = (Math::GMPz::Rmpz_init_set_ui(1), Math::GMPz::Rmpz_init_set_ui(1));

    foreach my $bit (split(//, Math::GMPz::Rmpz_get_str($n, 2))) {

        Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);
        Math::GMPz::Rmpz_mod($Q1, $Q1, $m);

        if ($bit) {
            Math::GMPz::Rmpz_mul($Q2, $Q1, $Q);
            Math::GMPz::Rmpz_mul($V1, $V1, $V2);
            Math::GMPz::Rmpz_powm_ui($V2, $V2, 2, $m);
            Math::GMPz::Rmpz_submul($V1, $P, $Q1);
            Math::GMPz::Rmpz_submul_ui($V2, $Q2, 2);
            Math::GMPz::Rmpz_mod($V1, $V1, $m);
        }
        else {
            Math::GMPz::Rmpz_set($Q2, $Q1);
            Math::GMPz::Rmpz_mul($V2, $V2, $V1);
            Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $m);
            Math::GMPz::Rmpz_submul($V2, $P, $Q1);
            Math::GMPz::Rmpz_submul_ui($V1, $Q2, 2);
            Math::GMPz::Rmpz_mod($V2, $V2, $m);
        }
    }

    Math::GMPz::Rmpz_mod($V1, $V1, $m);

    return ($V1, $V2);
}

sub _modular_lucas_UV {
    my ($P, $Q, $n, $m) = @_;

    if (    (ref($P) ? (Math::GMPz::Rmpz_cmpabs_ui($P, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($P) < $LUCAS_PQ_LIMIT))
        and (ref($Q) ? (Math::GMPz::Rmpz_cmpabs_ui($Q, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($Q) < $LUCAS_PQ_LIMIT))) {
        my ($U, $V);
        if (    FAST_MODE
            and HAS_PRIME_UTIL
            and (!ref($m) or Math::GMPz::Rmpz_fits_ulong_p($m))
            and (!ref($n) or Math::GMPz::Rmpz_fits_ulong_p($n))) {
            eval {
                ($U, $V) =
                  Math::Prime::Util::lucas_sequence(
                                                    (ref($m) ? Math::GMPz::Rmpz_get_ui($m) : $m),
                                                    (ref($P) ? Math::GMPz::Rmpz_get_si($P) : $P),
                                                    (ref($Q) ? Math::GMPz::Rmpz_get_si($Q) : $Q),
                                                    (ref($n) ? Math::GMPz::Rmpz_get_ui($n) : $n)
                                                   );
            };
        }
        else {
            eval { ($U, $V) = Math::Prime::Util::GMP::lucas_sequence($m, $P, $Q, $n) };
        }
        defined($U) && defined($V) && return (map { _str2obj($_) } ($U, $V));
    }

    $P = _any2mpz($P, 0) if !ref($P);
    $Q = _any2mpz($Q, 1) if !ref($Q);
    $n = _any2mpz($n, 2) if !ref($n);
    $m = _any2mpz($m, 3) if !ref($m);

    state $D = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_mul($D, $P, $P);
    Math::GMPz::Rmpz_submul_ui($D, $Q, 4);

    # When `gcd(P*P - 4*Q, m) = 1`, we can use a faster algorithm
    if (Math::GMPz::Rmpz_invert($D, $D, $m)) {

        my ($V1, $V2) = __lucasVmod__($P, $Q, $n, $m);

        Math::GMPz::Rmpz_mul_2exp($V2, $V2, 1);
        Math::GMPz::Rmpz_submul($V2, $V1, $P);
        Math::GMPz::Rmpz_mul($V2, $V2, $D);
        Math::GMPz::Rmpz_mod($V2, $V2, $m);

        return ($V2, $V1);
    }

    __lucasUVmod__($P, $Q, $n, $m);
}

sub _modular_lucas_U {
    my ($P, $Q, $n, $m) = @_;

    if (    (ref($P) ? (Math::GMPz::Rmpz_cmpabs_ui($P, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($P) < $LUCAS_PQ_LIMIT))
        and (ref($Q) ? (Math::GMPz::Rmpz_cmpabs_ui($Q, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($Q) < $LUCAS_PQ_LIMIT))) {
        my ($U, $V);
        if (    FAST_MODE
            and HAS_PRIME_UTIL
            and (!ref($m) or Math::GMPz::Rmpz_fits_ulong_p($m))
            and (!ref($n) or Math::GMPz::Rmpz_fits_ulong_p($n))) {
            if (HAS_PRIME_UTIL) {
                eval {
                    $U =
                      Math::Prime::Util::lucasumod(
                                                   (ref($P) ? Math::GMPz::Rmpz_get_si($P) : $P),
                                                   (ref($Q) ? Math::GMPz::Rmpz_get_si($Q) : $Q),
                                                   (ref($n) ? Math::GMPz::Rmpz_get_ui($n) : $n),
                                                   (ref($m) ? Math::GMPz::Rmpz_get_ui($m) : $m),
                                                  );
                };
            }
            else {
                eval {
                    ($U, $V) =
                      Math::Prime::Util::lucas_sequence(
                                                        (ref($m) ? Math::GMPz::Rmpz_get_ui($m) : $m),
                                                        (ref($P) ? Math::GMPz::Rmpz_get_si($P) : $P),
                                                        (ref($Q) ? Math::GMPz::Rmpz_get_si($Q) : $Q),
                                                        (ref($n) ? Math::GMPz::Rmpz_get_ui($n) : $n),
                                                       );
                };
            }
        }
        else {
            eval { $U = Math::Prime::Util::GMP::lucasumod($P, $Q, $n, $m) };
        }
        defined($U) && return _str2obj($U);
    }

    $P = _any2mpz($P, 0) if !ref($P);
    $Q = _any2mpz($Q, 1) if !ref($Q);
    $n = _any2mpz($n, 2) if !ref($n);
    $m = _any2mpz($m, 3) if !ref($m);

    state $D = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_mul($D, $P, $P);
    Math::GMPz::Rmpz_submul_ui($D, $Q, 4);

    # When `gcd(P*P - 4*Q, m) = 1`, we can use a faster algorithm
    if (Math::GMPz::Rmpz_invert($D, $D, $m)) {

        my ($V1, $V2) = __lucasVmod__($P, $Q, $n, $m);

        Math::GMPz::Rmpz_mul_2exp($V2, $V2, 1);
        Math::GMPz::Rmpz_submul($V2, $V1, $P);
        Math::GMPz::Rmpz_mul($V2, $V2, $D);
        Math::GMPz::Rmpz_mod($V2, $V2, $m);

        return $V2;
    }

    (__lucasUVmod__($P, $Q, $n, $m))[0];
}

sub _modular_lucas_V {
    my ($P, $Q, $n, $m) = @_;

    if (    (ref($P) ? (Math::GMPz::Rmpz_cmpabs_ui($P, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($P) < $LUCAS_PQ_LIMIT))
        and (ref($Q) ? (Math::GMPz::Rmpz_cmpabs_ui($Q, $LUCAS_PQ_LIMIT) < 0) : (CORE::abs($Q) < $LUCAS_PQ_LIMIT))) {
        my ($U, $V);
        if (    FAST_MODE
            and HAS_PRIME_UTIL
            and (!ref($m) or Math::GMPz::Rmpz_fits_ulong_p($m))
            and (!ref($n) or Math::GMPz::Rmpz_fits_ulong_p($n))) {
            if (HAS_PRIME_UTIL) {
                eval {
                    $V =
                      Math::Prime::Util::lucasvmod(
                                                   (ref($P) ? Math::GMPz::Rmpz_get_si($P) : $P),
                                                   (ref($Q) ? Math::GMPz::Rmpz_get_si($Q) : $Q),
                                                   (ref($n) ? Math::GMPz::Rmpz_get_ui($n) : $n),
                                                   (ref($m) ? Math::GMPz::Rmpz_get_ui($m) : $m),
                                                  );
                };
            }
            else {
                eval {
                    ($U, $V) =
                      Math::Prime::Util::lucas_sequence(
                                                        (ref($m) ? Math::GMPz::Rmpz_get_ui($m) : $m),
                                                        (ref($P) ? Math::GMPz::Rmpz_get_si($P) : $P),
                                                        (ref($Q) ? Math::GMPz::Rmpz_get_si($Q) : $Q),
                                                        (ref($n) ? Math::GMPz::Rmpz_get_ui($n) : $n),
                                                       );
                };
            }
        }
        else {
            eval { $V = Math::Prime::Util::GMP::lucasvmod($P, $Q, $n, $m) };
        }
        defined($V) && return _str2obj($V);
    }

    $P = _any2mpz($P, 0) if !ref($P);
    $Q = _any2mpz($Q, 1) if !ref($Q);
    $n = _any2mpz($n, 2) if !ref($n);
    $m = _any2mpz($m, 3) if !ref($m);

    (__lucasVmod__($P, $Q, $n, $m))[0];
}

sub lucasumod {
    my ($P, $Q, $n, $m) = @_;

    _valid(\$Q, \$n, \$m);

    $P = _any2mpz($$P, 0) // goto &nan;
    $Q = _any2mpz($$Q, 1) // goto &nan;
    $n = _any2mpz($$n, 2) // goto &nan;
    $m = _any2mpz($$m, 3) // goto &nan;

    # undefined for m=0
    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    # U_0(P, Q) = 0
    Math::GMPz::Rmpz_sgn($n) || return ZERO;

    # undefined for n < 0
    Math::GMPz::Rmpz_sgn($n) < 0 && goto &nan;

    bless \_modular_lucas_U($P, $Q, $n, $m);
}

*LucasUmod = \&lucasumod;
*lucasUmod = \&lucasumod;

sub lucasvmod {
    my ($P, $Q, $n, $m) = @_;

    _valid(\$Q, \$n, \$m);

    $P = _any2mpz($$P, 0) // goto &nan;
    $Q = _any2mpz($$Q, 1) // goto &nan;
    $n = _any2mpz($$n, 2) // goto &nan;
    $m = _any2mpz($$m, 3) // goto &nan;

    # undefined for m=0
    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    # undefined for n < 0
    Math::GMPz::Rmpz_sgn($n) < 0 && goto &nan;

    bless \_modular_lucas_V($P, $Q, $n, $m);
}

*LucasVmod = \&lucasvmod;
*lucasVmod = \&lucasvmod;

sub lucasuvmod {
    my ($P, $Q, $n, $m) = @_;

    _valid(\$Q, \$n, \$m);

    $P = _any2mpz($$P, 0) // goto &nan;
    $Q = _any2mpz($$Q, 1) // goto &nan;
    $n = _any2mpz($$n, 2) // goto &nan;
    $m = _any2mpz($$m, 3) // goto &nan;

    # undefined for m=0
    Math::GMPz::Rmpz_sgn($m) || return (nan(), nan());

    # undefined for n < 0
    Math::GMPz::Rmpz_sgn($n) < 0 && return (nan(), nan());

    my ($U, $V) = _modular_lucas_UV($P, $Q, $n, $m);

    ((bless \$U), (bless \$V));
}

*LucasUVmod = \&lucasuvmod;
*lucasUVmod = \&lucasuvmod;

#
## Chebyshev polynomials: T_n(x)
#

sub chebyshevt {
    my ($n, $x) = @_;

    _valid(\$n);

    $n = $$n;
    $n = _any2si($n) // (goto &nan) if ref($n);
    $n = -$n                        if $n < 0;
    $n == 0 and return ONE;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
    }

    $n == 1 and return $x;

    if (ref($x) eq __PACKAGE__) {
        if (ref($$x) eq 'Math::GMPz' or (__is_rat__($$x) and __is_int__($$x))) {
            return _set_int(Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::lucasv(2 * $$x, 1, $n), 2));
        }
    }

    # T_n(x) = 1/2 * ((x - sqrt(x^2 - 1))^n + (x + sqrt(x^2 - 1))^n)

    my $e = _set_int($n);
    my $Q = Sidef::Types::Number::Quadratic->new(ZERO, ONE, $x->mul($x)->dec);
    my $r = $Q->neg->add($x)->pow($e);

    $r->a;
}

*chebyshevT = \&chebyshevt;
*ChebyshevT = \&chebyshevt;

#
## Chebyshev polynomials: U_n(x)
#

sub chebyshevu {
    my ($n, $x) = @_;

    _valid(\$n);

    $n = $$n;
    $n = _any2si($n) // (goto &nan) if ref($n);
    $n == 0 and return ONE;

    my $negative = 0;

    if ($n < 0) {

        $n == -1 and return ZERO;
        $n == -2 and return MONE;

        $n        = -$n - 2;
        $negative = 1;
    }

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
    }

    if (ref($x) eq __PACKAGE__) {
        if (ref($$x) eq 'Math::GMPz' or (__is_rat__($$x) and __is_int__($$x))) {
            my $r = _set_int(Math::Prime::Util::GMP::lucasu(2 * $$x, 1, $n + 1));
            $r = $r->neg if $negative;
            return $r;
        }
    }

    # U_n(x) = ((x + sqrt(x^2 - 1))^(n+1) - (x - sqrt(x^2 - 1))^(n+1)) / (2 * sqrt(x^2 - 1))

    my $e = _set_int($n + 1);
    my $Q = Sidef::Types::Number::Quadratic->new(ZERO, ONE, $x->mul($x)->dec);

    my $r = $Q->add($x)->pow($e)->b;
    $r = $r->neg if $negative;
    $r;
}

*ChebyshevU = \&chebyshevu;
*chebyshevU = \&chebyshevu;

#
## Modular Chebyshev polynomials: T_n(x) mod m
#

sub chebyshevTmod {
    my ($n, $x, $m) = @_;

    _valid(\$n, \$x, \$m);

    $n = _any2mpz($$n, 5) // goto &nan;
    $x = $$x;
    $m = _any2mpz($$m, 6) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);    # copy
        Math::GMPz::Rmpz_abs($n, $n);
    }

    if (Math::GMPz::Rmpz_odd_p($m) and (ref($x) eq 'Math::GMPz' or (__is_rat__($x) and __is_int__($x)))) {
        return _set_int(2 * $x)->lucasVmod(ONE, (bless \$n), (bless \$m))->divmod(TWO, (bless \$m));
    }

    # T_n(x) = 1/2 * ((x - sqrt(x^2 - 1))^n + (x + sqrt(x^2 - 1))^n)

    my $Q = Sidef::Types::Number::Quadratic->new(ZERO, ONE, bless \__dec__(__mul__($x, $x)));
    my $r = ((bless \$x)->sub($Q))->powmod((bless \$n), (bless \$m));

    $r->a->mod(bless \$m);
}

#
## Modular Chebyshev polynomials: U_n(x) mod m
#

sub chebyshevUmod {
    my ($n, $x, $m) = @_;

    _valid(\$x, \$m);

    $n = _any2mpz($$n, 5) // goto &nan;
    $x = $$x;
    $m = _any2mpz($$m, 6) // goto &nan;

    my $negative = 0;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {

        if (Math::GMPz::Rmpz_cmp_si($n, -1) == 0) {
            return ((ZERO)->mod(bless \$m));
        }

        if (Math::GMPz::Rmpz_cmp_si($n, -2) == 0) {
            return ((MONE)->mod(bless \$m));
        }

        $n        = -$n - 2;
        $negative = 1;
    }

    if (ref($x) eq 'Math::GMPz' or (__is_rat__($x) and __is_int__($x))) {
        my $r = _set_int(2 * $x)->lucasUmod(ONE, (bless \$n)->inc, (bless \$m));
        $r = $r->neg->mod(bless \$m) if $negative;
        return $r;
    }

    # U_n(x) = ((x + sqrt(x^2 - 1))^(n+1) - (x - sqrt(x^2 - 1))^(n+1)) / (2 * sqrt(x^2 - 1))

    my $Q = Sidef::Types::Number::Quadratic->new(ZERO, ONE, bless \__dec__(__mul__($x, $x)));
    my $r = ((bless \$x)->add($Q))->powmod((bless \$n)->inc, (bless \$m))->b;

    $r = $r->neg if $negative;
    $r->mod(bless \$m);
}

#
## Legendre polynomials: P_n(x)
#

sub legendre_polynomial {
    my ($n, $x) = @_;

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
        $polynomial = 1;
    }

    $n = _any2ui($$n) // goto &nan;

    $n == 0 && return ONE;
    $n == 1 && return $x;

    if ($polynomial and $n >= 15 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("pollegendre($n)")) {
            return Sidef::Types::Number::Polynomial->new(Sidef::Types::String::String->new($res));
        }
    }

    my ($x1, $x2) = ($x->dec, $x->inc);

    if (!$polynomial) {
        $x1 = $$x1;
        $x2 = $$x2;
    }

    my $t = Math::GMPz::Rmpz_init();
    my $w = Math::GMPz::Rmpz_init_set_ui(1);

    my @terms;
    foreach my $k (0 .. $n) {

        Math::GMPz::Rmpz_mul($t, $w, $w);

        if ($polynomial) {
            push @terms, $x1->pow(_set_int($n - $k))->mul($x2->pow(_set_int($k)))->mul(bless \$t);
        }
        else {
            push @terms, __mul__(__mul__(__pow__($x1, $n - $k), __pow__($x2, $k)), $t);
        }

        Math::GMPz::Rmpz_mul_ui($w, $w, $n - $k);
        Math::GMPz::Rmpz_divexact_ui($w, $w, $k + 1);
    }

    if ($polynomial) {
        my $sum = _binsplit(\@terms, \&Sidef::Types::Number::Polynomial::add);
        Math::GMPz::Rmpz_set_ui($t, 0);
        Math::GMPz::Rmpz_setbit($t, $n);
        return $sum->div(bless \$t);
    }

    my $sum = _binsplit(\@terms, \&__add__);
    Math::GMPz::Rmpz_set_ui($t, 0);
    Math::GMPz::Rmpz_setbit($t, $n);
    bless \__div__($sum, $t);
}

*LegendreP = \&legendre_polynomial;
*legendrep = \&legendre_polynomial;
*legendreP = \&legendre_polynomial;

#
## The physicists' Hermite polynomials H_n(x)
#

sub hermiteH {
    my ($n, $x) = @_;

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
        $polynomial = 1;
    }

    $n = _any2ui($$n) // goto &nan;

    if ($polynomial and $n >= 250 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("polhermite($n)")) {
            return Sidef::Types::Number::Polynomial->new(Sidef::Types::String::String->new($res));
        }
    }

    $n == 0 && return ONE;
    $x = $x->add($x);
    $n == 1 && return $x;

    my $t = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init_set_ui(1);

    my $v = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_fac_ui($v, $n);

    my @terms;
    foreach my $m (0 .. $n >> 1) {

        Math::GMPz::Rmpz_mul($t, $v, $u);
        Math::GMPz::Rmpz_neg($t, $t) if ($m & 1);

        if ($polynomial) {
            push @terms, $x->pow(_set_int($n - ($m << 1)))->div(bless \$t);
        }
        else {
            push @terms, __div__(__pow__($$x, $n - ($m << 1)), $t);
        }

        my $d = ($n - ($m << 1)) * ($n - ($m << 1) - 1);
        Math::GMPz::Rmpz_divexact_ui($v, $v, $d) if $d;
        Math::GMPz::Rmpz_mul_ui($u, $u, $m + 1);
    }

    Math::GMPz::Rmpz_fac_ui($v, $n);

    if ($polynomial) {
        my $sum = _binsplit(\@terms, \&Sidef::Types::Number::Polynomial::add);
        return $sum->mul(bless \$v);
    }

    my $sum = _binsplit(\@terms, \&__add__);
    bless \__mul__($sum, $v);
}

*HermiteH            = \&hermiteH;
*hermite_polynomialH = \&hermiteH;

#
## The probabilists' Hermite polynomials He_n(x)
#

sub hermiteHe {
    my ($n, $x) = @_;

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
        $polynomial = 1;
    }

    $n = _any2ui($$n) // goto &nan;

    $n == 0 && return ONE;
    $n == 1 && return $x;

    my $t = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init_set_ui(1);

    my $v = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_fac_ui($v, $n);

    my @terms;
    foreach my $m (0 .. $n >> 1) {

        Math::GMPz::Rmpz_mul($t, $v, $u);
        Math::GMPz::Rmpz_mul_2exp($t, $t, $m);
        Math::GMPz::Rmpz_neg($t, $t) if ($m & 1);

        if ($polynomial) {
            push @terms, $x->pow(_set_int($n - ($m << 1)))->div(bless \$t);
        }
        else {
            push @terms, __div__(__pow__($$x, $n - ($m << 1)), $t);
        }

        my $d = ($n - ($m << 1)) * ($n - ($m << 1) - 1);
        Math::GMPz::Rmpz_divexact_ui($v, $v, $d) if $d;
        Math::GMPz::Rmpz_mul_ui($u, $u, $m + 1);
    }

    Math::GMPz::Rmpz_fac_ui($v, $n);

    if ($polynomial) {
        my $sum = _binsplit(\@terms, \&Sidef::Types::Number::Polynomial::add);
        return $sum->mul(bless \$v);
    }

    my $sum = _binsplit(\@terms, \&__add__);
    bless \__mul__($sum, $v);
}

*HermiteHe            = \&hermiteHe;
*hermite_polynomialHe = \&hermiteHe;

#
## Laguerre polynomials: L_n(x)
#

sub laguerreL {
    my ($n, $x) = @_;

    my $polynomial = 0;

    if (defined($x) and ref($x) ne 'Sidef::Types::Number::Polynomial') {
        _valid(\$x);
    }
    else {
        $x //= Sidef::Types::Number::Polynomial->new(1 => ONE);
        $polynomial = 1;
    }

    $n = _any2ui($$n) // goto &nan;
    $n || return ONE;

    my $t = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init_set_ui(1);
    my $w = Math::GMPz::Rmpz_init_set_ui(1);

    my @terms;
    foreach my $k (0 .. $n) {

        if ($k & 1) {
            Math::GMPz::Rmpz_neg($t, $w);
        }
        else {
            Math::GMPz::Rmpz_set($t, $w);
        }

        if ($polynomial) {
            push @terms, $x->pow(_set_int($k))->mul(bless \$t)->div(bless \$u);
        }
        else {
            push @terms, __div__(__mul__(__pow__($$x, $k), $t), $u);
        }

        Math::GMPz::Rmpz_mul_ui($u, $u, $k + 1);
        Math::GMPz::Rmpz_mul_ui($w, $w, $n - $k);
        Math::GMPz::Rmpz_divexact_ui($w, $w, $k + 1);
    }

    $polynomial
      ? _binsplit(\@terms, \&Sidef::Types::Number::Polynomial::add)
      : (bless \_binsplit(\@terms, \&__add__));
}

*laguerre            = \&laguerreL;
*Laguerre            = \&laguerreL;
*LaguerreL           = \&laguerreL;
*laguerre_polynomial = \&laguerreL;

sub fibonaccimod {
    my ($n, $k, $m) = @_;

    if (defined($m)) {
        _valid(\$k, \$m);

        $k = _any2ui($$k) // (goto &nan);

        if ($k == 2) {
            return $n->fibmod($m);
        }

        $n = _any2mpz($$n, 0) // goto &nan;
        $m = _any2mpz($$m, 1) // goto &nan;

        Math::GMPz::Rmpz_sgn($n) >= 0 or goto &nan;
        Math::GMPz::Rmpz_sgn($m) > 0  or goto &nan;

        $k < 0 and goto &nan;

        if ($k == 0) {
            $k = 1;
        }

        if (    1e3 * $k * $k < ULONG_MAX
            and Math::GMPz::Rmpz_fits_ulong_p($n)
            and Math::GMPz::Rmpz_cmp_ui($n, 1e3 * $k * $k) <= 0) {

            $n = Math::GMPz::Rmpz_get_ui($n);

            if ($n < $k - 1) {
                return ZERO;
            }

            # Algorithm due to M. F. Hasler, running in linear time with respect to n.
            # From: https://oeis.org/A302990

            my @f = map {
                ($_ < $k)
                  ? do {
                    my $z = Math::GMPz::Rmpz_init_set_ui(2);
                    Math::GMPz::Rmpz_powm_ui($z, $z, $_, $m);
                    $z;
                  }
                  : Math::GMPz::Rmpz_init_set_ui(1)
            } 1 .. ($k + 1);

            my $t = Math::GMPz::Rmpz_init();

            foreach my $i (2 * ++$k - 2 .. $n) {
                Math::GMPz::Rmpz_mul_2exp($t, $f[($i - 1) % $k], 1);
                Math::GMPz::Rmpz_sub($f[$i % $k], $t, $f[$i % $k]);
                Math::GMPz::Rmpz_mod($f[$i % $k], $f[$i % $k], $m);
            }

            my $r = $f[$n % $k];
            return bless \$r;
        }

        # Sublinear algorithm with time-complexity based on k.
        return Sidef::Math::Math->linear_recmod(_array([(ONE) x $k]), _array([(ZERO) x ($k - 1), ONE]), (bless \$n), (bless \$m));
    }
    else {
        _valid(\$k);
        $m = $k;
    }

    $n = _any2mpz($$n, 0) // goto &nan;
    $m = _any2mpz($$m, 1) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;
    Math::GMPz::Rmpz_sgn($m) > 0 or goto &nan;

    Math::GMPz::Rmpz_cmp_ui($m, 1) == 0 and return ZERO;

    bless \_modular_lucas_U(1, -1, $n, $m);
}

*fibmod        = \&fibonaccimod;
*fibonacci_mod = \&fibonaccimod;
*FibonacciMod  = \&fibonaccimod;

sub lucasmod {
    my ($n, $m) = @_;
    _valid(\$m);

    $n = _big2uistr($$n) // goto &nan;
    $m = _big2pistr($$m) // goto &nan;

    return ZERO if $m eq '1';

    my (undef, $r) = Math::Prime::Util::GMP::lucas_sequence($m, 1, -1, $n);
    _set_int($r);
}

*lucas_mod = \&lucasmod;
*LucasMod  = \&lucasmod;

sub fibonacci {
    my ($n, $k) = @_;

    $n = _any2ui($$n) // (goto &nan);

    if (defined($k)) {
        _valid(\$k);

        $k = _any2ui($$k) // (goto &nan);

        if ($k == 2) {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_fib_ui($z, $n);
            return bless \$z;
        }

        if ($n < $k - 1) {
            return ZERO;
        }

        state $crosspoints = {
                              3  => 6144,
                              4  => 14911,
                              5  => 32767,
                              6  => 65535,
                              7  => 98304,
                              8  => 196607,
                              9  => 339967,
                              10 => 465843,
                              11 => 729595,
                             };

        if ($n >= 1e6 and !exists($crosspoints->{$k}) and $k <= 50) {

            my $w = 0.0834627296565757;
            my $x = 1.07324642799115;
            my $y = 6.39808475499695;
            my $z = -6.14170531832506;

            my ($f1, $f2, $f3, $f4) = (@{$crosspoints}{qw(3 4 5 6)});

            for (1 .. ($k - 6)) {
                ($f1, $f2, $f3, $f4) = ($f2, $f3, $f4, $f1 * $z + $f2 * $y + $f3 * $x + $f4 * $w);
            }

            $crosspoints->{$k} = CORE::abs(CORE::int($f4));
        }

        # Use a sublinear algorithm, when it's faster, with time-complexity based on k.
        if (exists($crosspoints->{$k}) and $n > $crosspoints->{$k}) {
            return Sidef::Math::Math->linear_rec(_array([(ONE) x $k]), _array([(ZERO) x ($k - 1), ONE]), (bless \$n));
        }

        # Algorithm due to M. F. Hasler, running in linear time with respect to n.
        # From: https://oeis.org/A302990

        my @f = map {
            ($_ < $k)
              ? do {
                my $z = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_setbit($z, $_);
                $z;
              }
              : Math::GMPz::Rmpz_init_set_ui(1)
        } 1 .. ($k + 1);

        my $t = Math::GMPz::Rmpz_init();

        foreach my $i (2 * ++$k - 2 .. $n) {
            Math::GMPz::Rmpz_mul_2exp($t, $f[($i - 1) % $k], 1);
            Math::GMPz::Rmpz_sub($f[$i % $k], $t, $f[$i % $k]);
        }

        my $r = $f[$n % $k];
        return bless \$r;
    }

    my $z = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_fib_ui($z, $n);
    bless \$z;
}

*fib       = \&fibonacci;
*Fibonacci = \&fibonacci;

sub tribonacci {
    my ($n) = @_;
    $n->fibonacci(THREE);
}

sub tetranacci {
    my ($n) = @_;
    $n->fibonacci(FOUR);
}

sub fibbinary {    # A003714: Fibbinary numbers
    my ($n) = @_;

    $n = _any2mpz($$n, 0);
    $n = Math::GMPz::Rmpz_init_set($n);    # copy

    my $s    = Math::GMPz::Rmpz_init_set_ui(0);
    my @list = (Math::GMPz::Rmpz_init_set_ui(2), Math::GMPz::Rmpz_init_set_ui(1));

    while (1) {
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_add($t, $list[0], $list[1]);
        last if (Math::GMPz::Rmpz_cmp($t, $n) > 0);
        unshift @list, $t;
    }

    foreach my $d (@list) {
        Math::GMPz::Rmpz_mul_2exp($s, $s, 1);
        if (Math::GMPz::Rmpz_cmp($d, $n) <= 0) {
            Math::GMPz::Rmpz_add_ui($s, $s, 1);
            Math::GMPz::Rmpz_sub($n, $n, $d);
        }
    }

    bless \$s;
}

sub fibonorial {
    my ($n) = @_;

    $n = _any2ui($$n) // goto &nan;

    return ONE if ($n <= 2);

    my $x = Math::GMPz::Rmpz_init_set_ui(1);
    my $y = Math::GMPz::Rmpz_init_set_ui(1);
    my $t = Math::GMPz::Rmpz_init();

    my @terms;
    foreach my $k (1 .. $n - 2) {
        Math::GMPz::Rmpz_set($t, $y);
        Math::GMPz::Rmpz_add($y, $y, $x);
        Math::GMPz::Rmpz_set($x, $t);
        push @terms, Math::GMPz::Rmpz_init_set($y);
    }

    bless \_binsplit(\@terms, \&__mul__);
}

sub pisano_period {
    my ($n) = @_;

    # Assumes that Wall-Sun-Sun primes do not exist.

    $n = $$n;

    if (ref($n)) {
        $n = _any2mpz($n, 0) // goto &nan;
        Math::GMPz::Rmpz_sgn($n) <= 0 and return ZERO;
        Math::GMPz::Rmpz_cmp_ui($n, 1) == 0 and return ONE;
    }
    else {
        $n <= 0 and return ZERO;
        $n == 1 and return ONE;
    }

    my $prime_power_period = sub {
        my ($p, $e) = @_;
        foreach my $d (_divisors(Math::Prime::Util::GMP::subint($p, Math::Prime::Util::GMP::kronecker(5, $p)))) {
            if (_modular_lucas_U(1, -1, $d, $p) == 0) {
                $e == 1 and return $d;
                return Math::Prime::Util::GMP::mulint(Math::Prime::Util::GMP::powint($p, $e - 1), $d);
            }
        }
        die "Conjecture disproved for prime power: $p^$e";
    };

    my $d = Math::GMPz::Rmpz_init_set_str(Math::Prime::Util::GMP::lcm(map { $prime_power_period->($_->[0], $_->[1]) } _factor_exp($n)), 10);

    foreach my $k (0 .. 2) {

        Math::GMPz::Rmpz_add_ui($d, $d, 1);

        if (_modular_lucas_U(1, -1, $d, $n) == 1) {
            Math::GMPz::Rmpz_sub_ui($d, $d, 1);
            return bless \$d;
        }

        Math::GMPz::Rmpz_sub_ui($d, $d, 1);
        Math::GMPz::Rmpz_mul_2exp($d, $d, 1);
    }

    die "Conjecture disproved for n = $n";
}

sub motzkin {    # OEIS: A001006
    my ($n) = @_;

    $n = 1 + (_any2ui($$n) // (goto &nan));

    my $x = Math::GMPz::Rmpz_init_set_ui(0);
    my $y = Math::GMPz::Rmpz_init_set_ui(1);

    state $t = Math::GMPz::Rmpz_init_nobless();

    # Algorithm due to Peter Luschny, May 16 2016.
    foreach my $k (2 .. $n) {
        Math::GMPz::Rmpz_mul_ui($x, $x, 3 * $k * ($k - 1));
        Math::GMPz::Rmpz_mul_ui($t, $y, $k * (2 * $k - 1));
        Math::GMPz::Rmpz_add($x, $x, $t);
        Math::GMPz::Rmpz_divexact_ui($x, $x, ($k + 1) * ($k - 1));
        ($x, $y) = ($y, $x);
    }

    Math::GMPz::Rmpz_divexact_ui($y, $y, $n);
    bless \$y;
}

sub stirling {
    my ($x, $y) = @_;
    _valid(\$y);
    _set_int(Math::Prime::Util::GMP::stirling(_big2uistr($$x) // (goto &nan), _big2uistr($$y) // (goto &nan)));
}

*Stirling  = \&stirling;
*stirling1 = \&stirling;
*Stirling1 = \&stirling;

sub stirling2 {
    my ($x, $y) = @_;
    _valid(\$y);
    _set_int(Math::Prime::Util::GMP::stirling(_big2uistr($$x) // (goto &nan), _big2uistr($$y) // (goto &nan), 2));
}

*Stirling2 = \&stirling2;

sub stirling3 {
    my ($x, $y) = @_;
    _valid(\$y);
    _set_int(Math::Prime::Util::GMP::stirling(_big2uistr($$x) // (goto &nan), _big2uistr($$y) // (goto &nan), 3));
}

*Stirling3 = \&stirling3;

sub bell {
    my ($x) = @_;
    my $n = _any2ui($$x) // goto &nan;

    if ($n < 100) {
        return _set_int(Math::Prime::Util::GMP::vecsum(map { Math::Prime::Util::GMP::stirling($n, $_, 2) } 0 .. $n));
    }

    my @acc;

    my $t    = Math::GMPz::Rmpz_init();
    my $bell = Math::GMPz::Rmpz_init_set_ui(1);

    foreach my $k (1 .. $n) {

        Math::GMPz::Rmpz_set($t, $bell);

        foreach my $item (@acc) {
            Math::GMPz::Rmpz_add($t, $t, $item);
            Math::GMPz::Rmpz_set($item, $t);
        }

        unshift @acc, $bell;
        $bell = Math::GMPz::Rmpz_init_set($acc[-1]);
    }

    bless \$bell;
}

*bell_number = \&bell;
*Bell        = \&bell;

sub bellmod {
    my ($n, $m) = @_;

    # TODO: find a faster method.

    _valid(\$m);

    $n = _any2ui($$n)     // goto &nan;
    $m = _any2mpz($$m, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    # For small n, it's faster to just use bell(n) % m
    if ($n < 1000) {
        return $_[0]->bell->mod($_[1]);
    }

    my @acc;

    my $t    = Math::GMPz::Rmpz_init();
    my $bell = Math::GMPz::Rmpz_init_set_ui(1);

    my $native_m = 0;

    if (Math::GMPz::Rmpz_fits_ulong_p($m)) {
        $m        = Math::GMPz::Rmpz_get_ui($m);
        $native_m = 1;
    }

    foreach my $k (1 .. $n) {

        Math::GMPz::Rmpz_set($t, $bell);

        foreach my $item (@acc) {
            Math::GMPz::Rmpz_add($t, $t, $item);
            $native_m
              ? Math::GMPz::Rmpz_mod_ui($t, $t, $m)
              : Math::GMPz::Rmpz_mod($t, $t, $m);
            Math::GMPz::Rmpz_set($item, $t);
        }

        unshift @acc, Math::GMPz::Rmpz_init_set($bell);
        $bell = Math::GMPz::Rmpz_init_set($acc[-1]);
    }

    bless \$bell;
}

*Bellmod = \&bellmod;

sub quadratic_formula {
    my ($A, $B, $C) = @_;

    $A //= ZERO;
    $B //= ZERO;
    $C //= ZERO;

    _valid(\$B, \$C);

    $A = $$A;
    $B = $$B;
    $C = $$C;

    #
    ## (-b ± sqrt(b^2 - 4ac)) / (2a)
    #

    my $u = __mul__($B,              $B);                # b^2
    my $t = __mul__(__mul__($A, $C), 4);                 # 4ac
    my $s = __sqrt__(_any2mpfr_mpc(__sub__($u, $t)));    # sqrt(b^2 - 4ac)

    my $n1 = __sub__($s, $B);                            #   sqrt(b^2 - 4ac) - b
    my $n2 = __neg__(__add__($s, $B));                   # -(sqrt(b^2 - 4ac) + b)

    my $d = __add__($A, $A);                             # 2a

    my $x1 = __div__($n1, $d);                           # solution 1
    my $x2 = __div__($n2, $d);                           # solution 2

    ((bless \$x1), (bless \$x2));
}

sub quadratic_formulaQ {
    my ($A, $B, $C) = @_;

    $A //= ZERO;
    $B //= ZERO;
    $C //= ZERO;

    _valid(\$B, \$C);

    #
    ## (-b ± sqrt(b^2 - 4ac)) / (2a)
    #

    my $u = $B->mul($B);                                                     # b^2
    my $t = $A->mul($C)->mul(FOUR);                                          # 4ac
    my $s = Sidef::Types::Number::Quadratic->new(ZERO, ONE, $u->sub($t));    # sqrt(b^2 - 4ac)

    my $n1 = $s->sub($B);                                                    #   sqrt(b^2 - 4ac) - b
    my $n2 = $s->add($B)->neg;                                               # -(sqrt(b^2 - 4ac) + b)

    my $d = $A->add($A);                                                     # 2a

    my $x1 = $n1->div($d);                                                   # solution 1
    my $x2 = $n2->div($d);                                                   # solution 2

    ($x1, $x2);
}

sub cubic_formula {
    my ($A, $B, $C, $D) = @_;

    $A //= ZERO;
    $B //= ZERO;
    $C //= ZERO;
    $D //= ZERO;

    _valid(\$B, \$C, \$D);

    $A = $$A;
    $B = $$B;
    $C = $$C;
    $D = $$D;

    # Case when B = 0 and C = 0
    if (__cmp__($B, 0) == 0 and __cmp__($C, 0) == 0) {

        my $n      = __neg__($D);
        my $n_cbrt = __cbrt__(_any2mpfr_mpc($n));
        my $k_cbrt = __cbrt__(_any2mpfr_mpc($A));

        my $unit_cbrt = __cbrt__(_any2mpc(-1));
        my $r1        = __div__($n_cbrt, $k_cbrt);
        my $r2        = __neg__(__mul__($r1,        $unit_cbrt));
        my $r3        = __mul__(__mul__($unit_cbrt, $unit_cbrt), $r1);

        return ((bless \$r1), (bless \$r2), (bless \$r3));
    }

    my $A3    = __mul__($A, 3);
    my $AC    = __mul__($A, $C);
    my $BB    = __mul__($B, $B);     # b^2
    my $D0    = __sub__($BB, __mul__($AC, 3));
    my $Bp3   = __mul__($BB, $B);    # b^3
    my $ABC9  = __mul__(__mul__($AC,             $B), 9);
    my $AAD27 = __mul__(__mul__(__mul__($A, $A), $D), 27);
    my $D1    = __add__(__sub__(__add__($Bp3, $Bp3), $ABC9), $AAD27);

    my $W = __sqrt__(_any2mpfr_mpc(__sub__(__mul__($D1, $D1), __mul__(__pow__($D0, 3), 4))));
    my $M = __cbrt__(_any2mpfr_mpc(__div__(__sub__($D1, ((__sgn__($D0) || -1) == 1) ? $W : __neg__($W)), 2)));

    my @roots;

    my $R = 1;
    my $z = __div__(__sub__(__sqrt__(_any2mpc(-3)), 1), 2);

    foreach my $k (0 .. 2) {
        my $t = __mul__($M, $R);
        my $x = __neg__(__div__(__add__(__add__($B, $t), __div__($D0, $t)), $A3));
        push @roots, $x;
        $R = __mul__($R, $z) if ($k < 2);
    }

    @roots = map { bless \$_ } @roots;
    return @roots;
}

sub iquadratic_formula {
    my ($A, $B, $C) = @_;

    $A //= ZERO;
    $B //= ZERO;
    $C //= ZERO;

    _valid(\$B, \$C);

    $A = _any2mpz($$A, 0) // return (&nan, &nan);
    $B = _any2mpz($$B, 1) // return (&nan, &nan);
    $C = _any2mpz($$C, 2) // return (&nan, &nan);

    if (Math::GMPz::Rmpz_sgn($A) == 0) {    # detect division by zero
        return (&nan, &nan);
    }

    #
    ## floor((-b ± isqrt(b^2 - 4ac)) / (2a))
    #

    my $u = Math::GMPz::Rmpz_init();
    my $t = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_mul($t, $B, $B);        # b^2
    Math::GMPz::Rmpz_mul($u, $A, $C);        # ac
    Math::GMPz::Rmpz_mul_2exp($u, $u, 2);    # 4ac
    Math::GMPz::Rmpz_sub($t, $t, $u);        # b^2 - 4ac

    if (Math::GMPz::Rmpz_sgn($t) < 0) {      # t is negative: no real solution
        return (&nan, &nan);
    }

    Math::GMPz::Rmpz_sqrt($t, $t);           # isqrt(b^2 - 4ac)

    Math::GMPz::Rmpz_sub($u, $t, $B);        #   sqrt(b^2 - 4ac) - b
    Math::GMPz::Rmpz_add($t, $t, $B);        #   sqrt(b^2 - 4ac) + b
    Math::GMPz::Rmpz_neg($t, $t);            # -(sqrt(b^2 - 4ac) + b)

    Math::GMPz::Rmpz_div($u, $u, $A);
    Math::GMPz::Rmpz_div($t, $t, $A);

    Math::GMPz::Rmpz_div_2exp($u, $u, 1);
    Math::GMPz::Rmpz_div_2exp($t, $t, 1);

    ((bless \$u), (bless \$t));
}

*integer_quadratic_formula = \&iquadratic_formula;

sub modular_quadratic_formula {
    my ($x, $y, $z, $m) = @_;

    $x //= ZERO;
    $y //= ZERO;
    $z //= ZERO;
    $m // return _array();

    _valid(\$y, \$z, \$m);

    $x = __mod__($$x, $$m);
    $y = __mod__($$y, $$m);
    $z = __mod__($$z, $$m);

    (__is_int__($x) && __is_int__($y) && __is_int__($z))
      || return _array();

    $m = _any2mpz($$m) // return _array();
    $x = _any2mpz($x)  // return _array();
    $y = _any2mpz($y)  // return _array();
    $z = _any2mpz($z)  // return _array();

    # x must not be zero
    if (Math::GMPz::Rmpz_sgn($x) == 0) {
        return (bless \$y)->linear_congruence((bless \__neg__($z)), (bless \$m));
    }

    state $inv_a = Math::GMPz::Rmpz_init_nobless();

    if (Math::GMPz::Rmpz_odd_p($m) and Math::GMPz::Rmpz_invert($inv_a, $x, $m)) {

        # This part of code was translated from SymPy.

        $y = Math::GMPz::Rmpz_init_set($y);
        $z = Math::GMPz::Rmpz_init_set($z);

        Math::GMPz::Rmpz_mul($y, $y, $inv_a);
        Math::GMPz::Rmpz_mul($z, $z, $inv_a);

        if (Math::GMPz::Rmpz_odd_p($y)) {
            Math::GMPz::Rmpz_add($y, $y, $m);
        }

        Math::GMPz::Rmpz_div_2exp($y, $y, 1);

        my $D = __mod__(__sub__(__mul__($y, $y), $z), $m);
        my $S = (bless \$D)->sqrtmod_all(bless \$m);

        @$S || return $S;

        my @solutions;
        foreach my $k (@$S) {
            push @solutions, bless \__mod__(__sub__($$k, $y), $m);
        }

        return _array(\@solutions)->isort;
    }

    my $four_xm = __mul__(__mul__($x, 4), $m);    # 4*x*m

    # D = b^2 - 4*a*c (mod 4*a*m)
    my $D = __mod__(__sub__(__mul__($y, $y), __mul__(__mul__($x, $z), 4)), $four_xm);

    # Find all the solutions k to: k^2 == D (mod 4*x*m)
    my $S = (bless \$D)->sqrtmod_all(bless \$four_xm);

    @$S || return $S;

    my $two_a = __add__($x, $x);
    my $neg_b = __neg__($y);

    my @solutions;

    foreach my $k (@$S) {
        ## foreach my $u (__add__($neg_b, $$k), __sub__($neg_b, $$k)) {
        foreach my $u (__add__($neg_b, $$k)) {
            my $r = __mod__(__div__($u, $two_a), $m);
            ## __cmp__(__mod__(__add__(__add__(__mul__($x, __mul__($r, $r)), __mul__($y, $r)), $z), $m), 0) == 0 or die "error for solution: $r";
            push @solutions, bless \$r;
        }
    }

    _array(\@solutions)->isort->iuniq;
}

*quadratic_congruence = \&modular_quadratic_formula;

sub solve_quadratic_form {
    my ($d, $n) = @_;
    _valid(\$n);

    # Given a positive integer `n` and a positive integer `d`,
    # returns an array with `[x,y]` solutions to the equation:
    #   x^2 + d*y^2 = n

    if ($d->is_one) {
        return $n->sum_of_squares;
    }

    if ($d->is_neg) {
        return _array();
    }

    my $D = $d->neg;

    # Helper to find primitive solutions using Cornacchia-style method
    my $primitive_solutions = sub {
        my ($dd, $nn, $DD) = @_;

        if ($DD->kronecker($nn)->is_mone && _is_prob_prime($$nn)) {
            return [];
        }

        my @prim;
        foreach my $x (@{$DD->sqrtmod_all($nn)}) {
            my ($A, $B, $C) = ($nn, $x, $nn->isqrt);

            while (__cmp__($$B, $$C) > 0) {
                ($A, $B) = ($B, $A->mod($B));
            }

            my $t = $nn->sub($B->sqr);
            next unless $dd->divides($t);

            my $q = $t->idiv($dd);
            next unless $q->is_square;

            push @prim, [$B, $q->isqrt];
        }
        return \@prim;
    };

    # Handle all solutions via scaling primitive ones
    my @solutions;

    foreach my $s (@{$n->square_divisors}) {
        my $g = $s->isqrt;
        my $m = $n->idiv($s);

        my $prim_sols = $primitive_solutions->($d, $m, $D);

        foreach my $p (@$prim_sols) {
            my ($x1, $y1) = @$p;
            push @solutions, [$g->mul($x1), $g->mul($y1)];
        }
    }

    # Also include the trivial y=0 case if applicable
    if ($n->is_square) {
        push @solutions, [$n->isqrt, ZERO];
    }

    my %seen;

    @solutions = grep { !$seen{"${$_->[0]};${$_->[1]}"}++ } @solutions;
    @solutions = sort { ${$a->[0]} <=> ${$b->[0]} } @solutions;
    @solutions = map  { _array($_) } @solutions;

    return _array(\@solutions);
}

*cornacchia = \&solve_quadratic_form;

sub geometric_sum {
    my ($n, $r) = @_;
    _valid(\$r);

    # (r^(n+1) - 1) / (r-1)

    $n = $$n;
    $r = $$r;

    bless \__div__(__sub__(__pow__($r, __add__($n, 1)), 1), __sub__($r, 1));
}

sub geometric_summod {
    my ($n, $r, $m) = @_;
    _valid(\$r, \$m);

    # (r^(n+1) - 1) * invmod(r-1, m) (mod m)

    $n = _any2mpz($$n, 0) // goto &nan;
    $r = _any2mpz($$r, 1) // goto &nan;
    $m = _any2mpz($$m, 2) // goto &nan;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_add_ui($t, $n, 1);

    if (Math::GMPz::Rmpz_sgn($t) < 0) {
        Math::GMPz::Rmpz_invert($u, $r, $m) || goto &nan;
    }

    Math::GMPz::Rmpz_powm($t, $r, $t, $m);
    Math::GMPz::Rmpz_sub_ui($t, $t, 1);
    Math::GMPz::Rmpz_sub_ui($u, $r, 1);
    Math::GMPz::Rmpz_invert($u, $u, $m) || goto &nan;
    Math::GMPz::Rmpz_mul($t, $t, $u);
    Math::GMPz::Rmpz_mod($t, $t, $m);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($t)
      ? Math::GMPz::Rmpz_get_ui($t)
      : Math::GMPz::Rmpz_init_set($t);
    bless \$r2;
}

sub faulhaber_range {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    return ZERO if $to->lt($from);
    return $to->faulhaber_sum($k)->sub($from->dec->faulhaber_sum($k));
}

sub power_sum {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->power_sum($to)->sub($k->power_sum($from->dec));
    }

    $from->iroot($k)->faulhaber_sum($k);
}

sub power_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->power_count($to)->sub($k->power_count($from->dec));
    }

    $from->iroot($k);
}

sub faulhaber_sum {
    my ($n, $p) = @_;

    _valid(\$p);

    $n = _any2mpz($$n, 0) // goto &nan;
    $p = _any2ui($$p)     // goto &nan;

    if ($p == 0) {
        return _set_int($n);
    }

    if ($p == 1 or $p == 3) {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_add_ui($r, $n, 1);
        Math::GMPz::Rmpz_mul($r, $r, $n);
        Math::GMPz::Rmpz_div_2exp($r, $r, 1);
        Math::GMPz::Rmpz_mul($r, $r, $r) if ($p == 3);
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    state $z = Math::GMPz::Rmpz_init_nobless();

    if ($p == 2) {    # n*(n+1)*(2*n+1)/6
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_add_ui($z, $n, 1);
        Math::GMPz::Rmpz_mul($r, $z, $n);
        Math::GMPz::Rmpz_mul_2exp($z, $z, 1);
        Math::GMPz::Rmpz_sub_ui($z, $z, 1);
        Math::GMPz::Rmpz_mul($r, $r, $z);
        Math::GMPz::Rmpz_divexact_ui($r, $r, 6);
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    # When p >= n, sum the powers directly.
    if (Math::GMPz::Rmpz_cmp_ui($n, $p) <= 0) {
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set_ui($r, 0);
        foreach my $k (1 .. Math::GMPz::Rmpz_get_ui($n)) {
            Math::GMPz::Rmpz_ui_pow_ui($z, $k, $p);
            Math::GMPz::Rmpz_add($r, $r, $z);
        }
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    my @B = _bernoulli_numbers($p);

    state $u = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_ui($u, 1);

    my $q   = Math::GMPq::Rmpq_init();
    my $sum = Math::GMPq::Rmpq_init();
    Math::GMPq::Rmpq_set_ui($sum, 0, 1);

    # Sum_{k=1..n} k^p = 1/(p+1) * Sum_{j=0..p} binomial(p+1, j) * n^(p-j+1) * bernoulli(j)
    #                  = 1/(p+1) * Sum_{j=0..p} binomial(p+1, p-j) * n^(j+1) * bernoulli(p-j)

    foreach my $j (0 .. $p - 2) {

        Math::GMPz::Rmpz_mul($u, $u, $n);

        # Skip when bernoulli(p-j) == 0
        if (($p - $j) % 2 == 0) {
            Math::GMPz::Rmpz_bin_uiui($z, $p + 1, $j + 1);
            Math::GMPz::Rmpz_mul($z, $z, $u);
            Math::GMPq::Rmpq_mul_z($q, $B[(($p - $j) >> 1) + 1], $z);
            Math::GMPq::Rmpq_add($sum, $sum, $q);
        }
    }

    # sum += (1/2) * n^p * (2*n + p + 1)
    Math::GMPz::Rmpz_mul($u, $u, $n);
    Math::GMPz::Rmpz_mul_2exp($z, $n, 1);
    Math::GMPz::Rmpz_add_ui($z, $z, $p + 1);
    Math::GMPz::Rmpz_mul($u, $u, $z);
    Math::GMPq::Rmpq_set_ui($q, 1, 2);
    Math::GMPq::Rmpq_mul_z($q, $q, $u);
    Math::GMPq::Rmpq_add($sum, $sum, $q);

    # z = sum/(p+1)
    Math::GMPq::Rmpq_get_num($u, $sum);
    Math::GMPz::Rmpz_divexact_ui($u, $u, $p + 1);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($u)
      ? Math::GMPz::Rmpz_get_ui($u)
      : Math::GMPz::Rmpz_init_set($u);
    bless \$r2;
}

*faulhaber    = \&faulhaber_sum;
*Faulhaber    = \&faulhaber_sum;
*FaulhaberSum = \&faulhaber_sum;

sub multinomial {
    my ($n, @mset) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    my $bin  = Math::GMPz::Rmpz_init();
    my $sum  = Math::GMPz::Rmpz_init_set($n);
    my $prod = Math::GMPz::Rmpz_init_set_ui(1);

    foreach my $k (@mset) {
        _valid(\$k);

        $k = _any2si($$k) // goto &nan;

        $k < 0
          ? Math::GMPz::Rmpz_sub_ui($sum, $sum, -$k)
          : Math::GMPz::Rmpz_add_ui($sum, $sum, $k);

        $k < 0
          ? Math::GMPz::Rmpz_bin_si($bin, $sum, $k)
          : Math::GMPz::Rmpz_bin_ui($bin, $sum, $k);

        Math::GMPz::Rmpz_mul($prod, $prod, $bin);
    }

    bless \$prod;
}

sub catalan {
    my ($n, $k) = @_;

    # Catalan triangle
    # catalan(n, k) = binomial(n+k, k) - binomial(n+k, k-1)
    if (defined($k)) {
        _valid(\$k);

        $n = _any2mpz($$n, 0) // goto &nan;
        $k = _any2ui($$k)     // goto &nan;

        my $t = Math::GMPz::Rmpz_init();
        my $u = Math::GMPz::Rmpz_init();

        Math::GMPz::Rmpz_add_ui($t, $n, $k);
        Math::GMPz::Rmpz_bin_ui($u, $t, $k);
        ($k > 0)
          ? Math::GMPz::Rmpz_bin_ui($t, $t, $k - 1)
          : Math::GMPz::Rmpz_bin_si($t, $t, $k - 1);
        Math::GMPz::Rmpz_sub($u, $u, $t);

        return bless \$u;
    }

    $n = _any2ui($$n) // goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_bin_uiui($r, $n << 1, $n);
    Math::GMPz::Rmpz_divexact_ui($r, $r, $n + 1);
    bless \$r;
}

*Catalan = \&catalan;

sub binomial {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if (FAST_MODE and !ref($x) and $x <= 1e6 and $x >= 0 and $y >= 0) {

        if (HAS_PRIME_UTIL and $x <= ((INTSIZE == 32) ? 33 : 66)) {
            my $r = Math::Prime::Util::binomial($x, $y);
            return bless \$r;
        }

        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_bin_uiui($r, $x, $y);
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    $x = _any2mpz($x, 0) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();

    ($y < 0)
      ? Math::GMPz::Rmpz_bin_si($r, $x, $y)
      : Math::GMPz::Rmpz_bin_ui($r, $x, $y);

    my $r2 =
        Math::GMPz::Rmpz_fits_slong_p($r)
      ? Math::GMPz::Rmpz_get_si($r)
      : Math::GMPz::Rmpz_init_set($r);

    bless \$r2;
}

*nok = \&binomial;

# Multiply two polynomials modulo p^e, truncating safely to degree e-1
sub _poly_mul {
    my ($A, $B, $pk, $e) = @_;
    my @C = (0) x $e;
    for my $i (0 .. $e - 1) {
        next unless $A->[$i];
        for my $j (0 .. $e - $i - 1) {
            next unless $B->[$j];
            $C[$i + $j] =
              (HAS_PRIME_UTIL and $pk < ULONG_MAX)
              ? Math::Prime::Util::muladdmod($A->[$i], $B->[$j], $C[$i + $j], $pk)
              : Math::Prime::Util::GMP::muladdmod($A->[$i], $B->[$j], $C[$i + $j], $pk);
        }
    }
    return \@C;
}

# Shift a polynomial B(x) = A(x + h) modulo p^e
sub _poly_shift {
    my ($A, $h_str, $pk, $e) = @_;
    my @B = (0) x $e;
    for my $j (0 .. $e - 1) {
        next unless $A->[$j];
        my $h_pow = 1;
        for (my $m = $j ; $m >= 0 ; --$m) {
            my $bin =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::binomial($j, $m)
              : Math::Prime::Util::GMP::binomial($j, $m);
            my $term =
              (HAS_PRIME_UTIL and $pk < ULONG_MAX)
              ? Math::Prime::Util::mulmod($bin, $h_pow, $pk)
              : Math::Prime::Util::GMP::mulmod($bin, $h_pow, $pk);
            $B[$m] =
              (HAS_PRIME_UTIL and $pk < ULONG_MAX)
              ? Math::Prime::Util::muladdmod($term, $A->[$j], $B[$m], $pk)
              : Math::Prime::Util::GMP::muladdmod($term, $A->[$j], $B[$m], $pk);

            if ($m > 0) {
                $h_pow =
                  (HAS_PRIME_UTIL and $pk < ULONG_MAX)
                  ? Math::Prime::Util::mulmod($h_pow, $h_str, $pk)
                  : Math::Prime::Util::GMP::mulmod($h_pow, $h_str, $pk);
            }
        }
    }
    return \@B;
}

# Divide and conquer recursive evaluation of P(x, q)
sub _build_polynomial {
    my ($q_str, $poly, $pk, $e) = @_;

    if ($q_str eq "0") {
        my @res = (0) x $e;
        $res[0] = 1;
        return \@res;
    }
    if ($q_str eq "1") {
        return $poly;
    }

    my $h_str = Math::Prime::Util::GMP::rshiftint($q_str, 1);    # floor(q/2)

    my $P_h         = __SUB__->($h_str, $poly, $pk, $e);
    my $P_h_shifted = _poly_shift($P_h, $h_str, $pk, $e);
    my $P_2h        = _poly_mul($P_h, $P_h_shifted, $pk, $e);

    if (Math::Prime::Util::GMP::modint($q_str, 2) == 1) {
        my $two_h_str    = Math::Prime::Util::GMP::lshiftint($h_str, 1);
        my $poly_shifted = _poly_shift($poly, $two_h_str, $pk, $e);
        return _poly_mul($P_2h, $poly_shifted, $pk, $e);
    }

    return $P_2h;
}

sub _factorial_without_prime_pe {
    my ($n, $p, $e, $pk) = @_;

    if (Math::Prime::Util::GMP::cmpint($p, $n) > 0) {
        return (
                (HAS_PRIME_UTIL and $pk < ULONG_MAX)
                ? Math::Prime::Util::factorialmod($n, $pk)
                : Math::Prime::Util::GMP::factorialmod($n, $pk)
               );
    }

    my $fact = 1;
    my @c    = (1, (0) x ($e - 1));

    for my $j (1 .. $p - 1) {

        $fact = Math::Prime::Util::GMP::mulmod($fact, $j, $pk);
        my $inv = Math::Prime::Util::GMP::invmod($j, $pk);

        for (my $k = $e - 1 ; $k >= 1 ; --$k) {
            if ($c[$k - 1]) {
                $c[$k] = Math::Prime::Util::GMP::muladdmod($c[$k - 1], $inv, $c[$k], $pk);
            }
        }
    }

    my @poly;
    my $p_pow = 1;
    for my $k (0 .. $e - 1) {
        my $coeff = Math::Prime::Util::GMP::mulmod($c[$k], $fact, $pk);
        push @poly, Math::Prime::Util::GMP::mulmod($coeff, $p_pow, $pk);
        $p_pow = Math::Prime::Util::GMP::mulmod($p_pow, $p, $pk);
    }

    my $q = Math::Prime::Util::GMP::divint($n, $p);
    my $r = Math::Prime::Util::GMP::modint($n, $p);

    my $P_q = _build_polynomial($q, \@poly, $pk, $e);
    my $res = $P_q->[0];

    my $pq = Math::Prime::Util::GMP::mulint($q, $p);
    if ($r > 0) {
        for my $j (1 .. $r) {
            $res =
              (HAS_PRIME_UTIL and $pk < ULONG_MAX)
              ? Math::Prime::Util::mulmod($res, Math::Prime::Util::addint($pq, $j), $pk)
              : Math::Prime::Util::GMP::mulmod($res, Math::Prime::Util::GMP::addint($pq, $j), $pk);
        }
    }

    return $res;
}

sub _factorial_without_prime {
    my ($n, $p, $e, $pk, $from, $res) = @_;

    return 1 if (Math::Prime::Util::GMP::cmpint($n, 1) <= 0);

    if (Math::Prime::Util::GMP::cmpint($p, $n) > 0) {
        return (
                (HAS_PRIME_UTIL and $pk < ULONG_MAX)
                ? Math::Prime::Util::factorialmod($n, $pk)
                : Math::Prime::Util::GMP::factorialmod($n, $pk)
               );
    }

    if (Math::Prime::Util::GMP::cmpint($$from, $n) == 0) {
        return $$res;
    }

    if (Math::Prime::Util::GMP::cmpint($$from, $n) > 0) {
        $$from = 0;
        $$res  = 1;
    }

    my $q2_pk = Math::Prime::Util::GMP::mulint($p, $p);
    if ($p > 2 && Math::Prime::Util::GMP::cmpint($pk, $q2_pk) == 0) {
        my $a = Math::Prime::Util::GMP::divint($n, $p);
        my $b = Math::Prime::Util::GMP::modint($n, $p);

        my $pm1_fact =
          (HAS_PRIME_UTIL and $pk < ULONG_MAX)
          ? Math::Prime::Util::factorialmod(Math::Prime::Util::subint($p, 1), $pk)
          : Math::Prime::Util::GMP::factorialmod(Math::Prime::Util::GMP::subint($p, 1), $pk);

        my $b_fact =
          (HAS_PRIME_UTIL and $pk < ULONG_MAX)
          ? Math::Prime::Util::factorialmod($b, $pk)
          : Math::Prime::Util::GMP::factorialmod($b, $pk);

        my $Hb = 0;
        for my $j (1 .. $b) {
            $Hb =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::addmod($Hb, Math::Prime::Util::invmod($j, $p), $p)
              : Math::Prime::Util::GMP::addmod($Hb, Math::Prime::Util::GMP::invmod($j, $p), $p);
        }

        my $res_q2 = Math::Prime::Util::GMP::powmod($pm1_fact, $a, $pk);
        $res_q2 = Math::Prime::Util::GMP::mulmod($res_q2, $b_fact, $pk);

        if ($a > 0 && $Hb > 0) {
            my $ap   = Math::Prime::Util::GMP::mulmod($a,  $p,  $pk);
            my $apHb = Math::Prime::Util::GMP::mulmod($ap, $Hb, $pk);
            my $term = Math::Prime::Util::GMP::addmod(1, $apHb, $pk);
            $res_q2 = Math::Prime::Util::GMP::mulmod($res_q2, $term, $pk);
        }

        $$res  = $res_q2;
        $$from = $n;
        return $res_q2;
    }

    my $r_pe = _factorial_without_prime_pe($n, $p, $e, $pk);

    $$res  = $r_pe;
    $$from = $n;
    return $r_pe;
}

sub _small_k_binomialmod {
    my ($n_val, $k_val, $m_val, $p) = @_;

    $n_val = Math::GMPz::Rmpz_init_set_str($n_val, 10) if !ref($n_val);
    $m_val = Math::GMPz::Rmpz_init_set_str($m_val, 10) if !ref($m_val);

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n_val) and Math::GMPz::Rmpz_cmpabs_ui($m_val, 1e7) <= 0) {
        return Math::Prime::Util::binomialmod($n_val, $k_val, $m_val);
    }

    if (!$p and (my $exp = Math::Prime::Util::GMP::is_prime_power($m_val))) {
        $p = Math::Prime::Util::GMP::rootint($m_val, $exp);
    }

    if (!$p or $k_val <= 1e4) {
        my $bin = Math::GMPz::Rmpz_init();
        if (Math::GMPz::Rmpz_cmp_ui($n_val, 1e5) <= 0) {
            Math::GMPz::Rmpz_bin_uiui($bin, Math::GMPz::Rmpz_get_ui($n_val), $k_val);
        }
        else {
            Math::GMPz::Rmpz_bin_ui($bin, $n_val, $k_val);
        }
        Math::GMPz::Rmpz_mod($bin, $bin, $m_val);
        return $bin;
    }

    my $v = 0;
    state $num_mult = Math::GMPz::Rmpz_init_nobless();
    state $den_mult = Math::GMPz::Rmpz_init_nobless();
    state $temp     = Math::GMPz::Rmpz_init_nobless();
    state $nmi      = Math::GMPz::Rmpz_init_nobless();
    state $p_z      = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($num_mult, 1);
    Math::GMPz::Rmpz_set_ui($den_mult, 1);
    Math::GMPz::Rmpz_set_ui($p_z,      $p);

    Math::GMPz::Rmpz_set($nmi, $n_val);

    for my $i (0 .. $k_val - 1) {

        if (Math::GMPz::Rmpz_divisible_ui_p($nmi, $p)) {
            $v += Math::GMPz::Rmpz_remove($temp, $nmi, $p_z);
            Math::GMPz::Rmpz_mul($num_mult, $num_mult, $temp);
        }
        else {
            Math::GMPz::Rmpz_mul($num_mult, $num_mult, $nmi);
        }

        Math::GMPz::Rmpz_mod($num_mult, $num_mult, $m_val);

        my $den = $i + 1;
        while ($den % $p == 0) {
            $den =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divint($den, $p)
              : Math::Prime::Util::GMP::divint($den, $p);
            $v -= 1;
        }

        Math::GMPz::Rmpz_mul_ui($den_mult, $den_mult, $den);
        Math::GMPz::Rmpz_mod($den_mult, $den_mult, $m_val);
        Math::GMPz::Rmpz_sub_ui($nmi, $nmi, 1);
    }

    Math::GMPz::Rmpz_invert($temp, $den_mult, $m_val);

    my $ans = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($ans, $num_mult, $temp);
    Math::GMPz::Rmpz_mod($ans, $ans, $m_val);

    if ($v > 0) {
        Math::GMPz::Rmpz_powm_ui($temp, $p_z, $v, $m_val);
        Math::GMPz::Rmpz_mul($ans, $ans, $temp);
        Math::GMPz::Rmpz_mod($ans, $ans, $m_val);
    }

    return $ans;
}

sub _is_small_k_binomialmod {
    my ($n, $k, $m) = @_;

    $n >= 1e6 or return;

    if ($m >= 1e7 and $n >= 1e7 and $k <= 1e6) {
        return 1;
    }

    my $new_k = Math::Prime::Util::GMP::subint($n, $k);

    if ($new_k > 0 and $new_k < $k) {
        $k = $new_k;
    }

    $k <= 1e7 or return;

    my $sqrt_m   = Math::Prime::Util::GMP::sqrtint($m);
    my $m_over_n = Math::Prime::Util::GMP::divint($m, $n);

    $k < $sqrt_m and $k < $m_over_n;
}

sub _lucas_theorem {    # p is prime
    my ($n, $k, $p) = @_;

    my (@nd, @kd);
    while ($k) {
        my $np = Math::Prime::Util::GMP::modint($n, $p);
        my $kp = Math::Prime::Util::GMP::modint($k, $p);

        if ($kp > 0) {
            push @nd, $np;
            push @kd, $kp;
        }

        if ($kp > $np) { return 0 }
        $n = Math::Prime::Util::GMP::divint($n, $p);
        $k = Math::Prime::Util::GMP::divint($k, $p);
    }

    my $r = 1;
    foreach my $i (0 .. $#nd) {
        my $np = $nd[$i];
        my $kp = $kd[$i];
        my $rp = Math::Prime::Util::GMP::subint($np, $kp);

        if (_is_small_k_binomialmod($np, $kp, $p)) {
            my $bin = _small_k_binomialmod($np, $kp, $p, $p);
            $r = Math::Prime::Util::GMP::mulmod($r, $bin, $p);
        }
        else {
            my $x = Math::Prime::Util::GMP::factorialmod($np, $p);
            my $y = Math::Prime::Util::GMP::factorialmod($kp, $p);
            my $z = Math::Prime::Util::GMP::factorialmod($rp, $p);

            $y = Math::Prime::Util::GMP::mulmod($y, $z, $p);
            $x = Math::Prime::Util::GMP::divmod($x, $y, $p) if ($y ne '1');
            $r = Math::Prime::Util::GMP::mulmod($r, $x, $p);
        }
    }

    return $r;
}

sub _modular_binomial {
    my ($n, $k, $m) = @_;

    # Translation of binomod.gp v1.5 by Max Alekseyev, with some extra optimizations.

    if (Math::GMPz::Rmpz_cmp_ui($m, 1) == 0) {
        return 0;
    }

    if (Math::GMPz::Rmpz_sgn($k) < 0) {
        $k = $n - $k;
    }

    if (Math::GMPz::Rmpz_sgn($k) < 0) {
        return 0;
    }

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        my $x = Math::GMPz::Rmpz_even_p($k) ? 1 : -1;
        $x = Math::Prime::Util::GMP::mulint($x, __SUB__->(-$n + $k - 1, $k, $m));
        return $x;
    }

    if (Math::GMPz::Rmpz_cmp($k, $n) > 0) {
        return 0;
    }

    if (Math::GMPz::Rmpz_sgn($k) == 0 or Math::GMPz::Rmpz_cmp($k, $n) == 0) {
        return 1;
    }

    if (Math::GMPz::Rmpz_cmp_ui($k, 1) == 0 or $k == $n - 1) {
        return $n;
    }

    if (Math::GMPz::Rmpz_cmp($n - $k, $k) < 0) {
        $k = $n - $k;
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n) and Math::GMPz::Rmpz_cmpabs_ui($m, 1e7) <= 0) {
        return Math::Prime::Util::binomialmod($n, $k, $m);
    }

    if (Math::GMPz::Rmpz_cmp_ui($k, 1e4) <= 0) {
        return _small_k_binomialmod($n, $k, $m);
    }

    my @F;

    foreach my $pp (_factor_exp(Math::Prime::Util::GMP::absint($m))) {
        my ($p, $q) = @$pp;

        if ($q == 1) {
            if (HAS_PRIME_UTIL and $n < ULONG_MAX and $p < ULONG_MAX) {
                push @F, [Math::Prime::Util::binomialmod($n, $k, $p), $p];
            }
            else {
                push @F, [_lucas_theorem($n, $k, $p), $p];
            }
            next;
        }

        my $pq = Math::Prime::Util::GMP::powint($p, $q);

        if (Math::Prime::Util::GMP::cmpint($p, $n) > 0) {
            push @F, [_small_k_binomialmod($n, $k, $pq, $p), $pq];
            next;
        }

        my $d = __ilog__($n, $p) + 1;

        my (@np, @kp);

        {
            my $pi = 1;
            foreach my $i (0 .. $d) {
                push @np, Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::divint($n, $pi), $p);
                push @kp, Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::divint($k, $pi), $p);
                $pi = Math::Prime::Util::GMP::mulint($pi, $p);
            }
        }

        my @e;

        foreach my $i (0 .. $d) {
            $e[$i] = ($np[$i] < ($kp[$i] + (($i > 0) ? $e[$i - 1] : 0))) ? 1 : 0;
        }

        for (my $i = $d - 1 ; $i >= 0 ; --$i) {
            $e[$i] += $e[$i + 1];
        }

        if ($e[0] >= $q) {
            push @F, [0, Math::Prime::Util::GMP::powint($p, $q)];
            next;
        }

        if (_is_small_k_binomialmod($n, $k, Math::Prime::Util::GMP::mulint($p, $q))) {
            my $bin = _small_k_binomialmod($n, $k, $pq, $p);
            push @F, [$bin, $pq];
            next;
        }

        if (HAS_PRIME_UTIL and $n < ULONG_MAX and $pq < 1e7) {
            push @F, [Math::Prime::Util::binomialmod($n, $k, $pq), $pq];
            next;
        }

        my $rq  = $q - $e[0];
        my $prq = Math::Prime::Util::GMP::powint($p, $rq);

        my (@N, @K, @R);

        {
            my $pi = 1;
            my $r  = Math::Prime::Util::GMP::subint($n, $k);
            foreach my $i (0 .. $d) {
                push @N, Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::divint($n, $pi), $prq);
                push @K, Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::divint($k, $pi), $prq);
                push @R, Math::Prime::Util::GMP::modint(Math::Prime::Util::GMP::divint($r, $pi), $prq);
                $pi = Math::Prime::Util::GMP::mulint($pi, $p);
            }
        }

        my @NKR = (
                   sort { Math::Prime::Util::GMP::cmpint($a->[3], $b->[3]) }
                   map  { [$N[$_], $K[$_], $R[$_], Math::Prime::Util::GMP::vecsum($N[$_], $K[$_], $R[$_])] } 0 .. $#N
                  );

        @N = map { $_->[0] } @NKR;
        @K = map { $_->[1] } @NKR;
        @R = map { $_->[2] } @NKR;

        my %acc  = ('0' => 1);
        my $nfac = 1;

        if (FAST_MODE and $prq < ULONG_MAX and Math::Prime::Util::GMP::cmpint($p, $n) < 0) {
            foreach my $k (1 .. List::Util::min(List::Util::max(@N, @K, @R), 1e3)) {
                if ($k % $p) {
                    $nfac =
                      HAS_PRIME_UTIL
                      ? Math::Prime::Util::mulmod($nfac, $k, $prq)
                      : Math::Prime::Util::GMP::mulmod($nfac, $k, $prq);
                }
                $acc{$k} = $nfac;
            }
        }

        my $v = Math::Prime::Util::GMP::powmod($p, $e[0], $pq);

        {
            my $from = 0;
            my $res  = 1;

            foreach my $j (0 .. $d) {

                my @pairs;
                my ($x, $y, $z);

                ($x = $acc{$N[$j]}) // push(@pairs, [\$x, $N[$j]]);
                ($y = $acc{$K[$j]}) // push(@pairs, [\$y, $K[$j]]);
                ($z = $acc{$R[$j]}) // push(@pairs, [\$z, $R[$j]]);

                foreach my $pair (sort { Math::Prime::Util::GMP::cmpint($a->[1], $b->[1]) } @pairs) {
                    ${$pair->[0]} = _factorial_without_prime($pair->[1], $p, $rq, $prq, \$from, \$res);
                }

                $y = Math::Prime::Util::GMP::mulmod($y, $z, $pq);
                $x = Math::Prime::Util::GMP::divmod($x, $y, $pq) if ($y ne '1');
                $v = Math::Prime::Util::GMP::mulmod($v, $x, $pq);
            }
        }

        if ($p > 2 or $rq < 3) {
            $v = Math::Prime::Util::GMP::mulmod($v, ((($e[$rq - 1] // 0) % 2 == 0) ? 1 : -1), $pq);
        }

        push @F, [$v, $pq];
    }

    Math::Prime::Util::GMP::chinese(@F);
}

sub binomialmod {
    my ($n, $k, $m) = @_;

    _valid(\$k, \$m);

    $n = $$n;
    $k = $$k;
    $m = $$m;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($n) and !ref($k) and !ref($m) and CORE::abs($m) <= 1e7) {
        $m || goto &nan;
        my $r = Math::Prime::Util::GMP::modint(Math::Prime::Util::binomialmod($n, $k, $m), $m);
        return bless \$r;
    }

    $n = _any2mpz($n, 0) // (goto &nan);
    $k = _any2mpz($k, 1) // (goto &nan);
    $m = _any2mpz($m, 2) // (goto &nan);

    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    _set_int(Math::Prime::Util::GMP::modint(_modular_binomial($n, $k, $m), $m));
}

sub totient_range {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _big2istr($$n) // return _array();
    $k = _big2istr($$k) // return _array();

    my @array = map { ref($_) eq __PACKAGE__ ? $_ : _set_int($_) } (
                                                                 (HAS_PRIME_UTIL and $k < ULONG_MAX)
                                                                 ? Math::Prime::Util::euler_phi($n, $k)
                                                                 : _set_int($n)->range(_set_int($k))->map(Sidef::Types::String::String->new("totient"))->to_list
    );

    return _array(\@array);
}

sub moebius_range {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _big2istr($$n) // return _array();
    $k = _big2istr($$k) // return _array();

    my @array = map { $_ ? ($_ == 1) ? ONE : MONE : ZERO } (
                                                            HAS_PRIME_UTIL
                                                            ? Math::Prime::Util::moebius($n, $k)
                                                            : Math::Prime::Util::GMP::moebius($n, $k)
                                                           );

    return _array(\@array);
}

*mobius_range = \&moebius_range;

sub moebius {
    my ($n) = @_;

    if (scalar(@_) > 1) {
        goto &moebius_range;
    }

    $n = $$n;

    my $m =
      (FAST_MODE and HAS_PRIME_UTIL and !ref($n) and $n >= 0)
      ? Math::Prime::Util::moebius($n)
      : Math::Prime::Util::GMP::moebius(_big2uistr($n) // goto &nan);

    $m ? ($m == 1) ? ONE : MONE : ZERO;
}

*μ       = \&moebius;
*mu      = \&moebius;
*mobius  = \&moebius;
*möbius  = \&moebius;
*Möbius  = \&moebius;
*Moebius = \&moebius;

sub mertens {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        $x = _big2istr($$x) // return ZERO;
        $x = 1 if $x < 1;
        $y = _big2uistr($$y) // return ZERO;
    }
    else {
        $y = _big2uistr($$x) // return ZERO;
        $x = 1;
    }

    state $mertens_table = {

        # M(10^n), where M(x) is Mertens's function.
        # OEIS: https://oeis.org/A084237
        "10000000"                 => "1037",
        "100000000"                => "1928",
        "1000000000"               => "-222",
        "10000000000"              => "-33722",
        "100000000000"             => "-87856",
        "1000000000000"            => "62366",
        "10000000000000"           => "599582",
        "100000000000000"          => "-875575",
        "1000000000000000"         => "-3216373",
        "10000000000000000"        => "-3195437",
        "100000000000000000"       => "-21830254",
        "1000000000000000000"      => "-46758740",
        "10000000000000000000"     => "899990187",
        "100000000000000000000"    => "461113106",
        "1000000000000000000000"   => "-3395895277",
        "10000000000000000000000"  => "-2061910120",
        "100000000000000000000000" => "62467771689",

        # M(2^n), where M(n) is Mertens's function.
        # OEIS: https://oeis.org/A084236
        "1073741824"              => "-10374",
        "2147483648"              => "9569",
        "4294967296"              => "1814",
        "8589934592"              => "-10339",
        "17179869184"             => "-3421",
        "34359738368"             => "8435",
        "68719476736"             => "38176",
        "137438953472"            => "-28118",
        "274877906944"            => "38729",
        "549755813888"            => "-135944",
        "1099511627776"           => "101597",
        "2199023255552"           => "15295",
        "4398046511104"           => "-169338",
        "8796093022208"           => "259886",
        "17592186044416"          => "-474483",
        "35184372088832"          => "1726370",
        "70368744177664"          => "-3554573",
        "140737488355328"         => "-135443",
        "281474976710656"         => "3282200",
        "562949953421312"         => "1958235",
        "1125899906842624"        => "-1735147",
        "2251799813685248"        => "6657834",
        "4503599627370496"        => "-13927672",
        "9007199254740992"        => "-11901414",
        "18014398509481984"       => "48662015",
        "36028797018963968"       => "-48361472",
        "72057594037927936"       => "23952154",
        "144115188075855872"      => "51885062",
        "288230376151711744"      => "-15415164",
        "576460752303423488"      => "-89014828",
        "1152921504606846976"     => "-48425659",
        "2305843009213693952"     => "220660381",
        "4611686018427387904"     => "-248107163",
        "9223372036854775808"     => "580197744",
        "18446744073709551616"    => "-851764249",
        "36893488147419103232"    => "809210153",
        "73786976294838206464"    => "-1220538763",
        "147573952589676412928"   => "-925696220",
        "295147905179352825856"   => "2092394726",
        "590295810358705651712"   => "-3748189801",
        "1180591620717411303424"  => "9853266869",
        "2361183241434822606848"  => "-12658250658",
        "4722366482869645213696"  => "9558471405",
        "9444732965739290427392"  => "-6524408924",
        "18889465931478580854784" => "-6336351930",
        "37778931862957161709568" => "-4000846218",
                           };

    if ($x eq '1') {

        if (defined(my $value = $mertens_table->{$y})) {
            return _set_int($value);
        }

        if (HAS_PRIME_UTIL and $y < ULONG_MAX) {
            return _set_int($mertens_table->{$y} = Math::Prime::Util::mertens($y));
        }
    }

    # Support for large integers (slow for wide ranges)
    if ($y >= ~0) {

        $x = Math::GMPz::Rmpz_init_set_str("$x", 10);
        $y = Math::GMPz::Rmpz_init_set_str("$y", 10);

        my $sum = 0;

        for (; Math::GMPz::Rmpz_cmp($x, $y) <= 0 ; Math::GMPz::Rmpz_add_ui($x, $x, 1)) {
            $sum += Math::Prime::Util::GMP::moebius(Math::GMPz::Rmpz_get_str($x, 10));
        }

        return _set_int($sum);
    }

    return ZERO if ($y < $x);

    # Optimization for narrow ranges
    if (($x >= 10**4 and $y - $x <= 10**4) or "$x" / "$y" >= 0.999) {
        my $r = List::Util::sum(
                                HAS_PRIME_UTIL
                                ? Math::Prime::Util::moebius($x, $y)
                                : Math::Prime::Util::GMP::moebius($x, $y)
                               );
        return _set_int($r);
    }

    my $lookup_size = 2 * Math::Prime::Util::GMP::rootint($y, 3)**2;

    if ($y > 1e10) {
        $lookup_size >>= 1;
    }

    if ($y > 1e11) {
        $lookup_size >>= 1;
    }

    state @mertens_lookup;

    if (@mertens_lookup < $lookup_size) {
        $mertens_lookup[0] = 0;

        my @mu_range =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::moebius(scalar(@mertens_lookup), $lookup_size)
          : Math::Prime::Util::GMP::moebius(scalar(@mertens_lookup), $lookup_size);

        foreach my $i (@mertens_lookup .. $lookup_size) {
            $mertens_lookup[$i] = $mertens_lookup[$i - 1] + shift(@mu_range);
        }
    }

    use integer;

    my $mertens = sub {
        my ($n) = @_;

        # Algorithm based on the recursive identity:
        #   M(n) = 1 - Sum_{k=2..n} M(floor(n/k))

        if ($n <= $lookup_size) {
            return $mertens_lookup[$n];
        }

        if (exists $mertens_table->{$n}) {
            return $mertens_table->{$n};
        }

        # Using Dana Jacobsen's (++) optimizations from Math::Prime::Util::PP.
        my $s  = Math::Prime::Util::GMP::sqrtint($n);
        my $ns = $n / ($s + 1);

        my ($nk, $nk1) = ($n, $n >> 1);
        my $M = 1 - ($nk - $nk1);

        foreach my $k (2 .. $ns) {
            ($nk, $nk1) = ($nk1, $n / ($k + 1));
            $M -= ($nk <= $lookup_size) ? $mertens_lookup[$nk] : __SUB__->($nk);
            $M -= $mertens_lookup[$k] * ($nk - $nk1);
        }

        if ($s > $ns) {
            $M -= $mertens_lookup[$s] * ($n / $s - $ns);
        }

        $mertens_table->{$n} = $M;
    };

    my $value =
      ($x == 1)
      ? $mertens->($y)
      : ($mertens->($y) - $mertens->($x) + Math::Prime::Util::GMP::moebius($x));

    _set_int($value);
}

*moebius_sum = \&mertens;

sub exp_bigomega {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_positive || return ZERO;
    $k->pow($n->bigomega);
}

sub exp_bigomega_sum {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n);

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $n_obj = bless \$n;

    my @terms;
    foreach my $i (0 .. __ilog__($n, 2)) {
        my $i_obj = _set_int($i);
        push @terms, $i_obj->almost_prime_count($n_obj)->mul($k->pow($i_obj));
    }

    Sidef::Types::Number::Number::sum(@terms);
}

sub exp_omega {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_positive || return ZERO;
    $k->pow($n->omega);
}

sub exp_omega_sum {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n);

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $n_obj = bless \$n;

    my @terms;
    foreach my $i (0 .. CORE::int($n_obj->lgrt->numify) + 1) {
        my $i_obj = _set_int($i);
        push @terms, $i_obj->omega_prime_count($n_obj)->mul($k->pow($i_obj));
    }

    Sidef::Types::Number::Number::sum(@terms);
}

sub exp_squarefree_omega {    # k^omega(n) * mu(n)^2
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_positive   || return ZERO;
    $n->is_squarefree || return ZERO;
    $k->pow($n->omega);
}

sub exp_squarefree_omega_sum {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n);

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $n_obj = bless \$n;

    my @terms;
    foreach my $i (0 .. CORE::int($n_obj->lgrt->numify) + 1) {
        my $i_obj = _set_int($i);
        push @terms, $i_obj->squarefree_almost_prime_count($n_obj)->mul($k->pow($i_obj));
    }

    Sidef::Types::Number::Number::sum(@terms);
}

sub liouville_sum {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->liouville_sum->sub($from->dec->liouville_sum);
    }

    my $n = $$from;

    if (ref($n)) {
        $n = _big2istr($n) // goto &nan;
    }

    $n > 0 or return ZERO;

    state $liouville_table = {

        # L(10^n), where L(x) is Liouville's summatory function.
        # OIES: https://oeis.org/A090410
        '1'                      => '1',
        '10'                     => '0',
        '100'                    => '-2',
        '1000'                   => '-14',
        '10000'                  => '-94',
        '100000'                 => '-288',
        '1000000'                => '-530',
        '10000000'               => '-842',
        '100000000'              => '-3884',
        '1000000000'             => '-25216',
        '10000000000'            => '-116026',
        '100000000000'           => '-342224',
        '1000000000000'          => '-522626',
        '10000000000000'         => '-966578',
        '100000000000000'        => '-7424752',
        '1000000000000000'       => '-29445104',
        '10000000000000000'      => '-97617938',
        '100000000000000000'     => '-271676470',
        '1000000000000000000'    => '-618117940',
        '10000000000000000000'   => '-810056106',
        '100000000000000000000'  => '-6260758462',
        '1000000000000000000000' => '-34541748676',
                             };

    if (defined(my $value = $liouville_table->{$n})) {
        return bless \$value;
    }

    if (HAS_PRIME_UTIL and $n < ULONG_MAX and $n > 1e6) {    # this is faster for large n
        return ((bless \$n)->exp_bigomega_sum(MONE));
    }

    my $L    = 0;
    my $sqrt = Math::Prime::Util::GMP::sqrtint($n);

    state $mertens_table = [ 0,  1,  0, -1, -1, -2, -1, -2, -2, -2, -1, -2, -2, -3, -2, -1, -1, -2, -2, -3, -3, -2, -1, -2, -2, -2,
                            -1, -1, -1, -2, -3, -4, -4, -3, -2, -1, -1, -2, -1,  0,  0, -1, -2, -3, -3, -3, -2, -3, -3, -3, -3, -2,
                            -2, -3, -3, -2, -2, -1,  0, -1, -1, -2, -1, -1, -1,  0, -1, -2, -2, -1, -2, -3, -3, -4, -3, -3, -3, -2,
                            -3, -4, -4, -4, -3, -4, -4, -3, -2, -1, -1, -2, -2, -1, -1,  0,  1,  2,  2,  1,  1,  1,  1,  0, -1, -2,
                           ];

    my $u = 0;
    my $k = 1;

    while ($k <= $sqrt) {

        my $t =
          (HAS_PRIME_UTIL and $n < ULONG_MAX)
          ? Math::Prime::Util::divint($n, $k * $k)
          : Math::Prime::Util::GMP::divint($n, (($k * $k < ULONG_MAX) ? ($k * $k) : Math::Prime::Util::GMP::mulint($k, $k)));

        my $v =
          (HAS_PRIME_UTIL and $n < ULONG_MAX)
          ? Math::Prime::Util::sqrtint(Math::Prime::Util::divint($n, $t))
          : Math::Prime::Util::GMP::sqrtint(Math::Prime::Util::GMP::divint($n, $t));

        my $f = $v - $u;
        my $M =
          ($t <= 103)
          ? $mertens_table->[$t]
          : ${((FAST_MODE and $t < ULONG_MAX) ? (bless \$t) : _set_int($t))->mertens};

        $L += $f * (ref($M) ? Math::GMPz::Rmpz_get_si($M) : $M);
        $k += $f;
        $u = $v;
    }

    $liouville_table->{$n} = $L;
    bless \$L;
}

sub cyclotomic_polynomial {
    my ($n, $x) = @_;

    _valid(\$n);

    $n = _any2ui($$n) // goto &nan;

    if (!defined($x)) {

        if ($n == 0) {
            return Sidef::Types::Number::Polynomial->new();
        }
        elsif ($n == 1) {
            return Sidef::Types::Number::Polynomial->new(ONE)->dec;
        }

        if ($n >= 150 and $USE_PARI_GP) {
            if (my $res = _execute_pari_gp("polcyclo($n)")) {
                return Sidef::Types::Number::Polynomial->new(Sidef::Types::String::String->new($res));
            }
        }

        my @factor_exp = _factor_exp($n);

        # Generate the squarefree divisors of n, along
        # with the number of prime factors of each divisor
        my @sd;
        foreach my $pe (@factor_exp) {
            push @sd, map { [$_->[0] * $pe->[0], $_->[1] + 1] } @sd;
            push @sd, [$pe->[0], 1];
        }

        push @sd, [1, 0];

        my (@num, @den);
        foreach my $pair (@sd) {
            my ($d, $c) = @$pair;

            my $t    = CORE::int($n / $d);
            my $base = Sidef::Types::Number::Polynomial->new(_set_int($t))->dec;

            if ($c % 2 == 0) {
                push @num, $base;
            }
            else {
                push @den, $base;
            }
        }

        return _binsplit(\@num, \&Sidef::Types::Number::Polynomial::mul)->div(_binsplit(\@den, \&Sidef::Types::Number::Polynomial::mul));
    }

    _valid(\$x);
    $x = $$x;

    return ZERO if ($n == 0);

    return bless(\__dec__($x)) if ($n == 1);
    return bless(\__inc__($x)) if ($n == 2);

    my $x_is_mpz = ref($x) eq 'Math::GMPz';

    # Special case for x = 1: cyclotomic(n, 1) is A020500.
    if ($x_is_mpz ? (Math::GMPz::Rmpz_cmp_ui($x, 1) == 0) : __eq__($x, 1)) {
        my $k = Math::Prime::Util::GMP::is_prime_power($n) || return ONE;
        my $p = Math::Prime::Util::GMP::rootint($n, $k);
        return _set_int($p);
    }

    # Special case for x = -1: cyclotomic(n, -1) is A020513.
    if ($x_is_mpz ? (Math::GMPz::Rmpz_cmp_si($x, -1) == 0) : __eq__($x, -1)) {
        ($n % 2 == 0) || return ONE;
        my $k = Math::Prime::Util::GMP::is_prime_power($n >> 1) || return ONE;
        my $p = Math::Prime::Util::GMP::rootint($n >> 1, $k);
        return _set_int($p);
    }

    my @factor_exp = _factor_exp($n);

    # Generate the squarefree divisors of n, along
    # with the number of prime factors of each divisor
    my @sd;
    foreach my $pe (@factor_exp) {
        push @sd, map { [$_->[0] * $pe->[0], $_->[1] + 1] } @sd;
        push @sd, [$pe->[0], 1];
    }

    push @sd, [1, 0];

    my (@num, @den);
    foreach my $pair (@sd) {
        my ($d, $c) = @$pair;

        my $t    = CORE::int($n / $d);
        my $base = $x_is_mpz
          ? do {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_pow_ui($z, $x, $t);
            Math::GMPz::Rmpz_sub_ui($z, $z, 1);
            $z;
          }
          : __dec__(__pow__($x, $t));

        if ($c % 2 == 0) {
            push @num, $base;
        }
        else {
            push @den, $base;
        }
    }

    (scalar(@num) > 0 and scalar(@den) > 0) || return ONE;
    bless \__div__(_binsplit(\@num, \&__mul__), _binsplit(\@den, \&__mul__));
}

*cyclotomic = \&cyclotomic_polynomial;

sub cyclotomicmod {
    my ($n, $x, $m) = @_;

    _valid(\$n, \$x, \$m);

    my $M = $m;

    $n = _any2mpz($$n, 0) // goto &nan;
    $x = $$x;
    $m = _any2mpz($$m, 1) // goto &nan;

    Math::GMPz::Rmpz_sgn($m) || goto &nan;

    if (ref($x) ne 'Math::GMPz') {
        if (__is_int__($x)) {
            $x = _any2mpz($x, 2) // goto &nan;
        }
        else {
            $x = _modular_rational($x, $m) // goto &nan;
        }
    }

    # n must be >= 0
    (Math::GMPz::Rmpz_sgn($n) || return ZERO) > 0
      or goto &nan;

    return ZERO if (Math::GMPz::Rmpz_cmp_ui($m, 1) == 0);

    return bless(\__dec__($x))->mod($M) if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0);
    return bless(\__inc__($x))->mod($M) if (Math::GMPz::Rmpz_cmp_ui($n, 2) == 0);

    # Special case for x = 1: cyclotomic(n, 1) is A020500.
    if (Math::GMPz::Rmpz_cmp_ui($x, 1) == 0) {
        my $k = Math::Prime::Util::GMP::is_prime_power($n) || return ONE;
        my $p = Math::Prime::Util::GMP::rootint($n, $k);
        return _set_int(Math::Prime::Util::GMP::modint($p, $m));
    }

    # Special case for x = -1: cyclotomic(n, -1) is A020513.
    if (Math::GMPz::Rmpz_cmp_si($x, -1) == 0) {
        Math::GMPz::Rmpz_even_p($n) || return ONE;
        my $o = $n >> 1;
        my $k = Math::Prime::Util::GMP::is_prime_power($o) || return ONE;
        my $p = Math::Prime::Util::GMP::rootint($o, $k);
        return _set_int(Math::Prime::Util::GMP::modint($p, $m));
    }

    my @factor_exp = _factor_exp($n);

    # Generate the squarefree divisors of n, along
    # with the number of prime factors of each divisor
    my @sd;
    foreach my $pe (@factor_exp) {
        my ($p) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        push @sd, map { [$_->[0] * $p, $_->[1] + 1] } @sd;
        push @sd, [$p, 1];
    }

    push @sd, [$ONE, 0];

    my $base = Math::GMPz::Rmpz_init();
    my $prod = Math::GMPz::Rmpz_init_set_ui(1);

    foreach my $pair (@sd) {
        my ($d, $c) = @$pair;

        Math::GMPz::Rmpz_divexact($base, $n, $d);
        Math::GMPz::Rmpz_powm($base, $x, $base, $m);    # x^(n/d) mod m
        Math::GMPz::Rmpz_sub_ui($base, $base, 1);

        if ($c % 2 == 1) {
            Math::GMPz::Rmpz_invert($base, $base, $m) || goto &nan;
        }

        Math::GMPz::Rmpz_mul($prod, $prod, $base);
        Math::GMPz::Rmpz_mod($prod, $prod, $m);
    }

    bless \$prod;
}

sub cyclotomic_factor {
    my ($n, @bases) = @_;

    # ── Input validation and normalisation ─────────────────────────────────────
    $n = _any2mpz($$n) // return _array();
    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0 or return _array();

    if (@bases) {
        _valid(\(@bases));
        @bases = grep { defined } map { _any2mpz($$_) } @bases;
    }
    else {
        @bases = map { _any2mpz($_) } (2 .. __ilog__($n, 2));
    }

    $n = Math::GMPz::Rmpz_init_set($n);    # take a mutable copy

    # ── Squarefree-divisor cache ────────────────────────────────────────────────
    #
    # The Möbius product formula for the cyclotomic polynomial is:
    #
    #   Φ_k(x) = ∏_{d | k}  (x^(k/d) − 1)^{μ(d)}
    # ───────────────────────────────────────────────────────────────────────────
    my %sd_cache;
    my $squarefree_divisors_of = sub {
        my ($k) = @_;
        return $sd_cache{$k} if exists $sd_cache{$k};

        my @sd;
        for my $pp (_factor_exp($k)) {
            my $p = $pp->[0];
            push @sd, map { [$_->[0] * $p, $_->[1] + 1] } @sd;
            push @sd, [$p, 1];
        }
        push @sd, [1, 0];    # trivial divisor d=1, ω(1)=0
        $sd_cache{$k} = \@sd;
    };

    my $t    = Math::GMPz::Rmpz_init();    # holds x^(k/d) − 1 (mod m)
    my $tsav = Math::GMPz::Rmpz_init();    # saves $t before a potentially failing invert
    my $prod = Math::GMPz::Rmpz_init();    # Möbius product accumulator
    my $g    = Math::GMPz::Rmpz_init();    # GCD / factor scratch

    # ── Compute Φ_k(x) mod m
    my $cyclotomicmod = sub {
        my ($k, $x, $m) = @_;

        Math::GMPz::Rmpz_set_ui($prod, 1);

        for my $pair (@{$squarefree_divisors_of->($k)}) {
            my ($d, $omega) = @$pair;

            # t ← x^(k/d) − 1  (mod m)
            Math::GMPz::Rmpz_powm_ui($t, $x, CORE::int($k / $d), $m);
            Math::GMPz::Rmpz_sub_ui($t, $t, 1);

            if ($omega & 1) {    # μ(d) = −1 → need modular inverse
                Math::GMPz::Rmpz_set($tsav, $t);    # preserve $t before invert overwrites it
                unless (Math::GMPz::Rmpz_invert($t, $t, $m)) {

                    # $t is not invertible mod $m.
                    # gcd($tsav, $m) > 1 is a non-trivial factor; store in $g for caller.
                    Math::GMPz::Rmpz_gcd($g, $tsav, $m);
                    return undef;
                }
            }

            Math::GMPz::Rmpz_mul($prod, $prod, $t);
            Math::GMPz::Rmpz_mod($prod, $prod, $m);
        }

        $prod;
    };

    my @factors;

  OUTER: for my $x (@bases) {
        my $limit = 1 + __ilog__($n, $x);

        for (my $k = 3 ; $k <= $limit ; ++$k) {
            my $phi = $cyclotomicmod->($k, $x, $n);

            # If cyclotomicmod returned undef, a non-trivial gcd was already
            # written into $g; otherwise compute gcd(Φ_k(x), n) now.
            Math::GMPz::Rmpz_gcd($g, $n, $phi) if defined $phi;

            # Skip trivial (= 1) or full (= n) GCDs.
            next if Math::GMPz::Rmpz_cmp_ui($g, 1) == 0;
            next if Math::GMPz::Rmpz_cmp($g, $n) == 0;

            # Extract all copies of the factor from n.
            my $f = Math::GMPz::Rmpz_init_set($g);         # own copy before $g is reused
            my $v = Math::GMPz::Rmpz_remove($n, $n, $f);
            push @factors, ($f) x $v;

            last OUTER if (   Math::GMPz::Rmpz_cmp_ui($n, 1) == 0
                           or _is_prob_prime($n, 1));
        }
    }

    # If n was not fully factored, the remainder is a (probable) prime factor.
    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0 and push @factors, $n;

    @factors = sort { Math::GMPz::Rmpz_cmp($a, $b) } @factors;
    _array([map { bless \$_ } @factors]);
}

sub powerfree_sum {
    my ($k, $from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->powerfree_sum($to)->sub($k->powerfree_sum($from->dec));
    }

    _valid(\$from);

    my $n = _any2mpz($$from, 0) // return ZERO;
    $k = _any2ui($$k) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    return ZERO if ($k == 0);
    return ONE  if ($k == 1);

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $w = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_root($t, $n, $k);
    Math::GMPz::Rmpz_fits_ulong_p($t) || goto &nan;    # too large

    my $s   = Math::GMPz::Rmpz_get_ui($t);
    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    if (HAS_PRIME_UTIL) {
        Math::Prime::Util::forsquarefree(
            sub {

                # u = faulhaber(floor(n/v^k), 1)
                Math::GMPz::Rmpz_ui_pow_ui($w, $_, $k);
                Math::GMPz::Rmpz_div($t, $n, $w);
                Math::GMPz::Rmpz_mul($u, $t, $t);
                Math::GMPz::Rmpz_add($u, $u, $t);
                Math::GMPz::Rmpz_div_2exp($u, $u, 1);

                # u *= v^k
                Math::GMPz::Rmpz_mul($u, $u, $w);

                (scalar(@_) & 1)
                  ? Math::GMPz::Rmpz_sub($sum, $sum, $u)
                  : Math::GMPz::Rmpz_add($sum, $sum, $u);
            },
            $s
        );
    }
    else {
        my $m;
        for (my $v = 1 ; $v <= $s ; ++$v) {
            if ($m = HAS_PRIME_UTIL ? Math::Prime::Util::moebius($v) : Math::Prime::Util::GMP::moebius($v)) {

                # u = faulhaber(floor(n/v^k), 1)
                Math::GMPz::Rmpz_ui_pow_ui($w, $v, $k);
                Math::GMPz::Rmpz_div($t, $n, $w);
                Math::GMPz::Rmpz_mul($u, $t, $t);
                Math::GMPz::Rmpz_add($u, $u, $t);
                Math::GMPz::Rmpz_div_2exp($u, $u, 1);

                # u *= v^k
                Math::GMPz::Rmpz_mul($u, $u, $w);

                ($m == 1)
                  ? Math::GMPz::Rmpz_add($sum, $sum, $u)
                  : Math::GMPz::Rmpz_sub($sum, $sum, $u);
            }
        }
    }

    return bless \$sum;
}

sub squarefree_sum {
    (TWO)->powerfree_sum(@_);
}

sub cubefree_sum {
    (THREE)->powerfree_sum(@_);
}

sub _native_squarefree_count {
    my ($n) = @_;

    if (HAS_PRIME_UTIL) {
        return Math::Prime::Util::powerfree_count($n, 2);
    }

    my $s = Math::Prime::Util::GMP::sqrtint($n);

    if ($n < ((~0) >> 1)) {

        # Basic method from https://arxiv.org/pdf/1107.4890.pdf
        # Based on code by Dana Jacobsen (translated from Math::Prime::Util).

        use integer;

        my $I = Math::Prime::Util::GMP::rootint($n, 5);
        my $D = Math::Prime::Util::GMP::sqrtint($n / $I);

        my $S1 = $n;

        my $k       = 2;
        my @M       = (0, 1);
        my $mertens = 1;

        foreach my $mu (Math::Prime::Util::GMP::moebius(2, $D)) {
            if ($mu) {
                $S1      += $mu * ($n / ($k * $k));
                $mertens += $mu;
            }
            push @M, $mertens;
            ++$k;
        }

        my @Mx;
        my $Mxisum = 0;

        for (my $i = $I - 1 ; $i > 0 ; --$i) {

            my $Mxi = 1;
            my $xi  = Math::Prime::Util::GMP::sqrtint($n / $i);
            my $L   = Math::Prime::Util::GMP::sqrtint($xi);

            foreach my $j (1 .. ($xi / ($L + 1))) {
                $Mxi -= $M[$j] * (($xi / $j) - ($xi / ($j + 1)));
            }
            foreach my $j (2 .. $L) {
                $Mxi -= (($xi / $j) <= $D) ? $M[$xi / $j] : $Mx[$j * $j * $i];
            }
            $Mx[$i] = $Mxi;
            $Mxisum += $Mxi;
        }

        my $S2 = $Mxisum - ($I - 1) * $M[$D];

        return $S1 + $S2;
    }

    # Using moebius(1, sqrt(n)) for values of n <= 2^50
    if ($n < ~0 and $n <= 1125899906842624) {

        my ($count, $k) = (0, 0);

        foreach my $m (Math::Prime::Util::GMP::moebius(1, $s)) {
            ++$k;
            $count += $m * CORE::int($n / ($k * $k)) if $m;
        }

        return $count;
    }

    # Linear counting up to sqrt(n)

    my $count = 0;

    # TODO: segment 1..s into multiple [a,b] ranges and use moebius(a,b)
    my $m;
    foreach my $k (1 .. $s) {
        if ($m = Math::Prime::Util::GMP::moebius($k)) {
            $count += $m * CORE::int($n / ($k * $k));
        }
    }

    return $count;
}

sub powerfree_count {
    my ($k, $from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->powerfree_count($to)->sub($k->powerfree_count($from->dec));
    }

    _valid(\$from);

    my $n = _any2mpz($$from, 0) // return ZERO;
    $k = _any2ui($$k) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    return ZERO if ($k == 0);
    return ONE  if ($k == 1);

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _set_int(Math::Prime::Util::powerfree_count(Math::GMPz::Rmpz_get_ui($n), $k));
    }

    # Squarefree count for native integers
    if ($k == 2 and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _set_int(_native_squarefree_count(Math::GMPz::Rmpz_get_ui($n)));
    }

    _set_int(Math::Prime::Util::GMP::powerfree_count(Math::GMPz::Rmpz_get_str($n, 10), $k));
}

sub squarefree_count {
    (TWO)->powerfree_count(@_);
}

*square_free_count = \&squarefree_count;

sub cubefree_count {
    (THREE)->powerfree_count(@_);
}

sub is_nonsquarefree {
    $_[0]->is_squarefree->not && $_[0]->is_pos;
}

sub is_noncubefree {
    $_[0]->is_cubefree->not && $_[0]->is_pos;
}

sub is_nonpowerfree {
    $_[0]->is_powerfree($_[1])->not && $_[0]->is_pos;
}

sub _sieve_nonpowerfree {
    my ($A, $B, $k) = @_;

    # $A and $B are Math::GMPz objects
    # $k is a non-negative native integer

    state $m = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my $t = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_root($t, $B, $k);

    my @arr;

    if (
        !Math::GMPz::Rmpz_fits_slong_p($t) or do {
            Math::GMPz::Rmpz_sub($m, $B, $A);
            Math::GMPz::Rmpz_cmp($m, $t) < 0;
        }
      ) {    # too large or small range

        Math::GMPz::Rmpz_set($t, $A);

        my $k_obj = bless \$k;
        my $t_obj = bless \$t;

        my $native_A = Math::GMPz::Rmpz_fits_ulong_p($A);

        for (; Math::GMPz::Rmpz_cmp($t, $B) <= 0 ; Math::GMPz::Rmpz_add_ui($t, $t, 1)) {
            if (HAS_PRIME_UTIL and $native_A and Math::GMPz::Rmpz_fits_ulong_p($t)) {
                unless (Math::Prime::Util::is_powerfree(Math::GMPz::Rmpz_get_ui($t), $k)) {
                    push @arr, Math::GMPz::Rmpz_get_ui($t);
                }
            }
            else {
                unless ($t_obj->is_powerfree($k_obj)) {
                    push @arr,
                      (
                          Math::GMPz::Rmpz_fits_ulong_p($t)
                        ? Math::GMPz::Rmpz_get_ui($t)
                        : Math::GMPz::Rmpz_init_set($t)
                      );
                }
            }
        }

        return \@arr;
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($B)) {

        $A = Math::GMPz::Rmpz_get_ui($A);
        $B = Math::GMPz::Rmpz_get_ui($B);

        foreach my $j (2 .. Math::GMPz::Rmpz_get_ui($t)) {

            my $t = 1;
            my $m = Math::Prime::Util::powint($j, $k);
            my $u = Math::Prime::Util::divint($B, $m);

            if ($A > $m) {
                $t = Math::Prime::Util::divint($A, $m);
                if (Math::Prime::Util::mulint($t, $m) < $A) {
                    ++$t;
                }
            }

            foreach my $s ($t .. $u) {
                if (Math::Prime::Util::is_powerfree($s, $k)) {
                    push @arr, Math::Prime::Util::mulint($m, $s);
                }
            }
        }
    }
    else {
        foreach my $j (2 .. Math::GMPz::Rmpz_get_ui($t)) {

            Math::GMPz::Rmpz_ui_pow_ui($m, $j, $k);
            Math::GMPz::Rmpz_cdiv_q($t, $A, $m);
            Math::GMPz::Rmpz_div($u, $B, $m);

            for (; Math::GMPz::Rmpz_cmp($t, $u) <= 0 ; Math::GMPz::Rmpz_add_ui($t, $t, 1)) {
                if (    HAS_PRIME_UTIL
                    and Math::GMPz::Rmpz_fits_ulong_p($t)
                    and Math::Prime::Util::is_powerfree(Math::GMPz::Rmpz_get_ui($t), $k)) {
                    my $z = Math::GMPz::Rmpz_init();
                    Math::GMPz::Rmpz_mul_ui($z, $m, Math::GMPz::Rmpz_get_ui($t));
                    $z = Math::GMPz::Rmpz_get_ui($z) if Math::GMPz::Rmpz_fits_ulong_p($z);
                    push @arr, $z;
                }
                elsif ((bless \$t)->is_powerfree(bless \$k)) {
                    my $z = Math::GMPz::Rmpz_init();
                    Math::GMPz::Rmpz_mul($z, $m, $t);
                    $z = Math::GMPz::Rmpz_get_ui($z) if Math::GMPz::Rmpz_fits_ulong_p($z);
                    push @arr, $z;
                }
            }
        }
    }

    @arr = sort { $a <=> $b } @arr;
    return \@arr;
}

sub nonpowerfree {
    my ($k, $A, $B) = @_;

    _valid(\$A);

    $k = _any2ui($$k) || return _array();
    $A = _any2mpz($$A, 0) // return _array();

    if (defined($B)) {
        _valid(\$B);
        $B = _any2mpz($$B, 1) // return _array();
    }
    else {
        $B = $A;
        $A = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($A) <= 0) {
        $A = $ONE;
    }

    if (Math::GMPz::Rmpz_cmp($A, $B) > 0) {
        return _array();
    }

#<<<
    my @nonpowerfree = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_nonpowerfree($A, $B, $k) // return undef};
#>>>

    _array(\@nonpowerfree);
}

sub nonsquarefree {
    (TWO)->nonpowerfree(@_);
}

sub noncubefree {
    (THREE)->nonpowerfree(@_);
}

sub nonpowerfree_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    my $step_value = $TWO**$k;

    if ($step_value < 1e4) {
        $step_value = 1e4;
    }

    _generic_each($from, $to, $block, sub { $step_value }, sub { _sieve_nonpowerfree($_[0], $_[1], $k) });
}

*each_nonpowerfree = \&nonpowerfree_each;

sub nonsquarefree_each {
    (TWO)->nonpowerfree_each(@_);
}

*each_nonsquarefree = \&nonsquarefree_each;

sub noncubefree_each {
    (THREE)->nonpowerfree_each(@_);
}

*each_noncubefree = \&noncubefree_each;

sub nonpowerfree_count {
    my ($k, $from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->nonpowerfree_count($to)->sub($k->nonpowerfree_count($from->dec));
    }

    _valid(\$from);
    $from->sub($k->powerfree_count($from));
}

sub nonsquarefree_count {
    (TWO)->nonpowerfree_count(@_);
}

sub noncubefree_count {
    (THREE)->nonpowerfree_count(@_);
}

sub nonpowerfree_sum {
    my ($k, $from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->nonpowerfree_sum($to)->sub($k->nonpowerfree_sum($from->dec));
    }

    _valid(\$from);
    $from->faulhaber_sum(ONE)->sub($k->powerfree_sum($from));
}

sub nonsquarefree_sum {
    (TWO)->nonpowerfree_sum(@_);
}

sub noncubefree_sum {
    (THREE)->nonpowerfree_sum(@_);
}

sub nth_nonpowerfree {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 1 or goto &nan;
    }
    else {
        $k = 2;
    }

    $n = _any2mpz($$n) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        goto &nan;
    }

    if ($k == 1) {
        return ((bless \$n)->inc);
    }

    # If x is the n-th non-k-powerfree number, then:
    #   x/(x-n) ~ zeta(k)
    #   (x*zeta(k) - n*zeta(k))/x ~ 1
    #   zeta(k) - n*zeta(k)/x ~ 1

    my $prec = $k + Math::GMPz::Rmpz_sizeinbase($n, 2) + 2;

    my $f = Math::MPFR::Rmpfr_init2($prec);
    my $t = Math::MPFR::Rmpfr_init2($prec);
    my $u = Math::MPFR::Rmpfr_init2($prec);

    Math::MPFR::Rmpfr_zeta_ui($t, $k, $ROUND);      # t = zeta(k)
    Math::MPFR::Rmpfr_mul_z($f, $t, $n, $ROUND);    # f = n*zeta(k)

#<<<
    # Conjectured bounds:
    #   lower = ((zeta(k)*(n-1) / (zeta(k)-1)) - n)
    #   upper = ((zeta(k)*(n+1) / (zeta(k)-1)) + n)

    #~ my $w = Math::GMPz::Rmpz_init();
    #~ Math::GMPz::Rmpz_sub_ui($w, $n, 1);
    #~ Math::MPFR::Rmpfr_mul_z($f, $t, $w, $ROUND);
    #~ Math::MPFR::Rmpfr_sub_ui($u, $t, 1, $ROUND);
    #~ Math::MPFR::Rmpfr_div($f, $f, $u, $ROUND);
    #~ Math::MPFR::Rmpfr_sub_z($f, $f, $n, $ROUND);

    #~ my $min = bless \(_any2mpz($f) // goto &nan);

    #~ Math::GMPz::Rmpz_add_ui($w, $n, 1);
    #~ Math::MPFR::Rmpfr_mul_z($f, $t, $w, $ROUND);
    #~ Math::MPFR::Rmpfr_div($f, $f, $u, $ROUND);
    #~ Math::MPFR::Rmpfr_add_z($f, $f, $n, $ROUND);

    #~ my $max = bless \(_any2mpz($f) // goto &nan);
#>>>

    my $w = Math::GMPz::Rmpz_init_set($n);
    my $v = Math::GMPz::Rmpz_init();

    while (1) {

        Math::GMPz::Rmpz_mul_2exp($w, $w, 1);
        Math::MPFR::Rmpfr_div_z($u, $f, $w, $ROUND);    # u = n*zeta(k)/w
        Math::MPFR::Rmpfr_sub($u, $t, $u, $ROUND);      # u = zeta(k) - n*zeta(k)/w

        if (Math::MPFR::Rmpfr_cmp_ui($u, 1) >= 0) {

            if (Math::MPFR::Rmpfr_cmp_ui($u, 1) == 0) {    # exact result
                return bless \$w;
            }

            last;
        }
    }

    Math::GMPz::Rmpz_div_2exp($v, $w, 1);

    my $min = bless \$v;
    my $max = bless \$w;

    my $n_obj = bless \$n;
    my $k_obj = bless \$k;

    bsearch_min(
        $min, $max,
        Sidef::Types::Block::Block->new(
            code => sub {
                $k_obj->nonpowerfree_count($_[0])->cmp($n_obj);
            }
        )
    );
}

sub nth_nonsquarefree {
    my ($n) = @_;
    $n->nth_nonpowerfree(TWO);
}

sub nth_noncubefree {
    my ($n) = @_;
    $n->nth_nonpowerfree(THREE);
}

sub next_nonpowerfree {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
    }
    else {
        $k = TWO;
    }

    # TODO: optimize when n^(1/k) is too large.

    $k->nonpowerfree_count($n)->inc->nth_nonpowerfree($k);
}

sub next_nonsquarefree {
    my ($n) = @_;
    $n->next_nonpowerfree(TWO);
}

sub next_noncubefree {
    my ($n) = @_;
    $n->next_nonpowerfree(THREE);
}

sub prev_nonpowerfree {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
    }
    else {
        $k = TWO;
    }

    # TODO: optimize when n^(1/k) is too large.

    my $count = $k->nonpowerfree_count($n);
    $count = $count->dec if $n->is_nonpowerfree($k);
    $count->nth_nonpowerfree($k);
}

sub prev_nonsquarefree {
    my ($n) = @_;
    $n->prev_nonpowerfree(TWO);
}

sub prev_noncubefree {
    my ($n) = @_;
    $n->prev_nonpowerfree(THREE);
}

sub _sieve_powerfree {
    my ($A, $B, $k) = @_;

    # $A and $B are Math::GMPz objects
    # $k is a non-negative native integer

    my $nonpowerfree = _sieve_nonpowerfree($A, $B, $k) // return undef;

    my $i     = 0;
    my $max_i = $#{$nonpowerfree};

    my @powerfree;
    if (Math::GMPz::Rmpz_fits_slong_p($B)) {

        $A = Math::GMPz::Rmpz_get_ui($A);
        $B = Math::GMPz::Rmpz_get_ui($B);

        foreach my $k ($A .. $B) {
            if ($i <= $max_i and $k == $nonpowerfree->[$i]) {
                ++$i;
            }
            else {
                push @powerfree, $k;
            }
        }
    }
    else {
        my $t = Math::GMPz::Rmpz_init_set($A);

        for (; Math::GMPz::Rmpz_cmp($t, $B) <= 0 ; Math::GMPz::Rmpz_add_ui($t, $t, 1)) {
            if ($i <= $max_i and $t == $nonpowerfree->[$i]) {
                ++$i;
            }
            else {
                push @powerfree,
                  (
                      Math::GMPz::Rmpz_fits_ulong_p($t)
                    ? Math::GMPz::Rmpz_get_ui($t)
                    : Math::GMPz::Rmpz_init_set($t)
                  );
            }
        }
    }

    return \@powerfree;
}

sub powerfree {
    my ($k, $A, $B) = @_;

    _valid(\$A);

    $k = _any2ui($$k) || return _array();
    $A = _any2mpz($$A, 0) // return _array();

    if (defined($B)) {
        _valid(\$B);
        $B = _any2mpz($$B, 1) // return _array();
    }
    else {
        $B = $A;
        $A = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($A) <= 0) {
        $A = $ONE;
    }

    if (Math::GMPz::Rmpz_cmp($A, $B) > 0) {
        return _array();
    }

    if ($k == 2) {
        return ((bless \$A)->squarefree(bless \$B));
    }

#<<<
    my @powerfree = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_powerfree($A, $B, $k) // return undef};
#>>>

    _array(\@powerfree);
}

sub cubefree {
    (THREE)->powerfree(@_);
}

sub powerfree_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 2) {
        return ((bless \$from)->squarefree_each((bless \$to), $block));
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    my $step_value = 1e3;
    _generic_each($from, $to, $block, sub { $step_value }, sub { _sieve_powerfree($_[0], $_[1], $k) });
}

*each_powerfree = \&powerfree_each;

sub cubefree_each {
    (THREE)->powerfree_each(@_);
}

*each_cubefree = \&cubefree_each;

sub _nth_prime_lower {
    my ($n) = @_;
    CORE::int($n * (CORE::log($n) + CORE::log(CORE::log($n)) - 1));
}

sub _nth_prime_upper {
    my ($n) = @_;
    CORE::int($n * (CORE::log($n) + CORE::log(CORE::log($n))));
}

sub _nth_almost_prime_lower {
    my ($n, $k) = @_;

    # Edge Case: Basic validity
    return 1 if $n <= 0;
    return 2 if $n == 1;

    # For small n, the asymptotic formula may underestimate
    return 2 if $n < $k;

    # Case k=1: The Primes
    # The bound n * ln(n) is a valid lower bound for the nth prime for n >= 1.
    # (Rosser's Theorem states p_n > n * ln(n))
    if ($k == 1) {
        return CORE::int($n * CORE::log($n));
    }

    # We need ln(ln(n)) > 1 to ensure the divisor doesn't artificially inflate
    # the result (which would violate the "lower bound" property).
    # ln(ln(n)) > 1 implies ln(n) > e, or n > e^e (~15.15).
    # For n < 16, this asymptotic formula is unstable/inaccurate.
    return 0 if $n < 16;

    # Calculate (k-1)!
    my $fact_km1 = 1;
    $fact_km1 *= $_ for 2 .. ($k - 1);

    my $log_n     = CORE::log($n);
    my $log_log_n = CORE::log($log_n);

    # The asymptotic form is: n * ln(n) * (k-1)! / (ln(ln(n)))^(k-1)
    # Formula based on the inverse of Landau's approximation for pi_k(x)
    my $numerator   = $n * $log_n * $fact_km1;
    my $denominator = $log_log_n**($k - 1);

    # Result is truncated to an integer
    return CORE::int($numerator / $denominator);
}

# ---------------------------------------------------------------------------
# _prime_count_step($y_str)
# Compute an adaptive sieve-step for _prime_count_range.
# Returns a step clamped to [1, SIEVE_UTIL_MAX].
# ---------------------------------------------------------------------------
sub _prime_count_step {
    my ($y_str) = @_;

    # Use native float arithmetic intentionally here; precision loss is
    # acceptable because the result is only a performance hint and is
    # clamped below.
    my $y_approx = 0 + $y_str;    # explicit coercion
    if ($y_approx > 0) {
        my $step = _nth_prime_lower($y_approx + CORE::log($y_approx) * 2e3) - _nth_prime_lower($y_approx);
        return $step if ($step > 0 && $step <= SIEVE_UTIL_MAX && $step <= ULONG_MAX);
    }

    return SIEVE_INLINE_MAX;      # safe fallback
}

# ---------------------------------------------------------------------------
# _prime_count_range($x, $y)
# Count primes in the closed interval [$x, $y].
# Both arguments are decimal strings
# ---------------------------------------------------------------------------
sub _prime_count_range {
    my ($x, $y) = @_;

    my $diff = Math::Prime::Util::GMP::subint($y, $x);

    if ($diff < 0) {
        return 0;
    }

    if ($diff eq '0') {
        return (_is_prob_prime($x) ? 1 : 0);
    }

    # Fast path: native Math::Prime::Util handles small ranges up to ULONG_MAX
    if (HAS_PRIME_UTIL and $y < ULONG_MAX) {
        return Math::Prime::Util::prime_count("$x", "$y");
    }

    # Medium path: GMP sieve fits in memory
    if ($diff <= SIEVE_INLINE_MAX) {
        my $count = () = Math::Prime::Util::GMP::sieve_primes($x, $y);
        return $count;
    }

    # Large path: walk in adaptive steps so each sieve segment stays manageable
    my $step = _prime_count_step("$y");

    my $count = 0;
    my $i     = Math::GMPz::Rmpz_init_set_str("$x", 10);
    Math::GMPz::Rmpz_sub_ui($i, $i, 1);    # start one below $x

    my $end = Math::GMPz::Rmpz_init_set_str("$y", 10);

    while (Math::GMPz::Rmpz_cmp($i, $end) <= 0) {
        my $from = $i + 1;
        my $to   = $i + $step;
        $to = $end if $to > $end;

        $count += () = Math::Prime::Util::GMP::sieve_primes($from, $to);

        Math::GMPz::Rmpz_add_ui($i, $i, $step);
    }

    return $count;
}

# ---------------------------------------------------------------------------
# _count_primes_forward($checkpoint, $target, $initial_count)
# Return pi($target) given pi($checkpoint) = $initial_count,
# where $target >= $checkpoint.
# ---------------------------------------------------------------------------
sub _count_primes_forward {
    my ($checkpoint, $target, $initial_count) = @_;

    my $add   = 0;
    my $delta = $target - $checkpoint;

    if (FAST_MODE and $delta <= NAIVE_LOOP_MAX and $target < ULONG_MAX) {

        # Convert to native integers once, before the inner loop
        my $n  = ref($target)     ? Math::GMPz::Rmpz_get_ui($target)     : $target;
        my $cp = ref($checkpoint) ? Math::GMPz::Rmpz_get_ui($checkpoint) : $checkpoint;

        for (my $k = $cp + 1 ; $k <= $n ; ++$k) {
            ++$add
              if (
                  HAS_PRIME_UTIL
                  ? Math::Prime::Util::is_prime($k)
                  : Math::Prime::Util::GMP::is_prime($k)
                 );
        }
    }
    else {
        # Larger delta — delegate to segmented sieve
        $add = _prime_count_range(Math::Prime::Util::GMP::addint($checkpoint, 1), "$target");
    }

    return Math::Prime::Util::GMP::addint($initial_count, $add);
}

# ---------------------------------------------------------------------------
# _count_primes_backward($checkpoint, $target, $initial_count)
# Return pi($target) given pi($checkpoint) = $initial_count,
# where $target <= $checkpoint.
# ---------------------------------------------------------------------------
sub _count_primes_backward {
    my ($checkpoint, $target, $initial_count) = @_;

    my $subtract = 0;
    my $delta    = $checkpoint - $target;

    if (FAST_MODE and $delta <= NAIVE_LOOP_MAX and $checkpoint < ULONG_MAX) {

        # Convert to native integers once, before the inner loop
        my $n  = ref($target)     ? Math::GMPz::Rmpz_get_ui($target)     : $target;
        my $cp = ref($checkpoint) ? Math::GMPz::Rmpz_get_ui($checkpoint) : $checkpoint;

        for (my $k = $n + 1 ; $k <= $cp ; ++$k) {
            ++$subtract
              if (
                  HAS_PRIME_UTIL
                  ? Math::Prime::Util::is_prime($k)
                  : Math::Prime::Util::GMP::is_prime($k)
                 );
        }
    }
    else {
        # Larger delta — delegate to segmented sieve
        $subtract = _prime_count_range(Math::Prime::Util::GMP::addint($target, 1), "$checkpoint");
    }

    return Math::Prime::Util::GMP::subint($initial_count, $subtract);
}

# ---------------------------------------------------------------------------
# _find_prime_count_checkpoint($n, $sorted_keys_aref)
# Binary-search $sorted_keys_aref for the key closest to $n.
# Returns ($key, $distance, $direction) where $direction is
# 'above' (count up from key) or 'below' (count down from key),
# or (undef, undef, undef) when the table is empty.
# ---------------------------------------------------------------------------
sub _find_prime_count_checkpoint {
    my ($n, $keys) = @_;

    if ($n > ULONG_MAX) {
        $n = Math::GMPz::Rmpz_init_set_str("$n", 10);
    }

    my ($lo, $hi, $idx) = (0, $#$keys, -1);

    while ($lo <= $hi) {
        my $mid = ($lo + $hi) >> 1;
        my $cmp = $keys->[$mid] <=> $n;

        if ($cmp == 0) {
            $idx = $mid;
            last;    # exact hit
        }
        elsif ($cmp < 0) {
            $idx = $mid;       # best lower-bound candidate so far
            $lo  = $mid + 1;
        }
        else {
            $hi = $mid - 1;
        }
    }

    my $key_below = $idx >= 0      ? $keys->[$idx]     : undef;
    my $key_above = $idx < $#$keys ? $keys->[$idx + 1] : undef;

    # Pick whichever neighbour is closer; prefer counting upward on a tie
    if (defined $key_below && defined $key_above) {
        my $d_below = $n - $key_below;
        my $d_above = $key_above - $n;
        return (
                  ($d_below <= $d_above)
                ? ($key_below, $d_below, 'above')
                : ($key_above, $d_above, 'below')
               );
    }

    defined($key_below)
      ? ($key_below, $n - $key_below, 'above')
      : ($key_above, $key_above - $n, 'below');
}

# ---------------------------------------------------------------------------
# _count_from_checkpoint($y, $primepi_lookup, $primepi_keys)
# Internal helper: find the nearest precomputed checkpoint and walk to $y.
# Returns pi($y), or undef if no usable checkpoint exists.
# ---------------------------------------------------------------------------
sub _count_from_checkpoint {
    my ($y, $lookup, $keys) = @_;

    my ($checkpoint, $delta, $direction) = _find_prime_count_checkpoint($y, $keys);

    return undef unless defined $checkpoint;

    my $threshold = CORE::int(CORE::sqrt($checkpoint));
    $threshold = SIEVE_UTIL_MAX if ($threshold > SIEVE_UTIL_MAX);

    return undef if $delta > $threshold;

    say STDERR "prime_count($y): incremental from checkpoint $checkpoint (delta=$delta)"
      if $VERBOSE;

    my $count = $lookup->{"$checkpoint"};

    $direction eq 'above'
      ? _count_primes_forward($checkpoint, $y, $count)
      : _count_primes_backward($checkpoint, $y, $count);
}

# ---------------------------------------------------------------------------
# _build_pi_table($limit)
# Return a reference to an array A where A[n] = pi(n) for 0 <= n <= $limit.
# Built via a simple linear cumulative sum over a sieve bitfield — O(limit).
# ---------------------------------------------------------------------------
sub _build_pi_table {
    my ($limit) = @_;

    my @pi;

    my $k     = 0;
    my $count = 0;

    foreach my $p (@{_primes(2, $limit)}) {
        splice(@pi, $k, $p - $k + 1, ($count) x ($p - $k + 1));
        $k = $p;
        ++$count;
    }

    \@pi;
}

# ---------------------------------------------------------------------------
# _prime_count($x, $y)
# Internal: count primes in [$x, $y].  Both arguments are decimal strings.
# ---------------------------------------------------------------------------
sub _prime_count {
    my ($x, $y) = @_;

    # Normalise argument form: single arg means pi(y), two args means pi(x,y)
    if (defined $y) {
        $x = 2 if $x < 2;
    }
    else {
        ($x, $y) = (2, $x);
    }

    return 0 if ($y < $x or $y <= 1);

    # ----------------------------------------------------------------
    # State initialised once on first call
    # ----------------------------------------------------------------
    state $primepi_lookup = \%PRIMEPI_LOOKUP;

    state $primepi_lookup_keys = [sort { Math::GMPz::Rmpz_cmp($a, $b) }
                                  map  { (FAST_MODE and $_ < ULONG_MAX) ? Math::GMPz::Rmpz_init_set_ui($_) : Math::GMPz::Rmpz_init_set_str($_, 10) }
                                    keys %PRIMEPI_LOOKUP
                                 ];

    # Small dense pi(n) table for the range [0, $table_len]
    state $table_len = _next_prime(HAS_PRIME_UTIL ? 1e5 : 1e6);
    state $pi_table  = _build_pi_table($table_len);

    # ----------------------------------------------------------------
    # Fast path 1: $y fits in the dense table (O(1) lookup)
    # ----------------------------------------------------------------
    if ($x eq '2' && $y < $table_len) {
        return $pi_table->[$y];
    }

    # ----------------------------------------------------------------
    # Fast path 2: exact match in the precomputed lookup
    # ----------------------------------------------------------------
    if ($x eq '2') {
        if (defined(my $val = $primepi_lookup->{"$y"})) {
            return $val;
        }
    }

    # ----------------------------------------------------------------
    # Fast path 3: native Math::Prime::Util handles small $y efficiently
    # ----------------------------------------------------------------
    if (HAS_PRIME_UTIL && $y < 1e7) {
        return Math::Prime::Util::prime_count($x, $y);
    }

    my $diff = Math::Prime::Util::GMP::subint($y, $x);

    # ----------------------------------------------------------------
    # Medium path: checkpoint-based incremental counting
    # Worth trying when the range is large enough that a sieve over the
    # full interval would be slow, but the nearest precomputed value is
    # close enough to $y to make walking there cheap.
    # ----------------------------------------------------------------
    if ($diff > 1e6) {
        my $y_count = _count_from_checkpoint($y, $primepi_lookup, $primepi_lookup_keys);

        if (defined $y_count) {
            return $y_count if $x eq '2';
            my $x_count = _prime_count(Math::Prime::Util::GMP::subint($x, 1));
            return Math::Prime::Util::GMP::subint($y_count, $x_count);
        }
    }

    # ----------------------------------------------------------------
    # Large path 1: external primecount binary (arbitrary precision)
    # ----------------------------------------------------------------
    if ($diff > PRIMECOUNT_MIN && $USE_PRIMECOUNT) {
        my $y_count = `primecount $y`;

        if ($? == 0 && defined $y_count) {
            chomp $y_count;
            my $x_count = _prime_count(Math::Prime::Util::GMP::subint($x, 1));
            return Math::Prime::Util::GMP::subint($y_count, $x_count);
        }
    }

    # ----------------------------------------------------------------
    # Large path 2: narrow range near the top of the interval —
    # use a targeted sieve rather than a full pi(y) computation.
    # ----------------------------------------------------------------
    my $x_str    = "$x";
    my $y_str    = "$y";
    my $near_top = ($y >= 1e8 && $diff <= 1e4) || ($x_str / $y_str >= 0.9999);

    if ($near_top) {

        if (HAS_PRIME_UTIL && $y < PRIMECOUNT_MIN) {
            return Math::Prime::Util::prime_count($x_str, $y_str);
        }

        if ($diff >= 1e5 && $diff <= SIEVE_INLINE_MAX) {
            my $count = () = Math::Prime::Util::GMP::sieve_primes($x_str, $y_str);
            return $count;
        }

        return _prime_count_range($x_str, $y_str);
    }

    # ----------------------------------------------------------------
    # Large path 3: native Math::Prime::Util for sub-ULONG ranges
    # ----------------------------------------------------------------
    if (HAS_PRIME_UTIL and $y < ULONG_MAX) {
        return Math::Prime::Util::prime_count($x_str, $y_str);
    }

    my $y_count = _count_from_checkpoint($y, $primepi_lookup, $primepi_lookup_keys);

    if (defined $y_count) {
        return $y_count if $x_str eq '2';
        my $x_count = _prime_count(Math::Prime::Util::GMP::subint($x_str, 1));
        return Math::Prime::Util::GMP::subint($y_count, $x_count);
    }

    # ----------------------------------------------------------------
    # Fallback: full segmented sieve over the entire range
    # ----------------------------------------------------------------
    _prime_count_range($x_str, $y_str);
}

# ---------------------------------------------------------------------------
# prime_count (public method)
# ---------------------------------------------------------------------------
sub prime_count {
    my ($x, $y) = @_;

    if (defined $y) {
        _valid(\$y);
        $x = _big2istr($$x) // return ZERO;
        $x = 2 if $x < 2;
        $y = _big2uistr($$y) // return ZERO;
    }
    else {
        $y = _big2uistr($$x) // return ZERO;
        $x = 2;
    }

    return ZERO if $y < $x;

    _set_int(_prime_count($x, $y));
}

*primepi      = \&prime_count;
*count_primes = \&prime_count;
*primes_count = \&prime_count;

sub prime_count_lower {
    my ($n) = @_;

    $n = $$n;

    if (HAS_PRIME_UTIL and _fits_ulong($n)) {
        return _set_int(Math::Prime::Util::prime_count_lower(_get_ulong($n)));
    }

    $n = _any2mpz($n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    _set_int(Math::Prime::Util::GMP::prime_count_lower(Math::GMPz::Rmpz_get_str($n, 10)));
}

*primepi_lower = \&prime_count_lower;

sub prime_count_upper {
    my ($n) = @_;

    $n = $$n;

    if (HAS_PRIME_UTIL and _fits_ulong($n)) {
        return _set_int(Math::Prime::Util::prime_count_upper(_get_ulong($n)));
    }

    $n = _any2mpz($n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    _set_int(Math::Prime::Util::GMP::prime_count_upper(Math::GMPz::Rmpz_get_str($n, 10)));
}

*primepi_upper = \&prime_count_upper;

sub prime_power_count_lower {
    my ($n) = @_;

    $n = $$n;

    if (HAS_PRIME_UTIL and _fits_ulong($n)) {
        return _set_int(Math::Prime::Util::prime_power_count_lower(_get_ulong($n)));
    }

    $n = _any2mpz($n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $t     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $k (1 .. __ilog__($n, 2)) {
        Math::GMPz::Rmpz_root($t, $n, $k);
        my $v = ${(bless \$t)->prime_count_lower};
        ref($v)
          ? Math::GMPz::Rmpz_add($count, $count, $v)
          : Math::GMPz::Rmpz_add_ui($count, $count, $v);
    }

    bless \$count;
}

sub prime_power_count_upper {
    my ($n) = @_;

    $n = $$n;

    if (HAS_PRIME_UTIL and _fits_ulong($n)) {
        return _set_int(Math::Prime::Util::prime_power_count_upper(_get_ulong($n)));
    }

    $n = _any2mpz($n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $t     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $k (1 .. __ilog__($n, 2)) {
        Math::GMPz::Rmpz_root($t, $n, $k);
        my $v = ${(bless \$t)->prime_count_upper};
        ref($v)
          ? Math::GMPz::Rmpz_add($count, $count, $v)
          : Math::GMPz::Rmpz_add_ui($count, $count, $v);
    }

    bless \$count;
}

sub composite_count_lower {
    my ($n) = @_;
    $n->sub($n->prime_count_upper)->dec;
}

sub composite_count_upper {
    my ($n) = @_;
    $n->sub($n->prime_count_lower)->dec;
}

sub _nth_composite_lower_bound {
    my ($n) = @_;

    # n + n/log(n) + n/(log(n)**2)
    my $log_n = $n->log;
    $n->add($n->div($log_n))->add($n->div($log_n->mul($log_n)));
}

sub _nth_composite_upper_bound {
    my ($n) = @_;

    # n + n/log(n) + (3*n)/(log(n)**2))
    my $log_n = $n->log;
    $n->add($n->div($log_n))->add($n->mul(THREE)->div($log_n->mul($log_n)));
}

sub nth_composite_lower {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($z) <= 0) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp_ui($z, 3) <= 0) {
        return _set_int(4);
    }

    bsearch_min(
        $n->_nth_composite_lower_bound,
        $n->_nth_composite_upper_bound,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->composite_count_upper->cmp($n);
            }
        )
    );
}

*composite_lower = \&nth_composite_lower;

sub nth_composite_upper {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($z) < 0) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp_ui($z, 3) <= 0) {
        return _set_int(8);
    }

    bsearch_max(
        $n->_nth_composite_lower_bound,
        $n->_nth_composite_upper_bound,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->composite_count_lower->cmp($n);
            }
        )
    );
}

*composite_upper = \&nth_composite_upper;

sub _nth_prime_lower_bound {
    my ($n) = @_;
    my $log_n = $n->log;
    $n->mul($log_n->add($log_n->log)->dec);
}

sub _nth_prime_upper_bound {
    my ($n) = @_;
    my $log_n = $n->log;
    $n->mul($log_n->add($log_n->log));
}

sub nth_prime_lower {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_cmp_ui($z, 10) <= 0) {
        return $n->nth_prime;
    }

    bsearch_min(
        $n->_nth_prime_lower_bound,
        $n->_nth_prime_upper_bound,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->prime_count_upper->cmp($n);
            }
        )
    );
}

*prime_lower = \&nth_prime_lower;

sub nth_prime_upper {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_cmp_ui($z, 10) <= 0) {
        return $n->nth_prime;
    }

    bsearch_max(
        $n->_nth_prime_lower_bound,
        $n->_nth_prime_upper_bound,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->prime_count_lower->cmp($n);
            }
        )
    );
}

*prime_upper = \&nth_prime_upper;

sub nth_prime_power_lower {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($z) <= 0) {
        return ZERO;
    }

    bsearch_min(
        $n,
        $n->nth_prime_upper,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->prime_power_count_upper->cmp($n);
            }
        )
    );
}

*prime_power_lower = \&nth_prime_power_lower;

sub nth_prime_power_upper {
    my ($n) = @_;

    my $z = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($z) <= 0) {
        return ZERO;
    }

    bsearch_max(
        $n,
        $n->nth_prime_upper,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->prime_power_count_lower->cmp($n);
            }
        )
    );
}

*prime_power_upper = \&nth_prime_power_upper;

sub almost_prime_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {

        # TODO: implement k-almost prime counting in a given range and use it when |from-to| < sqrt(to).
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->almost_prime_count($to)->sub($k->almost_prime_count($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_count;
    }
    elsif ($k == 2) {
        return $_[1]->semiprime_count;
    }

    my $n = _any2mpz($$from) // return ZERO;

    if (Math::GMPz::Rmpz_cmp_ui($n, $k) <= 0) {
        return ZERO;
    }

    if ($k == 0) {
        return ONE;
    }

    state $pi_k_lookup = {
        '3' => {
                "10000000"             => "2444359",
                "100000000"            => "23727305",
                "1000000000"           => "229924367",
                "10000000000"          => "2227121996",
                "100000000000"         => "21578747909",
                "1000000000000"        => "209214982913",
                "10000000000000"       => "2030133769624",
                "100000000000000"      => "19717814526785",
                "1000000000000000"     => "191693417109381",
                "10000000000000000"    => "1865380637252270",
                "100000000000000000"   => "18168907486812690",
                "1000000000000000000"  => "177123437184971927",
                "10000000000000000000" => "1728190923820610000",
               },

        '4' => {
                "10000000"             => "2050696",
                "100000000"            => "20959322",
                "1000000000"           => "212385942",
                "10000000000"          => "2139236881",
                "100000000000"         => "21454599814",
                "1000000000000"        => "214499908019",
                "10000000000000"       => "2139634739326",
                "100000000000000"      => "21306682904040",
                "1000000000000000"     => "211905511283590",
                "10000000000000000"    => "2105504493045818",
                "100000000000000000"   => "20905484578206982",
                "1000000000000000000"  => "207458897819329031",
                "10000000000000000000" => "2057931819347474462",
               },

        '5' => {
                "10000000"            => "1349779",
                "100000000"           => "14371023",
                "1000000000"          => "150982388",
                "10000000000"         => "1570678136",
                "100000000000"        => "16218372618",
                "1000000000000"       => "166497674684",
                "10000000000000"      => "1701439985694",
                "100000000000000"     => "17323079621014",
                "1000000000000000"    => "175846040834673",
                "10000000000000000"   => "1780617141307093",
                "100000000000000000"  => "17993699600756449",
                "1000000000000000000" => "181520864946969233",
               },

        '6' => {
                "10000000"            => "774078",
                "100000000"           => "8493366",
                "1000000000"          => "91683887",
                "10000000000"         => "977694273",
                "100000000000"        => "10327249593",
                "1000000000000"       => "108264085934",
                "10000000000000"      => "1128049914377",
                "100000000000000"     => "11694704489580",
                "1000000000000000"    => "120734708167792",
                "10000000000000000"   => "1242063105505230",
                "100000000000000000"  => "12739510126065301",
                "1000000000000000000" => "130330025583399801",
               },

        '7' => {
                "10000000"            => "409849",
                "100000000"           => "4600247",
                "1000000000"          => "50678212",
                "10000000000"         => "550454756",
                "100000000000"        => "5913771637",
                "1000000000000"       => "62981797962",
                "10000000000000"      => "665997804082",
                "100000000000000"     => "7001087934965",
                "1000000000000000"    => "73232029374751",
                "10000000000000000"   => "762783057783010",
                "100000000000000000"  => "7916319351632036",
                "1000000000000000000" => "81898808371556517",
               },

        '8' => {
                "10000000"            => "207207",
                "100000000"           => "2367507",
                "1000000000"          => "26483012",
                "10000000000"         => "291646797",
                "100000000000"        => "3173159326",
                "1000000000000"       => "34192782745",
                "10000000000000"      => "365561221293",
                "100000000000000"     => "3882841742380",
                "1000000000000000"    => "41015564702074",
                "10000000000000000"   => "431227959019552",
                "100000000000000000"  => "4515480975731045",
                "1000000000000000000" => "47115876816676830",
               },

        '9' => {
                "10000000"             => "101787",
                "100000000"            => "1180751",
                "1000000000"           => "13377156",
                "10000000000"          => "148930536",
                "100000000000"         => "1636170477",
                "1000000000000"        => "17787688377",
                "10000000000000"       => "191742524399",
                "100000000000000"      => "2052389350029",
                "1000000000000000"     => "21838745177567",
                "10000000000000000"    => "231206458686127",
                "100000000000000000"   => "2437121982958248",
                "1000000000000000000"  => "25591920108631224",
                "10000000000000000000" => "267840642082525459",
               },

        '10' => {
                 "10000000"             => "49163",
                 "100000000"            => "578154",
                 "1000000000"           => "6618221",
                 "10000000000"          => "74342563",
                 "100000000000"         => "823164388",
                 "1000000000000"        => "9011965866",
                 "10000000000000"       => "97765974368",
                 "100000000000000"      => "1052666075366",
                 "1000000000000000"     => "11263041623194",
                 "10000000000000000"    => "119864659464824",
                 "100000000000000000"   => "1269754732725522",
                 "1000000000000000000"  => "13396817167474205",
                 "10000000000000000000" => "140847445420555406",
                },

        '11' => {
                 "10000000"             => "23448",
                 "100000000"            => "279286",
                 "1000000000"           => "3230577",
                 "10000000000"          => "36585097",
                 "100000000000"         => "407818620",
                 "1000000000000"        => "4490844534",
                 "10000000000000"       => "48972151631",
                 "100000000000000"      => "529781669333",
                 "1000000000000000"     => "5693047157230",
                 "10000000000000000"    => "60832290450373",
                 "100000000000000000"   => "646862625625663",
                 "1000000000000000000"  => "6849459596884350",
                 "10000000000000000000" => "72259172519243461",
                },

        '12' => {
                 "10000000"             => "11068",
                 "100000000"            => "133862",
                 "1000000000"           => "1563465",
                 "10000000000"          => "17836903",
                 "100000000000"         => "200051717",
                 "1000000000000"        => "2214357712",
                 "10000000000000"       => "24255601105",
                 "100000000000000"      => "263439785143",
                 "1000000000000000"     => "2841076717752",
                 "10000000000000000"    => "30457549169277",
                 "100000000000000000"   => "324855769153426",
                 "1000000000000000000"  => "3449587218984911",
                 "10000000000000000000" => "36489283363168885",
                },

        '13' => {
                 "100000000"          => "63724",
                 "1000000000"         => "751610",
                 "10000000000"        => "8641282",
                 "100000000000"       => "97493048",
                 "1000000000000"      => "1084343921",
                 "10000000000000"     => "11926066887",
                 "100000000000000"    => "129986121851",
                 "1000000000000000"   => "1406214194147",
                 "10000000000000000"  => "15117458069816",
                 "100000000000000000" => "161651958148243",
                },

        '14' => {
                 "100000000"          => "30143",
                 "1000000000"         => "359812",
                 "10000000000"        => "4167745",
                 "100000000000"       => "47294032",
                 "1000000000000"      => "528497496",
                 "10000000000000"     => "5835244859",
                 "100000000000000"    => "63809981451",
                 "1000000000000000"   => "692279429615",
                 "10000000000000000"  => "7461064471468",
                 "100000000000000000" => "79961411383369",
                },

        '15' => {
                 "100000000"          => "14221",
                 "1000000000"         => "171396",
                 "10000000000"        => "2002277",
                 "100000000000"       => "22864432",
                 "1000000000000"      => "256721609",
                 "10000000000000"     => "2845415088",
                 "100000000000000"    => "31214953362",
                 "1000000000000000"   => "339576381990",
                 "10000000000000000"  => "3668450035737",
                 "100000000000000000" => "39397266129801",
                },

        '16' => {
                 "100000000"          => "6644",
                 "1000000000"         => "81378",
                 "10000000000"        => "959377",
                 "100000000000"       => "11023759",
                 "1000000000000"      => "124381518",
                 "10000000000000"     => "1384019728",
                 "100000000000000"    => "15231822577",
                 "1000000000000000"   => "166147634770",
                 "10000000000000000"  => "1799025041767",
                 "100000000000000000" => "19359235812437",
                },

        '17' => {
                 "1000000000"         => "38537",
                 "10000000000"        => "458176",
                 "100000000000"       => "5301868",
                 "1000000000000"      => "60136853",
                 "10000000000000"     => "671874910",
                 "100000000000000"    => "7418588349",
                 "1000000000000000"   => "81141472649",
                 "10000000000000000"  => "880608939264",
                 "100000000000000000" => "9494920297227",
                },

        '18' => {
                 "10000000000"         => "218163",
                 "100000000000"        => "2544515",
                 "1000000000000"       => "29020610",
                 "10000000000000"      => "325615879",
                 "100000000000000"     => "3607646060",
                 "1000000000000000"    => "39569497036",
                 "10000000000000000"   => "430444178496",
                 "100000000000000000"  => "4650394499994",
                 "1000000000000000000" => "49950199374227",
                },

        '19' => {
                 "10000000000"          => "103657",
                 "100000000000"         => "1218224",
                 "1000000000000"        => "13978352",
                 "10000000000000"       => "157569342",
                 "100000000000000"      => "1752071168",
                 "1000000000000000"     => "19273117219",
                 "10000000000000000"    => "210163154940",
                 "100000000000000000"   => "2275177007273",
                 "1000000000000000000"  => "24480596954105",
                 "10000000000000000000" => "262036734664892",
                },

        '20' => {
                 "10000000000"          => "49032",
                 "100000000000"         => "581979",
                 "1000000000000"        => "6722135",
                 "10000000000000"       => "76139019",
                 "100000000000000"      => "849839564",
                 "1000000000000000"     => "9377172857",
                 "10000000000000000"    => "102511166598",
                 "100000000000000000"   => "1112106715395",
                 "1000000000000000000"  => "11987644585115",
                 "10000000000000000000" => "128513242515056",
                },
    };

    my $v = _big2uistr($$from) // return ZERO;

    if (exists($pi_k_lookup->{$k}) and defined(my $count = $pi_k_lookup->{$k}{$v})) {
        return _set_int($count);
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _set_int(Math::Prime::Util::almost_prime_count($k, Math::GMPz::Rmpz_get_ui($n)));
    }

    my $count = Math::GMPz::Rmpz_init_set_ui(0);
    state $t = Math::GMPz::Rmpz_init_nobless();

    sub {
        my ($m, $p, $k, $j) = @_;

        Math::GMPz::Rmpz_div($t, $n, $m);
        Math::GMPz::Rmpz_root($t, $t, $k);

        Math::GMPz::Rmpz_fits_ulong_p($t) || die "Too large value!";
        my $hi = Math::GMPz::Rmpz_get_ui($t);

        if ($k == 2) {

            foreach my $q (@{_primes($p, $hi)}) {

                Math::GMPz::Rmpz_mul_ui($t, $m, $q);
                Math::GMPz::Rmpz_div($t, $n, $t);

                my $w  = Math::GMPz::Rmpz_fits_ulong_p($t)        ? Math::GMPz::Rmpz_get_ui($t)        : Math::GMPz::Rmpz_get_str($t, 10);
                my $pi = (HAS_PRIME_UTIL and $w < PRIMECOUNT_MIN) ? Math::Prime::Util::prime_count($w) : _prime_count($w);

                if (FAST_MODE and $pi < ULONG_MAX) {
                    Math::GMPz::Rmpz_add_ui($count, $count, $pi - $j);
                }
                else {
                    Math::GMPz::Rmpz_set_str($t, "$pi", 10);
                    Math::GMPz::Rmpz_sub_ui($t, $t, $j);
                    Math::GMPz::Rmpz_add($count, $count, $t);
                }

                ++$j;
            }

            return;
        }

        for (my $q = $p ; $q <= $hi ; $q = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($q) : Math::Prime::Util::GMP::next_prime($q))) {
            __SUB__->($m * $q, $q, $k - 1, $j++);
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 0);

    bless \$count;
}

*pi_k           = \&almost_prime_count;
*almost_primepi = \&almost_prime_count;

sub bigomega_inverse_len {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->almost_prime_count($from, $to);
}

sub almost_prime_sum {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->almost_prime_sum($to)->sub($k->almost_prime_sum($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_sum;
    }

    my $n = _any2mpz($$from) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    if ($k == 0) {
        return ONE;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my $total = Math::GMPz::Rmpz_init_set_ui(0);

    sub {
        my ($m, $p, $k, $j) = @_;

        my $s = do {
            Math::GMPz::Rmpz_div($t, $n, $m);
            Math::GMPz::Rmpz_root($t, $t, $k);
            Math::GMPz::Rmpz_get_ui($t);
        };

        if ($k == 2) {

            foreach my $q (@{_primes($p, $s)}) {

                Math::GMPz::Rmpz_mul_ui($t, $m, $q);
                Math::GMPz::Rmpz_div($u, $n, $t);

                my $w  = Math::GMPz::Rmpz_fits_ulong_p($u)      ? Math::GMPz::Rmpz_get_ui($u)       : Math::GMPz::Rmpz_get_str($u, 10);
                my $ps = (HAS_PRIME_UTIL and $w < PRIMESUM_MIN) ? Math::Prime::Util::sum_primes($w) : ${_set_int($w)->sum_primes};

                if (FAST_MODE and $ps < ULONG_MAX) {
                    Math::GMPz::Rmpz_addmul_ui($total, $t, $ps - $j);
                }
                else {
                    Math::GMPz::Rmpz_set_str($u, "$ps", 10);
                    Math::GMPz::Rmpz_sub_ui($u, $u, $j);
                    Math::GMPz::Rmpz_addmul($total, $u, $t);
                }

                $j += $q;
            }

            return;
        }

        while ($p <= $s) {
            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));
            __SUB__->($m * $p, $p, $k - 1, $j);
            $j += $p;
            $p = $r;
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 0);

    bless \$total;
}

sub bigomega_inverse_sum {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->almost_prime_sum($from, $to);
}

sub squarefree_almost_prime_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->squarefree_almost_prime_count($to)->sub($k->squarefree_almost_prime_count($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_count;
    }

    my $n = _any2mpz($$from) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    if ($k == 0) {
        return ONE;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp($n, _cached_pn_primorial($k)) < 0) {
        return ZERO;
    }

    state $t     = Math::GMPz::Rmpz_init_nobless();
    state $count = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($count, 0);

    sub {
        my ($m, $p, $k, $j) = @_;

        Math::GMPz::Rmpz_div($t, $n, $m);
        Math::GMPz::Rmpz_root($t, $t, $k);

        Math::GMPz::Rmpz_fits_ulong_p($t) || die "Too large value!";
        my $hi = Math::GMPz::Rmpz_get_ui($t);

        if ($k == 2) {

            foreach my $q (@{_primes($p, $hi)}) {

                Math::GMPz::Rmpz_mul_ui($t, $m, $q);
                Math::GMPz::Rmpz_div($t, $n, $t);

                my $w  = Math::GMPz::Rmpz_fits_ulong_p($t)        ? Math::GMPz::Rmpz_get_ui($t)        : Math::GMPz::Rmpz_get_str($t, 10);
                my $pi = (HAS_PRIME_UTIL and $w < PRIMECOUNT_MIN) ? Math::Prime::Util::prime_count($w) : _prime_count($w);

                if (FAST_MODE and $pi < ULONG_MAX) {
                    Math::GMPz::Rmpz_add_ui($count, $count, $pi - $j);
                }
                else {
                    Math::GMPz::Rmpz_set_str($t, "$pi", 10);
                    Math::GMPz::Rmpz_sub_ui($t, $t, $j);
                    Math::GMPz::Rmpz_add($count, $count, $t);
                }

                ++$j;
            }

            return;
        }

        for (; $p <= $hi ; ++$j) {
            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));
            __SUB__->($m * $p, $r, $k - 1, $j + 1);
            $p = $r;
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 1);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($count)
      ? Math::GMPz::Rmpz_get_ui($count)
      : Math::GMPz::Rmpz_init_set($count);

    bless \$r2;
}

*squarefree_pi_k           = \&squarefree_almost_prime_count;
*squarefree_almost_primepi = \&squarefree_almost_prime_count;

sub squarefree_semiprime_count {
    (TWO)->squarefree_almost_prime_count(@_);
}

*squarefree_semiprimes_count = \&squarefree_semiprime_count;

sub squarefree_semiprime_sum {
    (TWO)->squarefree_almost_prime_sum(@_);
}

*squarefree_semiprimes_sum = \&squarefree_semiprime_sum;

sub squarefree_almost_prime_sum {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->squarefree_almost_prime_sum($to)->sub($k->squarefree_almost_prime_sum($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_sum;
    }

    my $n = _any2mpz($$from) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    if ($k == 0) {
        return ONE;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp($n, _cached_pn_primorial($k)) < 0) {
        return ZERO;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my $total = Math::GMPz::Rmpz_init_set_ui(0);

    sub {
        my ($m, $p, $k, $j) = @_;

        my $s = do {
            Math::GMPz::Rmpz_div($t, $n, $m);
            Math::GMPz::Rmpz_root($t, $t, $k);
            Math::GMPz::Rmpz_get_ui($t);
        };

        if ($k == 2) {

            foreach my $q (@{_primes($p, $s)}) {

                $j += $q;
                Math::GMPz::Rmpz_mul_ui($t, $m, $q);
                Math::GMPz::Rmpz_div($u, $n, $t);

                my $w  = Math::GMPz::Rmpz_fits_ulong_p($u)      ? Math::GMPz::Rmpz_get_ui($u)       : Math::GMPz::Rmpz_get_str($u, 10);
                my $ps = (HAS_PRIME_UTIL and $w < PRIMESUM_MIN) ? Math::Prime::Util::sum_primes($w) : ${_set_int($w)->sum_primes};

                if (FAST_MODE and $ps < ULONG_MAX) {
                    Math::GMPz::Rmpz_addmul_ui($total, $t, $ps - $j);
                }
                else {
                    Math::GMPz::Rmpz_set_str($u, "$ps", 10);
                    Math::GMPz::Rmpz_sub_ui($u, $u, $j);
                    Math::GMPz::Rmpz_addmul($total, $u, $t);
                }
            }

            return;
        }

        while ($p <= $s) {
            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));
            $j += $p;
            __SUB__->($m * $p, $r, $k - 1, $j);
            $p = $r;
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 0);

    bless \$total;
}

sub omega_prime_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {

        # TODO: implement k-omega prime counting in a given range and use it when |from-to| < sqrt(to).
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->omega_prime_count($to)->sub($k->omega_prime_count($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_power_count;
    }

    my $n = _any2mpz($$from) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    if ($k == 0) {
        return ONE;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp($n, _cached_pn_primorial($k)) < 0) {
        return ZERO;
    }

    if ((HAS_PRIME_UTIL or (HAS_PRIME_UTIL and $k < 15)) and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _set_int(Math::Prime::Util::omega_prime_count($k, Math::GMPz::Rmpz_get_ui($n)));
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $v = Math::GMPz::Rmpz_init_nobless();

    my $count = Math::GMPz::Rmpz_init_set_ui(0);

    sub {
        my ($m, $p, $k, $j, $s) = @_;

        $s //= do {
            Math::GMPz::Rmpz_div($t, $n, $m);
            Math::GMPz::Rmpz_root($t, $t, $k);
            Math::GMPz::Rmpz_get_ui($t);
        };

        if ($k == 2) {

            for (; $p <= $s ; ++$j) {

                my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));

                for (Math::GMPz::Rmpz_mul_ui($v, $m, $p) ; Math::GMPz::Rmpz_cmp($v, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($v, $v, $p)) {

                    Math::GMPz::Rmpz_div($t, $n, $v);

                    if (Math::GMPz::Rmpz_cmp_ui($t, $r) < 0) {
                        last;
                    }

                    my $w  = Math::GMPz::Rmpz_fits_ulong_p($t)        ? Math::GMPz::Rmpz_get_ui($t)        : Math::GMPz::Rmpz_get_str($t, 10);
                    my $pi = (HAS_PRIME_UTIL and $w < PRIMECOUNT_MIN) ? Math::Prime::Util::prime_count($w) : _prime_count($w);

                    if (FAST_MODE and $pi < ULONG_MAX) {
                        Math::GMPz::Rmpz_add_ui($count, $count, $pi - $j);
                    }
                    else {
                        Math::GMPz::Rmpz_set_str($t, "$pi", 10);
                        Math::GMPz::Rmpz_sub_ui($t, $t, $j);
                        Math::GMPz::Rmpz_add($count, $count, $t);
                    }

                    for (my $r2 = $r ; $r2 <= $w ; $r2 = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($r2) : Math::Prime::Util::GMP::next_prime($r2))) {

                        Math::GMPz::Rmpz_mul_ui($u, $v, $r2);
                        Math::GMPz::Rmpz_mul_ui($u, $u, $r2);

                        if (Math::GMPz::Rmpz_cmp($u, $n) > 0) {
                            last;
                        }

                        my $i = 0;
                        for (; Math::GMPz::Rmpz_cmp($u, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($u, $u, $r2)) {
                            ++$i;
                        }
                        Math::GMPz::Rmpz_add_ui($count, $count, $i);
                    }
                }

                $p = $r;
            }

            return;
        }

        for (; $p <= $s ; ++$j) {

            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));

            for (my $w = $m * $p ; Math::GMPz::Rmpz_cmp($w, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($w, $w, $p)) {

                Math::GMPz::Rmpz_div($t, $n, $w);
                Math::GMPz::Rmpz_root($t, $t, $k - 1);

                my $s = Math::GMPz::Rmpz_get_ui($t);
                last if ($r > $s);
                __SUB__->($w, $r, $k - 1, $j + 1, $s);
            }

            $p = $r;
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 1);

    bless \$count;
}

*omega_primepi = \&omega_prime_count;

sub omega_inverse_len {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->omega_prime_count($from, $to);
}

sub omega_prime_sum {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->omega_prime_sum($to)->sub($k->omega_prime_sum($from->dec));
    }

    $k = _any2ui($$k) // return ZERO;

    if ($k == 1) {
        return $_[1]->prime_power_sum;
    }

    my $n = _any2mpz($$from) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    if ($k == 0) {
        return ONE;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp($n, _cached_pn_primorial($k)) < 0) {
        return ZERO;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $v = Math::GMPz::Rmpz_init_nobless();

    my $total = Math::GMPz::Rmpz_init_set_ui(0);

    sub {
        my ($m, $p, $k, $j, $s) = @_;

        $s //= do {
            Math::GMPz::Rmpz_div($t, $n, $m);
            Math::GMPz::Rmpz_root($t, $t, $k);
            Math::GMPz::Rmpz_get_ui($t);
        };

        if ($k == 2) {

            while ($p <= $s) {

                $j += $p;
                my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));

                for (Math::GMPz::Rmpz_mul_ui($v, $m, $p) ; Math::GMPz::Rmpz_cmp($v, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($v, $v, $p)) {

                    Math::GMPz::Rmpz_div($u, $n, $v);

                    if (Math::GMPz::Rmpz_cmp_ui($u, $r) < 0) {
                        last;
                    }

                    my $w  = Math::GMPz::Rmpz_fits_ulong_p($u)      ? Math::GMPz::Rmpz_get_ui($u)       : Math::GMPz::Rmpz_get_str($u, 10);
                    my $ps = (HAS_PRIME_UTIL and $w < PRIMESUM_MIN) ? Math::Prime::Util::sum_primes($w) : ${_set_int($w)->sum_primes};

                    if (FAST_MODE and $ps < ULONG_MAX) {
                        Math::GMPz::Rmpz_addmul_ui($total, $v, $ps - $j);
                    }
                    else {
                        Math::GMPz::Rmpz_set_str($u, "$ps", 10);
                        Math::GMPz::Rmpz_sub_ui($u, $u, $j);
                        Math::GMPz::Rmpz_addmul($total, $u, $v);
                    }

                    for (my $r2 = $r ; $r2 <= $w ; $r2 = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($r2) : Math::Prime::Util::GMP::next_prime($r2))) {

                        Math::GMPz::Rmpz_mul_ui($u, $v, $r2);
                        Math::GMPz::Rmpz_mul_ui($u, $u, $r2);

                        if (Math::GMPz::Rmpz_cmp($u, $n) > 0) {
                            last;
                        }

                        for (; Math::GMPz::Rmpz_cmp($u, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($u, $u, $r2)) {
                            Math::GMPz::Rmpz_add($total, $total, $u);
                        }
                    }
                }

                $p = $r;
            }

            return;
        }

        while ($p <= $s) {

            $j += $p;
            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));

            for (my $w = $m * $p ; Math::GMPz::Rmpz_cmp($w, $n) <= 0 ; Math::GMPz::Rmpz_mul_ui($w, $w, $p)) {

                Math::GMPz::Rmpz_div($t, $n, $w);
                Math::GMPz::Rmpz_root($t, $t, $k - 1);

                my $s = Math::GMPz::Rmpz_get_ui($t);
                last if ($r > $s);
                __SUB__->($w, $r, $k - 1, $j, $s);
            }

            $p = $r;
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, 0);

    bless \$total;
}

sub omega_inverse_sum {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->omega_prime_sum($from, $to);
}

sub nth_omega_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 1 or goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->nth_prime_power;
    }

    my $k_obj = bless \$k;
    my $n_obj = $n;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) > 0 or do {
        return ONE if (Math::GMPz::Rmpz_sgn($n) == 0);    # not k-omega prime, but...
        goto &nan;
    };

    my $min = Math::GMPz::Rmpz_init();
    my $max = Math::GMPz::Rmpz_init_set($n);

    Math::GMPz::Rmpz_set($min, _cached_pn_primorial($k));
    Math::GMPz::Rmpz_mul_2exp($max, $min, 1);

    while (__cmp__(${$k_obj->omega_prime_count(bless \$max)}, $n) < 0) {
        Math::GMPz::Rmpz_set($min, $max);
        Math::GMPz::Rmpz_mul_ui($max, $max, 2);
    }

    if ((HAS_PRIME_UTIL or (HAS_PRIME_UTIL and $k <= 12)) and Math::GMPz::Rmpz_fits_ulong_p($max)) {
        my $r = Math::Prime::Util::nth_omega_prime($k, Math::GMPz::Rmpz_get_ui($n));
        if ($r) {
            return _set_int("$r");
        }
    }

    my $v     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init();

    while (1) {
        Math::GMPz::Rmpz_add($v, $min, $max);
        Math::GMPz::Rmpz_div_2exp($v, $v, 1);

        $count =
          ((HAS_PRIME_UTIL or (HAS_PRIME_UTIL and $k < 15)) and Math::GMPz::Rmpz_fits_ulong_p($v))
          ? Math::GMPz::Rmpz_init_set_ui(Math::Prime::Util::omega_prime_count($k, Math::GMPz::Rmpz_get_ui($v)))
          : ${$k_obj->omega_prime_count(bless \$v)};

        my $cmp = __cmp__($count, $n);

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($max, $v, 1);
        }
        elsif ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($min, $v, 1);
        }
        else {
            last;
        }
    }

    $k_obj->omega_primes((bless \$min), (bless \$v))->last;
}

sub next_omega_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->next_prime_power;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set(_cached_pn_primorial($k));

    # The smallest k-omega prime is primorial(p_k)
    if (Math::GMPz::Rmpz_cmp($n, $r) < 0) {
        return bless \$r;
    }

    if ($k <= 7 or $n > $r * ($k * $k)) {

        # Optimization for native integers
        if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_slong_p($n)) {
            $n = Math::GMPz::Rmpz_get_ui($n) + 1;
            until (Math::Prime::Util::is_omega_prime($k, $n)) {
                ++$n;
            }
            return _set_int($n);
        }

        Math::GMPz::Rmpz_add_ui($r, $n, 1);

        my $r_obj = bless \$r;

        until ($r_obj->is_omega_prime($k_obj)) {
            Math::GMPz::Rmpz_add_ui($r, $r, 1);
        }

        return $r_obj;
    }

    $k_obj->omega_prime_count($n_obj)->inc->nth_omega_prime($k_obj);
}

sub prev_omega_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->prev_prime_power;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set(_cached_pn_primorial($k));

    # The smallest k-omega prime is primorial(p_k)
    if (Math::GMPz::Rmpz_cmp($n, $r) <= 0) {
        goto &nan;
    }

    if ($k <= 7 or $n > $r * ($k * $k)) {

        # Optimization for native integers
        if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
            $n = Math::GMPz::Rmpz_get_ui($n) - 1;
            until (Math::Prime::Util::is_omega_prime($k, $n)) {
                --$n;
            }
            return bless \$n;
        }

        Math::GMPz::Rmpz_sub_ui($r, $n, 1);

        my $r_obj = bless \$r;

        until ($r_obj->is_omega_prime($k_obj)) {
            Math::GMPz::Rmpz_sub_ui($r, $r, 1);
        }

        return $r_obj;
    }

    my $count = $k_obj->omega_prime_count($n_obj);
    $count = $count->dec if $n_obj->is_omega_prime($k_obj);
    $count->nth_omega_prime($k_obj);
}

sub nth_squarefree_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 1 or goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->nth_prime;
    }

    my $k_obj = _set_int($k);
    my $n_obj = $n;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) > 0 or do {
        return ONE if (Math::GMPz::Rmpz_sgn($n) == 0);    # not k-almost prime, but...
        goto &nan;
    };

    my $min = Math::GMPz::Rmpz_init();
    my $max = Math::GMPz::Rmpz_init_set($n);

    Math::GMPz::Rmpz_set($min, _cached_pn_primorial($k));
    Math::GMPz::Rmpz_mul_2exp($max, $min, 1);

    while (__cmp__(${$k_obj->squarefree_almost_prime_count(bless \$max)}, $n) < 0) {
        Math::GMPz::Rmpz_set($min, $max);
        Math::GMPz::Rmpz_mul_ui($max, $max, 2);
    }

    my $v     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init();

    while (1) {
        Math::GMPz::Rmpz_add($v, $min, $max);
        Math::GMPz::Rmpz_div_2exp($v, $v, 1);

        $count = ${$k_obj->squarefree_almost_prime_count(bless \$v)};

        my $cmp = __cmp__($count, $n);

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($max, $v, 1);
        }
        elsif ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($min, $v, 1);
        }
        else {
            last;
        }
    }

    $k_obj->squarefree_almost_primes((bless \$min), (bless \$v))->last;
}

sub next_squarefree_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->next_prime;
    }
    elsif ($k == 2) {
        return $n->next_squarefree_semiprime;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set(_cached_pn_primorial($k));

    # The smallest squarefree k-almost prime is primorial(p_k)
    if (Math::GMPz::Rmpz_cmp($n, $r) < 0) {
        return bless \$r;
    }

    if ($k <= 7 or $n > $r * ($k * $k)) {

        # Optimization for native integers
        if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_slong_p($n)) {
            $n = Math::GMPz::Rmpz_get_ui($n) + 1;
            until (Math::Prime::Util::is_almost_prime($k, $n) and Math::Prime::Util::is_square_free($n)) {
                ++$n;
            }
            return bless \$n;
        }

        Math::GMPz::Rmpz_add_ui($r, $n, 1);

        my $r_obj = bless \$r;

        until ($r_obj->is_squarefree_almost_prime($k_obj)) {
            Math::GMPz::Rmpz_add_ui($r, $r, 1);
        }

        return $r_obj;
    }

    $k_obj->squarefree_almost_prime_count($n_obj)->inc->nth_squarefree_almost_prime($k_obj);
}

sub prev_squarefree_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->prev_prime;
    }
    elsif ($k == 2) {
        return $n->prev_squarefree_semiprime;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n, 0) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set(_cached_pn_primorial($k));

    # The smallest squarefree k-almost prime is primorial(p_k)
    if (Math::GMPz::Rmpz_cmp($n, $r) <= 0) {
        goto &nan;
    }

    if ($k <= 7 or $n > $r * ($k * $k)) {

        # Optimization for native integers
        if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
            $n = Math::GMPz::Rmpz_get_ui($n) - 1;
            until (Math::Prime::Util::is_almost_prime($k, $n) and Math::Prime::Util::is_square_free($n)) {
                --$n;
            }
            return bless \$n;
        }

        Math::GMPz::Rmpz_sub_ui($r, $n, 1);

        my $r_obj = bless \$r;

        until ($r_obj->is_squarefree_almost_prime($k_obj)) {
            Math::GMPz::Rmpz_sub_ui($r, $r, 1);
        }

        return $r_obj;
    }

    my $count = $k_obj->squarefree_almost_prime_count($n_obj);
    $count = $count->dec if $n_obj->is_squarefree_almost_prime($k_obj);
    $count->nth_squarefree_almost_prime($k_obj);
}

sub nth_sphenic {
    my ($n) = @_;
    $n->nth_squarefree_almost_prime(THREE);
}

sub next_sphenic {
    my ($n) = @_;
    $n->next_squarefree_almost_prime(THREE);
}

sub prev_sphenic {
    my ($n) = @_;
    $n->prev_squarefree_almost_prime(THREE);
}

sub prime_power_count {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        $x = _big2istr($$x) // return ZERO;
        $x = 2 if $x < 2;
        $y = _big2uistr($$y) // return ZERO;
    }
    else {
        $y = _big2uistr($$x) // return ZERO;
        $x = 2;
    }

    if (HAS_PRIME_UTIL and $x == 2 and $y < PRIMECOUNT_MIN) {
        return _set_int(Math::Prime::Util::prime_power_count($y));
    }

    # Support for large integers
    if ($y >= ~0) {

        $x = Math::GMPz::Rmpz_init_set_str("$x", 10);
        $y = Math::GMPz::Rmpz_init_set_str("$y", 10);

        if ($y - $x > 1e6) {

            if ($x == 2) {

                my $pp_count = ZERO;
                my $ilog2    = __ilog__($y, 2);

                for (my $k = 1 ; $k <= $ilog2 ; ++$k) {
                    my $root = Math::Prime::Util::GMP::rootint($y, $k);
                    $pp_count = $pp_count->add(_set_int(_prime_count($root)));
                }

                return $pp_count;
            }

            my $x_pp_count = _set_int($x - 1)->prime_power_count;
            my $y_pp_count = _set_int($y)->prime_power_count;

            my $pp_count = $y_pp_count->sub($x_pp_count);

            return $pp_count;
        }

        my $count = 0;

        for (; Math::GMPz::Rmpz_cmp($x, $y) <= 0 ; Math::GMPz::Rmpz_add_ui($x, $x, 1)) {
            ++$count if Math::Prime::Util::GMP::is_prime_power(Math::GMPz::Rmpz_get_str($x, 10));
        }

        return _set_int($count);
    }

    return ZERO if ($y < $x);

#<<<
    state $pp_table = [ 0,  0,  1,  2,  3,  4,  4,  5,  6,  7,  7,  8,  8,  9,  9,  9, 10, 11, 11, 12, 12,
                       12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 18, 18, 18, 19, 19, 19, 19, 20,
                       20, 21, 21, 21, 21, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 25, 25, 26, 26,
                       26, 27, 27, 27, 28, 28, 28, 28, 29, 29, 30, 30, 30, 30, 30, 30, 31, 31, 32, 32, 33,
                       33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35];

    state $pi_table = [ 0,  0,  1,  2,  2,  3,  3,  4,  4,  4,  4,  5,  5,  6,  6,  6,  6,  7,  7,  8,  8,  8,
                        8,  9,  9,  9,  9,  9,  9, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 14,
                       14, 14, 14, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 18, 18, 18, 18, 18,
                       18, 19, 19, 19, 19, 20, 20, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 23,
                       23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25];
#>>>

    # Optimization for narrow ranges
    if ($y - $x <= 100 or "$x" / "$y" >= 0.999) {

        if ($x <= 2 and $y <= 100) {
            return _set_int($pp_table->[$y]);
        }

        my $count = 0;

        for (; $x <= $y ; ++$x) {
            ++$count if Math::Prime::Util::GMP::is_prime_power($x);
        }

        return ZERO if ($count == 0);
        return ONE  if ($count == 1);

        return _set_int($count);
    }

    my $pp_count = sub {
        my ($n) = @_;

        return $pp_table->[$n] if $n <= 100;

        if (HAS_PRIME_UTIL and $n < PRIMECOUNT_MIN) {
            return Math::Prime::Util::prime_power_count($n);
        }

        my $count = 0;

        foreach my $k (1 .. (HAS_PRIME_UTIL ? Math::Prime::Util::logint($n, 2) : Math::Prime::Util::GMP::logint($n, 2))) {
            my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::rootint($n, $k) : Math::Prime::Util::GMP::rootint($n, $k));

            if ($r <= 100) {
                $count += $pi_table->[$r];
            }
            else {
                $count += (HAS_PRIME_UTIL and $r < PRIMECOUNT_MIN) ? Math::Prime::Util::prime_count($r) : _prime_count($r);
            }
        }

        $count;
    };

    my $x_pp_count = ($x == 2 ? 0 : $pp_count->($x - 1));
    my $y_pp_count = $pp_count->($y);

    my $count = $y_pp_count - $x_pp_count;

    bless \$count;
}

sub prime_power_sum {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->prime_power_sum->sub($from->dec->prime_power_sum);
    }

    # Simple formula:
    #   prime_power_sum(n) = Sum_{k=1..floor(log_2(n))} Sum_{p prime <= n^(1/k)} p^k

    # Optimized formula:
    #   a(n) = Sum_{p prime <= n} p
    #   b(n) = Sum_{p prime <= n^(1/2)} p^2
    #   c(n) = Sum_{p prime <= n^(1/3)} f(p)

    # prime_power_sum(n) = a(n) + b(n) + c(n)

    my $n = _big2pistr($$from) // return ZERO;

    my $sr = Math::Prime::Util::GMP::sqrtint($n);
    my $cr = Math::Prime::Util::GMP::rootint($n, 3);

    my $ps1 = _set_int($n)->sum_primes;
    my $ps2 = (TWO)->sum_primes(_set_int($sr), TWO);

    state $u = Math::GMPz::Rmpz_init_nobless();

    my $z  = Math::GMPz::Rmpz_init_set_str("$n", 10);
    my $pp = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $p (@{_primes(2, $cr)}) {

        # f(p) = sum(3..n.ilog(p), {|k| p**k })
        #      = (p**(n.ilog(p)+1) - 1)/(p-1) - p**2 - p - 1

        my $l = __ilog__($z, $p);
        Math::GMPz::Rmpz_ui_pow_ui($u, $p, $l + 1);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);
        Math::GMPz::Rmpz_divexact_ui($u, $u, $p - 1);
        if (FAST_MODE and $p * $p + $p + 1 < ULONG_MAX) {
            Math::GMPz::Rmpz_sub_ui($u, $u, $p * $p + $p + 1);
        }
        else {    # for very large n (>10^28 on 64-bit and >10^14 on 32-bit)
            my $t = Math::GMPz::Rmpz_init_set_ui($p);
            Math::GMPz::Rmpz_mul_ui($t, $t, $p);
            Math::GMPz::Rmpz_add_ui($t, $t, $p + 1);
            Math::GMPz::Rmpz_sub($u, $u, $t);
        }
        Math::GMPz::Rmpz_add($pp, $pp, $u);
    }

    $ps1->add($ps2)->add(bless \$pp);
}

*prime_powers_sum = \&prime_power_sum;

sub nth_prime {
    my ($n) = @_;

    $n = $$n;
    $n = (ref($n) ? _any2mpz($n) : $n) // goto &nan;
    $n < 0 and goto &nan;

    if ($n == 0) {
        return ONE;    # not a prime, but may be convenient...
    }

    state $nth_prime_lookup = {

        # (10^n)-th prime.
        # OEIS: https://oeis.org/A006988
        "10000000"                   => "179424673",
        "100000000"                  => "2038074743",
        "1000000000"                 => "22801763489",
        "10000000000"                => "252097800623",
        "100000000000"               => "2760727302517",
        "1000000000000"              => "29996224275833",
        "10000000000000"             => "323780508946331",
        "100000000000000"            => "3475385758524527",
        "1000000000000000"           => "37124508045065437",
        "10000000000000000"          => "394906913903735329",
        "100000000000000000"         => "4185296581467695669",
        "1000000000000000000"        => "44211790234832169331",
        "10000000000000000000"       => "465675465116607065549",
        "100000000000000000000"      => "4892055594575155744537",
        "1000000000000000000000"     => "51271091498016403471853",
        "10000000000000000000000"    => "536193870744162118627429",
        "100000000000000000000000"   => "5596564467986980643073683",
        "1000000000000000000000000"  => "58310039994836584070534263",
        "10000000000000000000000000" => "606527267811189857426370533",

        # (2^n)-th prime
        # OEIS: https://oeis.org/A033844
        "1073741824"               => "24563311309",
        "2147483648"               => "50685770167",
        "4294967296"               => "104484802057",
        "8589934592"               => "215187847711",
        "17179869184"              => "442795487221",
        "34359738368"              => "910399916939",
        "68719476736"              => "1870358526653",
        "137438953472"             => "3839726846311",
        "274877906944"             => "7877263558621",
        "549755813888"             => "16149760533341",
        "1099511627776"            => "33089240375501",
        "2199023255552"            => "67756520645329",
        "4398046511104"            => "138666449011757",
        "8796093022208"            => "283634652716357",
        "17592186044416"           => "579863159340527",
        "35184372088832"           => "1184895616861903",
        "70368744177664"           => "2420094683001859",
        "140737488355328"          => "4940729268330643",
        "281474976710656"          => "10082409897709157",
        "562949953421312"          => "20566476729238691",
        "1125899906842624"         => "41935796950796653",
        "2251799813685248"         => "85476377250109733",
        "4503599627370496"         => "174160587542317721",
        "9007199254740992"         => "354733509412061993",
        "18014398509481984"        => "722285281729443799",
        "36028797018963968"        => "1470194760556507397",
        "72057594037927936"        => "2991614170035124397",
        "144115188075855872"       => "6085631874569939777",
        "288230376151711744"       => "12375982557205846193",
        "576460752303423488"       => "25161232392544176197",
        "1152921504606846976"      => "51140670371058101123",
        "2305843009213693952"      => "103917116257220706127",
        "4611686018427387904"      => "211104554420210305087",
        "9223372036854775808"      => "428747374788279617303",
        "18446744073709551616"     => "870566678511500413493",
        "36893488147419103232"     => "1767268299972575740723",
        "73786976294838206464"     => "3586789210729460889317",
        "147573952589676412928"    => "7278050150447936843717",
        "295147905179352825856"    => "14764978793012287880219",
        "590295810358705651712"    => "29947588495888738082431",
        "1180591620717411303424"   => "60730194035557507211347",
        "2361183241434822606848"   => "123129946710886829498713",
        "4722366482869645213696"   => "249598086801961825095881",
        "9444732965739290427392"   => "505870764273226657981427",
        "18889465931478580854784"  => "1025087216809475771050003",
        "37778931862957161709568"  => "2076859014052740233944627",
        "75557863725914323419136"  => "4207073961494759547984247",
        "151115727451828646838272" => "8520834035044766488749161",
        "302231454903657293676544" => "17254990129969542495182251",
                              };

    if (exists($nth_prime_lookup->{$n})) {
        return _set_int($nth_prime_lookup->{$n});
    }

    if ($n > 1_000_000) {

        if ($n > (PRIMECOUNT_MIN >> 6) and $USE_PRIMECOUNT) {
            my $p = `primecount -n $n`;
            if ($? == 0 and defined($p)) {
                chomp($p);
                if ($p) {    # make sure p is not zero
                    return _set_int($p);
                }
            }
        }

        if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            if ((INTSIZE <= 32) ? ($n < 203280221) : 1) {
                return _set_int(Math::Prime::Util::nth_prime("$n"));
            }
        }

        my $min = _any2mpz(${_set_int($n)->nth_prime_lower});
        my $max = _any2mpz(${_set_int($n)->nth_prime_upper});

        if (Math::GMPz::Rmpz_fits_ulong_p($max)) {
            $min = Math::GMPz::Rmpz_get_ui($min);
            $max = Math::GMPz::Rmpz_get_ui($max);
        }

        my $k = 0;
        my $count;

        while (1) {
            $k =
              (HAS_PRIME_UTIL and $max < ULONG_MAX)
              ? Math::Prime::Util::divint(Math::Prime::Util::addint($min, $max), 2)
              : Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::addint($min, $max), 2);

            $count =
              (HAS_PRIME_UTIL and $k < PRIMECOUNT_MIN)
              ? Math::Prime::Util::prime_count($k)
              : _prime_count($k);

            if ($k > (ULONG_MAX >> 1)) {
                $k = _str2obj("$k");
            }

            if ($count > (ULONG_MAX >> 1)) {
                $count = _str2obj("$count");
            }

            if (CORE::abs($count - $n) <= CORE::int("$k"**(2 / 3))) {
                last;
            }

            my $cmp = $count <=> $n;

            if ($cmp > 0) {
                $max = $k - 1;
            }
            elsif ($cmp < 0) {
                $min = $k + 1;
            }
            else {
                last;
            }
        }

        if (!_is_prob_prime($k)) {
            $k = _prev_prime($k);
        }

        my $cmp = ($n <=> $count);
        while ($count != $n) {
            $k = ($cmp < 0) ? _prev_prime($k) : _next_prime($k);
            $count += $cmp;
        }

        return _set_int($k);
    }

    state @table;

    my $limit = 1000 + (2 * $n * CORE::int(CORE::log("$n")));
    $limit = 15_485_863 if $limit > 15_485_863;

    if (@table < $n) {
        $table[0] = 2;
        push @table, @{_primes($table[-1] + 1, $limit)};
    }

    _set_int($table[$n - 1]);
}

*prime = \&nth_prime;

sub nth_prime_power {
    my ($n) = @_;

    $n = $$n;
    $n = (ref($n) ? _any2mpz($n) : $n) // goto &nan;
    $n < 0 and goto &nan;

    return ONE   if ($n == 0);    # not a prime power, but...
    return TWO   if ($n == 1);
    return THREE if ($n == 2);

    # Lower and upper bounds
    my $min = $n;
    my $max = _nth_prime_upper(_big2uistr($n));

    if ($n > 1e6) {

        # Better bounds for the n-th prime power
        $min = _any2mpz(${_set_int($n)->nth_prime_power_lower});
        $max = _any2mpz(${_set_int($n)->nth_prime_power_upper});

        if (Math::GMPz::Rmpz_fits_ulong_p($max)) {
            $min = Math::GMPz::Rmpz_get_ui($min);
            $max = Math::GMPz::Rmpz_get_ui($max);
        }
    }

    if (HAS_PRIME_UTIL and $n < PRIMECOUNT_MIN and !ref($max)) {
        return _set_int(Math::Prime::Util::nth_prime_power($n));
    }

    my $k = 0;
    my $count;

    while (1) {
        $k =
          (HAS_PRIME_UTIL and $max < ULONG_MAX)
          ? Math::Prime::Util::divint(Math::Prime::Util::addint($min, $max), 2)
          : Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::addint($min, $max), 2);

        $count =
          (HAS_PRIME_UTIL and $k < PRIMECOUNT_MIN)
          ? Math::Prime::Util::prime_power_count($k)
          : ${_set_int($k)->prime_power_count};

        if ($k > (ULONG_MAX >> 1)) {
            $k = _str2obj("$k");
        }

        if ($count > (ULONG_MAX >> 1)) {
            $count = _str2obj("$count");
        }

        if (CORE::abs($count - $n) <= CORE::int(CORE::sqrt($k))) {
            last;
        }

        my $cmp = ($count <=> $n);

        if ($cmp > 0) {
            $max = $k - 1;
        }
        elsif ($cmp < 0) {
            $min = $k + 1;
        }
        else {
            last;
        }
    }

    until (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power($k) : Math::Prime::Util::GMP::is_prime_power($k)) {
        --$k;
    }

    my $cmp = ($n <=> $count);
    while ($count != $n) {
        do { $k += $cmp } until (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power($k) : Math::Prime::Util::GMP::is_prime_power($k));
        $count += $cmp;
    }

    _set_int($k);
}

sub composite_count {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->composite_count->sub($from->dec->composite_count);
    }

    my $n = _any2mpz($$from) // goto &nan;

    Math::GMPz::Rmpz_cmp_ui($n, 4) >= 0
      or return ZERO;

    my $pi = _set_int(_prime_count(Math::GMPz::Rmpz_get_str($n, 10)));
    bless \__dec__(__sub__($n, $$pi));    # n - pi(n) - 1
}

sub composite_sum {
    my ($from, $to, $k) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->composite_sum(undef, $k)->sub($from->dec->composite_sum(undef, $k));
    }

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
    }
    else {
        $k = 1;
    }

    if ($k == 0) {
        return $from->composite_count;
    }

    my $n = _any2mpz($$from) // goto &nan;

    Math::GMPz::Rmpz_cmp_ui($n, 4) >= 0
      or return ZERO;

    (bless \$n)->faulhaber_sum(bless \$k)->sub((TWO)->prime_sum((bless \$n), (bless \$k)))->dec;
}

*composites_sum = \&composite_sum;

sub nth_composite {
    my ($n) = @_;

    $n = $$n;
    $n = (ref($n) ? _any2mpz($n) : $n) // goto &nan;
    $n < 0 and goto &nan;

    return ONE         if ($n == 0);    # not composite, but...
    return _set_int(4) if ($n == 1);

    my ($min, $max);

    if ($n > 1e6) {

        # Better bounds for the n-th composite number
        $min = _any2mpz(${_set_int($n)->nth_composite_lower});
        $max = _any2mpz(${_set_int($n)->nth_composite_upper});

        if (Math::GMPz::Rmpz_fits_ulong_p($max)) {
            $min = Math::GMPz::Rmpz_get_ui($min);
            $max = Math::GMPz::Rmpz_get_ui($max);
        }
    }
    else {
        my $k = ref($n) ? _big2uistr($n) : $n;

        # Lower and upper bounds from A002808 (for n >= 4).
        $min = CORE::int($k + $k / CORE::log($k) + $k / (CORE::log($k)**2));
        $max = CORE::int($k + $k / CORE::log($k) + (3 * $k) / (CORE::log($k)**2));
    }

    if ($n < 4) {
        $min = 4;
        $max = 8;
    }

    my $k = 0;
    my $count;

    while (1) {

        $k =
          (HAS_PRIME_UTIL and $max < ULONG_MAX)
          ? Math::Prime::Util::divint(Math::Prime::Util::addint($min, $max), 2)
          : Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::addint($min, $max), 2);

        my $pi =
          (HAS_PRIME_UTIL and $k < PRIMECOUNT_MIN)
          ? Math::Prime::Util::prime_count($k)
          : _prime_count($k);

        if ($k > (ULONG_MAX >> 1)) {
            $k = _str2obj("$k");
        }

        if ($pi > (ULONG_MAX >> 1)) {
            $pi = _str2obj("$pi");
        }

        $count = $k - $pi - 1;

        if (CORE::abs($count - $n) <= CORE::int(CORE::sqrt($k))) {
            last;
        }

        my $cmp = $count <=> $n;

        if ($cmp > 0) {
            $max = $k - 1;
        }
        elsif ($cmp < 0) {
            $min = $k + 1;
        }
        else {
            last;
        }
    }

    if (_is_prob_prime($k)) {
        --$k;
    }

    my $cmp = ($n <=> $count);
    while ($count != $n) {
        do { $k += $cmp } while _is_prob_prime($k);
        $count += $cmp;
    }

    _set_int($k);
}

*composite = \&nth_composite;

sub nth_squarefree {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_fits_slong_p($n)
      or return ((bless \$n)->nth_powerfree(TWO));

    $n = _any2ui($n) // goto &nan;

    return ZERO if ($n == 0);    # not squarefree, but...
    return ONE  if ($n == 1);

    if (HAS_PRIME_UTIL) {
        my $r = Math::Prime::Util::nth_powerfree($n, 2);
        return bless \$r;
    }

    my $zeta2 = 1.64493406684822643647241516664603;
    my $k     = CORE::int($zeta2 * $n);

    my $count =
      HAS_PRIME_UTIL
      ? Math::Prime::Util::powerfree_count($k, 2)
      : _native_squarefree_count($k);

    until (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($k) : Math::Prime::Util::GMP::moebius($k)) {
        --$k;
    }

    my $cmp = ($n <=> $count);
    while ($count != $n) {
        do { $k += $cmp } until (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($k) : Math::Prime::Util::GMP::moebius($k));
        $count += $cmp;
    }

    _set_int($k);
}

sub nth_powerfree {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 2;
    $n = _any2mpz($$n, 0) // goto &nan;

    $k >= 2 or goto &nan;
    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;

    return ZERO if (Math::GMPz::Rmpz_sgn($n) == 0);         # not k-powerfree, but...
    return ONE  if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0);

    if ($k == 2 and Math::GMPz::Rmpz_fits_slong_p($n)) {
        return ((bless \$n)->nth_squarefree);
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_slong_p($n)) {
        my $r = Math::Prime::Util::nth_powerfree(Math::GMPz::Rmpz_get_ui($n), $k);
        return bless \$r;
    }

    _set_int(Math::Prime::Util::GMP::nth_powerfree(Math::GMPz::Rmpz_get_str($n, 10), $k));
}

sub nth_cubefree {
    my ($n) = @_;
    $n->nth_powerfree(THREE);
}

sub legendre {
    my ($x, $y) = @_;
    _valid(\$y);
    my $s = Math::GMPz::Rmpz_legendre(_any2mpz($$x, 0) // (goto &nan), _any2mpz($$y, 1) // (goto &nan));
    $s ? (($s == 1) ? ONE : MONE) : ZERO;
}

*Legendre        = \&legendre;
*legendre_symbol = \&legendre;

sub jacobi {
    my ($x, $y) = @_;
    _valid(\$y);
    my $s = Math::GMPz::Rmpz_jacobi(_any2mpz($$x, 0) // (goto &nan), _any2mpz($$y, 1) // (goto &nan));
    $s ? (($s == 1) ? ONE : MONE) : ZERO;
}

*Jacobi        = \&jacobi;
*jacobi_symbol = \&jacobi;

sub kronecker {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    my $s;

    if (FAST_MODE and !ref($x)) {
        if (!ref($y)) {
            $s = (HAS_PRIME_UTIL ? Math::Prime::Util::kronecker($x, $y) : Math::Prime::Util::GMP::kronecker($x, $y));
        }
        elsif ($x >= 0) {
            $s = Math::GMPz::Rmpz_ui_kronecker($x, _any2mpz($y, 0) // (goto &nan));
        }
        else {
            $s = Math::GMPz::Rmpz_si_kronecker($x, _any2mpz($y, 0) // (goto &nan));
        }
    }
    elsif (FAST_MODE and !ref($y)) {
        if ($y >= 0) {
            $s = Math::GMPz::Rmpz_kronecker_ui(_any2mpz($x, 0) // (goto &nan), $y);
        }
        else {
            $s = Math::GMPz::Rmpz_kronecker_si(_any2mpz($x, 0) // (goto &nan), $y);
        }
    }
    else {
        $s = Math::GMPz::Rmpz_kronecker(_any2mpz($x, 0) // (goto &nan), _any2mpz($y, 1) // (goto &nan));
    }

    $s ? (($s == 1) ? ONE : MONE) : ZERO;
}

*Kronecker        = \&kronecker;
*kronecker_symbol = \&kronecker;

sub kronecker_delta {
    my ($x, $y) = @_;
    _valid(\$y);
    __eq__($$x, $$y) ? ONE : ZERO;
}

*δ              = \&kronecker_delta;
*KroneckerDelta = \&kronecker_delta;

sub hclassno {
    my ($n) = @_;

    # Algorithm from Math::Prime::Util::PP

    $n = _any2mpz($$n, 0) // goto &nan;

    my $sgn = Math::GMPz::Rmpz_sgn($n);

    if ($sgn < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);    # copy
        Math::GMPz::Rmpz_abs($n, $n);
    }
    elsif ($sgn == 0) {
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_si($q, -1, 12);
        return bless \$q;
    }

    if (Math::GMPz::Rmpz_congruent_ui_p($n, 1, 4) or Math::GMPz::Rmpz_congruent_ui_p($n, 2, 4)) {
        return ZERO;
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        state $twelve = _set_int(12);
        return _set_int(Math::Prime::Util::hclassno(Math::GMPz::Rmpz_get_ui($n)))->div($twelve);
    }

    my $square = 0;

    # Pre-allocate all temporary variables to avoid loop allocation overhead
    state $t   = Math::GMPz::Rmpz_init_nobless();
    state $lim = Math::GMPz::Rmpz_init_nobless();
    state $d_z = Math::GMPz::Rmpz_init_nobless();    # For divisor conversion

    my $h  = Math::GMPz::Rmpz_init_set_ui(0);
    my $B  = Math::GMPz::Rmpz_init_set_ui(Math::GMPz::Rmpz_odd_p($n) ? 1 : 0);
    my $B2 = Math::GMPz::Rmpz_init_set($n);

    Math::GMPz::Rmpz_add_ui($B2, $B2, 1);
    Math::GMPz::Rmpz_div_2exp($B2, $B2, 2);

    if (Math::GMPz::Rmpz_sgn($B) == 0) {
        Math::GMPz::Rmpz_sqrt($lim, $B2);

        if (Math::GMPz::Rmpz_perfect_square_p($B2)) {
            $square = 1;
            Math::GMPz::Rmpz_sub_ui($lim, $lim, 1);
        }

        my $count = (
                     HAS_PRIME_UTIL
                     ? Math::Prime::Util::divisor_sum($B2, 0)
                     : Math::Prime::Util::GMP::sigma($B2, 0)
                    ) >> 1;

        Math::GMPz::Rmpz_add_ui($h, $h, $count);

        Math::GMPz::Rmpz_set_ui($B, 2);
        Math::GMPz::Rmpz_set($B2, $n);
        Math::GMPz::Rmpz_add_ui($B2, $B2, 4);
        Math::GMPz::Rmpz_div_2exp($B2, $B2, 2);
    }

    while (1) {

        Math::GMPz::Rmpz_mul_ui($t, $B2, 3);
        Math::GMPz::Rmpz_cmp($t, $n) < 0 or last;

        if (Math::GMPz::Rmpz_divisible_p($B2, $B)) {
            Math::GMPz::Rmpz_add_ui($h, $h, 1);
        }

        Math::GMPz::Rmpz_sqrt($lim, $B2);

        if (Math::GMPz::Rmpz_perfect_square_p($B2)) {
            Math::GMPz::Rmpz_add_ui($h, $h, 1);
            Math::GMPz::Rmpz_sub_ui($lim, $lim, 1);
        }

        my $count = 0;
        foreach my $d (_divisors($B2)) {
            if (FAST_MODE and $d < ULONG_MAX) {
                last     if Math::GMPz::Rmpz_cmp_ui($lim, $d) < 0;
                ++$count if Math::GMPz::Rmpz_cmp_ui($B,   $d) < 0;
            }
            else {
                Math::GMPz::Rmpz_set_str($d_z, "$d", 10);
                last     if Math::GMPz::Rmpz_cmp($d_z, $lim) > 0;
                ++$count if Math::GMPz::Rmpz_cmp($d_z, $B) > 0;
            }
        }

        Math::GMPz::Rmpz_add_ui($h, $h, 2 * $count) if ($count > 0);

        # B = B + 2
        Math::GMPz::Rmpz_add_ui($B, $B, 2);

        # B2 = (B^2 + n) / 4
        Math::GMPz::Rmpz_mul($B2, $B, $B);
        Math::GMPz::Rmpz_add($B2, $B2, $n);
        Math::GMPz::Rmpz_div_2exp($B2, $B2, 2);
    }

    Math::GMPz::Rmpz_mul_ui($t, $B2, 3);

    my $m = ($square ? 2 : 3);

    if ($square or Math::GMPz::Rmpz_cmp($t, $n) == 0) {
        Math::GMPz::Rmpz_mul_ui($h, $h, $m);
        Math::GMPz::Rmpz_add_ui($h, $h, 1);

        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_ui($q, 1, $m);
        Math::GMPq::Rmpq_mul_z($q, $q, $h);
        return bless \$q;
    }

    return bless \$h;
}

sub _sos_k2 {    # OEIS: A004018
    my ($n, $t, $v) = @_;
    return 0 if Math::GMPz::Rmpz_congruent_ui_p($t, 3, 4);

    my $count = Math::GMPz::Rmpz_init_set_ui(4);
    foreach my $pp (_factor_exp($t)) {
        my ($p, $e) = @$pp;
        my $r = (FAST_MODE and $p < ULONG_MAX) ? ($p % 4) : Math::Prime::Util::GMP::modint($p, 4);

        if ($r == 3) {
            return 0 unless $e % 2 == 0;
        }
        if ($r == 1) {
            Math::GMPz::Rmpz_mul_ui($count, $count, $e + 1);
        }
    }
    return $count;
}

sub _sos_k3 {    # OEIS: A005875
    my ($n, $t, $v) = @_;

    ((($v & 1) == 1) || !Math::GMPz::Rmpz_congruent_ui_p($t, 7, 8)) || return 0;

    if (_is_squarefree($n)) {
        if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
            my $count = 0;
            if (Math::GMPz::Rmpz_congruent_ui_p($n, 3, 8)) {
                $count = eval { Math::Prime::Util::GMP::mulint(Math::Prime::Util::hclassno(Math::GMPz::Rmpz_get_ui($n)), 2); };
            }
            else {
                my $tmp_n = $n << 2;
                $count =
                  Math::GMPz::Rmpz_fits_ulong_p($tmp_n)
                  ? eval { Math::Prime::Util::hclassno(Math::GMPz::Rmpz_get_ui($tmp_n)) }
                  : undef;
            }
            return Math::GMPz::Rmpz_init_set_str("$count", 10) if defined($count);
        }

        my $h;
        my $count = Math::GMPz::Rmpz_init();
        if (Math::GMPz::Rmpz_congruent_ui_p($n, 3, 8)) {
            $h = ${(bless \$n)->hclassno->mul(_set_int(24))};
        }
        else {
            $h = ${(bless \$n)->mul(_set_int(4))->hclassno->mul(_set_int(12))};
        }

        $h = _any2mpz($h, 0) if !ref($h);
        (ref($h) eq 'Math::GMPq')
          ? Math::GMPz::Rmpz_set_q($count, $h)
          : Math::GMPz::Rmpz_set($count, $h);

        return $count;
    }

    return undef;
}

sub _sos_k4 {    # OEIS: A000118
    my ($n, $t, $v) = @_;
    my $count = Math::Prime::Util::GMP::mulint(Math::Prime::Util::GMP::sigma(($v >= 1) ? ($t << 1) : $t), 8);
    return Math::GMPz::Rmpz_init_set_str($count, 10);
}

sub _sos_k6 {    # OEIS: A000141
    my ($n, $t, $v) = @_;

    # A000141: a(n) = 4( Sum_{ d|n, d == 3 mod 4} d^2 - Sum_{ d|n, d == 1 mod 4} d^2 )
    #              + 16( Sum_{ d|n, n/d == 1 mod 4} d^2 - Sum_{ d|n, n/d == 3 mod 4} d^2 )

    # a(n) = 16*A050470(n) - 4*A002173(n).

    # Multiplicative formulas by Jianing Song, where Chi = A101455:
    #   A050470: Multiplicative with a(p^e) = ((p^2)^(e+1) - Chi(p)^(e+1))/(p^2 - Chi(p)).
    #   A002173: Multiplicative with a(p^e) = ((p^2*Chi(p))^(e+1) - 1)/(p^2*Chi(p) - 1).

    my $prod1 = Math::GMPz::Rmpz_init_set_ui(1);
    my $prod2 = Math::GMPz::Rmpz_init_set_ui(1);
    my ($p1, $p2, $u1, $u2) = map { Math::GMPz::Rmpz_init() } 1 .. 4;

    foreach my $pp (_factor_exp($t)) {
        my ($p, $e) = @$pp;

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($p1, $p)
          : Math::GMPz::Rmpz_set_str($p1, $p, 10);

        Math::GMPz::Rmpz_pow_ui($u1, $p1, 2 * ($e + 1));
        Math::GMPz::Rmpz_set($u2, $u1);

        my $congr1_4 = Math::GMPz::Rmpz_congruent_ui_p($p1, 1, 4);

        if ($congr1_4 or $e % 2 == 1) {
            Math::GMPz::Rmpz_sub_ui($u1, $u1, 1);
            Math::GMPz::Rmpz_sub_ui($u2, $u2, 1);
        }
        else {
            Math::GMPz::Rmpz_add_ui($u1, $u1, 1);
            Math::GMPz::Rmpz_neg($u2, $u2);
            Math::GMPz::Rmpz_sub_ui($u2, $u2, 1);
        }

        Math::GMPz::Rmpz_mul($p1, $p1, $p1);
        Math::GMPz::Rmpz_set($p2, $p1);

        if ($congr1_4) {
            Math::GMPz::Rmpz_sub_ui($p1, $p1, 1);
        }
        else {
            Math::GMPz::Rmpz_add_ui($p1, $p1, 1);
            Math::GMPz::Rmpz_neg($p2, $p2);
        }
        Math::GMPz::Rmpz_sub_ui($p2, $p2, 1);

        Math::GMPz::Rmpz_divexact($u1, $u1, $p1);
        Math::GMPz::Rmpz_divexact($u2, $u2, $p2);

        Math::GMPz::Rmpz_mul($prod1, $prod1, $u1);
        Math::GMPz::Rmpz_mul($prod2, $prod2, $u2);
    }

    Math::GMPz::Rmpz_mul_2exp($prod1, $prod1, 4 + 2 * $v);
    Math::GMPz::Rmpz_mul_2exp($prod2, $prod2, 2);
    Math::GMPz::Rmpz_sub($prod1, $prod1, $prod2);

    return $prod1;
}

sub _sos_k8 {    # OEIS: A000143
    my ($n, $t, $v) = @_;

    # A138503: a(n) is multiplicative with a(2^e) = -(8^(e+1) - 15) / 7, a(p^e) = ((p^3)^(e+1) - 1) / (p^3 - 1).
    # A138503: Let n = 2^k * m, with m odd, then a(n) = -(8^(k+1) - 15)/7 * sigma_3(m).
    # r_8(n) = 16 * (-1)^n * -A138503(n)

    my $prod = Math::GMPz::Rmpz_init_set_ui(16);

    if ($v > 0) {
        my $s = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_ui_pow_ui($s, 8, $v + 1);
        Math::GMPz::Rmpz_sub_ui($s, $s, 15);
        Math::GMPz::Rmpz_divexact_ui($s, $s, 7);
        Math::GMPz::Rmpz_mul($prod, $prod, $s);
    }

    my $count = Math::Prime::Util::GMP::mulint($prod, Math::Prime::Util::GMP::sigma($t, 3));
    return Math::GMPz::Rmpz_init_set_str($count, 10);
}

sub _sos_k10 {    # OEIS: A000144
    my ($n, $t, $v) = @_;

    # Efficient formula for k = 10, due to Michael Somos:
    # r_10(n) = 4/5 * (A050456(n) + 16*A050468(n) + 8*A030212(n))

    # A050456 is multiplicative with:
    #   a(2^e) = 1
    #   a(p^e) = ((p^4)^(e+1) - 1) / (p^4 - 1)  if p == 1 (mod 4)
    #   a(p^e) = (1 - (-p^4)^(e+1)) / (1 + p^4) if p == 3 (mod 4)

    # A050468 is multiplicative with:
    #   a(2^e) = 16^e
    #   a(p^e) = ((p^4)^(e+1) - 1) / (p^4 - 1)          if p == 1 (mod 4)
    #   a(p^e) = ((p^4)^(e+1) - (-1)^(e+1)) / (p^4 + 1) if p == 3 (mod 4)

    # A030212 is multiplicative with:
    #   a(2^e) = (-4)^e
    #   a(p^e) = p^(2*e) * (1 + (-1)^e)/2             if p == 3 (mod 4)
    #   a(p^e) = a(p) * a(p^(e-1)) - p^4 * a(p^(e-2)) if p == 1 (mod 4)
    # where a(p) = 2 * Re( (x + i*y)^4 ) and p = x^2 + y^2 with even x.

    my @factors = _factor_exp($t);

    my $prod1 = Math::GMPz::Rmpz_init_set_ui(1);
    my $prod2 = Math::GMPz::Rmpz_init_set_ui(1);
    my $prod3 = Math::GMPz::Rmpz_init_set_ui(1);

    state $p  = Math::GMPz::Rmpz_init_nobless();
    state $u1 = Math::GMPz::Rmpz_init_nobless();
    state $u2 = Math::GMPz::Rmpz_init_nobless();

    if (Math::GMPz::Rmpz_congruent_ui_p($t, 3, 4)) {
        Math::GMPz::Rmpz_set_ui($prod3, 0);
    }

    foreach my $factor (Math::GMPz::Rmpz_sgn($prod3) == 0 ? () : @factors) {
        my ($p_str, $e) = @$factor;

        Math::GMPz::Rmpz_set_str($p, $p_str, 10);

        # Case 1: p == 3 (mod 4)
        if (Math::GMPz::Rmpz_congruent_ui_p($p, 3, 4)) {

            if ($e % 2 == 1) {
                Math::GMPz::Rmpz_set_ui($prod3, 0);
                last;    # Short-circuit: the whole product becomes zero
            }

            # a(p^e) = p^(2*e)
            Math::GMPz::Rmpz_pow_ui($u1, $p, 2 * $e);
            Math::GMPz::Rmpz_mul($prod3, $prod3, $u1);
        }

        # Case 2: p == 1 (mod 4)
        else {
            my ($x, $y) = _primitive_sum_of_two_squares($p);

            # Calculate a(p) algebraically: 2 * (x^4 - 6*x^2*y^2 + y^4)
            Math::GMPz::Rmpz_mul($u1, $x, $x);    # x^2
            Math::GMPz::Rmpz_mul($u2, $y, $y);    # y^2

            my $ap  = Math::GMPz::Rmpz_init();
            my $mid = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_mul($mid, $u1, $u2);
            Math::GMPz::Rmpz_mul($ap,  $u1, $u1);      # x^4
            Math::GMPz::Rmpz_mul($u2,  $u2, $u2);      # y^4
            Math::GMPz::Rmpz_mul_ui($mid, $mid, 6);    # 6*x^2*y^2

            Math::GMPz::Rmpz_sub($ap, $ap, $mid);
            Math::GMPz::Rmpz_add($ap, $ap, $u2);
            Math::GMPz::Rmpz_mul_ui($ap, $ap, 2);      # $ap = a(p)

            # Iterative recurrence for a(p^e)
            my $v1 = Math::GMPz::Rmpz_init_set($ap);
            if ($e > 1) {
                Math::GMPz::Rmpz_set_ui($u1, 1);
                Math::GMPz::Rmpz_set_ui($u2, 0);
                Math::GMPz::Rmpz_pow_ui($p, $p, 4);

                for (2 .. $e) {

                    # v2 = ap * v1 - p^4 * v0
                    Math::GMPz::Rmpz_mul($u2, $ap, $v1);
                    Math::GMPz::Rmpz_mul($u1, $p,  $u1);
                    Math::GMPz::Rmpz_sub($u2, $u2, $u1);

                    # Shift variables for next iteration
                    Math::GMPz::Rmpz_set($u1, $v1);
                    Math::GMPz::Rmpz_set($v1, $u2);
                }
            }
            Math::GMPz::Rmpz_mul($prod3, $prod3, $v1);
        }
    }

    # Finally, apply the 2^v component: a(2^v) = (-4)^v
    if ($v >= 1 and Math::GMPz::Rmpz_sgn($prod3)) {
        Math::GMPz::Rmpz_mul_2exp($prod3, $prod3, 2 * $v);
        Math::GMPz::Rmpz_neg($prod3, $prod3) if ($v % 2 == 1);
    }

    foreach my $pp (@factors) {
        my ($p_str, $e) = @$pp;

        (FAST_MODE and $p_str < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($p, $p_str)
          : Math::GMPz::Rmpz_set_str($p, $p_str, 10);

        Math::GMPz::Rmpz_pow_ui($u1, $p, 4 * ($e + 1));
        my $congr1_4 = Math::GMPz::Rmpz_congruent_ui_p($p, 1, 4);
        Math::GMPz::Rmpz_pow_ui($p, $p, 4);

        if ($congr1_4) {
            Math::GMPz::Rmpz_sub_ui($p, $p, 1);
            Math::GMPz::Rmpz_divexact($u1, $u1, $p);
            Math::GMPz::Rmpz_mul($prod1, $prod1, $u1);
            Math::GMPz::Rmpz_mul($prod2, $prod2, $u1);
            next;
        }

        # Here, we have: p == 3 (mod 4)

        Math::GMPz::Rmpz_set($u2, $u1);
        Math::GMPz::Rmpz_add_ui($p, $p, 1);

        if ($e % 2 == 1) {
            Math::GMPz::Rmpz_neg($u1, $u1);
            Math::GMPz::Rmpz_sub_ui($u2, $u2, 1);
        }
        else {
            Math::GMPz::Rmpz_add_ui($u2, $u2, 1);
        }

        Math::GMPz::Rmpz_add_ui($u1, $u1, 1);
        Math::GMPz::Rmpz_divexact($u1, $u1, $p);
        Math::GMPz::Rmpz_divexact($u2, $u2, $p);

        Math::GMPz::Rmpz_mul($prod1, $prod1, $u1);
        Math::GMPz::Rmpz_mul($prod2, $prod2, $u2);
    }

    Math::GMPz::Rmpz_mul_2exp($prod2, $prod2, 4 * $v) if ($v > 0);
    Math::GMPz::Rmpz_mul_2exp($prod2, $prod2, 4);
    Math::GMPz::Rmpz_mul_2exp($prod3, $prod3, 3);
    Math::GMPz::Rmpz_add($prod1, $prod1, $prod2);
    Math::GMPz::Rmpz_add($prod1, $prod1, $prod3);
    Math::GMPz::Rmpz_mul_2exp($prod1, $prod1, 2);
    Math::GMPz::Rmpz_divexact_ui($prod1, $prod1, 5);

    return $prod1;
}

# --- Isolated helper for k=12 prime evaluation ---
sub _compute_b_prime {
    my ($p) = @_;

    $p < ULONG_MAX || die "Too large value!";

    # M = (p - 1) / 2
    my $M = ref($p) ? Math::GMPz::Rmpz_get_ui($p) : $p;
    $M = ($M - 1) >> 1;

    my @S2 = (0) x ($M + 1);
    my @A;

    # Generate the sparse theta series A(q) up to degree M
    my $k = 0;
    while (1) {
        my $Tk = ($k * ($k + 1)) >> 1;
        last if $Tk > $M;
        my $val = 2 * $k + 1;
        $val = -$val if $k % 2 != 0;
        push @A, [$Tk, $val];
        $k++;
    }

    # Compute A(q)^2 in O(M) time due to sparsity
    for my $i (0 .. $#A) {
        my ($T1, $v1) = @{$A[$i]};
        for my $j (0 .. $#A) {
            my ($T2, $v2) = @{$A[$j]};
            my $sum_T = $T1 + $T2;
            last if $sum_T > $M;
            $S2[$sum_T] += $v1 * $v2;
        }
    }

    # Find the coefficient of q^M in (A(q)^2)^2
    my $ans    = Math::GMPz::Rmpz_init_set_ui(0);
    my $term   = Math::GMPz::Rmpz_init();
    my $z_s2_i = Math::GMPz::Rmpz_init();
    my $z_s2_j = Math::GMPz::Rmpz_init();

    for my $i (0 .. $M) {
        my $j = $M - $i;
        if ($S2[$i] && $S2[$j]) {
            Math::GMPz::Rmpz_set_str($z_s2_i, "$S2[$i]", 10);
            Math::GMPz::Rmpz_set_str($z_s2_j, "$S2[$j]", 10);
            Math::GMPz::Rmpz_mul($term, $z_s2_i, $z_s2_j);
            Math::GMPz::Rmpz_add($ans, $ans, $term);
        }
    }

    return $ans;
}

sub _sos_k12 {
    my ($n) = @_;

    # r_12(n) = A000145(n)
    #         = 8*σ_5​(n) - 512*σ_5​(n/4) + 16*b(n)
    #         = 8*sigma_5(n) - f(n) + g(n), where f(n) = 512*sigma_5(n/4) if 4|n, else 0, and g(n) = 16*A000735((n-1)/2) if n is odd, else 0
    #
    # where b(n) is strongly multiplicative with:
    #   b(p^e) = b(p) * b(p^(e−1)) - p^5 * b(p^(e-2)), for odd prime p.
    #   b(2^e) = 1
    #
    # b(p) is the coefficient of q^((p−1)/2) in A(q)^4, where:
    #    A(q) = Sum_{k>=0} ​(−1)^k * (2*k+1) * q^(k*(k+1)/2).

    # Part 1: Compute 8 * sigma_5(n)
    my $A = Math::Prime::Util::GMP::sigma($n, 5);
    $A = Math::Prime::Util::GMP::mulint($A, 8);
    my $termA = Math::GMPz::Rmpz_init_set_str("$A", 10);

    # Part 2: Compute 512 * sigma_5(n/4) if 4|n
    my $termB = Math::GMPz::Rmpz_init_set_ui(0);
    if (Math::GMPz::Rmpz_divisible_2exp_p($n, 2)) {
        my $n_over_4 = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_fdiv_q_2exp($n_over_4, $n, 2);

        my $B = Math::Prime::Util::GMP::sigma($n_over_4, 5);
        $B = Math::Prime::Util::GMP::mulint($B, 512);
        Math::GMPz::Rmpz_set_str($termB, "$B", 10);
    }

    my $ans = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub($ans, $termA, $termB);

    Math::GMPz::Rmpz_odd_p($n) || return $ans;

    # Part 3: Apply the sequence logic + 16*b(n) for odd numbers
    my $b_n = Math::GMPz::Rmpz_init_set_ui(1);

    state %b_prime_cache;
    undef %b_prime_cache if scalar(keys(%b_prime_cache)) > 1e5;

    foreach my $pp (_factor_exp($n)) {
        my ($p, $e) = @$pp;

        my $b_p = ($b_prime_cache{$p} //= _compute_b_prime($p));

        if ($e == 1) {
            Math::GMPz::Rmpz_mul($b_n, $b_n, $b_p);
            next;
        }

        # Handle Multiplicative Prime Powers: b(p^e) = b(p)*b(p^{e-1}) - p^5*b(p^{e-2})
        my $b_prev = Math::GMPz::Rmpz_init_set_ui(1);
        my $b_curr = Math::GMPz::Rmpz_init_set($b_p);

        my $p_pow5 = Math::GMPz::Rmpz_init_set_str($p, 10);
        Math::GMPz::Rmpz_pow_ui($p_pow5, $p_pow5, 5);

        my $b_next = Math::GMPz::Rmpz_init();
        my $term   = Math::GMPz::Rmpz_init();

        for my $k (2 .. $e) {
            Math::GMPz::Rmpz_mul($b_next, $b_p,    $b_curr);
            Math::GMPz::Rmpz_mul($term,   $p_pow5, $b_prev);
            Math::GMPz::Rmpz_sub($b_next, $b_next, $term);

            Math::GMPz::Rmpz_set($b_prev, $b_curr);
            Math::GMPz::Rmpz_set($b_curr, $b_next);
        }
        Math::GMPz::Rmpz_mul($b_n, $b_n, $b_curr);
    }

    Math::GMPz::Rmpz_mul_ui($b_n, $b_n, 16);
    Math::GMPz::Rmpz_add($ans, $ans, $b_n);

    return $ans;
}

sub _sos_k24 {
    my ($n) = @_;

    # r_24(n) = A000156(n)
    #         = (2^4*(sigma_{11}(n)- 2*sigma_{11}(n/2) + 2^{12}*sigma_{11}(n/4)) - 2^7*259*(-1)^n*tau(n) - 2^16*tau(n/2))/691

    # Sigma_{11}(x) terms (A013959)
    # They evaluate to 0 if the argument x is not an integer
    my $sigma_n   = Math::Prime::Util::GMP::sigma($n, 11);
    my $sigma_n_2 = Math::GMPz::Rmpz_divisible_ui_p($n, 2) ? Math::Prime::Util::GMP::sigma($n >> 1, 11) : 0;
    my $sigma_n_4 = Math::GMPz::Rmpz_divisible_ui_p($n, 4) ? Math::Prime::Util::GMP::sigma($n >> 2, 11) : 0;

    # Ramanujan's tau(x) terms (A000594)
    # They evaluate to 0 if the argument x is not an integer
    my $tau_n   = Math::Prime::Util::GMP::ramanujan_tau($n);
    my $tau_n_2 = Math::GMPz::Rmpz_even_p($n) ? Math::Prime::Util::GMP::ramanujan_tau($n >> 1) : 0;

    # Determine the parity sign for (-1)^n
    my $sign = Math::GMPz::Rmpz_even_p($n) ? 1 : -1;

    # Term 1: 2^4 * (sigma_{11}(n) - 2*sigma_{11}(n/2) + 2^{12}*sigma_{11}(n/4))
    my $t1    = Math::Prime::Util::GMP::mulint(2,    $sigma_n_2);
    my $t2    = Math::Prime::Util::GMP::mulint(4096, $sigma_n_4);
    my $t3    = Math::Prime::Util::GMP::subint($sigma_n, $t1);
    my $t4    = Math::Prime::Util::GMP::addint($t3, $t2);
    my $term1 = Math::Prime::Util::GMP::mulint(16, $t4);

    # Term 2: 2^7 * 259 * (-1)^n * tau(n)
    my $term2 = Math::Prime::Util::GMP::vecprod(33152, $sign, $tau_n);

    # Term 3: 2^16 * tau(n/2)
    my $term3 = Math::Prime::Util::GMP::mulint(65536, $tau_n_2);

    # Final Assembly
    my $t5  = Math::Prime::Util::GMP::subint($term1, $term2);
    my $t6  = Math::Prime::Util::GMP::subint($t5,    $term3);
    my $a_n = Math::Prime::Util::GMP::divint($t6, 691);

    Math::GMPz::Rmpz_init_set_str($a_n, 10);
}

# Core computation and caching engine
sub _compute_sos_count {
    my ($n, $k) = @_;

    my $sgn = Math::GMPz::Rmpz_sgn($n);

    $sgn < 0  and return 0;
    $sgn == 0 and return 1;

    return 0 if ($k <= 0);

    if ($k == 1) {
        return 2 if Math::GMPz::Rmpz_perfect_square_p($n);
        return 0;
    }

    # r_3(4*n) = r_3(n)
    if ($k == 3 and Math::GMPz::Rmpz_divisible_2exp_p($n, 2)) {
        $n = Math::GMPz::Rmpz_init_set($n);    # copy
        Math::GMPz::Rmpz_div_2exp($n, $n, 2);
    }

    # Dispatch to optimized hardcoded formulas for known k
    my %optimized_handlers = (
                              2  => \&_sos_k2,
                              3  => \&_sos_k3,
                              4  => \&_sos_k4,
                              6  => \&_sos_k6,
                              8  => \&_sos_k8,
                              10 => \&_sos_k10,
                              12 => \&_sos_k12,
                              24 => \&_sos_k24,
                             );

    if (exists $optimized_handlers{$k}) {

        my $t   = Math::GMPz::Rmpz_init_set($n);
        my $v   = Math::GMPz::Rmpz_remove($t, $t, $TWO);
        my $res = $optimized_handlers{$k}->($n, $t, $v);

        if (defined($res)) {
            return $res;
        }
        else {
            # Fallback otherwise
        }
    }

    # General Fallback (Arbitrary k) with bounds-checked cache
    state %cache;
    undef %cache if scalar(keys(%cache)) > 1e6;

    my $key = "$n $k";
    return $cache{$key} if exists $cache{$key};

    my $count = Math::GMPz::Rmpz_init_set_ui(0);
    my $tmp   = Math::GMPz::Rmpz_init_set($n);

    foreach my $v (0 .. Math::Prime::Util::GMP::sqrtint($n)) {
        my $u = _compute_sos_count($tmp, $k - 1);

        ref($u)
          ? Math::GMPz::Rmpz_addmul_ui($count, $u, (($v == 0) ? 1 : 2))
          : Math::GMPz::Rmpz_add_ui($count, $count, $u * (($v == 0) ? 1 : 2));

        Math::GMPz::Rmpz_sub_ui($tmp, $tmp, 2 * $v + 1);
    }

    return $cache{$key} = $count;
}

# Main entry point
sub sum_of_squares_count {
    my ($n, $k) = @_;

    # 1. Validation and Setup
    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
    }
    else {
        $k = 2;
    }

    $n = _any2mpz($$n, 0) // goto &nan;

    my $sgn = Math::GMPz::Rmpz_sgn($n);

    $sgn < 0  and return ZERO;
    $sgn == 0 and return ONE;

    if ($k <= 0) {
        return ZERO;
    }

    if ($k == 1) {
        return TWO if Math::GMPz::Rmpz_perfect_square_p($n);
        return ZERO;
    }

    # 2. Delegate to the recursive computation engine
    my $result_val = _compute_sos_count($n, $k);
    return _set_int($result_val);
}

*squares_r = \&sum_of_squares_count;

sub is_ntf {
    my ($x, $y) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        $x > 1 or return $FALSE;

        my $res;

        _valid(\$y);
        $y = $$y;

        if (!ref($y)) {
            $res = (($x < $y) and ($y % $x == 0));
        }
        elsif (ref($y) eq 'Math::GMPz') {
            $res = (Math::GMPz::Rmpz_cmp_ui($y, $x) > 0 and Math::GMPz::Rmpz_divisible_ui_p($y, $x));
        }

        if (defined($res)) {
            return ($res ? $TRUE : $FALSE);
        }
    }

    $x = _sanitize_mpz($x, 0) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($x, 1) > 0
      or return $FALSE;

    _valid(\$y);
    $y = _sanitize_mpz($$y, 1) // return $FALSE;

    (Math::GMPz::Rmpz_cmp($x, $y) < 0 and Math::GMPz::Rmpz_divisible_p($y, $x)) ? $TRUE : $FALSE;
}

*is_nontrivial_factor = \&is_ntf;

sub is_coprime {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x)) {
        my $r = (!ref($y)) ? ((HAS_PRIME_UTIL ? Math::Prime::Util::gcd($x, $y) : Math::Prime::Util::GMP::gcd($x, $y)) == 1) : do {
            $y = _sanitize_mpz($y, 0) // return $FALSE;
            Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $y, CORE::abs($x)) == 1;
        };
        return ($r ? $TRUE : $FALSE);
    }
    elsif (!ref($y)) {
        $x = _sanitize_mpz($x, 0) // return $FALSE;
        return ((Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $x, CORE::abs($y)) == 1) ? $TRUE : $FALSE);
    }

    $x = _sanitize_mpz($x, 0) // return $FALSE;
    $y = _sanitize_mpz($y, 1) // return $FALSE;

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($t, $x, $y);

    (Math::GMPz::Rmpz_cmp_ui($t, 1) == 0) ? $TRUE : $FALSE;
}

sub gcd {
    my (@vals) = @_;
    _valid(\(@vals));

    @vals || return ZERO;    # By convention, gcd of an empty set is 0.

    if (@vals == 1) {
        my $n = $vals[0];
        __is_int__($$n) || goto &nan;
        return $n->iabs;
    }

    if (@vals > 2) {

        my @terms = map { _any2mpz($$_) // goto &nan } @vals;
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set($r, shift(@terms));

        foreach my $z (@terms) {
            Math::GMPz::Rmpz_gcd($r, $r, $z);
            last if (Math::GMPz::Rmpz_cmp_ui($r, 1) == 0);
        }

        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);

        return bless \$r2;
    }

    my ($x, $y) = @vals;

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x)) {
        if (!ref($y)) {
            return bless(\(my $g = (HAS_PRIME_UTIL ? Math::Prime::Util::gcd($x, $y) : Math::Prime::Util::GMP::gcd($x, $y))));
        }
        $y = _sanitize_mpz($y, 0) // goto &nan;
        return bless(\(my $g = Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $y, CORE::abs($x))));
    }
    elsif (FAST_MODE and !ref($y)) {
        $x = _sanitize_mpz($x, 0) // goto &nan;
        return bless(\(my $g = Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $x, CORE::abs($y))));
    }

    $x = _sanitize_mpz($x, 0) // goto &nan;
    $y = _sanitize_mpz($y, 1) // goto &nan;

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub gcud {    # greatest common unitary divisor (OEIS: A165430)
    my (@vals) = @_;
    _valid(\(@vals));

    @vals || return ZERO;    # By convention, gcd of an empty set is 0.

    if (@vals == 1) {
        my $n = $vals[0];
        __is_int__($$n) || goto &nan;
        return $n->iabs;
    }

    my @terms = map { _any2mpz($$_) // goto &nan } @vals;
    my $g     = Math::GMPz::Rmpz_init_set($terms[0]);

    foreach my $i (1 .. $#terms) {
        Math::GMPz::Rmpz_gcd($g, $g, $terms[$i]);
        if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
            return bless \$g;
        }
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    foreach my $n (@terms) {
        next if (Math::GMPz::Rmpz_sgn($n) == 0);
        while (1) {
            Math::GMPz::Rmpz_divexact($t, $n, $g);
            Math::GMPz::Rmpz_gcd($t, $t, $g);
            last if (Math::GMPz::Rmpz_cmp_ui($t, 1) == 0);
            Math::GMPz::Rmpz_divexact($g, $g, $t);
        }
        last if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0);
    }

    $g = Math::GMPz::Rmpz_get_ui($g) if Math::GMPz::Rmpz_fits_ulong_p($g);
    bless \$g;
}

sub gcdext {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // return (nan(), nan());
    $k = _any2mpz($$k, 1) // return (nan(), nan());

    my $g = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init();
    my $v = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_gcdext($g, $u, $v, $n, $k);

    ((bless \$u), (bless \$v), (bless \$g));
}

sub __lcm__ {
    my ($n, $k) = @_;
    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_lcm($r, $n, $k);
    $r;
}

sub lcm {
    my (@vals) = @_;
    _valid(\(@vals));

    @vals or return ONE;    # By convention, lcm of an empty set is 1.

    if (@vals == 1) {
        my $n = $vals[0];
        __is_int__($$n) || goto &nan;
        return $n->iabs;
    }

    if (@vals > 2) {
        my @terms = map { _any2mpz($$_) // goto &nan } @vals;
        my $r     = _binsplit(\@terms, \&__lcm__);
        $r = Math::GMPz::Rmpz_get_ui($r) if Math::GMPz::Rmpz_fits_ulong_p($r);
        return bless \$r;
    }

    my ($x, $y) = @vals;

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y) and CORE::abs($x) * CORE::abs($y) < ULONG_MAX) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::lcm($x, $y)
          : Math::Prime::Util::GMP::lcm($x, $y);
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_lcm($r, $x, $y);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub consecutive_integer_lcm {
    my ($n) = @_;
    _set_int(Math::Prime::Util::GMP::consecutive_integer_lcm(_big2uistr($$n) // goto &nan));
}

*consecutive_lcm = \&consecutive_integer_lcm;

sub num2perm {
    my ($n, $k) = @_;
    _valid(\$k);
    my @perm = map { bless \$_ } Math::Prime::Util::GMP::numtoperm(_big2uistr($$n) // (return undef), _big2uistr($$k) // (return undef));
    _array(\@perm);
}

sub valuation {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {
        $y = CORE::abs($y) if ($y < 0);
        $y <= 1 and return ZERO;
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::valuation($x, $y)
          : Math::Prime::Util::GMP::valuation($x, $y);
        defined($r) or return ZERO;
        return bless \$r;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;

    Math::GMPz::Rmpz_cmpabs_ui($y, 1) <= 0 and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $r = Math::GMPz::Rmpz_remove($t, $x, $y);
    bless \$r;
}

*multiplicity = \&valuation;

sub remove {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($x) and !ref($y)) {

        CORE::abs($y) <= 1 and return bless \$x;

        my $v =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::valuation(CORE::abs($x), CORE::abs($y))
          : Math::Prime::Util::GMP::valuation(CORE::abs($x), CORE::abs($y));

        if ($v == 0) {
            return bless \$x;
        }

        if ($v == 1) {
            my $q =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divint($x, $y)
              : Math::Prime::Util::GMP::divint($x, $y);
            return bless \$q;
        }

        my $q =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divint($x, Math::Prime::Util::powint($y, $v))
          : Math::Prime::Util::GMP::divint($x, Math::Prime::Util::GMP::powint($y, $v));
        return bless \$q;
    }

    $x = _any2mpz($x, 0) // goto &nan;
    $y = _any2mpz($y, 1) // goto &nan;

    Math::GMPz::Rmpz_cmpabs_ui($y, 1) <= 0 and return $_[0];

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_remove($r, $x, $y);
    bless \$r;
}

*remdiv = \&remove;

sub make_coprime {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) == 0) {
        return _set_int($n);
    }

    my $r = Math::GMPz::Rmpz_init_set($n);
    my $g = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_gcd($g, $r, $k);

    while (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
        Math::GMPz::Rmpz_remove($r, $r, $g);
        Math::GMPz::Rmpz_gcd($g, $r, $g);
    }

    bless \$r;
}

sub useed {
    my ($n) = @_;

    state $_x = require Digest::SHA;

    my $z = _any2mpz($$n) // die "[ERROR] Number.useed(): invalid seed value <<$n>> (expected an integer)";

    my $hex = Math::GMPz::Rmpz_get_str($z, 16);
    $hex = substr($hex, 1) if (substr($hex, 0, 1) eq '-');

    my $bin  = CORE::pack('H*', $hex);
    my $seed = Digest::SHA::sha512($bin);

    while (CORE::length($seed) < 1024) {
        $seed = Digest::SHA::sha512($seed) . CORE::reverse($seed);
    }

    Math::Prime::Util::GMP::seed_csprng(1024, $seed);
    return $TRUE;
}

*iseed = \&useed;

sub urandomm {
    my ($n, $m) = @_;

    if (defined($m)) {
        _valid(\$m);

        $n = $$n;
        $m = $$m;

        $n = (ref($n) ? _any2mpz($n) : $n) // return ZERO;
        $m = (ref($m) ? _any2mpz($m) : $m) // return ZERO;

        if (__cmp__($n, $m) > 0) {
            ($n, $m) = ($m, $n);
        }

        if (ref($n)) {
            $n = _big2uistr($n) // 0;
        }
        else {
            $n >= 0 or do { $n = 0 };
        }

        if (ref($m)) {
            $m = _big2uistr($m) // 0;
        }
        else {
            $m >= 0 or do { $m = 0 };
        }

        return _set_int(Math::Prime::Util::GMP::urandomr($n, $m) || return ZERO);
    }

    _set_int(Math::Prime::Util::GMP::urandomm(_big2uistr($$n) || return ZERO) || return ZERO);
}

*urand = \&urandomm;

sub irand {
    my ($n, $m) = @_;

    if (defined($m)) {
        _valid(\$m);
    }
    else {
        ($n, $m) = (ZERO, $n);
    }

    $n = $$n;
    $m = $$m;

    $n = (ref($n) ? _any2mpz($n) : $n) // goto &nan;
    $m = (ref($m) ? _any2mpz($m) : $m) // goto &nan;

    if (ref($n) or ref($m)) {
        $n = _any2mpz($n) if !ref($n);
        $m = _any2mpz($m) if !ref($m);

        if (Math::GMPz::Rmpz_cmp($n, $m) > 0) {
            ($n, $m) = ($m, $n);
        }

        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_sub($r, $m, $n);
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
        Math::GMPz::Rmpz_set_str($r, Math::Prime::Util::GMP::urandomm(Math::GMPz::Rmpz_get_str($r, 10)) // "0", 10);
        Math::GMPz::Rmpz_add($r, $r, $n);
        return bless \$r;
    }

    if ($n > $m) {
        ($n, $m) = ($m, $n);
    }

    my $r =
      (FAST_MODE and $n == 0 and $m < ULONG_MAX) ? ($m + 1)
      : (
         HAS_PRIME_UTIL ? Math::Prime::Util::addint(Math::Prime::Util::subint($m, $n), 1)
         : Math::Prime::Util::GMP::addint(Math::Prime::Util::GMP::subint($m, $n), 1)
        );

    $r = Math::Prime::Util::GMP::urandomm($r) // 0;

    if ($n != 0) {
        $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::addint($r, $n)
          : Math::Prime::Util::GMP::addint($r, $n);
    }

    _set_int($r);
}

sub random_prime {
    my ($from, $to) = @_;

    $from = _big2uistr($$from) // (goto &nan);

    if (defined($to)) {
        _valid(\$to);
        $to = _big2uistr($$to) // (goto &nan);
        return _set_int(Math::Prime::Util::GMP::random_prime($from, $to) // goto &nan);
    }

    _set_int(Math::Prime::Util::GMP::random_prime(2, $from) // goto &nan);
}

sub random_bytes {
    my ($n) = @_;
    _array([map { bless(\(my $o = ord($_))) } split(//, Math::Prime::Util::GMP::random_bytes(_big2uistr($$n) // (return undef)))]);
}

sub random_string {
    my ($n) = @_;
    Sidef::Types::String::String->new(Math::Prime::Util::GMP::random_bytes(_big2uistr($$n) // (return undef)));
}

sub random_nbit_prime {
    my ($n) = @_;
    $n = _any2ui($$n) || goto &nan;
    _set_int(Math::Prime::Util::GMP::random_nbit_prime($n) || goto &nan);
}

sub random_nbit_safe_prime {
    my ($n) = @_;
    $n = _any2ui($$n) || goto &nan;
    _set_int(Math::Prime::Util::GMP::random_safe_prime($n) || goto &nan);
}

sub random_nbit_strong_prime {
    my ($n) = @_;
    $n = _any2ui($$n) || goto &nan;
    $n < 128 && goto &random_nbit_prime;
    _set_int(Math::Prime::Util::GMP::random_strong_prime($n) || goto &nan);
}

*random_strong_nbit_prime = \&random_nbit_strong_prime;

sub random_nbit_maurer_prime {
    my ($n) = @_;
    $n = _any2ui($$n) || goto &nan;
    _set_int(Math::Prime::Util::GMP::random_maurer_prime($n) || goto &nan);
}

*random_maurer_nbit_prime = \&random_nbit_maurer_prime;

sub random_ndigit_prime {
    my ($n) = @_;
    $n = _any2ui($$n) || goto &nan;
    _set_int(Math::Prime::Util::GMP::random_ndigit_prime($n) || goto &nan);
}

sub is_semiprime {
    my ($x) = @_;

    $x = $$x;

    if (FAST_MODE and !ref($x)) {
        my $r = (
                 $x >= 4
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_semiprime($x)
                        : Math::Prime::Util::GMP::is_semiprime($x)
                       )
                );
        return ($r ? $TRUE : $FALSE);
    }

    __is_int__($x) || return $FALSE;
    $x = _any2mpz($x) // return $FALSE;

    # When n is large enough, is_almost_prime(n,2) is faster
    if (Math::GMPz::Rmpz_sizeinbase($x, 10) > FACTORDB_MIN) {
        return _set_int($x)->is_almost_prime(TWO);
    }

    Math::GMPz::Rmpz_sgn($x) > 0
      or return $FALSE;

    my $result = Math::Prime::Util::GMP::is_semiprime(Math::GMPz::Rmpz_get_str($x, 10));

    $result ? $TRUE : $FALSE;
}

sub is_squarefree_semiprime {
    my ($n) = @_;
    if ($n->is_square) {
        return $FALSE;
    }
    $n->is_semiprime;
}

sub _semiprime_count {
    my ($n) = @_;

    state $pi2_table = {

        # Number of semiprimes <= 10^n
        # OEIS: https://oeis.org/A036352
        "10"                       => "4",
        "100"                      => "34",
        "1000"                     => "299",
        "10000"                    => "2625",
        "100000"                   => "23378",
        "1000000"                  => "210035",
        "10000000"                 => "1904324",
        "100000000"                => "17427258",
        "1000000000"               => "160788536",
        "10000000000"              => "1493776443",
        "100000000000"             => "13959990342",
        "1000000000000"            => "131126017178",
        "10000000000000"           => "1237088048653",
        "100000000000000"          => "11715902308080",
        "1000000000000000"         => "111329817298881",
        "10000000000000000"        => "1061057292827269",
        "100000000000000000"       => "10139482913717352",
        "1000000000000000000"      => "97123037685177087",
        "10000000000000000000"     => "932300026230174178",
        "100000000000000000000"    => "8966605849641219022",
        "1000000000000000000000"   => "86389956293761485464",
        "10000000000000000000000"  => "833671466551239927908",
        "100000000000000000000000" => "8056846659984852885191",

        # Number of semiprimes <= 2^n
        # OEIS: https://oeis.org/A125527
        "4294967296"          => "658662065",
        "8589934592"          => "1289149627",
        "17179869184"         => "2524532330",
        "34359738368"         => "4946320619",
        "68719476736"         => "9696090315",
        "137438953472"        => "19015826478",
        "274877906944"        => "37310368709",
        "549755813888"        => "73237005168",
        "1099511627776"       => "143817246008",
        "2199023255552"       => "282528883551",
        "4398046511104"       => "555237939294",
        "8796093022208"       => "1091574618496",
        "17592186044416"      => "2146738817329",
        "35184372088832"      => "4223287872953",
        "70368744177664"      => "8311168557633",
        "140737488355328"     => "16360940729894",
        "281474976710656"     => "32216929163102",
        "562949953421312"     => "63457786440404",
        "1125899906842624"    => "125027663135664",
        "2251799813685248"    => "246401562625117",
        "4503599627370496"    => "485727708027940",
        "9007199254740992"    => "957746412122119",
        "18014398509481984"   => "1888916491053636",
        "36028797018963968"   => "3726284941841117",
        "72057594037927936"   => "7352535573376770",
        "144115188075855872"  => "14510848832845041",
        "288230376151711744"  => "28644424519528512",
        "576460752303423488"  => "56555665405788147",
        "1152921504606846976" => "111685842309849897",
        "2305843009213693952" => "220598984393061174",
        "4611686018427387904" => "435802554444335050",
        "9223372036854775808" => "861101852602340550",
                       };

    if (defined(my $value = $pi2_table->{$n})) {
        return $value;
    }

    if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
        return Math::Prime::Util::semiprime_count($n);
    }

    my $t = 0;
    my $s = Math::Prime::Util::GMP::sqrtint($n);

    my $count = 0;
    foreach my $p (@{_primes(2, $s)}) {
        if (INTSIZE <= 32) {
            $count = Math::Prime::Util::GMP::addint($count, Math::Prime::Util::GMP::subint(_prime_count(Math::Prime::Util::GMP::divint($n, $p)), ++$t - 1));
        }
        else {
            $count += _prime_count(CORE::int($n / $p)) - ++$t + 1;
        }
    }

    return $count;
}

sub semiprime_count {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->semiprime_count->sub($from->dec->semiprime_count);
    }

    my $n = _big2uistr($$from) // return ZERO;
    _set_int(_semiprime_count($n));
}

*semiprimes_count = \&semiprime_count;

sub semiprime_sum {
    (TWO)->almost_prime_sum(@_);
}

*semiprimes_sum = \&semiprime_sum;

sub nth_semiprime {
    my ($n) = @_;

    $n = $$n;
    $n = (ref($n) ? _any2mpz($n) : $n) // goto &nan;
    $n < 0 and goto &nan;

    return ONE         if ($n == 0);    # not semiprime, but...
    return _set_int(4) if ($n == 1);

    my ($min, $max);

    if ($n < 3e3) {
        $min = 4;
        $max = 11465;
    }
    else {
        my $k = ref($n) ? _big2uistr($n) : $n;

        # n-th semiprime is ~ n * log(n) / log(log(n))
        $max = CORE::int($k * CORE::log($k) / CORE::log(CORE::log($k)));
        $min = CORE::int(0.965 * $max);
    }

    if (HAS_PRIME_UTIL and $max < ULONG_MAX) {
        return _set_int(Math::Prime::Util::nth_semiprime($n));
    }

    require Memoize;
    Memoize::memoize('_prime_count');

    my $k     = 0;
    my $count = 0;

    while (1) {
        $k =
          (HAS_PRIME_UTIL and $max < ULONG_MAX)
          ? Math::Prime::Util::divint(Math::Prime::Util::addint($min, $max), 2)
          : Math::Prime::Util::GMP::divint(Math::Prime::Util::GMP::addint($min, $max), 2);

        $count = _semiprime_count($k);

        if ($k > (ULONG_MAX >> 1)) {
            $k = _str2obj("$k");
        }

        if ($count > (ULONG_MAX >> 1)) {
            $count = _str2obj("$count");
        }

        if (CORE::abs($count - $n) <= CORE::sqrt($k)) {
            last;
        }

        my $cmp = ($count <=> $n);

        if ($cmp > 0) {
            $max = $k - 1;
        }
        elsif ($cmp < 0) {
            $min = $k + 1;
        }
        else {
            last;
        }
    }

    Memoize::unmemoize('_prime_count');

    until (HAS_PRIME_UTIL ? Math::Prime::Util::is_semiprime($k) : Math::Prime::Util::GMP::is_semiprime($k)) {
        --$k;
    }

    my $cmp = ($n <=> $count);
    while ($count != $n) {
        do { $k += $cmp } until (HAS_PRIME_UTIL ? Math::Prime::Util::is_semiprime($k) : Math::Prime::Util::GMP::is_semiprime($k));
        $count += $cmp;
    }

    _set_int($k);
}

*semiprime = \&nth_semiprime;

sub next_semiprime {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 4) < 0 and return _set_int(4);

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_semiprime($n) : Math::Prime::Util::GMP::is_semiprime($n)) {
            ++$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    until (Math::Prime::Util::GMP::is_semiprime(Math::GMPz::Rmpz_get_str($r, 10))) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    bless \$r;
}

sub prev_semiprime {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 4) <= 0 and goto &nan;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) - 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_semiprime($n) : Math::Prime::Util::GMP::is_semiprime($n)) {
            --$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    until (Math::Prime::Util::GMP::is_semiprime(Math::GMPz::Rmpz_get_str($r, 10))) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    bless \$r;
}

sub next_squarefree_semiprime {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 6) < 0 and return _set_int(6);

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        until (
               HAS_PRIME_UTIL
               ? (Math::Prime::Util::is_semiprime($n) && !Math::Prime::Util::is_square($n))
               : (Math::Prime::Util::GMP::is_semiprime($n) && !Math::Prime::Util::GMP::is_square($n))
          ) {
            ++$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    until (
        do {
            my $str = Math::GMPz::Rmpz_get_str($r, 10);
            Math::Prime::Util::GMP::is_semiprime($str) && !Math::Prime::Util::GMP::is_square($str);
        }
      ) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    bless \$r;
}

sub prev_squarefree_semiprime {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 6) <= 0 and goto &nan;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) - 1;
        until (
               HAS_PRIME_UTIL
               ? (Math::Prime::Util::is_semiprime($n) && !Math::Prime::Util::is_square($n))
               : (Math::Prime::Util::GMP::is_semiprime($n) && !Math::Prime::Util::GMP::is_square($n))
          ) {
            --$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    until (
        do {
            my $str = Math::GMPz::Rmpz_get_str($r, 10);
            Math::Prime::Util::GMP::is_semiprime($str) && !Math::Prime::Util::GMP::is_square($str);
        }
      ) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    bless \$r;
}

sub _primality_pretest {
    my ($n) = @_;

    if (FAST_MODE and !ref($n)) {
        $n > 1 or return;

        if ($n % 2 == 0) {
            return ($n == 2);
        }

        $n > 101 or return 1;

        if (HAS_PRIME_UTIL and INTSIZE >= 64) {
            Math::Prime::Util::gcd($n, 16294579238595022365) == 1 or return 0;
            Math::Prime::Util::gcd($n, 7145393598349078859) == 1  or return 0;
        }
        else {
            Math::Prime::Util::GMP::gcd($n, "16294579238595022365") eq '1' or return 0;
            Math::Prime::Util::GMP::gcd($n, "7145393598349078859") eq '1'  or return 0;
        }

        return 1;
    }

    $n = _sanitize_mpz($n, 0) // return;

    # Must be greater than 1 (first check)
    (Math::GMPz::Rmpz_cmp_ui($n, 1) > 0) || return;

    # Check for divisibility by 2
    if (Math::GMPz::Rmpz_even_p($n)) {
        return (Math::GMPz::Rmpz_cmp_ui($n, 2) == 0);
    }

    # Return early if n is too small
    Math::GMPz::Rmpz_cmp_ui($n, 101) > 0 or return 1;

    # Check for very small factors
    if (INTSIZE >= 64) {
        Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $n, 16294579238595022365) == 1 or return 0;
        Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $n, 7145393598349078859) == 1  or return 0;
    }
    else {
        Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $n, 3234846615) == 1 or return 0;
    }

    # Native integer -- return early
    Math::GMPz::Rmpz_fits_ulong_p($n) && return 1;

    # Size of n in base-10
    my $size = Math::GMPz::Rmpz_sizeinbase($n, 10);

    # When n is large enough, try to find a small factor
    if ($size > PRIMALITY_PRETEST_MIN) {

        state $g = Math::GMPz::Rmpz_init_nobless();

        my @checks = (1e3, 1e4, 1e5);

        if (INTSIZE >= 64 and $size >= 50_000) {
            push @checks, 1e7;
            push @checks, 1e8;
            push @checks, 1e9;
        }
        elsif ($size >= 9_000) {
            push @checks, 1e7;
            push @checks, 1e8;
        }
        elsif ($size >= 4_500) {
            push @checks, 1e6;
            push @checks, 1e7;
        }
        elsif ($size >= 4_000) {
            push @checks, 7968751;
        }
        elsif ($size >= 3_500) {
            push @checks, 5789445;
        }
        elsif ($size >= 3_000) {
            push @checks, 4201049;
        }
        elsif ($size >= 2_500) {
            push @checks, 2908076;
        }
        elsif ($size >= 2_000) {
            push @checks, 1670609;
        }
        elsif ($size >= 1_500) {
            push @checks, 908433;
        }

        foreach my $k (@checks) {

            say STDERR "[primality_pretest] Checking for prime factors < $k" if $VERBOSE;

            my $primorial = _cached_primorial($k);
            Math::GMPz::Rmpz_gcd($g, $primorial, $n);

            if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

                if ($VERBOSE) {
                    say STDERR "[primality_pretest] Composite with g = ", Math::GMPz::Rmpz_get_str($g, 10);
                }

                return 0;
            }
        }

        say STDERR "[primality_pretest] No small factor found..." if $VERBOSE;

        if ($size >= 3_000 and $USE_PFGW) {

            my ($res, $exit_code) = _execute_in_tmpdir("pfgw64 -q$n &> /dev/stdout");

            if (defined($res) and index($res, 'is composite') != -1) {
                say STDERR "[primality_pretest] PFGW: composite" if $VERBOSE;
                return 0;
            }

            if (defined($res) and $exit_code == 0) {
                say STDERR "[primality_pretest] PFGW: PRP found" if $VERBOSE;
                return 1;
            }
        }
    }

    return 1;
}

sub primality_pretest {
    my ($n) = @_;
    _primality_pretest($$n) ? $TRUE : $FALSE;
}

sub all_prime {
    my (@vals) = @_;
    _valid(\(@vals));

    foreach my $n (@vals) {
        _primality_pretest($$n) || return $FALSE;
    }

    my @strs;
    my %seen;

    foreach my $n (@vals) {
        my $str = ref($$n) ? (_big2uistr($$n) // return $FALSE) : $$n;
        next if $seen{$str}++;
        (
         (HAS_PRIME_UTIL and $str < ULONG_MAX)
         ? Math::Prime::Util::is_strong_pseudoprime($str, 2)
         : (CORE::length($str) > PRIMALITY_PRETEST_MIN ? Math::Prime::Util::GMP::is_strong_pseudoprime($str, 2) : Math::Prime::Util::GMP::is_prime($str))
        )
          || return $FALSE;
        push @strs, $str;
    }

    foreach my $n (@strs) {
        (
         (HAS_PRIME_UTIL and $n < ULONG_MAX)
         ? Math::Prime::Util::is_almost_extra_strong_lucas_pseudoprime($n)
         : (CORE::length($n) > PRIMALITY_PRETEST_MIN ? Math::Prime::Util::GMP::is_almost_extra_strong_lucas_pseudoprime($n) : 1)
        )
          || return $FALSE;
    }

    return $TRUE;
}

sub all_composite {
    my (@vals) = @_;
    _valid(\(@vals));

    my %seen;

    foreach my $n (@vals) {
        (_primality_pretest($$n) // return $FALSE) || next;
        my $str = _big2uistr($$n) // return $FALSE;
        next if $seen{$str}++;
        _is_prob_prime($str)
          && return $FALSE;
    }

    return $TRUE;
}

sub is_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_prime($n)
                        : Math::Prime::Util::GMP::is_prime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    _primality_pretest($n)
      && Math::Prime::Util::GMP::is_prime(_big2uistr($n) // return $FALSE) ? $TRUE : $FALSE;
}

sub is_emirp {
    my ($n, $base) = @_;

    $base //= TEN;
    my $n_rev = $n->flip($base);

    # Must not be a palindrome
    if ($n_rev->eq($n)) {
        return $FALSE;
    }

    $n_rev->all_prime($n);
}

sub is_gaussian_prime {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
    }
    else {
        $y = ZERO;
    }

    $x = _sanitize_mpz($$x, 0) // return $FALSE;
    $y = _sanitize_mpz($$y, 1) // return $FALSE;

    Math::Prime::Util::GMP::is_gaussian_prime($x, $y) ? $TRUE : $FALSE;
}

sub is_safe_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and HAS_NEW_PRIME_UTIL and !ref($n)) {
        return (Math::Prime::Util::is_safe_prime($n) ? $TRUE : $FALSE);
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    (Math::GMPz::Rmpz_odd_p($n) && _primality_pretest($n))
      || return $FALSE;

    my $t = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($t, $n, 1);
    Math::GMPz::Rmpz_div_2exp($t, $t, 1);

    _primality_pretest($t) || return $FALSE;

    (   Math::Prime::Util::GMP::is_strong_pseudoprime($t, 2)
     && Math::Prime::Util::GMP::is_strong_pseudoprime($n, 2)
     && Math::Prime::Util::GMP::is_almost_extra_strong_lucas_pseudoprime($t)) ? $TRUE : $FALSE;
}

sub _pollard_rho_factor {
    my ($r, $reps) = @_;

    my @factors;

    while (1) {
        my @f = Math::Prime::Util::GMP::pbrent_factor($r, $reps);

        if (scalar(@f) == 1) {
            @f = Math::Prime::Util::GMP::prho_factor($r, $reps);
        }

        $r = pop @f;
        my @new_factors;

        if (@f) {
            say STDERR "pbrent_factor(r, $reps): @f" if $VERBOSE;
            @new_factors = map { _is_prob_prime($_, 1) ? $_ : _factor($_) } @f;
            push @factors, @new_factors;
        }

        foreach my $p (@new_factors) {
            my $v = Math::Prime::Util::GMP::valuation($r, $p);
            if ($v > 0) {
                push @factors, ($p) x $v;
                $r = Math::Prime::Util::GMP::divint($r, Math::Prime::Util::GMP::powint($p, $v));
            }
        }

        if ($r < $reps * $reps) {
            push @factors, _factor($r);
            $r = 1;
            last;
        }
        elsif ($reps >= 1e4 and _is_prob_prime($r, 1)) {
            push @factors, $r;
            $r = 1;
            last;
        }

        @f or last;
    }

    return ($r, @factors);
}

sub _check_remainder_special {
    my ($remainder, $type) = @_;

    return _is_prob_prime($remainder, 1) if $type eq 'almost';

    if (HAS_PRIME_UTIL && Math::GMPz::Rmpz_fits_ulong_p($remainder)) {
        return Math::Prime::Util::is_prime_power(Math::GMPz::Rmpz_get_ui($remainder));
    }

    return _prime_power_factor($remainder);
}

# Helper function to consolidate size threshold checks
sub _should_break_trial_division {
    my ($j, $r_size) = @_;

    return 1 if $j >= 4 && $r_size <= 83;                  # <= 25 digits
    return 1 if $j >= 5 && $r_size <= 100;                 # <= 30 digits
    return 1 if $j >= 6 && $r_size <= 133;                 # <= 40 digits
    return 1 if $j >= 7 && $r_size <= 150;                 # <= 45 digits
    return 1 if $j >= 8 && $r_size <= 200;                 # <= 60 digits
    return 1 if $j >= 8 && $r_size <= 250 && $USE_YAFU;    # <= 75 digits

    return 0;
}

sub _process_special_factors {
    my ($n_obj, $k, $count, $trial_limit, $trial_factors_ref, $j, $type) = @_;

    my @special_factors = @{$n_obj->special_factors(_set_int($j - 6))};
    my @gcd_factors     = @{$n_obj->gcd_factors(_array([@special_factors, map { _set_int($_) } @$trial_factors_ref]))};

    if ($type eq 'almost' && scalar(@gcd_factors) > $k) {
        return {result => 0};
    }

    my (@prime_factors, @composite_factors);

    # Separate prime and composite factors
    foreach my $f (@gcd_factors) {
        if (_is_prob_prime($$f, 1)) {
            push @prime_factors, $f;
        }
        elsif (Math::GMPz::Rmpz_sizeinbase(_any2mpz($$f), 2) <= MEDIUM_NUMBER_MAX_BITS) {
            push @prime_factors, map { _set_int($_) } _factor($$f);
        }
        else {
            push @composite_factors, $f;
        }
    }

    if ($type eq 'almost' && (scalar(@prime_factors) + 2 * scalar(@composite_factors) > $k)) {
        return {result => 0};
    }

    my $prod      = Sidef::Types::Number::Number::prod(@prime_factors);
    my $c         = $n_obj->idiv($prod);
    my $remainder = _any2mpz($$c);

    @$trial_factors_ref =
      ($type eq 'omega')
      ? List::Util::uniq(map { "$$_" } @prime_factors)
      : map { $$_ } @prime_factors;

    $count = scalar(@$trial_factors_ref);

    return {result => ($count == $k)} if Math::GMPz::Rmpz_cmp_ui($remainder, 1) == 0;
    return {result => 0}              if $count >= $k;

    # Validate against remaining capacity
    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_str($t, "$trial_limit", 10);
    Math::GMPz::Rmpz_nextprime($t, $t);
    Math::GMPz::Rmpz_pow_ui($t, $t, $k - $count);
    return {result => 0} if Math::GMPz::Rmpz_cmp($remainder, $t) < 0;

    # Check for sufficiently large composite factors
    if ($j <= 8 && @composite_factors) {
        my $min_size = ($j == 8) ? FACTORDB_MIN : YAFU_MIN;
        if (Math::GMPz::Rmpz_sizeinbase(_any2mpz(${$composite_factors[-1]}), 10) > $min_size) {
            return {count => $count, remainder => $remainder, continue => 1};
        }
    }

    # Break down remaining composites
    foreach my $f (@composite_factors) {
        push @prime_factors, _factor($$f);
    }

    $count =
      ($type eq 'omega')
      ? scalar(List::Util::uniq(map { ref($_) ? "$$_" : $_ } @prime_factors))
      : scalar(@prime_factors);

    return {result => ($count == $k)};
}

sub _is_k_prime {
    my ($n_obj, $k, $type) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return $FALSE } : 2;

    # Handle trivial/special cases using the original object instance
    return $n_obj->is_one                                                if $k == 0;
    return $type eq 'almost' ? $n_obj->is_prime : $n_obj->is_prime_power if $k == 1;
    return $n_obj->is_semiprime                                          if $k == 2 && $type eq 'almost' && !ref($$n_obj);

    # Sanitize and assign to a new GMP value to preserve the $n_obj
    my $n_val = _sanitize_mpz($$n_obj) // return $FALSE;
    return $FALSE if Math::GMPz::Rmpz_sgn($n_val) <= 0;

    my $size = Math::GMPz::Rmpz_sizeinbase($n_val, 2);

    # Type-specific mathematical bounds checks
    if ($type eq 'almost') {
        return $FALSE if $k >= $size;    # Smallest k-almost prime is 2^k
    }
    elsif ($type eq 'omega') {
        return $FALSE if Math::GMPz::Rmpz_cmp($n_val, _cached_pn_primorial($k)) < 0;
    }
    else {
        die "Invalid type: <<$type>>";
    }

    # Fast path for small numbers using Math::Prime::Util
    if (HAS_PRIME_UTIL && Math::GMPz::Rmpz_fits_ulong_p($n_val)) {
        my $val = Math::GMPz::Rmpz_get_ui($n_val);
        my $result =
          ($type eq 'almost')
          ? Math::Prime::Util::is_almost_prime($k, $val)
          : Math::Prime::Util::is_omega_prime($k, $val);
        return $result ? $TRUE : $FALSE;
    }

    my $count     = 0;
    my $remainder = $n_val;
    my $min_size  = (INTSIZE <= 32) ? 32 : 40;

    # Heavy lifting for large numbers
    if ($size >= $min_size) {

        state $t = Math::GMPz::Rmpz_init_nobless();

        my @trial_factors;
        my $max_j = (INTSIZE <= 32) ? 8 : 9;

        foreach my $j (2 .. $max_j) {
            my $trial_limit = Math::Prime::Util::GMP::powint(10, $j);
            my ($r, @new_factors) = _primorial_trial_factor($remainder, $trial_limit);

            # Conjectured approach (Pollard's rho)
            my $max_conj_j = (INTSIZE <= 32) ? 5 : 7;
            if ($j <= $max_conj_j && $USE_CONJECTURES) {
                my $r_base10_size = Math::GMPz::Rmpz_sizeinbase($r, 10);

                if ($r_base10_size >= SPECIAL_FACTORS_MIN && $r_base10_size <= 500) {
                    if ($j <= 2) {
                        my ($rem, @f) = _pollard_rho_factor($r, 2 * $trial_limit);
                        $trial_limit = Math::Prime::Util::GMP::mulint($trial_limit, $trial_limit);
                        push @new_factors, @f;
                        $r = $rem;
                    }
                    else {
                        my $r_obj = _set_int($r);
                        my $lim   = $r_obj->iroot(_set_int($k - $count))->inc;
                        if (CORE::length("$lim") <= 20) {
                            $trial_limit = $lim;
                        }
                        else {
                            $trial_limit = _set_int($trial_limit)->sqr;
                        }

                        say STDERR "ECM trial limit: $trial_limit" if $VERBOSE;

                        my $f   = $r_obj->factor_upto($trial_limit);
                        my $rem = ${$f->pop};

                        push @new_factors, ($type eq 'omega') ? (map { "$$_" } @$f) : (map { $$_ } @$f);
                        $r = $rem;
                    }
                }
            }

            push @trial_factors, ($type eq 'omega') ? List::Util::uniq(@new_factors) : @new_factors;

            $count     = List::Util::max($count, scalar(@trial_factors));
            $remainder = _any2mpz($r);

            return ($count == $k ? $TRUE : $FALSE) if Math::GMPz::Rmpz_cmp_ui($remainder, 1) == 0;
            return $FALSE                          if $count >= $k;

            # Ensure remaining space is sufficient for needed prime factors
            Math::GMPz::Rmpz_set_str($t, "$trial_limit", 10);
            Math::GMPz::Rmpz_nextprime($t, $t);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k - $count);
            return $FALSE if Math::GMPz::Rmpz_cmp($remainder, $t) < 0;

            my $r_is_special = _check_remainder_special($remainder, $type);
            return ($count + 1 == $k ? $TRUE : $FALSE) if $r_is_special;
            return $FALSE if $count + ($r_is_special ? 1 : 2) > $k;

            # Size thresholds helper replaces the ladder of checks
            my $r_size = Math::GMPz::Rmpz_sizeinbase($remainder, 2);
            last if _should_break_trial_division($j, $r_size);

            # Try to find special factors for larger numbers
            if ($j >= 6) {

                # Safely pass the original $n_obj
                my $special_result = _process_special_factors($n_obj, $k, $count, $trial_limit, \@trial_factors, $j, $type);

                # Clean parsing based on the hash structure
                if (exists $special_result->{result}) {
                    return ($special_result->{result} ? $TRUE : $FALSE);
                }

                if (exists $special_result->{continue}) {
                    $count     = $special_result->{count};
                    $remainder = $special_result->{remainder};
                    next;
                }
            }
        }
    }

    return $FALSE if $count > $k;

    my @factors = ($type eq 'omega') ? _factor_exp($remainder) : _factor($remainder);
    $count += scalar(@factors);

    ($count == $k) ? $TRUE : $FALSE;
}

sub is_almost_prime {
    my ($n_obj, $k) = @_;
    _is_k_prime($n_obj, $k, 'almost');
}

sub is_omega_prime {
    my ($n_obj, $k) = @_;
    _is_k_prime($n_obj, $k, 'omega');
}

sub is_squarefree_almost_prime {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return $FALSE } : 2;

    if ($k == 0) {
        return $n->is_one;
    }
    elsif ($k == 1) {
        return $n->is_prime;
    }
    elsif ($k == 2) {
        if ($n->is_square) {
            return $FALSE;
        }
        return $n->is_semiprime;
    }

    my $n_obj = $n;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return $FALSE;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n);

        if (HAS_PRIME_UTIL) {
            if (Math::Prime::Util::is_almost_prime($k, $n) and Math::Prime::Util::is_square_free($n)) {
                return $TRUE;
            }
            return $FALSE;
        }

        if (Math::Prime::Util::is_square_free($n) and scalar(Math::Prime::Util::factor($n)) == $k) {
            return $TRUE;
        }
        return $FALSE;
    }

    my $size = Math::GMPz::Rmpz_sizeinbase($n, 2);

    if ($size > MEDIUM_NUMBER_MAX_BITS) {
        state $limit = _set_int(1e5);
        $n_obj->is_prob_squarefree($limit) || return $FALSE;
    }
    elsif ($size > SMALL_NUMBER_MAX_BITS) {
        state $limit = _set_int(1e4);
        $n_obj->is_prob_squarefree($limit) || return $FALSE;
    }
    elsif ($size > 64) {
        state $limit = _set_int(1e2);
        $n_obj->is_prob_squarefree($limit) || return $FALSE;
    }

    if ($n_obj->is_omega_prime(_set_int($k)) and $n_obj->is_squarefree) {
        return $TRUE;
    }

    return $FALSE;
}

sub is_prob_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_prime($n)
                        : Math::Prime::Util::GMP::is_prime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    _primality_pretest($n) || return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    (
       (CORE::length($n) > PRIMALITY_PRETEST_MIN)
     ? (Math::Prime::Util::GMP::is_strong_pseudoprime($n, 2) && Math::Prime::Util::GMP::is_frobenius_underwood_pseudoprime($n))
     : Math::Prime::Util::GMP::is_prime($n)
    ) ? $TRUE : $FALSE;
}

sub is_prov_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_prime($n)
                        : Math::Prime::Util::GMP::is_prime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    _primality_pretest($n)
      && Math::Prime::Util::GMP::is_provable_prime(_big2uistr($n) // return $FALSE) ? $TRUE : $FALSE;
}

*is_provable_prime = \&is_prov_prime;

sub is_bpsw_prime {
    my ($n) = @_;
    $n = $$n;
    _primality_pretest($n)
      && Math::Prime::Util::GMP::is_bpsw_prime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_bpsw_psp = \&is_bpsw_prime;
*is_bpsw_prp = \&is_bpsw_prime;

sub is_aks_prime {
    my ($n) = @_;
    $n = $$n;
    _primality_pretest($n)
      && Math::Prime::Util::GMP::is_aks_prime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

sub is_composite {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n <= 3
                   or (
                       HAS_PRIME_UTIL
                       ? Math::Prime::Util::is_prime($n)
                       : Math::Prime::Util::GMP::is_prime($n)
                      )
                ) ? $FALSE : $TRUE
               );
    }

    (_primality_pretest($n) // return $FALSE) || return $TRUE;
    _is_prob_prime(_big2uistr($n) // return $FALSE) ? $FALSE : $TRUE;
}

sub is_odd_composite {
    my ($n) = @_;
    $n->is_odd && $n->is_composite;
}

sub miller_rabin_random {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // 1 } : 1;
    $n = $$n;

    __is_int__($n)
      && Math::Prime::Util::GMP::miller_rabin_random(_big2uistr($n) // (return $FALSE), $k) ? $TRUE : $FALSE;
}

sub is_fermat_pseudoprime {
    my ($n, @bases) = @_;
    _valid(\(@bases));

    $n = $$n;

    if (ref($n)) {
        __is_int__($n) || return $FALSE;
    }

    if (scalar(@bases)) {
        @bases = map { ref($_) ? (_any2mpz($_) // return $FALSE) : $_ } map { $$_ } @bases;
    }
    else {
        @bases = (2);
    }

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($n)) {    # optimization
        foreach my $base (@bases) {

            $base == 1 && next;
            $base < 1  && next;
            $base < $n || next;

            Math::Prime::Util::gcd($n, $base) == 1
              || return $FALSE;

            Math::Prime::Util::is_pseudoprime($n, $base)
              || return $FALSE;
        }
    }

    $n = _any2mpz($n, 0);

    Math::GMPz::Rmpz_sgn($n) > 0
      or return $FALSE;

    state $g = Math::GMPz::Rmpz_init_nobless();

    foreach my $base (@bases) {
        if (!ref($base)) {
            Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $n, CORE::abs($base)) == 1
              or return $FALSE;
        }
        else {
            Math::GMPz::Rmpz_gcd($g, $base, $n);
            Math::GMPz::Rmpz_cmp_ui($g, 1) == 0
              or return $FALSE;
        }
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_sub_ui($g, $n, 1);

    foreach my $base (@bases) {
        ref($base)      ? Math::GMPz::Rmpz_set($t, $base)
          : ($base < 0) ? Math::GMPz::Rmpz_set_si($t, $base)
          :               Math::GMPz::Rmpz_set_ui($t, $base);
        Math::GMPz::Rmpz_powm($t, $t, $g, $n);
        Math::GMPz::Rmpz_cmp_ui($t, 1) == 0
          or return $FALSE;
    }

    return $TRUE;
}

*is_psp         = \&is_fermat_pseudoprime;
*is_fermat_psp  = \&is_fermat_pseudoprime;
*is_fermat_prp  = \&is_fermat_pseudoprime;
*is_pseudoprime = \&is_fermat_pseudoprime;

sub is_super_pseudoprime {
    my ($n, @bases) = @_;
    _valid(\(@bases));

    $n = $$n;
    __is_int__($n) || return $FALSE;

    my $z = _any2mpz($n, 0) // return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    Math::Prime::Util::GMP::is_pseudoprime(
        $n,
        do {
            @bases = grep { defined($_) and $_ > 1 } map { _big2uistr($$_) } @bases;
            @bases ? (@bases) : (2);
        }
    ) || return $FALSE;

    @bases = (2) if !@bases;

    my $check_conditions = sub {

        # Using Thomas Ordowski's criterion from A050217.
#<<<
        my $gcd = Math::Prime::Util::GMP::gcd(
            map {
                ($_ < ~0)
                    ? ($_ - 1)
                    : Math::Prime::Util::GMP::subint($_, 1)
            } @_
        );
#>>>

        foreach my $base (@bases) {
            Math::Prime::Util::GMP::powmod($base, $gcd, $n) eq '1'
              or return;
        }

        return 1;
    };

    my @factors = map { ref($_) ? Math::GMPz::Rmpz_get_str($_, 10) : $_ } _miller_factor($z);

    if (scalar(@factors) == 1 and Math::GMPz::Rmpz_sizeinbase($z, 10) > SPECIAL_FACTORS_MIN) {
        @factors = map { $$_ } @{_set_int($z)->special_factors};
    }

    if (scalar(@factors) > 1) {

        my @primes;
        my @composites;

        foreach my $f (@factors) {

            foreach my $base (@bases) {
                Math::Prime::Util::GMP::powmod($base, $f, $n) eq $base
                  or return $FALSE;
            }

            if (_is_prob_prime($f, 1)) {
                push @primes, $f;
            }
            else {
                push @composites, $f;
            }
        }

        if (scalar(@primes) > 1) {
            $check_conditions->(@primes)
              || return $FALSE;
        }

        @composites
          || return $TRUE;
    }

    @factors = map { _factor($_) } @factors;

    $check_conditions->(@factors) ? $TRUE : $FALSE;
}

*is_super_psp        = \&is_super_pseudoprime;
*is_superpseudoprime = \&is_super_pseudoprime;

# A141232 for b = 2;
# A141350 for b = 3;
# A140658 for b = 2,3;

sub is_over_pseudoprime {
    my ($n, @bases) = @_;
    _valid(\(@bases));

    $n = $$n;
    __is_int__($n) || return $FALSE;

    my $z = _any2mpz($n, 0) // return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    Math::Prime::Util::GMP::is_strong_pseudoprime(
        $n,
        do {
            @bases = grep { defined($_) and $_ > 1 } map { _big2uistr($$_) } @bases;
            @bases ? (@bases) : (2);
        }
    ) || return $FALSE;

    @bases = (2) if !@bases;

    my $check_conditions = sub {

#<<<
        my $gcd = Math::Prime::Util::GMP::gcd(
            map {
                ($_ < ~0)
                    ? ($_ - 1)
                    : Math::Prime::Util::GMP::subint($_, 1)
            } @_
        );
#>>>

        foreach my $base (@bases) {
            Math::Prime::Util::GMP::powmod($base, $gcd, $n) eq '1'
              or return;
        }

        scalar(@_) > 1 or return 1;

        my %znorder;

        foreach my $p (@_) {
            foreach my $base (@bases) {

                if (exists $znorder{$base}) {
                    if (HAS_PRIME_UTIL and $p < ULONG_MAX and $base < ULONG_MAX) {
                        Math::Prime::Util::powmod($base, $znorder{$base}, $p) == 1 or return;
                    }
                    else {
                        Math::Prime::Util::GMP::powmod($base, $znorder{$base}, $p) eq '1' or return;
                    }
                }

                my $zn =
                  (HAS_PRIME_UTIL and $p < ULONG_MAX and $base < ULONG_MAX)
                  ? Math::Prime::Util::znorder($base, $p)
                  : Math::Prime::Util::GMP::znorder($base, $p);

                if (exists $znorder{$base}) {
                    $znorder{$base} eq $zn or return;
                }
                else {
                    $znorder{$base} = $zn;
                }
            }
        }

        return 1;
    };

    my @factors = map { ref($_) ? Math::GMPz::Rmpz_get_str($_, 10) : $_ } _miller_factor($z);

    if (scalar(@factors) == 1 and Math::GMPz::Rmpz_sizeinbase($z, 10) > SPECIAL_FACTORS_MIN) {
        @factors = map { $$_ } @{_set_int($z)->special_factors};
    }

    if (scalar(@factors) > 1) {

        my @primes;
        my @composites;

        foreach my $f (@factors) {

            foreach my $base (@bases) {
                Math::Prime::Util::GMP::powmod($base, $f, $n) eq $base
                  or return $FALSE;
            }

            if (_is_prob_prime($f, 1)) {
                push @primes, $f;
            }
            else {
                push @composites, $f;
            }
        }

        if (scalar(@primes) > 1) {
            $check_conditions->(@primes)
              || return $FALSE;
        }

        @composites
          || return $TRUE;
    }

    @factors = map { _factor($_) } @factors;

    $check_conditions->(@factors) ? $TRUE : $FALSE;
}

*is_over_psp        = \&is_over_pseudoprime;
*is_overpseudoprime = \&is_over_pseudoprime;

sub is_euler_pseudoprime {
    my ($n, @bases) = @_;
    _valid(\(@bases));

    $n = $$n;

    if (ref($n)) {
        __is_int__($n) || return $FALSE;
    }

    if (scalar(@bases)) {
        @bases = map { ref($_) ? (_any2mpz($_) // return $FALSE) : $_ } map { $$_ } @bases;
    }
    else {
        @bases = (2);
    }

    if (FAST_MODE and !ref($n) and scalar(@bases) == 1 and $bases[0] > 1 and $bases[0] < ULONG_MAX) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_euler_pseudoprime($n, $bases[0])
                        : Math::Prime::Util::GMP::is_euler_pseudoprime($n, $bases[0])
                       )
                ) ? $TRUE : $FALSE
               );
    }
    elsif (FAST_MODE and HAS_PRIME_UTIL and !ref($n) and scalar(grep { $_ > 1 and $_ < ULONG_MAX } @bases) == scalar(@bases)) {
        return (($n > 1 and Math::Prime::Util::is_euler_pseudoprime($n, @bases)) ? $TRUE : $FALSE);
    }

    # Any value > 1 is an Euler-Jacobi pseudoprime to base 1
    if (scalar(@bases) == 1 and $bases[0] == 1) {
        return (($n > 1) ? $TRUE : $FALSE);
    }

    Math::Prime::Util::GMP::is_euler_pseudoprime(_big2pistr($n) // (return $FALSE), (grep { $_ ne '1' } map { _big2pistr($_) // return $FALSE } @bases))
      ? $TRUE
      : $FALSE;
}

*is_euler_psp = \&is_euler_pseudoprime;
*is_euler_prp = \&is_euler_pseudoprime;

sub is_strong_fermat_pseudoprime {
    my ($n, @bases) = @_;
    _valid(\(@bases));

    $n = $$n;

    if (ref($n)) {
        __is_int__($n) || return $FALSE;
    }

    if (scalar(@bases)) {
        @bases = map { ref($_) ? (_any2mpz($_) // return $FALSE) : $_ } map { $$_ } @bases;
    }
    else {
        @bases = (2);
    }

    if (FAST_MODE and !ref($n)) {

        $n < 1 and return $FALSE;

        foreach my $base (@bases) {

            $base < 1  and return $FALSE;
            $base == 1 and next;

            $base == $n
              && return $FALSE;

            (
             HAS_PRIME_UTIL
             ? Math::Prime::Util::gcd($n, $base)
             : Math::Prime::Util::GMP::gcd($n, $base)
              ) == 1
              || return $FALSE;

            (
             (HAS_PRIME_UTIL and $base < ULONG_MAX)
             ? Math::Prime::Util::is_strong_pseudoprime($n, $base)
             : Math::Prime::Util::GMP::is_strong_pseudoprime($n, $base)
            )
              || return $FALSE;
        }

        return $TRUE;
    }

    $n = _big2pistr($n) // return $FALSE;

    foreach my $base (map { _big2pistr($_) // return $FALSE } @bases) {

        $base eq '1' and next;

        $base eq $n
          && return $FALSE;

        Math::Prime::Util::GMP::gcd($n, $base) eq '1'
          || return $FALSE;

        Math::Prime::Util::GMP::is_strong_pseudoprime($n, $base)
          || return $FALSE;
    }

    return $TRUE;
}

*is_spsp               = \&is_strong_fermat_pseudoprime;
*miller_rabin          = \&is_strong_fermat_pseudoprime;
*is_strong_psp         = \&is_strong_fermat_pseudoprime;
*is_strong_prp         = \&is_strong_fermat_pseudoprime;
*is_strong_fermat_psp  = \&is_strong_fermat_pseudoprime;
*is_strong_fermat_prp  = \&is_strong_fermat_pseudoprime;
*is_strong_pseudoprime = \&is_strong_fermat_pseudoprime;

sub is_chebyshev_pseudoprime {    # OEIS: A175530
    my ($n) = @_;

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 7056721) < 0 and return $FALSE;
    Math::GMPz::Rmpz_odd_p($n) or return $FALSE;

    my $nstr = Math::GMPz::Rmpz_get_str($n, 10);

    # V_n(P,1) == P (mod n) for any integer P.
    foreach my $i (1 .. 10) {    # test with random values of P

        my $P = CORE::int(CORE::rand($LUCAS_PQ_LIMIT - 11)) + 11;
        my $V = _modular_lucas_V($P, 1, $n, $n);

        if ($V != ($P % $n)) {
            return $FALSE;
        }

        if ($i == 1 and _is_prob_prime($nstr, 1)) {
            return $FALSE;
        }
    }

    # V_n(P,1) == P (mod n) for any integer P.
    foreach my $P (1, 3 .. 10) {    # test with small P
        my $V = _modular_lucas_V($P, 1, $n, $n);

        if ($V != ($P % $n)) {
            return $FALSE;
        }
    }

    # Odd composite integer n is a Chebyshev pseudoprime iff:
    #       n == {+1,-1} (mod p-1)
    #       n == {+1,-1} (mod p+1)
    # for each prime p|n.

    my @factors = _factor($nstr);

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $v = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($t, $n, 1);
    Math::GMPz::Rmpz_add_ui($u, $n, 1);

    foreach my $p (@factors) {

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($v, $p)
          : Math::GMPz::Rmpz_set_str($v, $p, 10);

        Math::GMPz::Rmpz_sub_ui($v, $v, 1);
        Math::GMPz::Rmpz_divisible_p($t, $v)
          || Math::GMPz::Rmpz_divisible_p($u, $v)
          || return $FALSE;

        Math::GMPz::Rmpz_add_ui($v, $v, 2);
        Math::GMPz::Rmpz_divisible_p($u, $v)
          || Math::GMPz::Rmpz_divisible_p($t, $v)
          || return $FALSE;
    }

    return $TRUE;
}

*is_chebyshev     = \&is_chebyshev_pseudoprime;
*is_chebyshev_psp = \&is_chebyshev_pseudoprime;

sub is_lucasU_pseudoprime {    # true if U_n(P,Q) == 0 mod n
    my ($n, $P, $Q) = @_;

    __is_int__($$n) || return $FALSE;

    $P = defined($P) ? do { _valid(\$P); _any2mpz($$P) // return $FALSE } : +1;
    $Q = defined($Q) ? do { _valid(\$Q); _any2mpz($$Q) // return $FALSE } : -1;

    $n = _any2mpz($$n) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return $FALSE;

    my $k = 0;
    my $D = $P * $P - 4 * $Q;

    if (ref($D)) {
        Math::GMPz::Rmpz_perfect_square_p($D) && return $FALSE;
        $k = Math::GMPz::Rmpz_kronecker($D, $n) || return $FALSE;
    }
    else {
        Math::Prime::Util::GMP::is_square($D) && return $FALSE;
        $k = Math::Prime::Util::GMP::kronecker($D, $n) || return $FALSE;
    }

    my $U = _modular_lucas_U($P, $Q, $n - $k, $n);

    ($U == 0) ? $TRUE : $FALSE;
}

*is_lucasu_psp            = \&is_lucasU_pseudoprime;
*is_lucasU_psp            = \&is_lucasU_pseudoprime;
*is_fib_psp               = \&is_lucasU_pseudoprime;
*is_fib_prp               = \&is_lucasU_pseudoprime;
*is_fibonacci_psp         = \&is_lucasU_pseudoprime;
*is_fibonacci_prp         = \&is_lucasU_pseudoprime;
*is_fibonacci_pseudoprime = \&is_lucasU_pseudoprime;

sub is_lucasV_pseudoprime {    # true if V_n(P,Q) == P mod n
    my ($n, $P, $Q) = @_;

    __is_int__($$n) || return $FALSE;

    $P = defined($P) ? do { _valid(\$P); _any2mpz($$P) // return $FALSE } : 1;
    $Q = defined($Q) ? do { _valid(\$Q); _any2mpz($$Q) // return $FALSE } : -1;

    $n = _any2mpz($$n) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return $FALSE;

    my $V = _modular_lucas_V($P, $Q, $n, $n);

    ($V == ($P % $n)) ? $TRUE : $FALSE;
}

*is_lucasv_psp                 = \&is_lucasV_pseudoprime;
*is_lucasv_prp                 = \&is_lucasV_pseudoprime;
*is_lucasV_psp                 = \&is_lucasV_pseudoprime;
*is_lucasV_prp                 = \&is_lucasV_pseudoprime;
*is_bruckman_lucas_psp         = \&is_lucasV_pseudoprime;
*is_bruckman_lucas_prp         = \&is_lucasV_pseudoprime;
*is_bruckman_lucas_pseudoprime = \&is_lucasV_pseudoprime;

sub is_pell_lucas_pseudoprime {    # OEIS: A270342 (primes + composites), A270345 (composites)
    my ($n) = @_;

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 2) > 0
      or return $FALSE;

    my $V = _modular_lucas_V(2, -1, $n, $n);
    $V == 2 ? $TRUE : $FALSE;
}

*is_pell_lucas_psp = \&is_pell_lucas_pseudoprime;
*is_pell_lucas_prp = \&is_pell_lucas_pseudoprime;

sub is_pell_pseudoprime {    # OEIS: A099011 (odd composites)
    my ($n) = @_;

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 2) > 0 or return $FALSE;
    Math::GMPz::Rmpz_even_p($n) and return $FALSE;

    my $U = _modular_lucas_U(2, -1, $n, $n);

    if (Math::GMPz::Rmpz_ui_kronecker(2, $n) == 1) {
        return (($U == 1) ? $TRUE : $FALSE);
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    if (ref($U)) {
        Math::GMPz::Rmpz_add_ui($t, $U, 1);
    }
    else {
        Math::GMPz::Rmpz_set_ui($t, $U + 1);
    }

    Math::GMPz::Rmpz_cmp($t, $n) ? $FALSE : $TRUE;
}

*is_pell_psp = \&is_pell_pseudoprime;
*is_pell_prp = \&is_pell_pseudoprime;

sub is_strong_fibonacci_pseudoprime {
    my ($n) = @_;

    # A strong Fibonacci pseudoprime is a composite number n which satisfies the following congruence with Q = -1 and all P:
    #   V_n(P,Q) = P (mod n)

    # The first several strong Fibonacci pseudoprimes, are:
    #   443372888629441, 39671149333495681, 842526563598720001,
    #   2380296518909971201, 3188618003602886401, 33711266676317630401

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n) // return $FALSE;

    state $min = Math::GMPz::Rmpz_init_set_str_nobless("443372888629441", 10);

    Math::GMPz::Rmpz_cmp($n, $min) < 0 and return $FALSE;
    Math::GMPz::Rmpz_odd_p($n) or return $FALSE;

    my $nstr = Math::GMPz::Rmpz_get_str($n, 10);

    # Check if n is a Fermat pseudoprime to base-2.
    Math::Prime::Util::GMP::is_pseudoprime($nstr, 2)
      || return $FALSE;

    # V_n(P,-1) == P (mod n) for any integer P.
    foreach my $i (1 .. 10) {    # test with random values of P

        my $P = CORE::int(CORE::rand($LUCAS_PQ_LIMIT - 11)) + 11;
        my $V = _modular_lucas_V($P, -1, $n, $n);

        if ($V != ($P % $n)) {
            return $FALSE;
        }

        if ($i == 1 and _is_prob_prime($nstr, 1)) {
            return $FALSE;
        }
    }

    # V_n(P,-1) == P (mod n) for any integer P.
    foreach my $P (1, 3 .. 10) {    # test with small P
        my $V = _modular_lucas_V($P, -1, $n, $n);

        if ($V != ($P % $n)) {
            return $FALSE;
        }
    }

    # Odd composite integer n is a strong Fibonacci pseudoprime iff:
    #     1) n is a Carmichael number: p-1 | n-1
    #     2) 2(p + 1) | (n − 1) or 2(p + 1) | (n − p)
    # for each prime p|n.

    my @factors = _factor($nstr);

    state $nm1 = Math::GMPz::Rmpz_init_nobless();
    state $u   = Math::GMPz::Rmpz_init_nobless();
    state $v   = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($nm1, $n, 1);

    my %seen;

    foreach my $p (@factors) {

        if ($seen{$p}++) {    # not squarefree
            return $FALSE;
        }

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($v, $p)
          : Math::GMPz::Rmpz_set_str($v, $p, 10);

        # Check Korselt's criterion for Carmichael numbers:
        #   p-1 | n-1, for each p|n.

        Math::GMPz::Rmpz_sub_ui($u, $v, 1);
        Math::GMPz::Rmpz_divisible_p($nm1, $u) || return $FALSE;

        # Check if any of the following condition is satisfied:
        #    2(p + 1) | (n − 1)
        #    2(p + 1) | (n − p)

        Math::GMPz::Rmpz_sub($u, $n, $v);
        Math::GMPz::Rmpz_add_ui($v, $v, 1);
        Math::GMPz::Rmpz_mul_2exp($v, $v, 1);

        Math::GMPz::Rmpz_divisible_p($nm1, $v)
          || Math::GMPz::Rmpz_divisible_p($u, $v)
          || return $FALSE;
    }

    return $TRUE;
}

*is_strong_fib           = \&is_strong_fibonacci_pseudoprime;
*is_strong_fibonacci     = \&is_strong_fibonacci_pseudoprime;
*is_strong_fib_psp       = \&is_strong_fibonacci_pseudoprime;
*is_strong_fibonacci_psp = \&is_strong_fibonacci_pseudoprime;

sub is_bfw_psp {    # OEIS: A365514
    my ($n) = @_;

    # Reference:
    #   Strengthening the Baillie-PSW primality test
    #   https://arxiv.org/abs/2006.14425

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) <= 0
      && return $FALSE;

    if (Math::GMPz::Rmpz_even_p($n)) {
        return $TRUE if (Math::GMPz::Rmpz_cmp_ui($n, 2) == 0);
        return $FALSE;
    }

    my $P = 1;
    my $Q = undef;

    # Find first D for which kronecker(D, n) == -1
    for (my $k = 2 ; ; ++$k) {
        my $D = (-1)**$k * (2 * $k + 1);
        my $K = Math::GMPz::Rmpz_si_kronecker($D, $n);

        if ($K == 0) {
            my $g = Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $n, CORE::abs($D));
            if ($g > 1 and Math::GMPz::Rmpz_cmp_ui($n, $g) > 0) {
                return $FALSE;
            }
        }

        if ($k == 20 and Math::GMPz::Rmpz_perfect_square_p($n)) {
            return $FALSE;
        }

        if ($K == -1) {
            $Q =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divint(1 - $D, 4)
              : Math::Prime::Util::GMP::divint(1 - $D, 4);
            last;
        }
    }

    if ($Q == -1) {
        $P = 5;
        $Q = 5;
    }

    state $t = Math::GMPz::Rmpz_init();
    state $u = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_add_ui($t, $n, 1);

    my $V = _modular_lucas_V($P, $Q, $t, $n);
    ref($V)
      ? Math::GMPz::Rmpz_set($t, $V)
      : Math::GMPz::Rmpz_set_ui($t, $V);
    Math::GMPz::Rmpz_set_si($u, 2 * $Q);
    Math::GMPz::Rmpz_congruent_p($t, $u, $n) ? $TRUE : $FALSE;
}

*is_vpsp = \&is_bfw_psp;

sub is_bfsw_psp {    # a slightly stronger and faster BFW test
    my ($n) = @_;

    # No counter-examples are known to this test.
    # Also the test is not correlated with the BFW test, using different P and Q.

    # See also:
    #   Strengthening the Baillie-PSW primality test
    #   https://arxiv.org/abs/2006.14425

    __is_int__($$n) || return $FALSE;
    $n = _any2mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) <= 0
      && return $FALSE;

    if (Math::GMPz::Rmpz_even_p($n)) {
        return $TRUE if (Math::GMPz::Rmpz_cmp_ui($n, 2) == 0);
        return $FALSE;
    }

    my $P = undef;
    my $Q = -2;

    # Find the first P >= 2 such that D = P^2 - 4*Q has kronecker(D, n) == -1
    for ($P = 2 ; ; ++$P) {
        my $D = $P * $P - 4 * $Q;

        my $K = Math::GMPz::Rmpz_si_kronecker($D, $n);

        if ($K == -1) {
            last;
        }
        elsif ($K == 0 and Math::GMPz::Rmpz_cmp_ui($n, CORE::abs($D)) > 0) {
            return $FALSE;
        }
        elsif ($P == 20 and Math::GMPz::Rmpz_perfect_square_p($n)) {
            return $FALSE;
        }
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_add_ui($t, $n, 1);

    my $s = Math::GMPz::Rmpz_scan1($t, 0);
    Math::GMPz::Rmpz_div_2exp($t, $t, $s + 1);

    state $V1 = Math::GMPz::Rmpz_init_nobless();
    state $V2 = Math::GMPz::Rmpz_init_nobless();

    state $Q1 = Math::GMPz::Rmpz_init_nobless();
    state $Q2 = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($V1, 2);
    Math::GMPz::Rmpz_set_ui($V2, $P);

    Math::GMPz::Rmpz_set_ui($Q1, 1);
    Math::GMPz::Rmpz_set_ui($Q2, 1);

    foreach my $bit (split(//, Math::GMPz::Rmpz_get_str($t, 2))) {

        Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);
        Math::GMPz::Rmpz_mod($Q1, $Q1, $n);

        if ($bit) {
            Math::GMPz::Rmpz_mul_si($Q2, $Q1, $Q);
            Math::GMPz::Rmpz_mul($V1, $V1, $V2);
            Math::GMPz::Rmpz_powm_ui($V2, $V2, 2, $n);
            Math::GMPz::Rmpz_submul_ui($V1, $Q1, $P);
            Math::GMPz::Rmpz_submul_ui($V2, $Q2, 2);
            Math::GMPz::Rmpz_mod($V1, $V1, $n);
        }
        else {
            Math::GMPz::Rmpz_set($Q2, $Q1);
            Math::GMPz::Rmpz_mul($V2, $V2, $V1);
            Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $n);
            Math::GMPz::Rmpz_submul_ui($V2, $Q1, $P);
            Math::GMPz::Rmpz_submul_ui($V1, $Q2, 2);
            Math::GMPz::Rmpz_mod($V2, $V2, $n);
        }
    }

    Math::GMPz::Rmpz_mul($Q1, $Q1, $Q2);
    Math::GMPz::Rmpz_mod($Q1, $Q1, $n);

    Math::GMPz::Rmpz_mul_si($Q2, $Q1, $Q);
    Math::GMPz::Rmpz_mul($V1, $V1, $V2);
    Math::GMPz::Rmpz_submul_ui($V1, $Q1, $P);
    Math::GMPz::Rmpz_mul($Q2, $Q2, $Q1);

    for (1 .. $s) {
        Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $n);
        Math::GMPz::Rmpz_submul_ui($V1, $Q2, 2);
        Math::GMPz::Rmpz_powm_ui($Q2, $Q2, 2, $n);
    }

    Math::GMPz::Rmpz_set_si($t, 2 * $Q);
    Math::GMPz::Rmpz_congruent_p($V1, $t, $n) || return $FALSE;

    # Known counter-examples if we return true here:
    #   245, 1957, 5256479

    Math::GMPz::Rmpz_set_si($t, $Q * $Q);
    Math::GMPz::Rmpz_congruent_p($Q2, $t, $n) || return $FALSE;

    return $TRUE;
}

sub is_lucas_pseudoprime {
    my ($n, $P, $Q) = @_;

    if (defined($P) or defined($Q)) {
        return $n->is_lucasU_pseudoprime($P, $Q);
    }

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_lucas_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_lucas_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_lucas_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_lpsp          = \&is_lucas_pseudoprime;
*is_lucas_psp     = \&is_lucas_pseudoprime;
*is_lucas_prp     = \&is_lucas_pseudoprime;
*is_selfridge_prp = \&is_lucas_pseudoprime;

sub is_strong_lucas_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_strong_lucas_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_strong_lucas_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_strong_lucas_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_slpsp                = \&is_strong_lucas_pseudoprime;
*is_strong_lucas_psp     = \&is_strong_lucas_pseudoprime;
*is_strong_lucas_prp     = \&is_strong_lucas_pseudoprime;
*is_strong_selfridge_prp = \&is_strong_lucas_pseudoprime;

sub is_stronger_lucas_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_extra_strong_lucas_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_extra_strong_lucas_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_extra_strong_lucas_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_stronger_lucas_psp             = \&is_stronger_lucas_pseudoprime;
*is_stronger_lucas_prp             = \&is_stronger_lucas_pseudoprime;
*is_extra_strong_lucas_psp         = \&is_stronger_lucas_pseudoprime;
*is_extra_strong_lucas_prp         = \&is_stronger_lucas_pseudoprime;
*is_extra_strong_lucas_pseudoprime = \&is_stronger_lucas_pseudoprime;

sub is_strongish_lucas_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_almost_extra_strong_lucas_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_almost_extra_strong_lucas_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_almost_extra_strong_lucas_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_strongish_lucas_psp = \&is_strongish_lucas_pseudoprime;
*is_strongish_lucas_prp = \&is_strongish_lucas_pseudoprime;

sub is_plumb_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_euler_plumb_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_euler_plumb_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_euler_plumb_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_plumb_psp               = \&is_plumb_pseudoprime;
*is_plumb_prp               = \&is_plumb_pseudoprime;
*is_euler_plumb_psp         = \&is_plumb_pseudoprime;
*is_euler_plumb_prp         = \&is_plumb_pseudoprime;
*is_euler_plumb_pseudoprime = \&is_plumb_pseudoprime;

sub is_perrin_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_perrin_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_perrin_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_perrin_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_perrin_psp = \&is_perrin_pseudoprime;
*is_perrin_prp = \&is_perrin_pseudoprime;

sub is_frobenius_pseudoprime {
    my ($n, $k, $m) = @_;

    _valid(\$k, \$m) if defined($k);

    $n = $$n;

    __is_int__($n)
      && (
          defined($k)
          ? Math::Prime::Util::GMP::is_frobenius_pseudoprime(
                                                             _big2uistr($n) // (return $FALSE),
                                                             (defined($k) ? _big2istr($$k) // () : ()),
                                                             (defined($m) ? _big2istr($$m) // () : ()),
                                                            )
          : Math::Prime::Util::GMP::is_frobenius_pseudoprime(_big2uistr($n) // (return $FALSE))
         ) ? $TRUE : $FALSE;
}

*is_frobenius_psp = \&is_frobenius_pseudoprime;
*is_frobenius_prp = \&is_frobenius_pseudoprime;

sub is_frobenius_underwood_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_frobenius_underwood_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_frobenius_underwood_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_frobenius_underwood_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_underwood_psp           = \&is_frobenius_underwood_pseudoprime;
*is_underwood_prp           = \&is_frobenius_underwood_pseudoprime;
*is_underwood_pseudoprime   = \&is_frobenius_underwood_pseudoprime;
*is_frobenius_underwood_psp = \&is_frobenius_underwood_pseudoprime;
*is_frobenius_underwood_prp = \&is_frobenius_underwood_pseudoprime;

sub is_frobenius_khashin_pseudoprime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_frobenius_khashin_pseudoprime($n)
                        : Math::Prime::Util::GMP::is_frobenius_khashin_pseudoprime($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    __is_int__($n)
      && Math::Prime::Util::GMP::is_frobenius_khashin_pseudoprime(_big2uistr($n) // (return $FALSE)) ? $TRUE : $FALSE;
}

*is_khashin_psp           = \&is_frobenius_khashin_pseudoprime;
*is_khashin_prp           = \&is_frobenius_khashin_pseudoprime;
*is_khashin_pseudoprime   = \&is_frobenius_khashin_pseudoprime;
*is_frobenius_khashin_psp = \&is_frobenius_khashin_pseudoprime;
*is_frobenius_khashin_prp = \&is_frobenius_khashin_pseudoprime;

sub is_nminus1_prime {
    my ($n) = @_;

    $n = $$n;
    __is_int__($n) || return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    _is_prob_prime($n)
      && Math::Prime::Util::GMP::is_nminus1_prime($n) ? $TRUE : $FALSE;
}

*is_nm1_prime = \&is_nminus1_prime;
*is_pm1_prime = \&is_nminus1_prime;

sub is_nplus1_prime {
    my ($n) = @_;

    $n = $$n;
    __is_int__($n) || return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    _is_prob_prime($n)
      && Math::Prime::Util::GMP::is_nplus1_prime($n) ? $TRUE : $FALSE;
}

*is_np1_prime = \&is_nplus1_prime;
*is_pp1_prime = \&is_nplus1_prime;

sub is_llr_prime {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2ui($$n)     // return undef;
    $k = _any2mpz($$k, 0) // return $FALSE;

    my $t = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_setbit($t, $n);

    # k must be smaller than 2^n
    my $valid = 1;
    if (Math::GMPz::Rmpz_cmp($k, $t) >= 0) {
        $valid = 0;
    }

    # t = k * 2^n - 1
    Math::GMPz::Rmpz_mul($t, $t, $k);
    Math::GMPz::Rmpz_sub_ui($t, $t, 1);

    $valid
      || return ((bless \$t)->is_prob_prime);

    _primality_pretest($t) || return $FALSE;

    my $r = Math::Prime::Util::GMP::is_llr_prime(Math::GMPz::Rmpz_get_str($t, 10));

    if ($r < 0) {
        return ((bless \$t)->is_prob_prime);
    }

    ($r >= 1) ? $TRUE : $FALSE;
}

sub is_proth_prime {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2ui($$n)     // return undef;
    $k = _any2mpz($$k, 0) // return $FALSE;

    my $t = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_setbit($t, $n);

    # k must be smaller than 2^n
    my $valid = 1;
    if (Math::GMPz::Rmpz_cmp($k, $t) >= 0) {
        $valid = 0;
    }

    # t = k * 2^n + 1
    Math::GMPz::Rmpz_mul($t, $t, $k);
    Math::GMPz::Rmpz_add_ui($t, $t, 1);

    $valid
      || return ((bless \$t)->is_prob_prime);

    _primality_pretest($t) || return $FALSE;

    my $r = Math::Prime::Util::GMP::is_proth_prime(Math::GMPz::Rmpz_get_str($t, 10));

    if ($r < 0) {
        return ((bless \$t)->is_prob_prime);
    }

    ($r >= 1) ? $TRUE : $FALSE;
}

sub is_ecpp_prime {
    my ($n) = @_;

    $n = $$n;

    __is_int__($n) || return $FALSE;
    $n = _big2uistr($n) // return $FALSE;

    _is_prob_prime($n)
      && Math::Prime::Util::GMP::is_ecpp_prime($n) ? $TRUE : $FALSE;
}

{
    # Note: This list should be updated whenever new Mersenne primes are found.
    # Refer: https://www.mersenne.org/
    my %MERSENNE_PRIME_EXPONENTS;
    @MERSENNE_PRIME_EXPONENTS{
        (
            2,        3,        5,        7,        13,       17,       19,       31,       61,       89,       107,      127,
            521,      607,      1279,     2203,     2281,     3217,     4253,     4423,     9689,     9941,     11213,    19937,
            21701,    23209,    44497,    86243,    110503,   132049,   216091,   756839,   859433,   1257787,  1398269,  2976221,
            3021377,  6972593,  13466917, 20996011, 24036583, 25964951, 30402457, 32582657, 37156667, 42643801, 43112609, 57885161,
            74207281, 77232917, 82589933, 136279841
        )
    } = ();

    sub is_mersenne_prime {
        my ($n) = @_;

        $n = $$n;

        if (ref($n)) {
            __is_int__($n) || return $FALSE;
            $n = _big2pistr($n) // return $FALSE;
        }

        if (exists $MERSENNE_PRIME_EXPONENTS{$n}) {
            return $TRUE;
        }

        if ($n < 69_369_389 or !_is_prob_prime($n)) {

            # According to GIMPS, all exponents below 69369389 have been tested and verified. (2024-10-23)
            # https://www.mersenne.org/report_milestones/
            return $FALSE;
        }

        if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            return (Math::Prime::Util::is_mersenne_prime($n) ? $TRUE : $FALSE);
        }

        Math::Prime::Util::GMP::is_mersenne_prime($n) ? $TRUE : $FALSE;
    }
}

sub primes_each {
    my ($from, $to, $block) = @_;

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return undef;
        $to   = _any2mpz($$to)   // return undef;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return undef;
        $from  = $TWO;
    }

    if (Math::GMPz::Rmpz_cmp_ui($from, 1) <= 0) {
        $from = $TWO;
    }

#<<<
    _generic_each(
        $from, $to, $block,
        sub {
            my ($from) = @_;

            my $t    = Math::GMPz::Rmpz_get_d($from);
            my $step = _nth_prime_lower($t + CORE::log($t) * 2e3) - _nth_prime_lower($t);

            if ($step <= 0 or $step > 1e6) {
                $step = 1e6;
            }

            $step;
        },
        sub { _primes($_[0], $_[1]) }
    );
#>>>
}

*each_prime = \&primes_each;

sub composites_each {
    my ($from, $to, $block) = @_;

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return undef;
        $to   = _any2mpz($$to)   // return undef;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return undef;
        $from  = $TWO + $TWO;
    }

    if (Math::GMPz::Rmpz_cmp_ui($from, 3) <= 0) {
        $from = $TWO + $TWO;
    }

    _generic_each(
        $from, $to, $block,
        sub {
            1e5;
        },
        sub {
            my ($from, $to) = @_;

            if (ref($to) eq 'Math::GMPz' and Math::GMPz::Rmpz_fits_slong_p($to)) {
                $to   = Math::GMPz::Rmpz_get_ui($to);
                $from = Math::GMPz::Rmpz_get_ui($from) if (ref($from) eq 'Math::GMPz');
            }

            my @list;

            if (FAST_MODE and HAS_PRIME_UTIL and !ref($to)) {
                Math::Prime::Util::forcomposites(sub { push @list, $_ }, $from, $to);
                return \@list;
            }

            for (my $k = $from ; $k <= $to ; ++$k) {
                if (!_is_prob_prime($k)) {
                    push @list, $k;
                }
            }

            \@list;
        }
    );
}

*each_composite = \&composites_each;

sub primes {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        $x = _any2mpz($$x, 0) // return _array();
        $y = _any2mpz($$y, 1) // return _array();
    }
    else {
        $x = _any2mpz($$x, 0) // return _array();
        $y = $x;
        $x = 2;
    }

    if ($x > $y) {
        return _array();
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($y)) {
        if (ref($x)) {
            $x = (Math::GMPz::Rmpz_sgn($x) <= 0) ? 2 : Math::GMPz::Rmpz_get_ui($x);
        }
        $y = Math::GMPz::Rmpz_get_ui($y);
        return _array([map { bless \$_ } @{Math::Prime::Util::primes($x, $y)}]);
    }

    $x = (_big2uistr($x) // 0) if ref($x);
    $y = (_big2uistr($y) // 0);

    Math::Prime::Util::GMP::subint($y, $x) < ULONG_MAX
      or return undef;    # too large range

    _array([map { ($_ < ULONG_MAX) ? (bless \$_) : _set_int($_) } @{_primes($x, $y)}]);
}

sub prime_cluster {
    my ($lo, $hi, @diffs) = @_;

    _valid(\$lo);
    _valid(\$hi);
    _valid(\(@diffs));

    $lo = _big2uistr($$lo) // return undef;
    $hi = _big2uistr($$hi) // return undef;

    @diffs = map { _big2uistr($$_) // return undef } @diffs;

    my @primes = map { _set_int($_) } (
                                       HAS_PRIME_UTIL
                                       ? Math::Prime::Util::sieve_prime_cluster($lo, $hi, @diffs)
                                       : Math::Prime::Util::GMP::sieve_prime_cluster($lo, $hi, @diffs)
                                      );

    _array(\@primes);
}

*sieve_prime_cluster = \&prime_cluster;

sub _combine_crt {
    my ($arr_ref, $M, $p, $S_p_ref) = @_;

    my $Minv =
      HAS_PRIME_UTIL
      ? Math::Prime::Util::invmod($M % $p, $p)
      : Math::Prime::Util::GMP::invmod($M % $p, $p);

    my @res;
    foreach my $r (@$arr_ref) {
        my $r_mod_p = $r % $p;
        foreach my $s (@$S_p_ref) {

            # Solve r + k*M ≡ s (mod p) -> k ≡ (s - r) * Minv (mod p)
            push @res, (((($s - $r_mod_p) % $p) * $Minv) % $p) * $M + $r;
        }
    }

    return \@res;
}

sub _remainders_mod_p {
    my ($p, $terms, $alphas) = @_;

    my @bad;    # bad[m] = 1 means m is forbidden modulo p

    foreach my $i (0 .. $#$terms) {

        my $m = $terms->[$i];
        my $k = $alphas->[$i];

        if (ref($m) or ref($k)) {
            $m = Math::Prime::Util::GMP::modint($m, $p);
            $k = Math::Prime::Util::GMP::modint($k, $p);
        }
        else {
            $m %= $p;
            $k %= $p;
        }

        if ($m == 0) {

            # Term is constant mod p
            if ($k == 0) {

                # Always 0 mod p -> no admissible residue exists
                return ();
            }
            next;    # This term forbids no residue for this p
        }

        # Forbid the unique residue m ≡ -k * m^{-1} (mod p)
        my $m_inv =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::invmod($m, $p)
          : Math::Prime::Util::GMP::invmod($m, $p);
        my $m_forbid = (-$k * $m_inv) % $p;
        $bad[$m_forbid] = 1;
    }

    my @S_p;
    foreach my $m (0 .. $p - 1) {
        push(@S_p, $m) if !$bad[$m];
    }
    return @S_p;
}

sub _remainders_for_primes {
    my ($primes) = @_;

    my $residues = [0];
    my $M        = 1;

    foreach my $pair (@$primes) {
        my ($p, $rems) = @$pair;
        $residues = _combine_crt($residues, $M, $p, $rems);
        $M *= $p;
    }

    [sort { $a <=> $b } @$residues];
}

sub _deltas {
    my ($integers) = @_;

    my @deltas;
    my $prev = 0;

    foreach my $n (@$integers) {
        push @deltas, $n - $prev;
        $prev = $n;
    }

    CORE::shift(@deltas);
    return \@deltas;
}

sub _select_optimal_primes {
    my ($A, $B, $terms, $alphas) = @_;

    my $range = $B - $A + 1;

    if ($range <= 0) {
        return;
    }

    my $target_modulus = (
                          1 + (
                               HAS_PRIME_UTIL
                               ? Math::Prime::Util::rootint($range, 5)
                               : Math::Prime::Util::GMP::rootint($range, 5)
                              )
                         )**4;

    my $M = 1;
    my @primes;

    for (my $p = 2 ; $M <= $target_modulus ; $p = _next_prime($p)) {
        my @S_p = _remainders_mod_p($p, $terms, $alphas);

        if (scalar(@S_p) == $p) {
            next;    # skip trivial primes
        }

        CORE::push(@primes, [$p, \@S_p]);
        $M *= $p;
    }

    return ($M, \@primes);
}

sub _linear_forms_primes_base {
    my ($A, $B, $pairs, $iter) = @_;

    # TODO: add support for $iter = 1, returning a Sidef::Object::Enumerator iterator

    _valid(\$A);
    _valid(\$B);

    my @terms  = map { $_->[0] } @$pairs;
    my @alphas = map { $_->[1] } @$pairs;

    _valid(\(@terms));
    _valid(\(@alphas));

    $A = _any2mpz(_big2uistr($$A) // return _array());
    $B = _any2mpz(_big2uistr($$B) // return _array());

    @terms  = map { _any2mpz($_) } map { _big2uistr($$_) // return undef } @terms;
    @alphas = map { _any2mpz($_) } map { _big2istr($$_)  // return undef } @alphas;

    if (Math::GMPz::Rmpz_cmp($A, $B) > 0 or !@terms or scalar(@alphas) != scalar(@terms)) {
        return _array();
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    # Check to see if we can do the computation using native ints (would be faster)
    my $native_int = 1;

    foreach my $i (0 .. $#terms) {

        if (!Math::GMPz::Rmpz_fits_ulong_p($terms[$i])) {
            $native_int = 0;
            last;
        }

        if (!Math::GMPz::Rmpz_fits_slong_p($alphas[$i])) {
            $native_int = 0;
            last;
        }

        Math::GMPz::Rmpz_mul($t, $B, $terms[$i]);
        if (!Math::GMPz::Rmpz_fits_ulong_p($t)) {
            $native_int = 0;
            last;
        }

        Math::GMPz::Rmpz_add($t, $t, $alphas[$i]);
        if (!Math::GMPz::Rmpz_fits_ulong_p($t)) {
            $native_int = 0;
            last;
        }
    }

    # $native_int = 0;

    if ($native_int) {
        @terms  = map { Math::GMPz::Rmpz_get_ui($_) } @terms;
        @alphas = map { Math::GMPz::Rmpz_get_si($_) } @alphas;
        $A      = Math::GMPz::Rmpz_get_ui($A);
        $B      = Math::GMPz::Rmpz_get_ui($B);
    }

    # Compute the list of primes, based on the size of the range (adaptive strategy)
    my ($M, $primes) = _select_optimal_primes($A, $B, \@terms, \@alphas);

    my $key = do {
        local $" = " ";
        "@terms | @alphas | " . join(' ', map { $_->[0] } @$primes);
    };

    state $cache = {};

    my $r = undef;
    if (exists $cache->{$key}) {
        $r = $cache->{$key};
    }
    else {
        $cache         = {};                                # clear cache
        $r             = _remainders_for_primes($primes);
        $cache->{$key} = $r;
    }

    if (!@$r) {
        return _array();
    }

    my @d = @{_deltas($r)};

    while (@d and $d[0] == 0) {
        CORE::shift(@d);
    }

    push @d, Math::Prime::Util::GMP::subint(Math::Prime::Util::GMP::addint($r->[0], $M), $r->[-1]);

    my $compute_small_values = 0;
    my $small_values_limit   = ($B < 500) ? $B : 500;
    my $original_A           = undef;

    if ($A <= $small_values_limit) {
        $original_A           = $A;
        $A                    = $small_values_limit + 1;
        $compute_small_values = 1;

        if (!$native_int) {
            $A = Math::GMPz::Rmpz_init_set_ui($A) if !ref($A);
        }
    }

    my $m     = Math::GMPz::Rmpz_init_set_str("$r->[0]", 10);
    my $d_len = scalar(@d);

    my $d_sum = Math::Prime::Util::GMP::vecsum(@d);
    my $times = Math::Prime::Util::GMP::divint($A, $d_sum);

    if ($times ne '0') {
        state $u = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_set_str($t, $d_sum, 10);
        Math::GMPz::Rmpz_set_str($u, $times, 10);
        Math::GMPz::Rmpz_mul($t, $t, $u);
        Math::GMPz::Rmpz_add($m, $m, $t);
    }

    my $j     = 0;
    my $ok    = 1;
    my @range = (0 .. $#terms);

    my @arr;

    if ($native_int) {

        $m = Math::GMPz::Rmpz_get_ui($m);

        while ($m < $A) {
            $m += $d[$j++ % $d_len];
        }

        if ($compute_small_values) {
            foreach my $k ($original_A .. $small_values_limit) {
                $ok = 1;

                foreach my $i (@range) {
                    if (
                        !(
                           HAS_PRIME_UTIL
                           ? Math::Prime::Util::is_prime($terms[$i] * $k + $alphas[$i])
                           : Math::Prime::Util::GMP::is_prime($terms[$i] * $k + $alphas[$i])
                         )
                      ) {
                        $ok = 0;
                        last;
                    }
                }

                if ($ok) {
                    push @arr, _set_int($k);
                }
            }
        }

        while ($m <= $B) {

            $ok = 1;

            foreach my $i (@range) {
                if (
                    !(
                       HAS_PRIME_UTIL
                       ? Math::Prime::Util::is_prime($terms[$i] * $m + $alphas[$i])
                       : Math::Prime::Util::GMP::is_prime($terms[$i] * $m + $alphas[$i])
                     )
                  ) {
                    $ok = 0;
                    last;
                }
            }

            if ($ok) {
                my $r = $m;
                push @arr, bless \$r;
            }

            $m += $d[$j++ % $d_len];
        }

        return _array(\@arr);
    }

    if ($compute_small_values) {
        foreach my $v ($original_A .. $small_values_limit) {

            $ok = 1;
            foreach my $i (@range) {

                my $k     = $terms[$i];
                my $alpha = $alphas[$i];

                Math::GMPz::Rmpz_mul_ui($t, $k, $v);
                Math::GMPz::Rmpz_add($t, $t, $alpha);

                if (!Math::GMPz::Rmpz_probab_prime_p($t, 23) or Math::GMPz::Rmpz_sgn($t) <= 0) {
                    $ok = 0;
                    last;
                }
            }

            if ($ok) {
                push @arr, _set_int($v);
            }
        }
    }

    while (Math::GMPz::Rmpz_cmp($m, $A) < 0) {
        Math::GMPz::Rmpz_add_ui($m, $m, $d[$j++ % $d_len]);
    }

    while (Math::GMPz::Rmpz_cmp($m, $B) <= 0) {

        $ok = 1;
        foreach my $i (@range) {

            my $k     = $terms[$i];
            my $alpha = $alphas[$i];

            Math::GMPz::Rmpz_mul($t, $k, $m);
            Math::GMPz::Rmpz_add($t, $t, $alpha);

            if (!Math::GMPz::Rmpz_probab_prime_p($t, 23) or Math::GMPz::Rmpz_sgn($t) <= 0) {
                $ok = 0;
                last;
            }
        }

        if ($ok) {
            push @arr, _set_int($m);
        }

        Math::GMPz::Rmpz_add_ui($m, $m, $d[$j++ % $d_len]);
    }

    _array(\@arr);
}

sub linear_forms_primes {
    my ($A, $B, @pairs) = @_;
    _linear_forms_primes_base($A, $B, \@pairs);
}

sub composites {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = 4;
    }

    if (ref($from) and Math::GMPz::Rmpz_cmp_ui($from, 3) <= 0) {
        $from = 4;
    }

    if (ref($to) and Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = 0;
    }

    if ($from > $to) {
        return _array();
    }

    if (_fits_ulong($to)) {
        $to   = _get_ulong($to);
        $from = Math::GMPz::Rmpz_get_ui($from) if (ref($from) eq 'Math::GMPz');
    }

    my @list;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($to)) {
        Math::Prime::Util::forcomposites(
            sub {
                push(@list, bless(\(my $o = $_)));
            },
            $from,
            $to
        );
    }
    else {
        for (my $k = $from ; $k - 1 < $to ; ++$k) {
            if (!_is_prob_prime($k)) {
                push(@list, ref($k) ? _set_int($k) : (bless(\(my $o = $k))));
            }
        }
    }

    _array(\@list);
}

sub smooth_numbers {
    my ($n, $primes, $block) = @_;

    my @primes = @$primes;
    @primes || return _array();

    _valid(\(@primes));
    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_sgn($n) > 0
      or return _array();

    @primes = map { ref($$_) ? (_fits_ulong($$_) ? _get_ulong($$_) : _any2mpz($$_)) : $$_ } @primes;

    # Optimization when n is a native integer
    if (Math::GMPz::Rmpz_cmp_ui($n, ULONG_MAX) < 0) {

        $n = Math::GMPz::Rmpz_get_ui($n);

        my @h = (ONE);
        foreach my $p (@primes) {
            my $p_obj = _set_int($p);
            foreach my $k (@h) {
                my $t = $$k * $p;
                if ($t <= $n and $t < ULONG_MAX) {
                    my $t_obj = bless \$t;
                    if (defined($block) ? $block->($t_obj, $p_obj) : 1) {
                        push @h, $t_obj;
                    }
                }
            }
        }

        @h = sort { $$a <=> $$b } @h;
        return _array(\@h);
    }

    my @h = (ONE);

    foreach my $p (@primes) {
        my $p_obj = _set_int($p);
        foreach my $k (@h) {
            my $t = __mul__($p, $$k);
            if (__cmp__($t, $n) <= 0) {
                my $t_obj = bless \$t;
                if (defined($block) ? $block->($t_obj, $p_obj) : 1) {
                    push @h, $t_obj;
                }
            }
        }
    }

    @h = sort { $$a <=> $$b } @h;
    return _array(\@h);
}

sub next_primes {
    my ($n, $start) = @_;

    $start = defined($start) ? do { _valid(\$start); _big2uistr($$start) // 0 } : 2;
    $n     = _any2ui($$n) // return _array();

    my @primes;

    if (HAS_PRIME_UTIL and $start < (ULONG_MAX >> 1)) {
        for (my $it = Math::Prime::Util::prime_iterator($start) ; $n > 0 ; --$n) {
            my $p = $it->();
            push @primes, bless \$p;
        }
        return _array(\@primes);
    }

    if (_is_prob_prime($start)) {
        ## ok
    }
    else {
        $start = _next_prime($start);
    }

    for (my $p = $start ; $n > 0 ; --$n, ($p = _next_prime($p))) {
        push @primes, _set_int($p);
    }

    _array(\@primes);
}

sub next_composites {
    my ($n, $start) = @_;

    $n = _any2ui($$n) // return _array();

    if (defined($start)) {
        _valid(\$start);
        $start = $start->dec->next_composite;
    }
    else {
        $start = _set_int(4);
    }

    my @composites;

    for (my $c = $start ; $n > 0 ; --$n, ($c = $c->next_composite)) {
        push @composites, $c;
    }

    _array(\@composites);
}

sub prev_primes {
    my ($n, $start) = @_;

    $n = _any2ui($$n) // return _array();

    _valid(\$start);
    $start = $start->inc->prev_prime;

    my @primes;

    for (my $c = $start ; $n > 0 ; --$n, ($c = $c->prev_prime)) {
        last if $c->is_nan;
        push @primes, $c;
    }

    _array(\@primes);
}

sub prev_composites {
    my ($n, $start) = @_;

    $n = _any2ui($$n) // return _array();

    _valid(\$start);
    $start = $start->inc->prev_composite;

    my @composites;

    for (my $c = $start ; $n > 0 ; --$n, ($c = $c->prev_composite)) {
        last if $c->is_nan;
        push @composites, $c;
    }

    _array(\@composites);
}

sub pn_primes {
    my ($x, $y) = @_;

    if (defined($y)) {
        _valid(\$y);
        return $x->nth_prime->primes($y->nth_prime);
    }

    $x->nth_prime->primes;
}

sub _prime_sum_mpz {
    my ($n, $k) = @_;

    # Simple implementation of the prime-summation function:
    #   Sum_{p prime <= n} p^k, for fixed k >= 0.

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) <= 0) {
        return ZERO;
    }

    my $r = Math::Prime::Util::GMP::sqrtint(Math::GMPz::Rmpz_get_str($n, 10));

    goto &nan if ($r > ULONG_MAX);

    my $t = Math::GMPz::Rmpz_init_set_ui(0);
    state $u  = Math::GMPz::Rmpz_init_nobless();
    state $p2 = Math::GMPz::Rmpz_init_nobless();
    state $ip = Math::GMPz::Rmpz_init_nobless();

    my $t_obj = bless \$t;
    my $k_obj = _set_int($k);

    my @S_small = (0);
    my @S_large = (0);

    for my $i (1 .. $r) {
        Math::GMPz::Rmpz_tdiv_q_ui($t, $n, $i);
        push @S_large, _any2mpz(${$t_obj->faulhaber_sum($k_obj)});
    }

    for my $v (1 .. $r) {
        Math::GMPz::Rmpz_set_ui($t, $v);
        push @S_small, _any2mpz(${$t_obj->faulhaber_sum($k_obj)});
    }

    my ($target, $max_i);
    foreach my $p (@{_primes(2, $r)}) {

        Math::GMPz::Rmpz_set_ui($p2, $p);
        Math::GMPz::Rmpz_mul($p2, $p2, $p2);

        my $cp = $S_small[$p - 1];
        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
        Math::GMPz::Rmpz_tdiv_q($u, $n, $p2);

        $max_i = (Math::GMPz::Rmpz_cmp_ui($u, $r) < 0) ? Math::GMPz::Rmpz_get_ui($u) : $r;

        for my $i (1 .. $max_i) {
            Math::GMPz::Rmpz_set_ui($ip, $i);
            Math::GMPz::Rmpz_mul_ui($ip, $ip, $p);

            if (Math::GMPz::Rmpz_cmp_ui($ip, $r) <= 0) {
                $target = $S_large[$i * $p];
            }
            else {
                Math::GMPz::Rmpz_tdiv_q($ip, $n, $ip);
                $target = $S_small[Math::GMPz::Rmpz_get_ui($ip)];
            }
            Math::GMPz::Rmpz_sub($u, $target, $cp);
            Math::GMPz::Rmpz_submul($S_large[$i], $u, $t);
        }

        if (Math::GMPz::Rmpz_cmp_ui($p2, $r) <= 0) {
            my $p2_ui = Math::GMPz::Rmpz_get_ui($p2);
            for (my $v = $r ; $v >= $p2_ui ; $v--) {
                Math::GMPz::Rmpz_sub($u, $S_small[CORE::int($v / $p)], $cp);
                Math::GMPz::Rmpz_submul($S_small[$v], $u, $t);
            }
        }
    }

    _set_int($S_large[1] - 1);
}

sub _prime_sum_native {
    my ($n, $k) = @_;

    # Simple implementation of the prime-summation function:
    #   Sum_{p prime <= n} p^k, for fixed k >= 0.

    if ($n <= 1) {
        return ZERO;
    }

    my $r = Math::Prime::Util::GMP::sqrtint($n);

    goto &nan if ($r > ULONG_MAX);

    my $t = Math::GMPz::Rmpz_init_set_ui(0);
    state $u = Math::GMPz::Rmpz_init_nobless();

    my $t_obj = bless \$t;
    my $k_obj = _set_int($k);

    my @S_small = (0);
    my @S_large = (0);

    for my $i (1 .. $r) {
        Math::GMPz::Rmpz_set_ui($t, CORE::int($n / $i));
        push @S_large, _any2mpz(${$t_obj->faulhaber_sum($k_obj)});
    }

    for my $v (1 .. $r) {
        Math::GMPz::Rmpz_set_ui($t, $v);
        push @S_small, _any2mpz(${$t_obj->faulhaber_sum($k_obj)});
    }

    my ($target, $max_i, $p2, $ip, $cp);
    foreach my $p (@{_primes(2, $r)}) {
        $p2 = $p * $p;
        $cp = $S_small[$p - 1];
        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);

        $max_i = CORE::int($n / $p2);
        $max_i = $r if ($max_i > $r);

        for my $i (1 .. $max_i) {
            $ip     = $i * $p;
            $target = ($ip <= $r) ? $S_large[$ip] : $S_small[CORE::int($n / $ip)];
            Math::GMPz::Rmpz_sub($u, $target, $cp);
            Math::GMPz::Rmpz_submul($S_large[$i], $u, $t);
        }

        if ($p2 <= $r) {
            for (my $v = $r ; $v >= $p2 ; $v--) {
                Math::GMPz::Rmpz_sub($u, $S_small[CORE::int($v / $p)], $cp);
                Math::GMPz::Rmpz_submul($S_small[$v], $u, $t);
            }
        }
    }

    _set_int($S_large[1] - 1);
}

sub primes_sum {
    my ($from, $to, $k) = @_;

    if (defined($to)) {
        _valid(\$to);
        $from = _big2istr($$from) // return ZERO;
        $from = 2 if $from < 2;
        $to   = _big2uistr($$to) // return ZERO;
    }
    else {
        $to   = _big2uistr($$from) // return ZERO;
        $from = 2;
    }

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
    }
    else {
        $k = 1;
    }

    if ($from > $to) {
        return ZERO;
    }

    if ($k == 0) {
        return _set_int($from)->prime_count(_set_int($to));
    }

    state $table_len = _next_prime(HAS_PRIME_UTIL ? 1e5 : 1e6);

    state $prime_sum_table = do {
        my @prime_sums;

        my $k   = 0;
        my $sum = 0;

        foreach my $p (@{_primes(2, $table_len)}) {
            splice(@prime_sums, $k, $p - $k + 1, ($sum) x ($p - $k + 1));
            $k = $p;
            $sum += $p;
        }

        \@prime_sums;
    };

    if ($k == 1 and $from eq '2') {
        if ($to < $table_len) {
            return _set_int($prime_sum_table->[$to]);
        }
    }

    if ($k == 1 and $from eq '2') {

        if ($to >= PRIMESUM_MIN and $to < 1e30 and $USE_PRIMESUM) {
            my $sum = `primesum $to`;

            if ($? == 0 and defined($sum)) {
                chomp $sum;
                if ($sum) {    # make sure the sum is not zero
                    return _set_int($sum);
                }
            }
        }
    }

    if ($k == 1 and $from ne '2' and $USE_PRIMESUM) {
        my $diff = Math::Prime::Util::GMP::subint($to, $from);

        if ($diff >= PRIMESUM_MIN and $diff < 1e30) {

            my $y_sum = `primesum $to`;

            if ($? == 0 and defined($y_sum)) {
                chomp $y_sum;
                my $x_sum = _set_int(Math::Prime::Util::GMP::subint($from, 1))->prime_sum;
                return _set_int(Math::Prime::Util::GMP::subint($y_sum, $x_sum));
            }
        }
    }

    if (HAS_PRIME_UTIL and $k == 1) {
        my $r = Math::Prime::Util::sum_primes($from, $to);
        return _set_int("$r");
    }

    if ($from > 2) {

        if ($from / $to >= 0.999) {
            return _set_int(Math::Prime::Util::GMP::vecsum(map { Math::Prime::Util::GMP::powint($_, $k) } @{_primes($from, $to)}));
        }

        return ((TWO)->sum_primes(_set_int($to), _set_int($k))->sub((TWO)->sum_primes(_set_int($from)->dec, _set_int($k))));
    }

    if (FAST_MODE and $to < ULONG_MAX) {
        return _prime_sum_native($to, $k);
    }

    my $n = _any2mpz($to) // goto &nan;
    _prime_sum_mpz($n, $k);
}

*prime_sum  = \&primes_sum;
*sum_primes = \&primes_sum;

sub prev_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        $n > 2 or goto &nan;
        return
          _set_int(
                   (
                    HAS_PRIME_UTIL
                    ? Math::Prime::Util::prev_prime($n)
                    : Math::Prime::Util::GMP::prev_prime($n)
                   )
                     || goto &nan
                  );
    }

    _set_int(Math::Prime::Util::GMP::prev_prime(_big2uistr($n) // goto &nan) || goto &nan);
}

sub next_prime {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n) and $n >= 0) {
        return
          _set_int(
                   HAS_PRIME_UTIL
                   ? Math::Prime::Util::next_prime($n)
                   : Math::Prime::Util::GMP::next_prime($n)
                  );
    }

    _set_int(Math::Prime::Util::GMP::next_prime(_big2uistr($n) // goto &nan));
}

sub next_twin_prime {
    my ($n) = @_;
    _set_int(Math::Prime::Util::GMP::next_twin_prime(_big2uistr($$n) // goto &nan) || goto &nan);
}

sub twin_primes {
    my ($lo, $hi) = @_;

    if (!defined($hi)) {
        $hi = $lo;
        $lo = THREE;
    }

    $lo->prime_cluster($hi, TWO);
}

sub is_twin_prime {
    my ($n) = @_;
    $n->add(TWO)->all_prime($n);
}

sub is_sexy_prime {
    my ($n) = @_;
    state $SIX = _set_int(6);
    $n->add($SIX)->all_prime($n);
}

sub is_sophie_germain {
    my ($n) = @_;
    $n->mul(TWO)->inc->all_prime($n);
}

*is_sophie_germain_prime = \&is_sophie_germain;

sub is_balanced_prime {
    my ($n, $order) = @_;

    $order //= ONE;
    $order = _any2ui($$order) // return $FALSE;

    $n->is_prime || return $FALSE;
    $order       || return $TRUE;

    if ($order > 1) {
        my $order_obj = bless \$order;

        my $prev = $order_obj->prev_primes($n->dec);
        my $next = $order_obj->next_primes($n->inc);

        scalar(@$prev) == scalar(@$next) or return $FALSE;
        my $sum = Sidef::Types::Number::Number::sum(@$prev, @$next);
        return $sum->div(_set_int(2 * $order))->eq($n);
    }

    my $prev = $n->prev_prime;
    my $next = $n->next_prime;

    $n->sub($prev)->eq($next->sub($n));
}

sub prev_composite {
    my ($n) = @_;

    $n = $$n;

    # Optimization for native integers
    if (FAST_MODE and !ref($n)) {
        $n <= 4 and goto &nan;
        $n = $n - 1;
        return (bless \$n) if (($n & 1) == 0);
        --$n               if _is_prob_prime($n);
        return bless \$n;
    }

    $n = _any2mpz($n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 4) <= 0 and goto &nan;

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    if (Math::GMPz::Rmpz_even_p($r)) {
        return bless \$r;
    }

    if (_is_prob_prime($r)) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    bless \$r;
}

sub next_composite {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 3) <= 0 and return _set_int(4);

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        return (bless \$n) if (($n & 1) == 0);
        ++$n               if _is_prob_prime($n);
        return (bless \$n);
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    if (Math::GMPz::Rmpz_even_p($r)) {
        return bless \$r;
    }

    if (_is_prob_prime($r)) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    bless \$r;
}

sub next_prime_power {
    my ($n) = @_;

    $n = $$n;

    # Optimization for native integers
    if (FAST_MODE and !ref($n) and $n >= 0 and $n < (ULONG_MAX >> 1)) {
        ++$n;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power($n) : Math::Prime::Util::GMP::is_prime_power($n)) {
            ++$n;
        }
        return bless \$n;
    }

    $n = _any2mpz($n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) < 0 and goto &nan;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power($n) : Math::Prime::Util::GMP::is_prime_power($n)) {
            ++$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    until (Math::Prime::Util::GMP::is_prime_power(Math::GMPz::Rmpz_get_str($r, 10))) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    bless \$r;
}

sub prev_prime_power {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 2) <= 0 and goto &nan;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) - 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power($n) : Math::Prime::Util::GMP::is_prime_power($n)) {
            --$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    until (Math::Prime::Util::GMP::is_prime_power(Math::GMPz::Rmpz_get_str($r, 10))) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    bless \$r;
}

sub next_squarefree {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;

    my $sgn = Math::GMPz::Rmpz_sgn($n);
    $sgn < 0  and goto &nan;
    $sgn == 0 and return ONE;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($n) : Math::Prime::Util::GMP::moebius($n)) {
            ++$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    my $r_obj = bless \$r;

    until ($r_obj->is_squarefree) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    $r_obj;
}

sub prev_squarefree {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 1) <= 0 and goto &nan;

    # Optimization for native integers
    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) - 1;
        until (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($n) : Math::Prime::Util::GMP::moebius($n)) {
            --$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    my $r_obj = bless \$r;

    until ($r_obj->is_squarefree) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    $r_obj;
}

sub next_powerfree {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 2 or goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 2) {
        return $n->next_squarefree;
    }

    $n = _any2mpz($$n, 0) // goto &nan;

    my $sgn = Math::GMPz::Rmpz_sgn($n);
    $sgn < 0  and goto &nan;
    $sgn == 0 and return ONE;

    # Optimization for native integers
    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_slong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) + 1;
        until (Math::Prime::Util::is_powerfree($n, $k)) {
            ++$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_add_ui($r, $n, 1);

    my $k_obj = bless \$k;
    my $r_obj = bless \$r;

    until ($r_obj->is_powerfree($k_obj)) {
        Math::GMPz::Rmpz_add_ui($r, $r, 1);
    }

    $r_obj;
}

sub next_cubefree {
    my ($n) = @_;
    $n->next_powerfree(THREE);
}

sub prev_powerfree {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 2 or goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 2) {
        return $n->prev_squarefree;
    }

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_cmp_ui($n, 1) <= 0 and goto &nan;

    # Optimization for native integers
    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n) - 1;
        until (Math::Prime::Util::is_powerfree($n, $k)) {
            --$n;
        }
        return bless \$n;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub_ui($r, $n, 1);

    my $k_obj = bless \$k;
    my $r_obj = bless \$r;

    until ($r_obj->is_powerfree($k_obj)) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }

    $r_obj;
}

sub prev_cubefree {
    my ($n) = @_;
    $n->prev_powerfree(THREE);
}

sub znorder {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    if (FAST_MODE and !ref($y) and !ref($x)) {
        my $r = (
                 HAS_PRIME_UTIL
                 ? Math::Prime::Util::znorder($x, $y)
                 : Math::Prime::Util::GMP::znorder($x, $y)
                ) // goto &nan;
        return bless \$r;
    }

    $x = _big2istr($x) // goto &nan;
    $y = _big2istr($y) // goto &nan;

    if (CORE::length($y) >= SPECIAL_FACTORS_MIN) {
        Math::Prime::Util::GMP::gcd($y, $x) eq '1' or goto &nan;
        $y = Math::Prime::Util::GMP::absint($y) if (substr($y, 0, 1) eq '-');
        $x = Math::Prime::Util::GMP::modint($x, $y);
        my @znorders;
        foreach my $pp (_factor_exp($y)) {
            my ($p, $e) = @$pp;
            if (HAS_PRIME_UTIL and $e == 1 and $p < ULONG_MAX) {
                push @znorders, Math::Prime::Util::znorder($x, $p);
            }
            else {
                push @znorders, Math::Prime::Util::GMP::znorder($x, (($e == 1) ? $p : Math::Prime::Util::GMP::powint($p, $e)));
            }
        }
        return _set_int(Math::Prime::Util::GMP::lcm(@znorders));
    }

    _set_int(Math::Prime::Util::GMP::znorder($x, $y) // goto &nan);
}

*multiplicative_order = \&znorder;

sub znprimroot {
    my ($n) = @_;
    _set_int(Math::Prime::Util::GMP::znprimroot(_big2uistr($$n) // (goto &nan)) // goto &nan);
}

sub _znlog_bsgs {    # Baby-step, Giant-step algorithm
    my ($a, $g, $p, $n) = @_;

    # Calculate m = ceil(sqrt(n))
    state $m = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_sqrt($m, $n);
    Math::GMPz::Rmpz_add_ui($m, $m, 1);

    my $limit = 1_000_000;
    my $m_ui  = Math::GMPz::Rmpz_cmp_ui($m, $limit) <= 0 ? Math::GMPz::Rmpz_get_ui($m) : $limit;

    # Calculate inverse of g
    state $invg = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_invert($invg, $g, $p) || return undef;

    state $gm            = Math::GMPz::Rmpz_init_nobless();
    state $current_baby  = Math::GMPz::Rmpz_init_nobless();
    state $current_giant = Math::GMPz::Rmpz_init_nobless();

    for (my $max_m = 1 ; ; $max_m <<= 1) {    # TODO: improve this

        my %baby_steps;
        Math::GMPz::Rmpz_set_ui($current_baby, 1);

        # Phase 1: Baby Steps
        # Calculate g^j mod p for 0 <= j < m
        for my $j (0 .. $max_m - 1) {

            my $key = Math::GMPz::Rmpz_get_str($current_baby, 62);

            # Keep the smallest j for a given value to ensure minimal exponent
            $baby_steps{$key} = $j if !exists $baby_steps{$key};

            Math::GMPz::Rmpz_mul($current_baby, $current_baby, $g);
            Math::GMPz::Rmpz_mod($current_baby, $current_baby, $p);
        }

        # Phase 2: Giant Steps setup
        # Calculate gm = g^{-m} mod p
        Math::GMPz::Rmpz_powm_ui($gm, $invg, $max_m, $p);
        Math::GMPz::Rmpz_set($current_giant, $a);

        # Calculate a * (g^{-m})^i mod p for 0 <= i < m
        for my $i (0 .. $max_m - 1) {
            my $key = Math::GMPz::Rmpz_get_str($current_giant, 62);

            if (exists $baby_steps{$key}) {
                my $j = $baby_steps{$key};

                # Result = i * m + j
                my $result = Math::GMPz::Rmpz_init_set_ui($i);
                Math::GMPz::Rmpz_mul_ui($result, $result, $max_m);
                Math::GMPz::Rmpz_add_ui($result, $result, $j);
                Math::GMPz::Rmpz_mod($result, $result, $n);

                return $result;
            }

            Math::GMPz::Rmpz_mul($current_giant, $current_giant, $gm);
            Math::GMPz::Rmpz_mod($current_giant, $current_giant, $p);
        }

        last if $max_m >= $m_ui;
    }

    return undef;
}

# Pollard's rho for discrete logarithm in a group of prime order
# Returns x such that g^x = h (mod n) where g has order p (prime)
sub _znlog_pollard_rho {
    my ($g, $h, $p, $n, $max_tries) = @_;

    $max_tries //= 10;

    # Trivial cases
    if (Math::GMPz::Rmpz_cmp_ui($h, 1) == 0) {
        return Math::GMPz::Rmpz_init_set_ui(0);
    }
    if (Math::GMPz::Rmpz_cmp($g, $h) == 0) {
        return Math::GMPz::Rmpz_init_set_ui(1);
    }

    # For very small prime orders, use Baby-Step, Giant-Step algorithm
    if (Math::GMPz::Rmpz_cmp_ui($p, 100) <= 0) {

        my $res = _znlog_bsgs($h, $g, $n, $p);

        if (defined($res)) {
            return $res;
        }

        # Alternatively, brute-force search
        my $t = Math::GMPz::Rmpz_init_set_ui(1);
        for my $i (0 .. Math::GMPz::Rmpz_get_ui($p) - 1) {
            if (Math::GMPz::Rmpz_cmp($t, $h) == 0) {
                return Math::GMPz::Rmpz_init_set_ui($i);
            }
            Math::GMPz::Rmpz_mul($t, $t, $g);
            Math::GMPz::Rmpz_mod($t, $t, $n);
        }
        return undef;
    }

    state $rng = Math::GMPz::zgmp_randinit_default_nobless();

    state $tmp = Math::GMPz::Rmpz_init_nobless();
    state $a1  = Math::GMPz::Rmpz_init_nobless();
    state $b1  = Math::GMPz::Rmpz_init_nobless();
    state $x1  = Math::GMPz::Rmpz_init_nobless();
    state $a2  = Math::GMPz::Rmpz_init_nobless();
    state $b2  = Math::GMPz::Rmpz_init_nobless();
    state $x2  = Math::GMPz::Rmpz_init_nobless();

    state $da = Math::GMPz::Rmpz_init_nobless();
    state $db = Math::GMPz::Rmpz_init_nobless();

    state $invdb = Math::GMPz::Rmpz_init_nobless();

    foreach my $attempt (1 .. $max_tries) {

        # Random starting point (a,b) with X = g^a * h^b
        Math::GMPz::Rmpz_urandomm($a1, $b1, $rng, $p, 2);    # 0 <= a1 < p

        Math::GMPz::Rmpz_powm($x1,  $g, $a1, $n);            # g^a1 mod n
        Math::GMPz::Rmpz_powm($tmp, $h, $b1, $n);            # h^b1 mod n
        Math::GMPz::Rmpz_mul($x1, $x1, $tmp);
        Math::GMPz::Rmpz_mod($x1, $x1, $n);

        # Hare starts same as tortoise
        Math::GMPz::Rmpz_set($a2, $a1);
        Math::GMPz::Rmpz_set($b2, $b1);
        Math::GMPz::Rmpz_set($x2, $x1);

        while (1) {

            # Tortoise step (Inlined)
            my $r1 = Math::GMPz::Rmpz_mod_ui($tmp, $x1, 3);
            if ($r1 == 0) {
                Math::GMPz::Rmpz_add_ui($a1, $a1, 1);
                Math::GMPz::Rmpz_mul($x1, $x1, $g);
                Math::GMPz::Rmpz_mod($x1, $x1, $n);
            }
            elsif ($r1 == 1) {
                Math::GMPz::Rmpz_add_ui($b1, $b1, 1);
                Math::GMPz::Rmpz_mul($x1, $x1, $h);
                Math::GMPz::Rmpz_mod($x1, $x1, $n);
            }
            else {
                Math::GMPz::Rmpz_mul_2exp($a1, $a1, 1);
                Math::GMPz::Rmpz_mul_2exp($b1, $b1, 1);
                Math::GMPz::Rmpz_mod($a1, $a1, $p);
                Math::GMPz::Rmpz_mod($b1, $b1, $p);
                Math::GMPz::Rmpz_powm_ui($x1, $x1, 2, $n);
            }

            # Hare step (Inlined, two iterations)
            for (1 .. 2) {
                my $r2 = Math::GMPz::Rmpz_mod_ui($tmp, $x2, 3);
                if ($r2 == 0) {
                    Math::GMPz::Rmpz_add_ui($a2, $a2, 1);
                    Math::GMPz::Rmpz_mul($x2, $x2, $g);
                    Math::GMPz::Rmpz_mod($x2, $x2, $n);
                }
                elsif ($r2 == 1) {
                    Math::GMPz::Rmpz_add_ui($b2, $b2, 1);
                    Math::GMPz::Rmpz_mul($x2, $x2, $h);
                    Math::GMPz::Rmpz_mod($x2, $x2, $n);
                }
                else {
                    Math::GMPz::Rmpz_mul_2exp($a2, $a2, 1);
                    Math::GMPz::Rmpz_mul_2exp($b2, $b2, 1);
                    Math::GMPz::Rmpz_mod($a2, $a2, $p);
                    Math::GMPz::Rmpz_mod($b2, $b2, $p);
                    Math::GMPz::Rmpz_powm_ui($x2, $x2, 2, $n);
                }
            }

            if (Math::GMPz::Rmpz_cmp($x1, $x2) == 0) {

                # Collision: g^{a1} h^{b1} = g^{a2} h^{b2}
                Math::GMPz::Rmpz_sub($da, $a1, $a2);
                Math::GMPz::Rmpz_mod($da, $da, $p);

                Math::GMPz::Rmpz_sub($db, $b2, $b1);
                Math::GMPz::Rmpz_mod($db, $db, $p);

                if (Math::GMPz::Rmpz_sgn($db) == 0) {
                    last;    # degenerate case, restart
                }

                Math::GMPz::Rmpz_invert($invdb, $db, $p) || last;

                my $x = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul($x, $da, $invdb);
                Math::GMPz::Rmpz_mod($x, $x, $p);

                # Verify
                Math::GMPz::Rmpz_powm($tmp, $g, $x, $n);
                if (Math::GMPz::Rmpz_cmp($tmp, $h) == 0) {
                    return $x;
                }
                last;    # verification failed, restart
            }
        }
    }
    return undef;
}

# Solve g^x = a (mod n) where g has order exactly p^e * r,
# and we want x modulo p^e.
sub _znlog_prime_power {
    my ($a, $g, $n, $p, $e, $full_order) = @_;

    my $L = $full_order;
    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_pow_ui($r, $p, $e);    # p^e
    Math::GMPz::Rmpz_tdiv_q($r, $L, $r);    # r = L / p^e

    state $g0 = Math::GMPz::Rmpz_init_nobless();
    state $a0 = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_powm($g0, $g, $r, $n);
    Math::GMPz::Rmpz_powm($a0, $a, $r, $n);

    my $x = Math::GMPz::Rmpz_init_set_ui(0);

    state $cur_g = Math::GMPz::Rmpz_init_nobless();
    state $cur_a = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set($cur_g, $g0);
    Math::GMPz::Rmpz_set($cur_a, $a0);

    state $f = Math::GMPz::Rmpz_init_nobless();    # multiplier for current digit
    Math::GMPz::Rmpz_set_ui($f, 1);

    state $tmp   = Math::GMPz::Rmpz_init_nobless();
    state $sub_g = Math::GMPz::Rmpz_init();
    state $sub_a = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_pow_ui($tmp, $p, $e - 1);
    Math::GMPz::Rmpz_powm($sub_g, $g0, $tmp, $n);

    foreach my $i (0 .. $e - 1) {

        Math::GMPz::Rmpz_pow_ui($tmp, $p, $e - $i - 1);    # p^(e-1-i)
        Math::GMPz::Rmpz_powm($sub_a, $cur_a, $tmp, $n);

        my $d = _znlog_pollard_rho($sub_g, $sub_a, $p, $n) // return undef;

        # x += d * f
        Math::GMPz::Rmpz_mul($tmp, $d, $f);
        Math::GMPz::Rmpz_add($x, $x, $tmp);

        # f *= p
        Math::GMPz::Rmpz_mul($f, $f, $p);

        # Remove the found part: cur_a = cur_a * inv(cur_g^d) mod n
        Math::GMPz::Rmpz_powm($tmp, $cur_g, $d, $n);
        Math::GMPz::Rmpz_invert($tmp, $tmp, $n) || return undef;
        Math::GMPz::Rmpz_mul($cur_a, $cur_a, $tmp);
        Math::GMPz::Rmpz_mod($cur_a, $cur_a, $n);

        # Next generator: cur_g = cur_g^p mod n
        Math::GMPz::Rmpz_powm($cur_g, $cur_g, $p, $n);
    }
    return $x;
}

# Solve g^x = a (mod n) where gcd(g, n) = 1, using Pohlig-Hellman over order of g.
# Suitable when n is a prime power (or when called per prime-power factor of n).
sub _znlog_coprime_prime_power {
    my ($a, $g, $n) = @_;

    my $order = _any2mpz((Math::Prime::Util::GMP::znorder($g, $n) // return undef), 4);

    state $tmp   = Math::GMPz::Rmpz_init_nobless();
    state $p_mpz = Math::GMPz::Rmpz_init_nobless();

    # Quick necessary condition: a must lie in the subgroup generated by g
    Math::GMPz::Rmpz_powm($tmp, $a, $order, $n);
    if (Math::GMPz::Rmpz_cmp_ui($tmp, 1) != 0) {
        return undef;
    }

    # Trivial case
    if (Math::GMPz::Rmpz_cmp_ui($order, 1) == 0) {
        if (Math::GMPz::Rmpz_cmp_ui($a, 1) == 0) {
            return 0;
        }
        return undef;
    }

    # Factor the order into prime powers and solve for each
    my @factors  = _factor_exp($order);
    my @residues = ();

    foreach my $pp (@factors) {
        my ($p, $e) = @$pp;
        Math::GMPz::Rmpz_set_str($p_mpz, $p, 10);
        my $x = _znlog_prime_power($a, $g, $n, $p_mpz, $e, $order) // return undef;
        push @residues, [$x, Math::Prime::Util::GMP::powint($p, $e)];
    }

    # Combine via CRT
    my $x = Math::Prime::Util::GMP::chinese(@residues) // return undef;

    # Verify
    Math::GMPz::Rmpz_set_str($tmp, $x, 10);
    Math::GMPz::Rmpz_powm($tmp, $g, $tmp, $n);
    if (Math::GMPz::Rmpz_cmp($tmp, $a) == 0) {
        return $x;
    }
    return undef;
}

sub _znlog_pohlig_hellman {
    my ($a, $g, $n, $order) = @_;

    my $tmp = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_gcd($tmp, $g, $n);

    # Handle non-coprime case: gcd(g, n) > 1
    if (Math::GMPz::Rmpz_cmp_ui($tmp, 1) != 0) {

        my $g_pow = Math::GMPz::Rmpz_init_set_ui(1);    # g^k mod n (original n), for direct equality check
        my $n_red = Math::GMPz::Rmpz_init_set($n);      # modulus being reduced
        my $a_red = Math::GMPz::Rmpz_init_set($a);      # target being reduced
        my $d_acc = Math::GMPz::Rmpz_init_set_ui(1);    # accumulated product: (g/D_1)*(g/D_2)*...*(g/D_k) mod n_red

        my $k = 0;
        while (1) {

            Math::GMPz::Rmpz_gcd($tmp, $g, $n_red);
            Math::GMPz::Rmpz_cmp_ui($tmp, 1) == 0 and last;

            # Check if g^k already equals a (mod n)
            if (Math::GMPz::Rmpz_cmp($g_pow, $a) == 0) {
                return $k;
            }

            Math::GMPz::Rmpz_divisible_p($a_red, $tmp) || return undef;

            Math::GMPz::Rmpz_div($n_red, $n_red, $tmp);
            Math::GMPz::Rmpz_div($a_red, $a_red, $tmp);

            Math::GMPz::Rmpz_div($tmp, $g, $tmp);
            Math::GMPz::Rmpz_mul($d_acc, $d_acc, $tmp);
            Math::GMPz::Rmpz_mul($g_pow, $g_pow, $g);
            Math::GMPz::Rmpz_mod($d_acc, $d_acc, $n_red);
            Math::GMPz::Rmpz_mod($g_pow, $g_pow, $n);

            ++$k;
        }

        # Final direct check after stripping
        if (Math::GMPz::Rmpz_cmp($g_pow, $a) == 0) {
            return $k;
        }

        # Phase 2: gcd(g, n_red) = 1 now; solve g^y = a_red * inv(d_acc) (mod n_red)
        Math::GMPz::Rmpz_invert($tmp, $d_acc, $n_red) || return undef;
        Math::GMPz::Rmpz_mul($tmp, $tmp, $a_red);

        my $new_a = Math::GMPz::Rmpz_init();
        my $new_g = Math::GMPz::Rmpz_init();

        Math::GMPz::Rmpz_mod($new_a, $tmp, $n_red);
        Math::GMPz::Rmpz_mod($new_g, $g,   $n_red);

        my $y = __SUB__->($new_a, $new_g, $n_red) // return undef;

        return ($y + $k);
    }

    # Coprime case: gcd(g, n) = 1

    # Factor n into prime powers
    my @n_factors = _factor_exp($n);

    # Composite n: solve g^x = a (mod p^e) for each prime-power factor, then CRT
    my @residues = ();

    my $pe  = Math::GMPz::Rmpz_init();
    my $g_i = Math::GMPz::Rmpz_init();
    my $a_i = Math::GMPz::Rmpz_init();

    foreach my $pp (@n_factors) {
        my ($p, $e) = @$pp;

        Math::GMPz::Rmpz_set_str($pe, $p, 10);
        Math::GMPz::Rmpz_pow_ui($pe, $pe, $e);
        Math::GMPz::Rmpz_mod($g_i, $g, $pe);
        Math::GMPz::Rmpz_mod($a_i, $a, $pe);

        my $r     = _znlog_coprime_prime_power($a_i, $g_i, $pe) // return undef;
        my $ord_i = Math::Prime::Util::GMP::znorder($g_i, $pe)  // return undef;

        push @residues, [$r, $ord_i];
    }

    my $x = _any2mpz(Math::Prime::Util::GMP::chinese(@residues) // return undef);

    # Verify the result
    Math::GMPz::Rmpz_powm($tmp, $g, $x, $n);
    if (Math::GMPz::Rmpz_cmp($tmp, $a) == 0) {
        return $x;
    }

    return undef;
}

sub znlog {
    my ($a, $g, $n) = @_;
    _valid(\$g, \$n);

    # TODO: add also the "index calculus" advanced method for solving the discrete logarithm

    $a = _any2mpz($$a, 1) // goto &nan;
    $g = _any2mpz($$g, 2) // goto &nan;
    $n = _any2mpz($$n, 3) // goto &nan;

    my $sgn = Math::GMPz::Rmpz_sgn($n) || goto &nan;

    if ($sgn < 0) {
        $n = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_abs($n, $n);
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        return ZERO;
    }

    $a = Math::GMPz::Rmpz_init_set($a);    # copy
    $g = Math::GMPz::Rmpz_init_set($g);    # copy

    Math::GMPz::Rmpz_mod($a, $a, $n);
    Math::GMPz::Rmpz_mod($g, $g, $n);

    if (   Math::GMPz::Rmpz_cmp_ui($a, 1) == 0
        or Math::GMPz::Rmpz_cmp_ui($g, 0) == 0) {
        return ZERO;
    }

    _set_int(_znlog_pohlig_hellman($a, $g, $n) // goto &nan);
}

*discrete_log = \&znlog;

sub rad {    # A007947
    my ($n) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {    # native integer

        $n < 0 and goto &nan;

        if (HAS_PRIME_UTIL) {
            $n || return ZERO;
            my $r = Math::Prime::Util::vecprod(map { $_->[0] } Math::Prime::Util::factor_exp($n));
            return bless \$r;
        }
    }

    $n || return ZERO;
    _set_int(Math::Prime::Util::GMP::vecprod(map { $_->[0] } _factor_exp($n)));
}

*squarefree_kernel = \&rad;

sub powerfree_part {
    my ($k, $n) = @_;

    # Multiplicative with:
    #   a(p^e, k) = p^(e mod k)

    _valid(\$n);

    $k = _any2ui($$k)    // goto &nan;
    $n = _big2uistr($$n) // goto &nan;

    $k <= 0   and goto &nan;
    $n eq '0' and return ZERO;

    _set_int(
             Math::Prime::Util::GMP::vecprod(map { ($_->[1] == 1) ? $_->[0] : Math::Prime::Util::GMP::powint($_->[0], $_->[1]) }
                                             grep { $_->[1] } map { [$_->[0], $_->[1] % $k] } _factor_exp($n))
            );
}

sub core {    # A007913
    (TWO)->powerfree_part($_[0]);
}

*squarefree_part = \&core;

sub powerfree_part_sum {
    my ($k, $from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->powerfree_part_sum($to)->sub($k->powerfree_part_sum($from->dec));
    }

    _valid(\$from);

    my $n = _any2mpz($$from) // return ZERO;
    $k = _any2ui($$k) // return ZERO;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return ZERO;

    return ZERO        if ($k == 0);
    return (bless \$n) if ($k == 1);

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $w = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_root($t, $n, $k);
    Math::GMPz::Rmpz_fits_ulong_p($t) || goto &nan;    # too large

    my $s   = Math::GMPz::Rmpz_get_ui($t);
    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    if (HAS_PRIME_UTIL) {
        Math::Prime::Util::forfactored(
            sub {

                # u = faulhaber(floor(n/v^k), 1)
                Math::GMPz::Rmpz_ui_pow_ui($w, $_, $k);
                Math::GMPz::Rmpz_div($t, $n, $w);
                Math::GMPz::Rmpz_mul($u, $t, $t);
                Math::GMPz::Rmpz_add($u, $u, $t);
                Math::GMPz::Rmpz_div_2exp($u, $u, 1);

                Math::GMPz::Rmpz_set_ui($w, 1);

                my $prev = 1;
                foreach my $p (@_) {
                    next if ($p == $prev);
                    Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
                    Math::GMPz::Rmpz_ui_sub($t, 1, $t);
                    Math::GMPz::Rmpz_mul($w, $w, $t);
                    $prev = $p;
                }

                Math::GMPz::Rmpz_mul($u, $u, $w);
                Math::GMPz::Rmpz_add($sum, $sum, $u);
            },
            $s
        );
    }
    else {
        my $m;
        for (my $v = 1 ; $v <= $s ; ++$v) {

            # u = faulhaber(floor(n/v^k), 1)
            Math::GMPz::Rmpz_ui_pow_ui($w, $v, $k);
            Math::GMPz::Rmpz_div($t, $n, $w);
            Math::GMPz::Rmpz_mul($u, $t, $t);
            Math::GMPz::Rmpz_add($u, $u, $t);
            Math::GMPz::Rmpz_div_2exp($u, $u, 1);

            Math::GMPz::Rmpz_set_ui($w, 1);

            my $prev = 1;
            foreach my $p (_factor($v)) {
                next if ($p == $prev);
                Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
                Math::GMPz::Rmpz_ui_sub($t, 1, $t);
                Math::GMPz::Rmpz_mul($w, $w, $t);
                $prev = $p;
            }

            Math::GMPz::Rmpz_mul($u, $u, $w);
            Math::GMPz::Rmpz_add($sum, $sum, $u);
        }
    }

    return bless \$sum;
}

sub arithmetic_derivative {
    my ($x) = @_;

    my $deriv = sub {
        my ($n) = @_;

        # (-a)' = -(a')
        if (Math::GMPz::Rmpz_sgn($n) < 0) {
            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_neg($t, $n);
            $t = __SUB__->($t);
            Math::GMPz::Rmpz_neg($t, $t);
            return $t;
        }

        my $u = Math::GMPz::Rmpz_init();
        my $d = Math::GMPz::Rmpz_init_set_ui(0);
        my $s = Math::GMPz::Rmpz_get_str($n, 10);

        return $d if ($s eq '0' or $s eq '1');

        foreach my $pk (_factor_exp($s)) {
            my ($p, $k) = @$pk;

            # a(n) = Sum_{p^k|n} (n*k)/p

            if (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_divexact_ui($u, $n, $p);
            }
            else {
                Math::GMPz::Rmpz_set_str($u, "$p", 10);
                Math::GMPz::Rmpz_divexact($u, $n, $u);
            }

            Math::GMPz::Rmpz_addmul_ui($d, $u, $k);
        }

        return $d;
    };

    my $n = $$x;

    # (a/b)' = (a'b - b'a) / b^2
    if (ref($n) eq 'Math::GMPq') {

        my $t1 = Math::GMPz::Rmpz_init();
        my $t2 = Math::GMPz::Rmpz_init();

        Math::GMPq::Rmpq_get_num($t1, $n);    # a
        Math::GMPq::Rmpq_get_den($t2, $n);    # b

        my $d1 = $deriv->($t1);               # a'
        my $d2 = $deriv->($t2);               # b'

        Math::GMPz::Rmpz_mul($d1, $d1, $t2);  # d1 = a' * b
        Math::GMPz::Rmpz_mul($d2, $d2, $t1);  # d2 = b' * a
        Math::GMPz::Rmpz_mul($t2, $t2, $t2);  # t2 = b^2
        Math::GMPz::Rmpz_sub($d1, $d1, $d2);  # d1 = (a'b - b'a)

        # q = d1 / t2
        my $q = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set_num($q, $d1);
        Math::GMPq::Rmpq_set_den($q, $t2);
        Math::GMPq::Rmpq_canonicalize($q);
        return bless \$q;
    }

    bless \($deriv->(_any2mpz($n) // goto &nan));
}

*derivative = \&arithmetic_derivative;

sub logarithmic_derivative {
    my ($n) = @_;
    $n->arithmetic_derivative->div($n);
}

sub lpf {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n) and $n < 0xffffffff) {
        $n >= 0 or goto &nan;
        return _set_int($n) if ($n <= 1);
        my @f = _factor($n);
        return _set_int($f[0]);
    }

    $n = _any2mpz($n) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) >= 0 or goto &nan;

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) <= 0) {
        return _set_int($n);
    }

    foreach my $p (2, 3, 5, 7) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $p)) {
            return _set_int($p);
        }
    }

    my $size = Math::GMPz::Rmpz_sizeinbase($n, 2);

    foreach my $j (2 .. 8) {

        my (undef, $f) = _primorial_trial_factor($n, 10**$j);

        if (defined($f)) {
            return _set_int($f);
        }

        last if (($j >= 5) && ($size <= 100));    # 30 digits
        last if (($j >= 6) && ($size <= 133));    # 40 digits
        last if (($j >= 7) && ($size <= 150));    # 45 digits
    }

    my @f = _factor($n);
    _set_int($f[0]);
}

sub lpf_sum {    # sum of lpf(k)
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->lpf_sum->sub($from->dec->lpf_sum);
    }

    my $n = $$from;

    if (ref($n)) {
        $n = _big2istr($n) // goto &nan;
    }

    if ($n < 0) {
        return ZERO;
    }

    my $s = Math::Prime::Util::GMP::sqrtint($n);

    if ($s > ULONG_MAX) {    # too large
        goto &nan;
    }

    if (HAS_PRIME_UTIL and ((INTSIZE >= 64) ? ($n <= 1e10) : ($n <= 1e5))) {

        my $pi  = 0;
        my $sum = 0;

        foreach my $p (@{Math::Prime::Util::primes($s)}) {
            $sum += $p * Math::Prime::Util::legendre_phi(CORE::int($n / $p), $pi++);
        }

        return _set_int($sum)->add(_set_int(_next_prime($s))->sum_primes(_set_int($n)));
    }

    my $pi  = 0;
    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $p (@{_primes(2, $s)}) {
        my $d = Math::Prime::Util::GMP::divint($n, $p);
        my $r =
          (HAS_PRIME_UTIL and $d < ULONG_MAX)
          ? Math::Prime::Util::legendre_phi($d, $pi)
          : ${(bless \$p)->rough_count(_set_int($d))};
        my $w = Math::Prime::Util::GMP::mulint($r, $p);
        if (FAST_MODE and $w < ULONG_MAX) {
            Math::GMPz::Rmpz_add_ui($sum, $sum, $w);
        }
        else {
            state $t = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_set_str($t, "$w", 10);
            Math::GMPz::Rmpz_add($sum, $sum, $t);
        }
        ++$pi;
    }

    (bless \$sum)->add(_set_int(_next_prime($s))->sum_primes(_set_int($n)));
}

sub gpf {
    my ($n) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n >= 0 or goto &nan;
    }

    if ($n eq '0' or $n eq '1') {
        return bless \$n;
    }

    my @f = _factor($n);
    _set_int($f[-1]);
}

sub gpf_sum {    # sum of gpf(k)
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $to->gpf_sum->sub($from->dec->gpf_sum);
    }

    my $n = $$from;

    if (ref($n)) {
        $n = _big2istr($n) // goto &nan;
    }

    if ($n < 0) {
        return ZERO;
    }

    my $s = Math::Prime::Util::GMP::sqrtint($n);

    if ($s > ULONG_MAX) {    # too large
        goto &nan;
    }

    if (HAS_PRIME_UTIL and ((INTSIZE >= 64) ? ($n <= 1e10) : ($n <= 1e5))) {

        my $sum = 0;

        foreach my $p (@{Math::Prime::Util::primes($s)}) {
            $sum += $p * Math::Prime::Util::smooth_count(CORE::int($n / $p), $p);
        }

        for (my $p = Math::Prime::Util::next_prime($s) ; $p <= $n ; $p = Math::Prime::Util::next_prime($p)) {

            my $u = CORE::int($n / $p);
            my $r = CORE::int($n / $u);

            $sum += $u * (
                          ($r - $p < PRIMESUM_MIN)
                          ? Math::Prime::Util::sum_primes($p, $r)
                          : ${_set_int($p)->sum_primes(_set_int($r))}
                         );
            $p = $r;
        }

        return _set_int($sum);
    }

    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $p (@{_primes(2, $s)}) {
        my $d = Math::Prime::Util::GMP::divint($n, $p);
        my $r =
          (HAS_PRIME_UTIL and $d < ULONG_MAX)
          ? Math::Prime::Util::smooth_count($d, $p)
          : ${(bless \$p)->smooth_count(_set_int($d))};
        my $w = Math::Prime::Util::GMP::mulint($r, $p);
        if (FAST_MODE and $w < ULONG_MAX) {
            Math::GMPz::Rmpz_add_ui($sum, $sum, $w);
        }
        else {
            state $t = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_set_str($t, "$w", 10);
            Math::GMPz::Rmpz_add($sum, $sum, $t);
        }
    }

    if ($n > ULONG_MAX) {
        $n = Math::GMPz::Rmpz_init_set_str("$n", 10);
    }

    for (my $p = _next_prime($s) ; $p <= $n ; $p = _next_prime($p)) {

        my $u = Math::Prime::Util::GMP::divint($n, $p);
        my $r = Math::Prime::Util::GMP::divint($n, $u);

        my $w = Math::Prime::Util::GMP::mulint($u, _set_int($p)->sum_primes(_set_int($r)));

        if (FAST_MODE and $w < ULONG_MAX) {
            Math::GMPz::Rmpz_add_ui($sum, $sum, $w);
        }
        else {
            state $t = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_set_str($t, "$w", 10);
            Math::GMPz::Rmpz_add($sum, $sum, $t);
        }

        $p = $r;
    }

    bless \$sum;
}

sub gcd_factors {
    my ($n, $arr) = @_;

    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_sgn($n) > 0
      or return _array();

    my $z = Math::GMPz::Rmpz_init_set($n);    # copy
    state $t = Math::GMPz::Rmpz_init_nobless();

    my @gcds;
    my %seen_k;
    my %seen_gcd;

    foreach my $k (@$arr) {
        _valid(\$k);
        my $m = _any2mpz($$k) // next;
        next if $seen_k{Math::GMPz::Rmpz_get_str($m, 10)}++;
        Math::GMPz::Rmpz_gcd($t, $z, $m);
        Math::GMPz::Rmpz_cmp_ui($t, 1) > 0 or next;
        Math::GMPz::Rmpz_cmp($t, $z) < 0   or next;
        if (!$seen_gcd{Math::GMPz::Rmpz_get_str($t, 10)}++) {
            push @gcds, Math::GMPz::Rmpz_init_set($t);
        }
    }

    @gcds = sort { Math::GMPz::Rmpz_cmp($a, $b) } @gcds;

    my @factors;

    foreach my $g (@gcds) {

        Math::GMPz::Rmpz_gcd($t, $g, $z);
        Math::GMPz::Rmpz_cmp_ui($t, 1) > 0 or next;
        Math::GMPz::Rmpz_cmp($t, $z) < 0   or next;

        my $v = Math::GMPz::Rmpz_remove($z, $z, $t);
        push(@factors, (Math::GMPz::Rmpz_init_set($t)) x $v);
    }

    if (Math::GMPz::Rmpz_cmp_ui($z, 1) > 0) {
        push @factors, $z;
    }

    @factors = sort { Math::GMPz::Rmpz_cmp($a, $b) } @factors;
    @factors = map  { bless \$_ } @factors;

    _array(\@factors);
}

*gcd_factor = \&gcd_factors;

sub fibonacci_factor {
    my ($n, $upto) = @_;

    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    $upto =
      defined($upto)
      ? do { _any2ui($$upto) // return _array(bless \$n) }
      : CORE::int(1.4747698 * __ilog__($n, 2));

    my @factors;
    my $g = Math::GMPz::Rmpz_init();

    my ($P, $Q) = (3, 1);

    my $U0 = Math::GMPz::Rmpz_init_set_ui(0);
    my $U1 = Math::GMPz::Rmpz_init_set_ui(1);

    my $V0 = Math::GMPz::Rmpz_init_set_ui(2);
    my $V1 = Math::GMPz::Rmpz_init_set_ui($P);

    foreach my $k (2 .. $upto) {

        # my ($U, $V) = Math::Prime::Util::GMP::lucas_sequence($n, $P, $Q, $k);

        Math::GMPz::Rmpz_set($g, $U1);
        Math::GMPz::Rmpz_mul_ui($U1, $U1, $P);
        Math::GMPz::Rmpz_submul_ui($U1, $U0, $Q);
        Math::GMPz::Rmpz_mod($U1, $U1, $n);
        Math::GMPz::Rmpz_set($U0, $g);

        Math::GMPz::Rmpz_set($g, $V1);
        Math::GMPz::Rmpz_mul_ui($V1, $V1, $P);
        Math::GMPz::Rmpz_submul_ui($V1, $V0, $Q);
        Math::GMPz::Rmpz_mod($V1, $V1, $n);
        Math::GMPz::Rmpz_set($V0, $g);

        foreach my $param ([$U1, 0], [$V1, -$P, -2 * $Q, 0]) {

            my ($t, @deltas) = @$param;

            foreach my $delta (@deltas) {

                ($delta >= 0)
                  ? Math::GMPz::Rmpz_add_ui($g, $t, $delta)
                  : Math::GMPz::Rmpz_sub_ui($g, $t, -$delta);

                Math::GMPz::Rmpz_gcd($g, $g, $n);

                if (    Math::GMPz::Rmpz_cmp_ui($g, 1) > 0
                    and Math::GMPz::Rmpz_cmp($g, $n) < 0) {
                    my $r = Math::GMPz::Rmpz_init_set($g);
                    push @factors, bless \$r;
                }
            }
        }
    }

    if (!@factors) {
        return _array(bless \$n);
    }

    (bless \$n)->gcd_factors(_array(\@factors));
}

*fib_factor = \&fibonacci_factor;

sub sophie_germain_factor {
    my ($n) = @_;

    # A simple factorization method, based on Sophie Germain's identity:
    #   x^4 + 4y^4 = (x^2 + 2xy + 2y^2) * (x^2 - 2xy + 2y^2)

    # This method is also effective for numbers of the form: n^4 + 4^(2k+1).

    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    state $w = Math::GMPz::Rmpz_init_nobless();
    state $z = Math::GMPz::Rmpz_init_nobless();

    my $sophie_germain_decomposition = sub {
        my ($n) = @_;

        state $t = Math::GMPz::Rmpz_init_nobless();
        state $u = Math::GMPz::Rmpz_init_nobless();

        Math::GMPz::Rmpz_root($t, $n, 4);
        Math::GMPz::Rmpz_pow_ui($w, $t, 4);
        Math::GMPz::Rmpz_sub($u, $n, $w);
        Math::GMPz::Rmpz_div_2exp($u, $u, 2);

        if (Math::GMPz::Rmpz_root($u, $u, 4)) {

            # n = t^4 + 4*u^4
            Math::GMPz::Rmpz_pow_ui($z, $u, 4);
            Math::GMPz::Rmpz_mul_2exp($z, $z, 2);
            Math::GMPz::Rmpz_add($w, $w, $z);

            if (Math::GMPz::Rmpz_cmp($w, $n) == 0) {
                return ($t, $u);
            }
        }

        Math::GMPz::Rmpz_mul_2exp($t, $n, 2);
        Math::GMPz::Rmpz_root($t, $t, 4);
        Math::GMPz::Rmpz_div_2exp($t, $t, 1);
        Math::GMPz::Rmpz_pow_ui($z, $t, 4);
        Math::GMPz::Rmpz_mul_2exp($z, $z, 2);
        Math::GMPz::Rmpz_sub($u, $n, $z);

        if (Math::GMPz::Rmpz_root($u, $u, 4)) {

            # n = u^4 + 4*t^4
            Math::GMPz::Rmpz_pow_ui($w, $u, 4);
            Math::GMPz::Rmpz_add($w, $w, $z);

            if (Math::GMPz::Rmpz_cmp($w, $n) == 0) {
                return ($u, $t);
            }
        }

        return;
    };

    my ($x, $y) = $sophie_germain_decomposition->($n);

    if (!defined($x) or !defined($y)) {
        return _array([bless \$n]);
    }

    my $p = Math::GMPz::Rmpz_init();
    my $q = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_mul($w, $x, $y);
    Math::GMPz::Rmpz_mul_2exp($w, $w, 1);
    Math::GMPz::Rmpz_mul($z, $x, $x);

    Math::GMPz::Rmpz_sub($p, $z, $w);
    Math::GMPz::Rmpz_add($q, $z, $w);

    Math::GMPz::Rmpz_mul($w, $y, $y);
    Math::GMPz::Rmpz_mul_2exp($w, $w, 1);

    Math::GMPz::Rmpz_add($p, $p, $w);
    Math::GMPz::Rmpz_add($q, $q, $w);

    return _array([(bless \$p), (bless \$q)]);
}

*germain_factor = \&sophie_germain_factor;

sub _is_small_number {
    my ($z) = @_;
    return Math::GMPz::Rmpz_fits_ulong_p($z)
      || Math::GMPz::Rmpz_sizeinbase($z, 2) <= SMALL_NUMBER_MAX_BITS;
}

sub _get_recursive_factors {
    my ($factor, $rec_tried, $n) = @_;

    return () if ($rec_tried->{$$factor}++ || $factor->sqr->gt($n));

    say STDERR "Recursively factoring: $$factor" if $VERBOSE;
    return @{$factor->special_factors};
}

sub _is_trivial_factor {
    my ($factor) = @_;
    return (!ref($$factor) or Math::GMPz::Rmpz_sizeinbase($$factor, 2) <= SMALL_NUMBER_MAX_BITS or _is_prob_prime($$factor, 1));
}

sub _create_collector {
    my ($factors, $rec_tried, $factorized_ref, $n) = @_;

    return sub {
        my ($f) = @_;
        my @arr = @$f;
        my $rem = pop(@arr);

        return unless @arr;
        push(@$factors, @arr);
        push(@arr,      $rem);

        while (@arr) {
            my $factor = shift(@arr);

            if (_is_trivial_factor($factor)) {
                $$factorized_ref ||= 1;
            }
            else {
                my @new_factors = _get_recursive_factors($factor, $rec_tried, $n);

                if (@new_factors >= 2) {
                    push @arr,      @new_factors;
                    push @$factors, @new_factors;
                }
                elsif ($factor->is_perfect_power) {
                    my @perfect = (($factor->perfect_root) x CORE::int($factor->perfect_power));
                    push @arr,      @perfect;
                    push @$factors, @perfect;
                }
                else {
                    $$factorized_ref = 0;
                    last;
                }
            }
        }
    };
}

sub _try_factorization_methods {
    my ($n, $mp1, $collector, $factorized_ref) = @_;

    my @methods = (
                   sub { $n->is_perfect_power ? [($n->perfect_root) x CORE::int($n->perfect_power)] : undef },
                   sub { $n->trial_factor($mp1->mul(_set_int(1e5))) },
                   sub { $n->germain_factor },
                   sub { $n->holf_factor($mp1->mul(_set_int(1e4))) },
                   sub { $n->fermat_factor($mp1->mul(_set_int(1e3))) },
                   ($n->len->lt(_set_int(500)) ? sub { $n->flt_factor($mp1->mul(_set_int(1e2))) } : ()),
                   sub { $n->phi_finder_factor($mp1->mul(_set_int(1e3))) },
                   sub { $n->dop_factor($mp1->mul($n->ilog2->isqrt)->mul(TWO)) },
                   sub { $n->miller_factor($mp1->mul(_set_int(5))) },
                   sub { $n->fibonacci_factor },
                   sub { $n->lucas_factor(undef, $mp1->mul(TWO)) },
                   sub { $n->cop_factor($mp1->mul($n->ilog2->isqrt->shift_right(ONE))) },
                   sub { $n->pell_factor($mp1->mul(_set_int(1e3))) },
                  );

    for my $method (@methods) {
        last if $$factorized_ref;
        my $result = $method->();
        $collector->($result) if defined($result);
    }
}

sub _separate_factors {
    my ($factors) = @_;
    my (@prime, @composite);

    for my $f (@$factors) {
        if (_is_prob_prime($$f, 1)) {
            push @prime, $f;
        }
        elsif (!ref($$f) or Math::GMPz::Rmpz_sizeinbase($$f, 2) <= SMALL_NUMBER_MAX_BITS) {
            local $SPECIAL_FACTORS = 0;
            push @prime, map { _set_int($_) } _factor($$f);
        }
        else {
            push @composite, $f;
        }
    }

    return (\@prime, \@composite);
}

sub _build_factorization_blocks {
    my ($m) = @_;

    return (
        Sidef::Types::Block::Block->new(code => sub { $_[0]->fermat_factor($m->mul(_set_int(1e3))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->holf_factor($m->mul(_set_int(1e4))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->pell_factor($m->mul(_set_int(5e2))) }),

        #Sidef::Types::Block::Block->new(code => sub { $_[0]->flt_factor(_set_int(_random_prime(1e4)), $m->mul(_set_int(1e2))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->pm1_factor($m->mul(_set_int(20000)), $m->mul(_set_int(200000))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->ecm_factor($m->mul(_set_int(600)),  $m->mul(_set_int(20))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->ecm_factor($m->mul(_set_int(2000)), $m->mul(_set_int(10))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->pm1_factor($m->mul(_set_int(1e5))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->pp1_factor($m->mul(_set_int(5e4))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->pbrent_factor($m->mul(_set_int(1e5))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->chebyshev_factor($m->mul(_set_int(5e3))) }),
        Sidef::Types::Block::Block->new(code => sub { $_[0]->phi_finder_factor($m->mul(_set_int(1e2))) }),
    );
}

sub _apply_recursive_methods {
    my ($composite, $m) = @_;

    state $methods = {};
    $methods->{$m} //= [_build_factorization_blocks($m)];

    for my $block (@{$methods->{$m}}) {
        $composite = [map { @{$_->factor($block)} } @$composite];
    }

    # Apply cyclotomic factorization for m >= 2 (however, it's too slow)
#<<<
    #~ if ($m->ge(TWO)) {
        #~ $composite = [
            #~ map {
                #~ _is_prob_prime($$_, 1)
                  #~ ? $_
                  #~ : @{$_->cyclotomic_factor(map { _set_int($_) } 2 .. CORE::int($m->mul(_set_int(5))))}
              #~ } @$composite
        #~ ];
    #~ }
#>>>

    return $composite;
}

sub _factorize_small {
    my ($z) = @_;
    local $SPECIAL_FACTORS = 0;

    my @factors =
      Math::GMPz::Rmpz_fits_ulong_p($z)
      ? map { bless \$_ } _factor(Math::GMPz::Rmpz_get_ui($z))
      : map { _set_int($_) } _factor(Math::GMPz::Rmpz_get_str($z, 10));

    return _array(\@factors);
}

sub special_factors {
    my ($n, $m) = @_;

    # Initialize and validate parameters
    $m = defined($m) ? (_valid(\$m), $m) : ONE;
    my $z = _any2mpz($$n) // return _array();

    return _array() if Math::GMPz::Rmpz_sgn($z) <= 0;

    # Handle small numbers directly
    return _factorize_small($z) if _is_small_number($z);

    # Handle prime numbers
    return _array([bless \$z]) if _is_prob_prime($z, 1);

    # Main factorization
    my (@factors, %rec_tried, $factorized);
    my $mp1 = $m->inc;

    # Collect factors using various methods
    my $collect_factors = _create_collector(\@factors, \%rec_tried, \$factorized, $n);

    _try_factorization_methods($n, $mp1, $collect_factors, \$factorized);

    # Process factors
    @factors = @{$n->gcd_factors(_array([@factors]))};
    my ($prime_factors, $composite_factors) = _separate_factors(\@factors);

    # Apply recursive methods if needed
    $composite_factors = _apply_recursive_methods($composite_factors, $m)
      if (@$composite_factors && $m->is_positive);

    return $n->gcd_factors(_array([@$prime_factors, @$composite_factors]));
}

*special_factor = \&special_factors;

sub factor {
    my ($n, $block) = @_;

    if (defined($block)) {

        my %cache;

        my $f = _array([$n])->recmap(
            Sidef::Types::Block::Block->new(
                code => sub {
                    my ($n) = @_;
                    _is_prob_prime($$n, 1) ? _array() : do {
                        my $factors = do { $cache{"$n"} //= $block->run($n) };
                        $factors->first(-1)->concat($factors->last(-1))->iuniq;
                    };
                }
            )
        );
        return $n->gcd_factors($f);
    }

    $n = $$n;

    if (ref($n)) {
        $n = _big2pistr($n) // return _array();
    }
    else {
        $n > 0 or return _array();
    }

    _array([map { _set_int($_) } _factor($n)]);
}

*factors       = \&factor;
*prime_factors = \&factor;

sub factor_exp {
    my ($n) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2pistr($n) // return _array();
    }
    else {
        $n > 0 or return _array();
    }

    my @pairs;
    foreach my $pk (_factor_exp($n)) {
        my ($p, $k) = @$pk;
        push @pairs, bless([(($p < ULONG_MAX) ? (bless \$p) : _set_int($p)), (bless \$k)], 'Sidef::Types::Array::Array');
    }

    _array(\@pairs);
}

*factors_exp = \&factor_exp;

sub prime_signature {
    my ($n) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2pistr($n) // return _array();
    }
    else {
        $n > 0 or return _array();
    }

    if (HAS_NEW_PRIME_UTIL and $n < ULONG_MAX) {
        my @exp = Math::Prime::Util::prime_signature($n);
        @exp = map { bless \$_ } @exp;
        return _array(\@exp);
    }

    my @exp = sort { $b <=> $a } map { $_->[1] } _factor_exp($n);
    @exp = map { bless \$_ } @exp;
    _array(\@exp);
}

sub _factor_remainder {
    my ($r, $size, $ecm_table) = @_;

    if (ref($r) eq 'Math::GMPz') {
        $r = Math::GMPz::Rmpz_get_str($r, 10);
    }

    # Check primality
    if (_is_prob_prime($r, 1)) {
        return ($r);
    }

    # Check perfect power
    if (my $k = _power_factor($r)) {
        my $base = Math::Prime::Util::GMP::rootint($r, $k);
        say STDERR "is_power(r): $base^$k" if $VERBOSE;
        return ((_factor_remainder($base, $size, $ecm_table)) x $k);
    }

    # Recalculate size for remainder
    my $max_size = (Math::Prime::Util::GMP::logint($r, 2) >> 1) + 1;
    if ($size > $max_size) {
        $size = $max_size;
    }

    my $limit_isqrt = Math::Prime::Util::GMP::powint(2, (($size >> 1) + 1));
    $limit_isqrt = 100 if ($limit_isqrt < 100);

    my @ecm_params;

    # ECM parameter selection
    foreach my $i (0 .. CORE::int(@$ecm_table / 3 - 1)) {

        my ($B1, $curves) = (@{$ecm_table}[3 * $i + 1, 3 * $i + 2]);

        # Scale curves with difficulty
        my $scale_factor = ($size > 100) ? 2.5 : 2.0;
        $curves = CORE::int($curves * $scale_factor);

        push @ecm_params, [$B1, $curves];

        if ($ecm_table->[3 * $i] >= $size) {
            last;
        }
    }

    my @factors;

    while (1) {
        my @f;

        # Early termination for primes
        if (_is_prob_prime($r, 1)) {
            last;
        }

        if ($r < 0xffffffff) {    # 2^32 - 1
            @f =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::factor($r)
              : Math::Prime::Util::GMP::factor($r);
            $r = 1;
            say STDERR "factor(r): @f" if $VERBOSE;
        }

        # Method selection based on expected factor size
        elsif ($size < 20) {

            # Very small: use Pollard Rho
            my $effort = Math::Prime::Util::GMP::mulint(2, $limit_isqrt);
            @f = Math::Prime::Util::GMP::prho_factor($r, $effort);
            $r = pop @f;
            say STDERR "prho_factor(r, $effort): @f" if (@f && $VERBOSE);
        }
        elsif ($size < 35) {

            # Small to medium: try Brent first, then Rho
            my $effort = Math::Prime::Util::GMP::mulint(2, $limit_isqrt);
            @f = Math::Prime::Util::GMP::pbrent_factor($r, $effort);
            $r = pop @f;
            say STDERR "pbrent_factor(r, $effort): @f" if (@f && $VERBOSE);

            if (!@f) {
                @f = Math::Prime::Util::GMP::prho_factor($r, $effort);
                $r = pop @f;
                say STDERR "prho_factor(r, $effort): @f" if (@f && $VERBOSE);
            }
        }
        else {
            while (defined(my $ecm_param = shift(@ecm_params))) {
                my ($B1, $curves) = @$ecm_param;
                say STDERR "Trying ecm_factor(r, $B1, $curves)" if $VERBOSE;
                my @tmp_f = Math::Prime::Util::GMP::ecm_factor($r, $B1, $curves);

                if (scalar(@tmp_f) >= 2) {
                    unshift(@ecm_params, $ecm_param);
                    @f = @tmp_f;
                    $r = pop @f;
                    say STDERR "ecm_factor(r, $B1, $curves): @f" if (@f && $VERBOSE);
                    last;
                }
            }
        }

        if (!@f) {
            last;
        }

        my @new_factors;

        foreach my $factor (@f) {
            if (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime($factor) : Math::Prime::Util::GMP::is_prime($factor)) {
                push @new_factors, $factor;
            }
            else {
                push @new_factors, _factor($factor);
            }
        }

        push @factors, @new_factors;

        # Batch process all factors efficiently
        if (@new_factors) {

            # Build product of all new factors
            my $product = Math::Prime::Util::GMP::vecprod(@new_factors);

            # Use GCD to find common factors efficiently
            my $g = Math::Prime::Util::GMP::gcd($r, $product);

            if ($g ne '1' and $g ne $r) {

                # Find valuation for each prime in the GCD
                foreach my $p (@new_factors) {
                    my $v = Math::Prime::Util::GMP::valuation($r, $p);
                    if ($v > 0) {
                        push @factors, ($p) x $v;
                        $r = Math::Prime::Util::GMP::divint($r, Math::Prime::Util::GMP::powint($p, $v));
                    }
                }
            }
        }

        # Recalculate size for remainder
        my $max_size = (Math::Prime::Util::GMP::logint($r, 2) >> 1) + 1;
        if ($size > $max_size) {
            $size = $max_size;
        }
    }

    if ($r ne '1') {
        push @factors, $r;
    }

    return @factors;
}

sub factor_upto {
    my ($n, $limit) = @_;

    # Reference:
    #   https://members.loria.fr/PZimmermann/records/ecm/params.html
    state $ecm_table = [
        qw(
          30      1358        2
          35      1270        5
          40      1629        10
          45      4537        10
          50      12322       9
          55      12820       18
          60      21905       21
          65      24433       41
          70      32918       66
          75      64703       71
          80      76620       119
          85      155247      123
          90      183849      219
          95      245335      321
          100     445657      339
          105     643986      468
          110     1305195     439
          115     1305195     818
          120     3071166     649
          125     3784867     949
          130     4572523     1507
          135     7982718     1497
          140     9267681     2399
          145     22025673    1826
          150     22025673    3159
          155     26345943    4532
          160     35158748    6076
          165     46919468    8177
          170     47862548    14038
          175     153319098   7166
          180     153319098   12017
          185     188949210   16238
          190     410593604   13174
          195     496041799   17798
          200     491130495   29584
          205     1067244762  23626
          210     1056677983  38609
          215     1328416470  49784
          220     1315263832  81950
          225     2858117139  63461
          )
    ];

    my $r = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_sgn($r) > 0
      or return _array();

    if (defined($limit)) {
        $limit = _big2pistr($$limit) // return _array([$n]);
    }
    else {
        $limit = Math::Prime::Util::GMP::sqrtint($r);
    }

    my $size     = Math::Prime::Util::GMP::logint($limit, 2) + 1;
    my $max_size = (Math::GMPz::Rmpz_sizeinbase($r, 2) >> 1) + 1;    # ilog_2(sqrt(r))

    if ($size > $max_size) {
        $size = $max_size;
    }

    my @factors;

    # Trial division first for small factors
    if ($size > 13) {
        my $trial_limit = ($size > 16) ? (1 << 16) : (1 << $size);

        ($r, @factors) = _primorial_trial_factor($r, $trial_limit);

        if (@factors > 0) {

            # If fully factored, return result
            if (Math::GMPz::Rmpz_cmp_ui($r, 1) == 0) {
                return _array([map { bless \$_ } @factors]);
            }
        }
    }
    else {
        return $n->trial_factor(_set_int(1 << $size));
    }

    # Factor the remainder
    push @factors, _factor_remainder($r, $size, $ecm_table);
    _array([map { _set_int($_) } @factors])->isort;
}

sub trial_factor {
    my ($n, $k) = @_;

    if (!defined($k)) {
        $n = _big2pistr($$n) // (return _array());
        my ($r, @factors) = _adaptive_trial_factor($n);
        @factors = map { bless \$_ } @factors;
        if (Math::GMPz::Rmpz_cmp_ui($r, 1) > 0) {
            $r = Math::GMPz::Rmpz_get_ui($r) if Math::GMPz::Rmpz_fits_ulong_p($r);
            push @factors, bless \$r;
        }
        return _array(\@factors);
    }

    _valid(\$k);
    __is_int__($$n) || return _array();

    $n = _any2mpz($$n, 0) // return _array();
    $k = _any2ui($$k)     // return _array();

    return _array()               if Math::GMPz::Rmpz_sgn($n) <= 0;
    return _array([_set_int($n)]) if $k <= 0;

    my ($r, @factors) = _primorial_trial_factor($n, $k);

    @factors = map { bless \$_ } @factors;

    if (Math::GMPz::Rmpz_cmp_ui($r, 1) > 0) {
        $r = Math::GMPz::Rmpz_get_ui($r) if Math::GMPz::Rmpz_fits_ulong_p($r);
        push @factors, bless \$r;
    }

    return _array(\@factors);
}

sub prho_factor {
    my ($n, $k) = @_;
    _valid(\$k) if defined($k);
    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::prho_factor(_big2pistr($$n) // (return _array()), (defined($k) ? (_big2pistr($$k) || return _array($n)) : ()),)
           ]
          );
}

*rho_factor = \&prho_factor;

sub pbrent_factor {
    my ($n, $k) = @_;
    _valid(\$k) if defined($k);
    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::pbrent_factor(_big2pistr($$n) // (return _array()), (defined($k) ? (_big2pistr($$k) || return _array($n)) : ()))
           ]
          );
}

*rho_brent_factor = \&pbrent_factor;

sub pminus1_factor {
    my ($n, $B1, $B2) = @_;

    _valid(\$B1) if defined($B1);
    _valid(\$B2) if defined($B2);

    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::pminus1_factor(
                                                     _big2pistr($$n) // (return _array()),
                                                     (defined($B1) ? (_big2pistr($$B1) || return _array($n)) : ()),
                                                     (defined($B2) ? (_big2pistr($$B2) || return _array($n)) : ())
                                                    )
           ]
          );
}

*pm1_factor = \&pminus1_factor;

sub pplus1_factor {
    my ($n, $B1) = @_;
    _valid(\$B1) if defined($B1);
    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::pplus1_factor(_big2pistr($$n) // (return _array()), (defined($B1) ? (_big2pistr($$B1) || return _array($n)) : ()))
           ]
          );
}

*pp1_factor = \&pplus1_factor;

sub chebyshev_factor {
    my ($n, $B, $x) = @_;

    # The Chebyshev factorization method, taking
    # advantage of the smoothness of p-1 or p+1.

    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    $B = defined($B) ? do { _valid(\$B); _any2ui($$B) || return _array(bless \$n) }         : 1e5;
    $x = defined($x) ? do { _valid(\$x); Math::GMPz::Rmpz_init_set(_any2mpz($$x) // $TWO) } : Math::GMPz::Rmpz_init_set_ui(CORE::int(CORE::rand(1e9)));

    my $i = Math::GMPz::Rmpz_init_set_ui(2);

    # Try to compute invmod(2, n)
    # If n is even, return faster
    Math::GMPz::Rmpz_invert($i, $i, $n)
      || return _array([TWO, (($n == 2) ? () : _set_int($n >> 1))]);

    state $V1 = Math::GMPz::Rmpz_init_nobless();
    state $V2 = Math::GMPz::Rmpz_init_nobless();

    my $chebyshevTmod = sub {
        my ($v, $x) = @_;

        Math::GMPz::Rmpz_mul_2exp($x, $x, 1);

        Math::GMPz::Rmpz_set_ui($V1, 2);
        Math::GMPz::Rmpz_set($V2, $x);

        my @bits;
        while ($v) {
            unshift @bits, $v & 1;
            $v >>= 1;
        }

        foreach my $bit (@bits) {

            if ($bit) {
                Math::GMPz::Rmpz_mul($V1, $V1, $V2);
                Math::GMPz::Rmpz_powm_ui($V2, $V2, 2, $n);
                Math::GMPz::Rmpz_sub($V1, $V1, $x);
                Math::GMPz::Rmpz_sub_ui($V2, $V2, 2);
                Math::GMPz::Rmpz_mod($V1, $V1, $n);
            }
            else {
                Math::GMPz::Rmpz_mul($V2, $V2, $V1);
                Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $n);
                Math::GMPz::Rmpz_sub($V2, $V2, $x);
                Math::GMPz::Rmpz_sub_ui($V1, $V1, 2);
                Math::GMPz::Rmpz_mod($V2, $V2, $n);
            }
        }

        Math::GMPz::Rmpz_mul($x, $V1, $i);
        Math::GMPz::Rmpz_mod($x, $x, $n);
    };

    my $g     = Math::GMPz::Rmpz_init();
    my $lnB   = CORE::log($B);
    my $sqrtB = Math::Prime::Util::GMP::sqrtint($B);

    foreach my $p (@{_primes(2, $sqrtB)}) {
        $chebyshevTmod->($p**CORE::int($lnB / CORE::log($p)), $x);
    }

    for (my $p = _next_prime($sqrtB) ; $p <= $B ; $p = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p))) {

        $chebyshevTmod->($p, $x);    # T_k(x) (mod n)

        Math::GMPz::Rmpz_sub_ui($g, $x, 1);
        Math::GMPz::Rmpz_gcd($g, $g, $n);

        if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

            if (Math::GMPz::Rmpz_cmp($g, $n) == 0) {
                return _array([bless \$n]);
            }

            my $x = Math::GMPz::Rmpz_init();
            my $y = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_set($y, $g);
            Math::GMPz::Rmpz_divexact($x, $n, $g);

            my @f = map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } ($x, $y);
            return _array(\@f);
        }
    }

    _array([bless(\$n)]);
}

sub holf_factor {
    my ($n, $k) = @_;
    _valid(\$k) if defined($k);
    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::holf_factor(_big2pistr($$n) // (return _array()), (defined($k) ? (_big2pistr($$k) || return _array($n)) : 1e4))
           ]
          );
}

sub _miller_factor {
    my ($n, $tries) = @_;

    # $n is a Math::GMPz object holding a positive value
    # $tries is a positive native integer or `undef`

    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _factor(Math::GMPz::Rmpz_get_ui($n));
    }

    if (_is_prob_prime($n, 1)) {
        return ($n);
    }

    state $nm1 = Math::GMPz::Rmpz_init_nobless();    # n-1
    state $d   = Math::GMPz::Rmpz_init_nobless();    # (n-1) >> s

    Math::GMPz::Rmpz_sub_ui($nm1, $n, 1);

    my $s = Math::GMPz::Rmpz_scan1($nm1, 0) || return ($n);
    my $r = $s - 1;

    Math::GMPz::Rmpz_div_2exp($d, $nm1, $s);

    $tries //= 1 + CORE::int(200 / $s);

    state $x = Math::GMPz::Rmpz_init();
    state $g = Math::GMPz::Rmpz_init();
    state $t = Math::GMPz::Rmpz_init();
    state $N = Math::GMPz::Rmpz_init();

    my @all_factors;

    Math::GMPz::Rmpz_set($N, $n);

    for (1 .. $tries) {

        my $p = _random_prime(ULONG_MAX >> 1);
        Math::GMPz::Rmpz_set_ui($g, $p);
        Math::GMPz::Rmpz_powm($x, $g, $d, $n);

        next if (Math::GMPz::Rmpz_cmp_ui($x, 1) == 0);

        foreach my $k (0 .. $r) {

            last if (Math::GMPz::Rmpz_cmp($x, $nm1) == 0);

            # gcd(x +/- 1, n) may be a non-trivial factor of n
            Math::GMPz::Rmpz_sub_ui($g, $x, 1);
            Math::GMPz::Rmpz_gcd($g, $g, $n);

            if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0 and Math::GMPz::Rmpz_cmp($g, $n) < 0) {

                Math::GMPz::Rmpz_gcd($t, $N, $g);

                if (Math::GMPz::Rmpz_cmp_ui($t, 1) > 0 and Math::GMPz::Rmpz_cmp($t, $N) < 0) {
                    if (_is_prob_prime($t, 1)) {
                        Math::GMPz::Rmpz_remove($N, $N, $t);
                        push @all_factors, Math::GMPz::Rmpz_init_set($t);
                    }
                    else {
                        Math::GMPz::Rmpz_divexact($t, $N, $t);
                        if (_is_prob_prime($t, 1)) {
                            Math::GMPz::Rmpz_remove($N, $N, $t);
                            push @all_factors, Math::GMPz::Rmpz_init_set($t);
                        }
                    }
                }

                push @all_factors, Math::GMPz::Rmpz_init_set($g);

                if (scalar(@all_factors) >= 10 or Math::GMPz::Rmpz_fits_ulong_p($N) or _is_prob_prime(Math::GMPz::Rmpz_get_str($N, 10), 1)) {
                    return map { $$_ } @{((bless \$n)->gcd_factors(_array([map { bless \$_ } @all_factors])))};
                }

                # Recursive approach
                #~ Math::GMPz::Rmpz_divexact($t, $n, $g);

                #~ my @g_factors = (_is_prob_prime($g, 1) ? Math::GMPz::Rmpz_init_set($g) : __SUB__->(Math::GMPz::Rmpz_init_set($g)));
                #~ my @t_factors = (_is_prob_prime($t, 1) ? Math::GMPz::Rmpz_init_set($t) : __SUB__->(Math::GMPz::Rmpz_init_set($t)));

                #~ return (@g_factors, @t_factors);
            }

            Math::GMPz::Rmpz_powm_ui($x, $x, 2, $n);
            last if (Math::GMPz::Rmpz_cmp_ui($x, 1) == 0);
        }
    }

    if (@all_factors) {
        return map { $$_ } @{((bless \$n)->gcd_factors(_array([map { bless \$_ } @all_factors])))};
    }

    my @holf_factors = Math::Prime::Util::GMP::holf_factor(Math::GMPz::Rmpz_get_str($n, 10), 10_000);

    if (scalar(@holf_factors) > 1) {
        return (
                map { _is_prob_prime($_, 1) ? $_ : __SUB__->($_) }
                map { Math::GMPz::Rmpz_init_set_str($_, 10) } @holf_factors
               );
    }

    return ($n);
}

sub miller_factor {
    my ($n, $tries) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) > 0 or return _array();

    if (defined($tries)) {
        _valid(\$tries);
        $tries = _any2ui($$tries) || return _array([_set_int($n)]);
    }

    my @factors = sort { $$a <=> $$b } map { bless \$_ } _miller_factor($n, $tries);

    _array(\@factors);
}

*miller_rabin_factor = \&miller_factor;

sub _lucas_factor {
    my ($n, $j, $tries) = @_;

    # $n is a Math::GMPz object holding a positive value
    # $j is a signed native integer or `undef`
    # $tries is an unsigned native integer or `undef`

    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _factor(Math::GMPz::Rmpz_get_ui($n));
    }

    if (_is_prob_prime($n, 1)) {
        return ($n);
    }

    if (!defined($j)) {
        my @factors = __SUB__->($n, 1, $tries);
        @factors = map { _is_prob_prime($_, 1) ? $_ : __SUB__->($_, -1, $tries) } @factors;
        return @factors;
    }

    state $npj = Math::GMPz::Rmpz_init_nobless();    # n + j

    ($j < 0)
      ? Math::GMPz::Rmpz_sub_ui($npj, $n, -$j)
      : Math::GMPz::Rmpz_add_ui($npj, $n, +$j);

    my $s = Math::GMPz::Rmpz_scan1($npj, 0) || return ($n);

    Math::GMPz::Rmpz_div_2exp($npj, $npj, $s);       # (n+j) >> s
    my $dstr = Math::GMPz::Rmpz_get_str($npj, 10);

    $tries //= 1 + CORE::int(100 / $s);

    my $PQ_limit = $LUCAS_PQ_LIMIT;

    state $g = Math::GMPz::Rmpz_init();
    state $t = Math::GMPz::Rmpz_init();
    state $N = Math::GMPz::Rmpz_init();

    my @all_factors;

    Math::GMPz::Rmpz_set($N, $n);

    my $nstr = Math::GMPz::Rmpz_get_str($n, 10);

    foreach my $i (1 .. $tries) {

        my $P = 1 + CORE::int(CORE::rand($PQ_limit));
        my $Q = 1 + CORE::int(CORE::rand($PQ_limit));

        $Q *= -1 if (CORE::rand(1) < 0.5);

        my $delta = $P * $P - 4 * $Q;

        next
          if (
              HAS_PRIME_UTIL
              ? Math::Prime::Util::is_square($delta)
              : Math::Prime::Util::GMP::is_square($delta)
             );

        my ($U1, $V1, $Q1) =
          map { Math::GMPz::Rmpz_init_set_str($_, 10) } Math::Prime::Util::GMP::lucas_sequence($nstr, $P, $Q, $dstr);

        for my $k (1 .. $s) {

            foreach my $x ($U1, $V1, $P) {

                if (ref($x)) {
                    Math::GMPz::Rmpz_gcd($g, $x, $n);
                }
                else {
                    Math::GMPz::Rmpz_sub_ui($g, $V1, $x);
                    Math::GMPz::Rmpz_gcd($g, $g, $n);
                }

                if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0 and Math::GMPz::Rmpz_cmp($g, $n) < 0) {

                    Math::GMPz::Rmpz_gcd($t, $N, $g);

                    if (Math::GMPz::Rmpz_cmp_ui($t, 1) > 0 and Math::GMPz::Rmpz_cmp($t, $N) < 0) {
                        if (_is_prob_prime($t, 1)) {
                            Math::GMPz::Rmpz_remove($N, $N, $t);
                            push @all_factors, Math::GMPz::Rmpz_init_set($t);
                        }
                        else {
                            Math::GMPz::Rmpz_divexact($t, $N, $t);
                            if (_is_prob_prime($t, 1)) {
                                Math::GMPz::Rmpz_remove($N, $N, $t);
                                push @all_factors, Math::GMPz::Rmpz_init_set($t);
                            }
                        }
                    }

                    push @all_factors, Math::GMPz::Rmpz_init_set($g);

                    if (scalar(@all_factors) >= 5 or Math::GMPz::Rmpz_fits_ulong_p($N) or _is_prob_prime(Math::GMPz::Rmpz_get_str($N, 10), 1)) {
                        return map { $$_ } @{((bless \$n)->gcd_factors(_array([map { bless \$_ } @all_factors])))};
                    }

                    # Recursive approach
                    #~ Math::GMPz::Rmpz_divexact($t, $n, $g);

                    #~ my @g_factors = (_is_prob_prime($g, 1) ? Math::GMPz::Rmpz_init_set($g) : __SUB__->(Math::GMPz::Rmpz_init_set($g)));
                    #~ my @t_factors = (_is_prob_prime($t, 1) ? Math::GMPz::Rmpz_init_set($t) : __SUB__->(Math::GMPz::Rmpz_init_set($t)));

                    #~ return (@g_factors, @t_factors);
                }
            }

            Math::GMPz::Rmpz_mul($U1, $U1, $V1);
            Math::GMPz::Rmpz_mod($U1, $U1, $n);
            Math::GMPz::Rmpz_powm_ui($V1, $V1, 2, $n);
            Math::GMPz::Rmpz_submul_ui($V1, $Q1, 2);
            Math::GMPz::Rmpz_powm_ui($Q1, $Q1, 2, $n);
        }
    }

    if (@all_factors) {
        return map { $$_ } @{((bless \$n)->gcd_factors(_array([map { bless \$_ } @all_factors])))};
    }

    my @holf_factors = Math::Prime::Util::GMP::holf_factor($nstr, 10_000);

    if (scalar(@holf_factors) > 1) {
        return (map { _is_prob_prime($_, 1) ? $_ : __SUB__->($_, $j) } map { Math::GMPz::Rmpz_init_set_str($_, 10) } @holf_factors);
    }

    return ($n);
}

sub lucas_factor {
    my ($n, $j, $tries) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) > 0 or return _array();

    if (defined($j)) {
        _valid(\$j);
        $j = _any2si($$j) // undef;
    }

    if (defined($tries)) {
        _valid(\$tries);
        $tries = _any2ui($$tries) || return _array([_set_int($n)]);
    }

    my @factors = sort { $$a <=> $$b } map { bless \$_ } _lucas_factor($n, $j, $tries);

    _array(\@factors);
}

*lucas_miller_factor = \&lucas_factor;

sub fermat_factor {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) || return _array([_set_int($n)]) } : 1e4;

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    my $p = Math::GMPz::Rmpz_init();    # p = floor(sqrt(n))
    my $q = Math::GMPz::Rmpz_init();    # q = p^2 - n

    Math::GMPz::Rmpz_sqrtrem($p, $q, $n);
    Math::GMPz::Rmpz_neg($q, $q);

    foreach my $j (1 .. $k) {

        Math::GMPz::Rmpz_addmul_ui($q, $p, 2);

        Math::GMPz::Rmpz_add_ui($q, $q, 1);
        Math::GMPz::Rmpz_add_ui($p, $p, 1);

        if (Math::GMPz::Rmpz_perfect_square_p($q)) {
            Math::GMPz::Rmpz_sqrt($q, $q);

            my $r1 = Math::GMPz::Rmpz_init();
            my $r2 = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_sub($r1, $p, $q);
            Math::GMPz::Rmpz_add($r2, $p, $q);

            return _array([bless(\$r1), bless(\$r2)]);
        }
    }

    _array([_set_int($n)]);
}

sub phi_finder_factor {
    my ($n, $k) = @_;

    # Phi-finder algorithm for semiprimes, due to Kyle Kloster (2010)

    $n = _any2mpz($$n) // return _array();
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) || return _array(bless \$n) } : 1e4;

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    my $E = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($E, $n);
    Math::GMPz::Rmpz_mul_2exp($E, $E, 1);
    Math::GMPz::Rmpz_sub($E, $n, $E);
    Math::GMPz::Rmpz_add_ui($E, $E, 1);

    my $E0 = Math::GMPz::Rmpz_init_set_ui(2);
    while (Math::GMPz::Rmpz_divisible_p($n, $E0)) {
        Math::GMPz::Rmpz_nextprime($E0, $E0);
    }
    Math::GMPz::Rmpz_powm($E0, $E0, -$E, $n);

    my $i = 0;
    my $L = Math::GMPz::Rmpz_sizeinbase($n, 2) - 1;

    # Repeat until E0 is a power of 2
    while (Math::GMPz::Rmpz_popcount($E0) != 1) {
        Math::GMPz::Rmpz_mul_2exp($E0, $E0, $L);
        Math::GMPz::Rmpz_mod($E0, $E0, $n);
        return _array(bless \$n) if (++$i > $k);
    }

    my $t = 0;
    my $u = Math::GMPz::Rmpz_init();

    foreach my $j (0 .. $L) {
        Math::GMPz::Rmpz_set_ui($u, 2);
        Math::GMPz::Rmpz_powm_ui($u, $u, $j, $n);
        if (Math::GMPz::Rmpz_cmp($u, $E0) == 0) {
            $t = $j;
            last;
        }
    }

    my $phi = Math::GMPz::Rmpz_init_set_ui($i * $L);
    Math::GMPz::Rmpz_sub($phi, $phi, $E);
    Math::GMPz::Rmpz_sub_ui($phi, $phi, $t);
    Math::GMPz::Rmpz_abs($phi, $phi);

    my $q = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sub($q, $n, $phi);
    Math::GMPz::Rmpz_add_ui($q, $q, 1);
    Math::GMPz::Rmpz_mul($E, $q, $q);
    Math::GMPz::Rmpz_submul_ui($E, $n, 4);
    Math::GMPz::Rmpz_abs($E, $E);
    Math::GMPz::Rmpz_sqrt($E, $E);
    Math::GMPz::Rmpz_add($E, $E, $q);
    Math::GMPz::Rmpz_div_2exp($E, $E, 1);
    Math::GMPz::Rmpz_gcd($E, $E, $n);

    if (Math::GMPz::Rmpz_cmp_ui($E, 1) > 0 and Math::GMPz::Rmpz_cmp($n, $E) > 0) {
        Math::GMPz::Rmpz_divexact($q, $n, $E);
        my @factors = map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } ($E, $q);
        return _array(\@factors);
    }

    _array(bless \$n);
}

sub _extract_divisors {
    my ($n, $params, $seen_divisor) = @_;

    my ($r1, $e1, $r2, $e2) = @$params;

    my @d1 = _divisors($e1);
    my @d2 = _divisors($e2);

    @d1 = map {
        my $x = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($x, $r1, $_);
        $x;
    } @d1;

    @d2 = map {
        my $y = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_pow_ui($y, $r2, $_);
        $y;
    } @d2;

    state $g = Math::GMPz::Rmpz_init_nobless();

    my @factors;

    foreach my $x (@d1) {
        foreach my $y (@d2) {
            foreach my $j (-1, 1) {

                ($j == 1)
                  ? Math::GMPz::Rmpz_sub($g, $x, $y)
                  : Math::GMPz::Rmpz_add($g, $x, $y);

                Math::GMPz::Rmpz_gcd($g, $g, $n);

                if (    Math::GMPz::Rmpz_cmp_ui($g, 1) > 0
                    and Math::GMPz::Rmpz_cmp($g, $n) < 0
                    and !$seen_divisor->{Math::GMPz::Rmpz_get_str($g, 16)}++) {
                    push @factors, Math::GMPz::Rmpz_init_set($g);
                }
            }
        }
    }

    @factors;
}

# Congruence of powers factorization method
sub cop_factor {
    my ($n, $upto) = @_;

    $n = _any2mpz($$n) // return _array();

    if (defined($upto)) {
        _valid(\$upto);
        $upto = _any2ui($$upto) || return _array([_set_int($n)]);
    }

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    my @congr_powers_params;

    my $process = sub {
        my ($root, $e) = @_;

        for my $j (-1, 0, 1) {

            my $k = Math::GMPz::Rmpz_init();
            my $u = Math::GMPz::Rmpz_init();

            ($j < 0)
              ? Math::GMPz::Rmpz_sub_ui($k, $root, CORE::abs($j))
              : Math::GMPz::Rmpz_add_ui($k, $root, $j);

            Math::GMPz::Rmpz_powm_ui($u, $k, $e, $n);

            foreach my $z ($u, $n - $u) {

                if (Math::GMPz::Rmpz_perfect_power_p($z)) {

                    my $t = _power_factor($z) || 1;

                    if ($t > 1) {
                        my $r = Math::GMPz::Rmpz_init();
                        Math::GMPz::Rmpz_root($r, $z, $t);
                        push @congr_powers_params, [$r, $t, $k, $e];
                    }
                }
            }
        }
    };

    my $n_log2 = Math::GMPz::Rmpz_sizeinbase($n, 2);

    if (defined($upto) and $n_log2 > $upto) {
        $n_log2 = $upto;
    }

    my @range = CORE::reverse(2 .. $n_log2);

    for my $e (@range) {
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_root($r, $n, $e);
        $process->($r, $e);
    }

    for my $r (@range) {
        my $e = __ilog__($n, $r);
        $process->(Math::GMPz::Rmpz_init_set_ui($r), $e);
    }

    my %seen_param;
    @congr_powers_params = grep { !$seen_param{join(' ', @$_)}++ } @congr_powers_params;

    my @divisors;
    my %seen_divisor;

    foreach my $args (@congr_powers_params) {
        push @divisors, _extract_divisors($n, $args, \%seen_divisor);
    }

    (bless \$n)->gcd_factors(_array([map { bless \$_ } @divisors]));
}

# Difference of powers factorization method
sub dop_factor {
    my ($n, $upto) = @_;

    $n = _any2mpz($$n) // return _array();

    if (defined($upto)) {
        $upto = _any2ui($$upto) || return _array([_set_int($n)]);
    }

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    my @diff_powers_params;

    my $diff_power_check = sub {
        my ($r1, $e1) = @_;

        # u = r1^e1
        state $u = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_pow_ui($u, $r1, $e1);

        # dx = abs(u - n)
        state $dx = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_sub($dx, $u, $n);
        Math::GMPz::Rmpz_abs($dx, $dx);

        if (Math::GMPz::Rmpz_perfect_power_p($dx)) {

            my $e2 = _power_factor(Math::GMPz::Rmpz_init_set($dx)) || 1;
            my $r2 = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_root($r2, $dx, $e2);
            push @diff_powers_params, [$r1, $e1, $r2, $e2];
        }
    };

    my $n_log2 = Math::GMPz::Rmpz_sizeinbase($n, 2);

    if (defined($upto) and $n_log2 > $upto) {
        $n_log2 = $upto;
    }

    my @range = CORE::reverse(2 .. $n_log2);

    # Sum and difference of powers of the form a^k ± b^k, where a and b are small.
    if (0) {
        foreach my $k (@range) {

            my $t  = __ilog__($n, $k);
            my $r1 = Math::GMPz::Rmpz_init_set_ui($k);

            $diff_power_check->($r1, $t);        # sum of powers
            $diff_power_check->($r1, $t + 1);    # difference of powers
        }
    }

    # Sum and difference of powers of the form a^k ± b^k, where a and b are large.
    foreach my $e1 (@range) {

        my $t = Math::GMPz::Rmpz_init();
        my $u = Math::GMPz::Rmpz_init();

        Math::GMPz::Rmpz_root($t, $n, $e1);
        Math::GMPz::Rmpz_add_ui($u, $t, 1);

        $diff_power_check->($t, $e1);    # sum of powers
        $diff_power_check->($u, $e1);    # difference of powers
    }

    my @divisors;
    my %seen_divisor;

    foreach my $args (@diff_powers_params) {
        push @divisors, _extract_divisors($n, $args, \%seen_divisor);
    }

    (bless \$n)->gcd_factors(_array([map { bless \$_ } @divisors]));
}

# "Fermat's Little Theorem" factorization method
sub flt_factor {
    my ($n, $base, $reps) = @_;

    $n = _any2mpz($$n) // return _array();

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    $base = defined($base) ? do { _valid(\$base); _any2ui($$base) // 2 }                        : 2;
    $reps = defined($reps) ? do { _valid(\$reps); _any2ui($$reps) || return _array(bless \$n) } : 1e4;

    state $z = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();
    state $g = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($z, $base);
    Math::GMPz::Rmpz_set_ui($t, $base);

    Math::GMPz::Rmpz_powm($z, $z, $n, $n);

    # Cannot factor Fermat pseudoprimes
    if (Math::GMPz::Rmpz_cmp_ui($z, $base) == 0) {
        return _array([bless \$n]);
    }

    my $multiplier = $base * $base;

    if ($multiplier > ULONG_MAX) {
        return _array([bless \$n]);
    }

    foreach my $j (1 .. $reps) {

        Math::GMPz::Rmpz_mul_ui($t, $t, $multiplier);
        Math::GMPz::Rmpz_mod($t, $t, $n) if ($j % 10 == 0);
        Math::GMPz::Rmpz_sub($g, $z, $t);
        Math::GMPz::Rmpz_gcd($g, $g, $n);

        if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

            if (Math::GMPz::Rmpz_cmp($g, $n) == 0) {
                return _array([bless \$n]);
            }

            my $x = Math::GMPz::Rmpz_init();
            my $y = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_set($y, $g);
            Math::GMPz::Rmpz_divexact($x, $n, $g);

            my @f = map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } ($x, $y);
            return _array(\@f);
        }
    }

    _array([bless(\$n)]);
}

sub pell_factor {
    my ($n, $reps) = @_;

    # Simple version of the continued-fraction factorization method.
    # Efficient for numbers that have factors relatively close to sqrt(n)

    $n    = _any2mpz($$n, 0) // return _array();
    $reps = defined($reps) ? do { _valid(\$reps); _any2ui($$reps) || return _array([_set_int($n)]) } : 1e4;

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    if (Math::GMPz::Rmpz_perfect_square_p($n)) {
        my $t = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_sqrt($t, $n);
        return _array([(bless \$t), (bless \$t)]);
    }

    my $x = Math::GMPz::Rmpz_init();
    my $y = Math::GMPz::Rmpz_init();
    my $z = Math::GMPz::Rmpz_init_set_ui(1);

    my $t = Math::GMPz::Rmpz_init();
    my $w = Math::GMPz::Rmpz_init();
    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sqrt($x, $n);
    Math::GMPz::Rmpz_set($y, $x);

    Math::GMPz::Rmpz_add($w, $x, $x);
    Math::GMPz::Rmpz_set($r, $w);

    my $f2 = Math::GMPz::Rmpz_init_set($x);
    my $f1 = Math::GMPz::Rmpz_init_set_ui(1);

    foreach my $k (1 .. $reps) {

        # y = r*z - y
        Math::GMPz::Rmpz_mul($t, $r, $z);
        Math::GMPz::Rmpz_sub($y, $t, $y);

        # z = (n - y*y) / z
        Math::GMPz::Rmpz_mul($t, $y, $y);
        Math::GMPz::Rmpz_sub($t, $n, $t);
        Math::GMPz::Rmpz_divexact($z, $t, $z);

        # r = (x + y) / z
        Math::GMPz::Rmpz_add($t, $x, $y);

        # Floor division: floor((x+y)/z)
        # Math::GMPz::Rmpz_div($r, $t, $z);

        # Round (x+y)/z to nearest integer
        Math::GMPz::Rmpz_set($r, $z);
        Math::GMPz::Rmpz_addmul_ui($r, $t, 2);
        Math::GMPz::Rmpz_div($r, $r, $z);
        Math::GMPz::Rmpz_div_2exp($r, $r, 1);

        # f1 = (f1 + r*f2) % n
        Math::GMPz::Rmpz_addmul($f1, $f2, $r);
        Math::GMPz::Rmpz_mod($f1, $f1, $n);

        # swap f1 with f2
        ($f1, $f2) = ($f2, $f1);

        if (Math::GMPz::Rmpz_perfect_square_p($z)) {

            my $g = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_sqrt($g, $z);
            Math::GMPz::Rmpz_sub($g, $f1, $g);
            Math::GMPz::Rmpz_gcd($g, $g, $n);

            if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

                if (Math::GMPz::Rmpz_cmp($g, $n) == 0) {
                    return _array([_set_int($n)]);
                }

                my $x = Math::GMPz::Rmpz_init();
                my $y = Math::GMPz::Rmpz_init();

                Math::GMPz::Rmpz_set($y, $g);
                Math::GMPz::Rmpz_divexact($x, $n, $g);

                my @f = map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } ($x, $y);
                return _array(\@f);
            }
        }

        last if (Math::GMPz::Rmpz_cmpabs_ui($z, 1) == 0);
    }

    _array([_set_int($n)]);
}

sub mbe_factor {
    my ($n, $reps) = @_;

    $n = _any2mpz($$n, 0) // return _array();

    Math::GMPz::Rmpz_cmp_ui($n, 1) > 0
      or return _array();

    $reps = defined($reps) ? do { _valid(\$reps); _any2ui($$reps) || return _array([_set_int($n)]) } : 10;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $g = Math::GMPz::Rmpz_init_nobless();

    state $A = Math::GMPz::Rmpz_init_nobless();
    state $B = Math::GMPz::Rmpz_init_nobless();
    state $C = Math::GMPz::Rmpz_init_nobless();

    my $n_str = Math::GMPz::Rmpz_get_str($n, 10);

    foreach my $k (1 .. $reps) {

        # Deterministic version
        #Math::GMPz::Rmpz_div_ui($t, $n, $k + 1);

        # Randomized version
        Math::GMPz::Rmpz_set_str($t, Math::Prime::Util::GMP::urandomm($n_str), 10);

        Math::GMPz::Rmpz_set($A, $t);
        Math::GMPz::Rmpz_set($B, $t);
        Math::GMPz::Rmpz_set_ui($C, 1);

        foreach my $i (0 .. Math::GMPz::Rmpz_sizeinbase($B, 2) - 1) {

            if (Math::GMPz::Rmpz_tstbit($B, $i)) {

                Math::GMPz::Rmpz_powm($C, $A, $C, $n);
                Math::GMPz::Rmpz_sub_ui($g, $C, 1);
                Math::GMPz::Rmpz_gcd($g, $g, $n);

                if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

                    if (Math::GMPz::Rmpz_cmp($g, $n) == 0) {
                        return _array([_set_int($n)]);
                    }

                    my $x = Math::GMPz::Rmpz_init();
                    my $y = Math::GMPz::Rmpz_init();

                    Math::GMPz::Rmpz_set($y, $g);
                    Math::GMPz::Rmpz_divexact($x, $n, $g);

                    my @f = map { bless \$_ } sort { Math::GMPz::Rmpz_cmp($a, $b) } ($x, $y);
                    return _array(\@f);
                }
            }

            Math::GMPz::Rmpz_powm($A, $A, $A, $n);
        }
    }

    _array([_set_int($n)]);
}

sub squfof_factor {
    my ($n, $k) = @_;
    _valid(\$k) if defined($k);
    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::squfof_factor(_big2pistr($$n) // (return _array()), (defined($k) ? (_big2pistr($$k) || return _array($n)) : 1e4))
           ]
          );
}

sub ecm_factor {
    my ($n, $B1, $curves) = @_;

    _valid(\$B1)     if defined($B1);
    _valid(\$curves) if defined($curves);

    _array(
           [map { _set_int($_) }
              Math::Prime::Util::GMP::ecm_factor(
                                                 _big2pistr($$n) // (return _array()),
                                                 (defined($B1)     ? (_big2pistr($$B1)     || return _array($n)) : ()),
                                                 (defined($curves) ? (_big2pistr($$curves) || return _array($n)) : ()),
                                                )
           ]
          );
}

sub qs_factor {
    my ($n) = @_;
    _array([map { _set_int($_) } Math::Prime::Util::GMP::qs_factor(_big2pistr($$n) // (return _array()))]);
}

sub dirichlet_convolution {
    my ($n, $f, $g) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    $f //= Sidef::Types::Block::Block->new(code => sub { ONE });
    $g //= Sidef::Types::Block::Block->new(code => sub { ONE });

    my @terms;
    my $result = ZERO;

    foreach my $d (_divisors($n)) {

        my $t =
          (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($d)
          : Math::GMPz::Rmpz_init_set_str("$d", 10);

        my $u = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_divexact($u, $n, $t);

        $result = $result->add($f->run(bless \$t)->mul($g->run(bless \$u)));
    }

    return $result;
}

*dconv = \&dirichlet_convolution;

sub dirichlet_hyperbola {
    my ($n, $f, $g, $F, $G) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    $f //= Sidef::Types::Block::Block->new(code => sub { ONE });
    $g //= Sidef::Types::Block::Block->new(code => sub { ONE });

    $F //= Sidef::Types::Block::Block->new(code => sub { $_[0] });
    $G //= Sidef::Types::Block::Block->new(code => sub { $_[0] });

    my $s = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($s, $n);
    Math::GMPz::Rmpz_fits_slong_p($s) || goto &nan;

    my $q_obj;
    my $native_n = Math::GMPz::Rmpz_fits_ulong_p($n);

    if ($native_n) {
        $n = Math::GMPz::Rmpz_get_ui($n);
    }
    else {
        $q_obj = bless \(my $value = Math::GMPz::Rmpz_init_set_ui(0));
    }

    my $sum = 0;

    my ($f_r, $g_r, $F_r, $G_r);

    foreach my $k (1 .. Math::GMPz::Rmpz_get_ui($s)) {

        if ($native_n) {
            $q_obj = bless \(
                my $r =
                  HAS_PRIME_UTIL
                ? Math::Prime::Util::divint($n, $k)
                : Math::Prime::Util::GMP::divint($n, $k)

            );
        }
        else {
            Math::GMPz::Rmpz_div_ui($$q_obj, $n, $k);
        }

        my $k_obj = bless \$k;

        $f_r = $f->($k_obj);
        $g_r = $g->($k_obj);

        $f_r = $$f_r if ref($f_r);
        $g_r = $$g_r if ref($g_r);

        if (!ref($f_r) and $f_r == 0) {
            $G_r = 0;
            $F_r = $F->($q_obj);
        }
        elsif (!ref($g_r) and $g_r == 0) {
            $F_r = 0;
            $G_r = $G->($q_obj);
        }
        else {
            $F_r = $F->($q_obj);
            $G_r = $G->($q_obj);
        }

        $F_r = $$F_r if ref($F_r);
        $G_r = $$G_r if ref($G_r);

        $sum = __add__($sum, __add__(__mul__($f_r, $G_r), __mul__($g_r, $F_r)));
    }

    my $s_obj = _set_int($s);

    $F_r = $F->($s_obj);
    $G_r = $G->($s_obj);

    $F_r = $$F_r if ref($F_r);
    $G_r = $$G_r if ref($G_r);

    $sum = __sub__($sum, __mul__($F_r, $G_r));
    bless \$sum;
}

*dirichlet_sum = \&dirichlet_hyperbola;

sub sum_remainders {
    my ($n, $v) = @_;

    # a(n,v) = Sum_{k=1..n} v % k

    _valid(\$v);

    $n = _any2mpz($$n, 0) // goto &nan;
    $v = _any2mpz($$v, 1) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    my $S = sub {    # Sum_{k=1..n} sigma(k) = Sum_{k=1..n} k*floor(n/k)
        my ($n) = @_;

        if (Math::GMPz::Rmpz_sgn($n) < 0) {

            # Support for negative n:
            #   S(-n) = n*(n+1)/2 + S(n-1)

            # Based on the formula:
            # Sum_{k=1..n} -k*floor(n/-k) = Sum_{k=1..n} k*ceiling(n/k)
            #                             = n*(n+1)/2 + Sum_{k=1..n-1} sigma(k).

            $n = Math::GMPz::Rmpz_init_set($n);
            Math::GMPz::Rmpz_abs($n, $n);

            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_add_ui($t, $n, 1);
            Math::GMPz::Rmpz_mul($t, $t, $n);
            Math::GMPz::Rmpz_div_2exp($t, $t, 1);
            Math::GMPz::Rmpz_sub_ui($n, $n, 1);

            my $r = __SUB__->($n);
            Math::GMPz::Rmpz_add($t, $t, $r);
            return $t;
        }

        my $t = Math::GMPz::Rmpz_init();
        my $u = Math::GMPz::Rmpz_init();
        my $T = Math::GMPz::Rmpz_init_set_ui(0);

        my $s = Math::Prime::Util::GMP::sqrtint($n);

        foreach my $k (1 .. $s) {

            # T += faulhaber(idiv(n,k), 1) + k*idiv(n,k)
            Math::GMPz::Rmpz_div_ui($t, $n, $k);
            Math::GMPz::Rmpz_add_ui($u, $t, 1);
            Math::GMPz::Rmpz_mul($u, $u, $t);
            Math::GMPz::Rmpz_div_2exp($u, $u, 1);
            Math::GMPz::Rmpz_mul_ui($t, $t, $k);
            Math::GMPz::Rmpz_add($u, $u, $t);
            Math::GMPz::Rmpz_add($T, $T, $u);
        }

        # T -= faulhaber(s, 1)*s
        Math::GMPz::Rmpz_set_str($u, $s, 10);
        Math::GMPz::Rmpz_add_ui($t, $u, 1);
        Math::GMPz::Rmpz_mul($t, $t, $u);
        Math::GMPz::Rmpz_div_2exp($t, $t, 1);
        Math::GMPz::Rmpz_mul($t, $t, $u);
        Math::GMPz::Rmpz_sub($T, $T, $t);

        return $T;
    };

    my $G = sub {    # Sum_{k=A..B} k*floor(B/k)
        my ($A, $B) = @_;

        if (Math::GMPz::Rmpz_sgn($B) < 0) {

            # Support for negative B:
            #   G(a,-b) = T(b) - T(a-1) + G(a, b-1)
            # where T(n) = n*(n+1)/2

            $B = Math::GMPz::Rmpz_init_set($B);
            Math::GMPz::Rmpz_abs($B, $B);

            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_add_ui($t, $B, 1);
            Math::GMPz::Rmpz_mul($t, $t, $B);
            Math::GMPz::Rmpz_div_2exp($t, $t, 1);

            my $t2 = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_sub_ui($t2, $A, 1);
            Math::GMPz::Rmpz_mul($t2, $t2, $A);
            Math::GMPz::Rmpz_div_2exp($t2, $t2, 1);

            Math::GMPz::Rmpz_sub($t, $t, $t2);
            Math::GMPz::Rmpz_sub_ui($B, $B, 1);

            my $r = __SUB__->($A, $B);
            Math::GMPz::Rmpz_add($t, $t, $r);
            return $t;
        }

        my $t = Math::GMPz::Rmpz_init();
        my $u = Math::GMPz::Rmpz_init();
        my $v = Math::GMPz::Rmpz_init();
        my $w = Math::GMPz::Rmpz_init();

        my $T = Math::GMPz::Rmpz_init_set_ui(0);

        while (Math::GMPz::Rmpz_cmp($A, $B) <= 0) {

            Math::GMPz::Rmpz_div($t, $B, $A);
            Math::GMPz::Rmpz_div($u, $B, $t);

            # v = u*(u+1)/2
            Math::GMPz::Rmpz_add_ui($v, $u, 1);
            Math::GMPz::Rmpz_mul($v, $v, $u);
            Math::GMPz::Rmpz_div_2exp($v, $v, 1);

            # w = (a-1)*a/2
            Math::GMPz::Rmpz_sub_ui($w, $A, 1);
            Math::GMPz::Rmpz_mul($w, $w, $A);
            Math::GMPz::Rmpz_div_2exp($w, $w, 1);

            # T += t*(v - w)
            Math::GMPz::Rmpz_sub($v, $v, $w);
            Math::GMPz::Rmpz_mul($v, $v, $t);
            Math::GMPz::Rmpz_add($T, $T, $v);

            Math::GMPz::Rmpz_add_ui($A, $u, 1);
        }

        return $T;
    };

    # Optimization when n < sqrt(v)
    if (Math::GMPz::Rmpz_cmpabs($n * $n, $v) <= 0 and Math::GMPz::Rmpz_fits_ulong_p($n)) {

        my $sum = Math::GMPz::Rmpz_init_set_ui(0);

        $n = Math::GMPz::Rmpz_get_ui($n);

        if (Math::GMPz::Rmpz_fits_ulong_p($v)) {
            $v = Math::GMPz::Rmpz_get_ui($v);
            foreach my $k (1 .. $n) {
                Math::GMPz::Rmpz_add_ui($sum, $sum, $v % $k);
            }
        }
        else {
            state $t = Math::GMPz::Rmpz_init_nobless();
            foreach my $k (1 .. $n) {
                Math::GMPz::Rmpz_add_ui($sum, $sum, Math::GMPz::Rmpz_mod_ui($t, $v, $k));
            }
        }

        return bless \$sum;
    }

    # a(n,v) = n*v - S(v) + G(n+1, v)

    my $x = $S->($v);
    my $y = $G->($n + 1, $v);

    my $negative = 0;

    if (Math::GMPz::Rmpz_sgn($v) < 0) {
        $v = Math::GMPz::Rmpz_init_set($v);
        Math::GMPz::Rmpz_abs($v, $v);
        $negative = 1;
    }

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_mul($r, $n, $v);
    Math::GMPz::Rmpz_add($r, $r, $y);
    Math::GMPz::Rmpz_sub($r, $r, $x);

    if ($negative) {
        Math::GMPz::Rmpz_neg($r, $r);
    }

    bless \$r;
}

*sum_of_remainders = \&sum_remainders;

sub _divisors_bounded_native {
    my ($n_ui, $k_ui) = @_;

    my @d           = (1);
    my @prime_pairs = grep { $_->[0] <= $k_ui } _factor_exp($n_ui);

    foreach my $pp (@prime_pairs) {
        my ($p, $e) = @$pp;
        my @t;
        my $r = 1;

        for my $i (1 .. $e) {
            $r *= $p;
            foreach my $u (@d) {
                my $prod = $u * $r;
                push(@t, $prod) if ($prod <= $k_ui);
            }
        }
        push @d, @t;
    }

    _array([map { bless \$_ } sort { $a <=> $b } @d]);
}

sub _divisors_bounded_gmp {
    my ($n, $k) = @_;

    my @factors_exp;
    my $native_k = Math::GMPz::Rmpz_fits_ulong_p($k);
    my $k_ui     = ($native_k ? Math::GMPz::Rmpz_get_ui($k) : 0);

    # Gather factors into [prime, exponent] arrays
    if ($native_k and ($k_ui <= 1e6 or $k_ui == 1e7 or $k_ui == 1e8)) {
        my (undef, @flat_factors) = _primorial_trial_factor($n, $k_ui);
        my %counts;
        $counts{$_}++ for @flat_factors;
        @factors_exp = map { [$_, $counts{$_}] } keys %counts;
    }
    else {
        @factors_exp = grep { Math::Prime::Util::GMP::cmpint($_->[0], $k) <= 0 } _factor_exp($n);
    }

    return _array([ONE]) unless @factors_exp;

    my @d = (1);
    state $t     = Math::GMPz::Rmpz_init_nobless();
    state $r     = Math::GMPz::Rmpz_init_nobless();
    state $p_mpz = Math::GMPz::Rmpz_init_nobless();

    foreach my $pp (@factors_exp) {
        my ($p, $e) = @$pp;

        Math::GMPz::Rmpz_set_ui($r, 1);

        unless ($p < ULONG_MAX) {
            Math::GMPz::Rmpz_set_str($p_mpz, "$p", 10);
        }

        my @t;
        for my $i (1 .. $e) {

            if ($p < ULONG_MAX) {
                Math::GMPz::Rmpz_mul_ui($r, $r, $p);
            }
            else {
                Math::GMPz::Rmpz_mul($r, $r, $p_mpz);
            }

            foreach my $u (@d) {

                ref($u)
                  ? Math::GMPz::Rmpz_mul($t, $r, $u)
                  : Math::GMPz::Rmpz_mul_ui($t, $r, $u);

                if (Math::GMPz::Rmpz_cmp($t, $k) <= 0) {
                    my $val =
                      $native_k ? Math::GMPz::Rmpz_get_ui($t)
                      : (
                           Math::GMPz::Rmpz_fits_ulong_p($t) ? Math::GMPz::Rmpz_get_ui($t)
                         : Math::GMPz::Rmpz_init_set($t)
                        );
                    push(@t, $val);
                }
            }
        }
        push @d, @t;
    }

    _array([map { bless \$_ } sort { $a <=> $b } @d]);
}

sub _divisors_bounded {
    my ($n, $k_ref) = @_;

    _valid(\$k_ref);

    my $k = _any2mpz($$k_ref, 1) // return _array();
    Math::GMPz::Rmpz_sgn($k) > 0 or return _array();

    # When k > n, cap k at n
    if (Math::GMPz::Rmpz_cmp($k, $n) > 0) {
        $k = Math::GMPz::Rmpz_init_set($n);
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, ULONG_MAX >> 1) < 0) {
        return _divisors_bounded_native(Math::GMPz::Rmpz_get_ui($n), Math::GMPz::Rmpz_get_ui($k));
    }

    _divisors_bounded_gmp($n, $k);
}

# Divisors d of n, such that d <= k, with k = n when `k` is not specified
sub divisors {
    my ($n, $k) = @_;
    $n = $$n;

    # Fast path: Unbounded, native integer
    if (FAST_MODE and !ref($n) and !defined($k)) {
        $n > 0 or return _array();
        my @divs =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::divisors($n)
          : Math::Prime::Util::GMP::divisors($n);
        return _array([map { bless \$_ } @divs]);
    }

    # Validate and convert $n to GMPz
    $n = _any2mpz($n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) > 0 or return _array();

    # Branch to Bounded divisors if $k is defined
    if (defined($k)) {
        return _divisors_bounded($n, $k);
    }

    # Fallback: unbounded
    _array([map { ($_ < ULONG_MAX) ? (bless \$_) : _set_int($_) } _divisors($n)]);
}

sub partitions {
    my ($n, $max_value) = @_;

    $n = _any2ui($$n) // return _array();

    if (defined($max_value)) {
        _valid(\$max_value);
        $max_value = _any2ui($$max_value) // return _array();
    }
    else {
        $max_value = $n;
    }

    my @results;
    my @path;

    sub {
        my ($n, $max_part) = @_;

        if ($n == 0) {
            unshift @results, _array([@path]);
            return;
        }

        my $upper = ($n < $max_part ? $n : $max_part);

        for my $part (1 .. $upper) {
            push @path, bless \$part;
            __SUB__->($n - $part, $part);
            pop @path;    # backtrack
        }
      }
      ->($n, $max_value);

    return _array(\@results);
}

sub strict_partitions {
    my ($n, $max_value) = @_;
    $n = _any2ui($$n) // return _array();
    if (defined($max_value)) {
        _valid(\$max_value);
        $max_value = _any2ui($$max_value) // return _array();
    }
    else {
        $max_value = $n;
    }
    my @results;
    my @path;
    sub {
        my ($n, $max_part) = @_;
        if ($n == 0) {
            unshift @results, _array([@path]);
            return;
        }
        my $upper = ($n < $max_part ? $n : $max_part);
        for my $part (1 .. $upper) {
            push @path, bless \$part;
            __SUB__->($n - $part, $part - 1);
            pop @path;    # backtrack
        }
      }
      ->($n, $max_value);
    return _array(\@results);
}

sub multisets {
    my ($n, $k, $max_sum) = @_;

    _valid(\$k);

    $n = _any2ui($$n) // return _array();
    $k = _any2ui($$k) // return _array();

    if (defined($max_sum)) {
        _valid(\$max_sum);
        $max_sum = _any2ui($$max_sum) // return _array();
    }

    my @results;
    my @path;

    sub {
        my ($pos, $max_value, $sum) = @_;

        if ($pos == $n) {
            push @results, _array([@path]);
            return;
        }

        for my $v (1 .. $max_value) {
            if (defined $max_sum) {
                last if ($sum + $v > $max_sum);
            }
            push @path, bless \$v;
            __SUB__->($pos + 1, $v, $sum + $v);
            pop @path;    # backtrack
        }
      }
      ->(0, $k, 0);

    return _array(\@results);
}

sub multiplicative_partitions {
    my ($n, $max_value, $max_sum) = @_;

    # See also:
    #   https://oeis.org/A001055

    $n = _big2uistr($$n) // return _array();

    if (defined($max_value)) {
        _valid(\$max_value);
        $max_value = _big2pistr($$max_value) // return _array();
    }

    if (defined($max_sum)) {
        _valid(\$max_sum);
        $max_sum = _big2pistr($$max_sum) // return _array();
    }

    my @divs = _divisors($n);
    CORE::shift(@divs);    # remove divisor '1'

    my @results;
    my $end       = $#divs;
    my @divs_objs = map { _set_int($_) } @divs;
    my @path;

    sub {
        my ($target, $min_idx, $curr_sum) = @_;

        if ($target == 1) {
            CORE::push(@results, _array([@path]));
            return;
        }

        for my $i ($min_idx .. $end) {
            my $d = $divs[$i];

            # Prune branch if the divisor exceeds the remaining target
            last if Math::Prime::Util::GMP::cmpint($d, $target) > 0;

            if (defined($max_value)) {
                last if Math::Prime::Util::GMP::cmpint($d, $max_value) > 0;
            }

            my $new_sum;
            if (defined($max_sum)) {
                $new_sum = Math::Prime::Util::GMP::addint($curr_sum, $d);
                last if Math::Prime::Util::GMP::cmpint($new_sum, $max_sum) > 0;
            }

            if (Math::Prime::Util::GMP::modint($target, $d) eq '0') {
                push @path, $divs_objs[$i];
                __SUB__->(Math::Prime::Util::GMP::divint($target, $d), $i, $new_sum);
                pop @path;    # backtrack
            }
        }
      }
      ->($n, 0, 0);

    @results = map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, scalar(@$_)] } @results;

    return _array(\@results);
}

*mpartitions = \&multiplicative_partitions;

sub antidivisors {
    my ($n) = @_;

    $n = $$n;

    my @antidivisors;

    foreach my $d (@{(bless \$n)->divisors}) {
        my $y = __add__($$d, $$d);
        if (__cmp__($n, $y) > 0 and __cmp__(__mod__($n, $y), 0) != 0) {
            push @antidivisors, $y;
        }
    }

    my $n2 = __add__($n, $n);

    foreach my $d (@{(bless \__dec__($n2))->divisors}) {
        if (__cmp__($n, $$d) > 0 and __cmp__($$d, 2) >= 0 and __cmp__(__mod__($n, $$d), 0) != 0) {
            push @antidivisors, $$d;
        }
    }

    foreach my $d (@{(bless \__inc__($n2))->divisors}) {
        if (__cmp__($n, $$d) > 0 and __cmp__($$d, 2) >= 0 and __cmp__(__mod__($n, $$d), 0) != 0) {
            push @antidivisors, $$d;
        }
    }

    @antidivisors = sort { $a <=> $b } @antidivisors;
    _array([map { bless \$_ } @antidivisors]);
}

sub udivisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        my $pp;

        if ($e <= 2) {    # p^e where e <= 2

            if (FAST_MODE and $p < ULONG_MAX) {
                $pp = Math::GMPz::Rmpz_init_set_ui($p);
            }
            else {
                $pp = Math::GMPz::Rmpz_init_set_str("$p", 10);
            }

            if ($e == 2) {
                Math::GMPz::Rmpz_mul($pp, $pp, $pp);
            }
        }
        else {    # p^e where e >= 3

            $pp = Math::GMPz::Rmpz_init();

            if (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_ui_pow_ui($pp, $p, $e);
            }
            else {
                Math::GMPz::Rmpz_set_str($pp, "$p", 10);
                Math::GMPz::Rmpz_pow_ui($pp, $pp, $e);
            }
        }

        my @t;

        foreach my $d (@d) {
            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_mul($t, $d, $pp);
            push @t, $t;
        }

        push @d, $pp;
        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    unshift @d, ONE;

    _array(\@d);
}

*unitary_divisors = \&udivisors;

sub edivisors {    # OEIS: A322791
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d = (Math::GMPz::Rmpz_init_set_ui(1));
    my $r = Math::GMPz::Rmpz_init();

    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        my @t;
        foreach my $k (_divisors($e)) {

            if (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_ui_pow_ui($r, $p, $k);
            }
            else {
                Math::GMPz::Rmpz_set_str($r, $p, 10);
                Math::GMPz::Rmpz_pow_ui($r, $r, $k) if ($k > 1);
            }

            foreach my $u (@d) {
                my $t = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul($t, $u, $r);
                push @t, $t;
            }
        }

        @d = @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
}

*exponential_divisors = \&edivisors;

sub idivisors {    # OEIS: A077609
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d = ($ONE);
    my $r = Math::GMPz::Rmpz_init();

    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        my @t;
        Math::GMPz::Rmpz_set($r, $p);

        foreach my $k (1 .. $e) {

            if (($e & $k) == $k) {
                foreach my $u (@d) {
                    my $t = Math::GMPz::Rmpz_init();
                    Math::GMPz::Rmpz_mul($t, $u, $r);
                    push @t, $t;
                }
            }

            Math::GMPz::Rmpz_mul($r, $r, $p) if ($k < $e);
        }

        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
}

*infinitary_divisors = \&idivisors;

sub biudivisors {    # OEIS: A222266
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) > 0 or return _array();

    my @d = ($ONE);
    my $r = Math::GMPz::Rmpz_init();
    my $w = Math::GMPz::Rmpz_init();

    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        my @t;
        Math::GMPz::Rmpz_set($r, $p);

        foreach my $k (1 .. $e) {

            Math::GMPz::Rmpz_divexact($w, $n, $r);

            if ((bless \$w)->gcud(bless \$r)->is_one) {
                foreach my $u (@d) {
                    my $t = Math::GMPz::Rmpz_init();
                    Math::GMPz::Rmpz_mul($t, $u, $r);
                    push @t, $t;
                }
            }

            Math::GMPz::Rmpz_mul($r, $r, $p) if ($k < $e);
        }

        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
}

*bdivisors           = \&biudivisors;
*bi_unitary_divisors = \&biudivisors;

sub prime_power_divisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my $u = Math::GMPz::Rmpz_init();

    my @d;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        push @d, $p;
        next if ($e == 1);

        Math::GMPz::Rmpz_set($u, $p);

        foreach my $i (2 .. $e) {
            Math::GMPz::Rmpz_mul($u, $u, $p);
            push @d, Math::GMPz::Rmpz_init_set($u);
        }
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
}

sub prime_power_udivisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        my $pp;

        if ($e <= 2) {    # p^e where e <= 2

            if (FAST_MODE and $p < ULONG_MAX) {
                $pp = Math::GMPz::Rmpz_init_set_ui($p);
            }
            else {
                $pp = Math::GMPz::Rmpz_init_set_str("$p", 10);
            }

            if ($e == 2) {
                Math::GMPz::Rmpz_mul($pp, $pp, $pp);
            }
        }
        else {    # p^e where e >= 3

            $pp = Math::GMPz::Rmpz_init();

            if (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_ui_pow_ui($pp, $p, $e);
            }
            else {
                Math::GMPz::Rmpz_set_str($pp, $p, 10);
                Math::GMPz::Rmpz_pow_ui($pp, $pp, $e);
            }
        }

        push @d, $pp;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
}

*prime_power_unitary_divisors = \&prime_power_udivisors;
*unitary_prime_power_divisors = \&prime_power_udivisors;

sub powerfree_divisors {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _big2pistr($$n) // return _array();
    $k = _any2ui($$k) || return _array();

    if ($k == 1) {
        return _array([ONE]);
    }

    my $r = Math::GMPz::Rmpz_init();

    my @d;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        if ($k <= $e) {
            $e = $k - 1;
        }

        Math::GMPz::Rmpz_set($r, $p);

        my @t;
        foreach my $i (1 .. $e) {
            foreach my $d (@d) {
                my $t = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul($t, $r, $d);
                push @t, $t;
            }
            push @t, Math::GMPz::Rmpz_init_set($r);
            Math::GMPz::Rmpz_mul($r, $r, $p) if ($i < $e);
        }

        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    unshift @d, ONE;

    _array(\@d);
}

sub squarefree_divisors {
    (TWO)->powerfree_divisors($_[0]);
}

sub cubefree_divisors {
    (THREE)->powerfree_divisors($_[0]);
}

my $power_divisors_func = sub {
    my ($k, $factor_exp) = @_;

    my @d = ($ONE);
    my $r = Math::GMPz::Rmpz_init();

    foreach my $pe (grep { $_->[1] >= $k } @$factor_exp) {

        my ($p, $e) = @$pe;

        $p =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        my @t;
        for (my $i = $k ; $i <= $e ; $i += $k) {

            Math::GMPz::Rmpz_pow_ui($r, $p, $i);

            foreach my $d (@d) {
                my $z = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul($z, $r, $d);
                push @t, $z;
            }
        }

        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
};

sub power_divisors {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _big2pistr($$n) // return _array();
    $k = _any2ui($$k) || return _array();

    $power_divisors_func->($k, [_factor_exp($n)]);
}

sub perfect_power_divisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @lists;
    my @factor_exp = _factor_exp($n);
    my $max_k      = List::Util::max(map { $_->[1] } @factor_exp);

    if (!defined($max_k) or $max_k == 1) {
        return _array([ONE]);
    }

    foreach my $k (2 .. $max_k) {
        push @lists, $power_divisors_func->($k, \@factor_exp);
    }

    _array([map { @$_ } @lists])->isort->iuniq;
}

*pp_divisors = \&perfect_power_divisors;

sub square_divisors {
    (TWO)->power_divisors($_[0]);
}

sub cube_divisors {
    (THREE)->power_divisors($_[0]);
}

my $power_udivisors_func = sub {
    my ($k, $factor_exp) = @_;

    my @d = ($ONE);
    foreach my $pe (grep { $_->[1] % $k == 0 } @$factor_exp) {
        my ($p, $e) = @$pe;

        my $pp =
          (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_init_set_ui($p)
          : Math::GMPz::Rmpz_init_set_str("$p", 10);

        if ($e == 2) {
            Math::GMPz::Rmpz_mul($pp, $pp, $pp);
        }
        else {
            Math::GMPz::Rmpz_pow_ui($pp, $pp, $e);
        }

        my @t;
        foreach my $d (@d) {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_mul($z, $pp, $d);
            push @t, $z;
        }
        push @d, @t;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    _array(\@d);
};

sub power_udivisors {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _big2pistr($$n) // return _array();
    $k = _any2ui($$k) || return _array();

    return $power_udivisors_func->($k, [_factor_exp($n)]);
}

*unitary_power_divisors = \&power_udivisors;
*power_unitary_divisors = \&power_udivisors;

sub perfect_power_udivisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @lists;
    my @factor_exp = _factor_exp($n);
    my $max_k      = List::Util::max(map { $_->[1] } @factor_exp);

    if (!defined($max_k) or $max_k == 1) {
        return _array([ONE]);
    }

    foreach my $k (2 .. $max_k) {
        push @lists, $power_udivisors_func->($k, \@factor_exp);
    }

    _array([map { @$_ } @lists])->isort->iuniq;
}

*pp_udivisors                   = \&perfect_power_udivisors;
*perfect_power_unitary_divisors = \&perfect_power_udivisors;

sub square_udivisors {
    (TWO)->power_udivisors($_[0]);
}

sub cube_udivisors {
    (THREE)->power_udivisors($_[0]);
}

sub powerfree_udivisors {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _big2pistr($$n) // return _array();
    $k = _any2ui($$k) || return _array();

    if ($k == 1) {
        return _array([ONE]);
    }

    my @d;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        $e < $k or next;

        my $r = Math::GMPz::Rmpz_init();

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($r, $p, $e);
        }
        else {
            Math::GMPz::Rmpz_set_str($r, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($r, $r, $e);
        }

        my @tmp = ($r);

        foreach my $d (@d) {
            my $t = Math::GMPz::Rmpz_init();
            Math::GMPz::Rmpz_mul($t, $d, $r);
            push @tmp, $t;
        }

        push @d, @tmp;
    }

    @d = sort { Math::GMPz::Rmpz_cmp($a, $b) } @d;
    @d = map  { bless \$_ } @d;

    unshift @d, ONE;

    _array(\@d);
}

sub squarefree_udivisors {
    (TWO)->powerfree_udivisors($_[0]);
}

sub cubefree_udivisors {
    (THREE)->powerfree_udivisors($_[0]);
}

sub prime_divisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d;
    foreach my $pk (_factor_exp($n)) {
        my $p = $pk->[0];
        if (FAST_MODE and $p < ULONG_MAX) {
            push @d, bless \$p;
        }
        else {
            $p = Math::GMPz::Rmpz_init_set_str("$p", 10);
            push @d, bless \$p;
        }
    }

    _array(\@d);
}

sub prime_udivisors {
    my ($n) = @_;

    $n = _big2pistr($$n) // return _array();

    my @d;
    foreach my $pk (_factor_exp($n)) {
        if ($pk->[1] == 1) {
            my $p = $pk->[0];
            push @d, _set_int($p);
        }
    }

    _array(\@d);
}

*unitary_prime_divisors = \&prime_udivisors;
*prime_unitary_divisors = \&prime_udivisors;

sub exp_mangoldt {
    my ($n) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {    # native integer

        $n < 0 and goto &nan;

        if (HAS_PRIME_UTIL) {
            $n || return ZERO;
            $n = Math::Prime::Util::exp_mangoldt($n);
            $n == 1 and return ONE;
            return bless \$n;
        }
    }

    $n || return ZERO;
    $n = Math::Prime::Util::GMP::exp_mangoldt($n);
    $n eq '1' and return ONE;
    _set_int($n);
}

sub mangoldt {
    $_[0]->exp_mangoldt->log;
}

sub exp_mangoldt_sum {
    my ($n) = @_;

    # Formula:
    #   exp_mangoldt_sum(n) = n - prime_power_count(n) + Sum_{k=1..floor(log_2(n))} prime_sum(floor(n^(1/k)))

    my $z     = _any2mpz($$n, 0);
    my $t     = Math::GMPz::Rmpz_init();
    my @terms = $n->prime_power_count->neg;
    my $t_obj = bless \$t;

    foreach my $k (1 .. Math::GMPz::Rmpz_sizeinbase($z, 2) - 1) {
        Math::GMPz::Rmpz_root($t, $z, $k);
        push @terms, $t_obj->prime_sum;
    }

    Sidef::Types::Number::Number::sum($n, @terms);
}

sub primitive_part {
    my ($n, $f) = @_;
    $f // return $n->exp_mangoldt;
    my $z = _any2mpz($$n, 5) // goto &nan;

    my (@u, @v);

    foreach my $d (@{$n->squarefree_divisors}) {
        my $t = Math::GMPz::Rmpz_init();

        ref($$d)
          ? Math::GMPz::Rmpz_divexact($t, $z, $$d)
          : Math::GMPz::Rmpz_divexact_ui($t, $z, $$d);

        my $r = $f->run(bless \$t);
        my $m = Math::Prime::Util::GMP::moebius($$d);

        ($m == 1) ? push(@u, $$r) : push(@v, $$r);
    }

    my $u = @u ? _binsplit(\@u, \&__mul__) : $ONE;
    my $v = @v ? _binsplit(\@v, \&__mul__) : $ONE;

    bless \__div__($u, $v);
}

sub euler_phi {
    my ($n, $k) = @_;

    if (defined($k)) {
        goto &jordan_totient;
    }

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::euler_phi($n)
          : Math::Prime::Util::GMP::totient($n);
        return bless \$r;
    }

    $n = _big2uistr($n) // goto &nan;

    if (CORE::length($n) < SPECIAL_FACTORS_MIN) {
        return _set_int(Math::Prime::Util::GMP::totient($n));
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pp (_factor_exp($n)) {
        my ($p, $e) = @$pp;

        # Multiplicative with a(p^e) = (p - 1)*p^(e-1)

        if (FAST_MODE and $p < ULONG_MAX) {
            push @terms, $p - 1;

            if ($e >= 2) {

                if ($e == 2) {
                    push @terms, $p;
                    next;
                }

                Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e - 1);
                push @terms,
                  (
                      Math::GMPz::Rmpz_fits_ulong_p($t)
                    ? Math::GMPz::Rmpz_get_ui($t)
                    : Math::GMPz::Rmpz_init_set($t)
                  );
            }
        }
        else {

            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_sub_ui($t, $t, 1);
            push @terms, Math::GMPz::Rmpz_init_set($t);

            if ($e >= 2) {
                Math::GMPz::Rmpz_add_ui($t, $t, 1);
                Math::GMPz::Rmpz_pow_ui($t, $t, $e - 1) if ($e != 2);
                push @terms, Math::GMPz::Rmpz_init_set($t);
            }
        }
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

*EulerPhi      = \&euler_phi;
*eulerphi      = \&euler_phi;
*euler_totient = \&euler_phi;
*totient       = \&euler_phi;

sub totient_sum {
    my ($n, $k) = @_;

    # TODO: use a faster formula
    # https://github.com/trizen/perl-scripts/blob/master/Math/partial_sums_of_euler_totient_function_fast.pl

    # TODO: generalize to compute: Sum_{k=1..n} k^m * J_i(k)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    my $k_obj = bless \$k;

    my $f = sub { $_[0]->ipow($k_obj) };
    my $g = sub { $_[0]->moebius };

    my $F = sub { $_[0]->faulhaber_sum($k_obj) };
    my $G = sub { $_[0]->mertens };

    if ($k == 1) {
        $f = sub { $_[0] };
    }
    elsif ($k == 2) {
        $f = sub { $_[0]->sqr };
    }

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

*euler_phi_sum      = \&totient_sum;
*jordan_totient_sum = \&totient_sum;

sub _n_over_d_divisors {
    my ($N, $d, $u, $D) = @_;

    # N = a positive integer
    # d = a divisor of N
    # u = temporary Math::GMPz object
    # D = array ref with divisors of N, assumed sorted ascending

    Math::GMPz::Rmpz_divexact($u, $N, $d);

    # When u = N/d is a native integer, call Math::Prime::Util::divisors().
    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($u)) {
        return Math::Prime::Util::divisors(Math::GMPz::Rmpz_get_ui($u));
    }

    # When N has too many divisors, it's faster
    # to simply compute the divisors of u = N/d
    if (scalar(@$D) > 1e4) {
        return _divisors($u);
    }

    my $d_str = Math::GMPz::Rmpz_get_str($d, 10);
    my $u_str = Math::GMPz::Rmpz_get_str($u, 10);

    state $limit = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_sqrt($u, $u);
    Math::GMPz::Rmpz_mul($limit, $u, $d);

    my (@front, @back);

    foreach my $k (@$D) {

        if (FAST_MODE and $k < ULONG_MAX) {
            last if Math::GMPz::Rmpz_cmp_ui($limit, $k) < 0;
            Math::GMPz::Rmpz_gcd_ui($u, $d, $k);
        }
        else {
            Math::GMPz::Rmpz_set_str($u, $k, 10);
            last if Math::GMPz::Rmpz_cmp($limit, $u) < 0;
            Math::GMPz::Rmpz_gcd($u, $u, $d);
        }

        if (Math::GMPz::Rmpz_cmp($u, $d) == 0) {

            # We found x. Immediately calculate its pair y.
            my $x = Math::Prime::Util::GMP::divint($k,     $d_str);
            my $y = Math::Prime::Util::GMP::divint($u_str, $x);

            push @front, $x;
            unshift @back, $y if $x ne $y;
        }
    }

    # Return combined halves to naturally maintain the ascending sorted order
    return (@front, @back);
}

sub _dynamic_preimage {
    my ($N, $L, $D, %opt) = @_;

    # Based on invphi.gp ver. 2.1 by Max Alekseyev.

    # With memory optimization by Dana Jacobsen
    # https://github.com/danaj/Math-Prime-Util/issues/83

    # Phase 1: Determine which intermediate values are actually needed
    my %needed = (Math::GMPz::Rmpz_get_str($N, 10) => undef);
    my @operations;

    my $u       = Math::GMPz::Rmpz_init();
    my $unitary = $opt{unitary};

    foreach my $l (@$L) {
        my @current_ops;

        foreach my $pair (@$l) {
            my ($x, $y) = @$pair;

            foreach my $d (_n_over_d_divisors($N, $x, $u, $D)) {

                (FAST_MODE and $d < ULONG_MAX)
                  ? Math::GMPz::Rmpz_mul_ui($u, $x, $d)
                  : do {
                    Math::GMPz::Rmpz_set_str($u, $d, 10);
                    Math::GMPz::Rmpz_mul($u, $u, $x);
                  };

                # Only track operations that lead to needed values
                if (exists $needed{Math::GMPz::Rmpz_get_str($u, 10)}) {
                    undef $needed{$d};
                    push(@current_ops, [$d, $y, Math::GMPz::Rmpz_init_set($u)]);
                }
            }
        }

        unshift(@operations, \@current_ops) if @current_ops;
    }

    undef %needed;

    my %R = (1 => [1]);

    foreach my $ops (@operations) {
        my %t;

        foreach my $op (@$ops) {
            my ($d, $y, $F) = @$op;

            if (exists $R{$d}) {

                my $key  = Math::GMPz::Rmpz_get_str($F, 10);
                my @list = @{$R{$d}};

                if ($unitary) {
                    @list = grep {
                        ref($_)
                          ? do {
                            Math::GMPz::Rmpz_gcd($u, $y, $_);
                            Math::GMPz::Rmpz_cmp_ui($u, 1) == 0;
                          }
                          : do {
                            Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $y, $_) == 1;
                          }
                    } @list;
                }

                push @{$t{$key}}, map {
                    my $w = Math::GMPz::Rmpz_init();
                    ref($_) ? Math::GMPz::Rmpz_mul($w, $y, $_) : Math::GMPz::Rmpz_mul_ui($w, $y, $_);
                    $w = Math::GMPz::Rmpz_get_ui($w) if Math::GMPz::Rmpz_fits_ulong_p($w);
                    $w;
                } @list;
            }
        }

        foreach my $k (keys %t) {
            push @{$R{$k}}, @{delete $t{$k}};
        }
    }

    $R{$N} // [];
}

sub _dynamic_preimage_minmax {
    my ($N, $L, $D, %opt) = @_;

    # Based on invphi.gp ver. 2.1 by Max Alekseyev.

    my %R = (1 => $ONE);

    my $u = Math::GMPz::Rmpz_init();
    my $w = Math::GMPz::Rmpz_init();

    my $min = $opt{min};

    foreach my $l (@$L) {
        my %t;

        foreach my $pair (@$l) {
            my ($x, $y) = @$pair;

            foreach my $d (_n_over_d_divisors($N, $x, $u, $D)) {
                if (exists $R{$d}) {

                    (FAST_MODE and $d < ULONG_MAX)
                      ? Math::GMPz::Rmpz_mul_ui($u, $x, $d)
                      : do {
                        Math::GMPz::Rmpz_set_str($u, $d, 10);
                        Math::GMPz::Rmpz_mul($u, $u, $x);
                      };

                    my $key = Math::GMPz::Rmpz_get_str($u, 10);

                    Math::GMPz::Rmpz_mul($w, $y, $R{$d});

                    if (
                        !exists($t{$key})
                        or (
                            $min
                            ? (Math::GMPz::Rmpz_cmp($w, $t{$key}) < 0)
                            : (Math::GMPz::Rmpz_cmp($w, $t{$key}) > 0)
                           )
                      ) {
                        $t{$key} = Math::GMPz::Rmpz_init_set($w);
                    }
                }
            }
        }

        while (my ($key, $value) = each %t) {
            if (
                !exists($R{$key})
                or (
                    $min
                    ? (Math::GMPz::Rmpz_cmp($value, $R{$key}) < 0)
                    : (Math::GMPz::Rmpz_cmp($value, $R{$key}) > 0)
                   )
              ) {
                $R{$key} = $value;
            }
        }
    }

    $R{$N};
}

sub _dynamic_preimage_len_bigint {
    my ($N, $L, $D) = @_;

    # Based on invphi.gp ver. 2.1 by Max Alekseyev.

    my %R = (1 => Math::GMPz::Rmpz_init_set_ui(1));
    my $u = Math::GMPz::Rmpz_init();

    foreach my $l (@$L) {
        my %t;

        foreach my $pair (@$l) {

            my $x = $pair->[0];

            foreach my $d (_n_over_d_divisors($N, $x, $u, $D)) {
                if (exists $R{$d}) {

                    (FAST_MODE and $d < ULONG_MAX)
                      ? Math::GMPz::Rmpz_mul_ui($u, $x, $d)
                      : do {
                        Math::GMPz::Rmpz_set_str($u, $d, 10);
                        Math::GMPz::Rmpz_mul($u, $u, $x);
                      };

                    my $key = Math::GMPz::Rmpz_get_str($u, 10);

                    if (!exists $t{$key}) {
                        $t{$key} = Math::GMPz::Rmpz_init_set_ui(0);
                    }

                    Math::GMPz::Rmpz_add($t{$key}, $t{$key}, $R{$d});
                }
            }
        }

        while (my ($key, $value) = each %t) {
            if (!exists $R{$key}) {
                $R{$key} = Math::GMPz::Rmpz_init_set_ui(0);
            }
            Math::GMPz::Rmpz_add($R{$key}, $R{$key}, $value);
        }
    }

    if (exists $R{$N}) {
        return Math::GMPz::Rmpz_get_str($R{$N}, 10);
    }

    return 0;
}

sub _dynamic_preimage_len {
    my ($N, $L, $D) = @_;

    # Based on invphi.gp ver. 2.1 by Max Alekseyev.

    my %R = (1 => 1);
    my $u = Math::GMPz::Rmpz_init();

    foreach my $l (@$L) {
        my %t;

        foreach my $pair (@$l) {

            my $x = $pair->[0];

            foreach my $d (_n_over_d_divisors($N, $x, $u, $D)) {
                if (exists $R{$d}) {

                    (FAST_MODE and $d < ULONG_MAX)
                      ? Math::GMPz::Rmpz_mul_ui($u, $x, $d)
                      : do {
                        Math::GMPz::Rmpz_set_str($u, $d, 10);
                        Math::GMPz::Rmpz_mul($u, $u, $x);
                      };

                    $t{Math::GMPz::Rmpz_get_str($u, 10)} += $R{$d};
                }
            }
        }

        while (my ($key, $value) = each %t) {
            $R{$key} += $value;
        }
    }

    my $r = $R{$N} // 0;
    ($r < ~0) || goto &_dynamic_preimage_len_bigint;
    return $r;
}

sub _cook_euler_phi {
    my ($N) = @_;

    my $p = Math::GMPz::Rmpz_init();
    my $v = Math::GMPz::Rmpz_init();

    my %L;
    my @D = _divisors($N);

    foreach my $d (@D) {

        Math::Prime::Util::GMP::is_prime(Math::Prime::Util::GMP::addint($d, 1)) || next;

        (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($p, $d)
          : Math::GMPz::Rmpz_set_str($p, $d, 10);

        Math::GMPz::Rmpz_add_ui($p, $p, 1);

        my $t = Math::GMPz::Rmpz_remove($v, $N, $p);

        push @{$L{$p}}, map {

            # [(p-1)*p^(k-1), p^k]

            my $x = Math::GMPz::Rmpz_init();
            my $y = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_pow_ui($v, $p, $_ - 1);
            Math::GMPz::Rmpz_pow_ui($y, $p, $_);
            Math::GMPz::Rmpz_sub_ui($x, $p, 1);
            Math::GMPz::Rmpz_mul($x, $x, $v);

            [$x, $y]
        } 1 .. $t + 1;
    }

    ([values %L], \@D);
}

sub inverse_totient {
    my ($n) = @_;

    # Algorithm "invphi" from invphi.gp ver. 2.1 by Max Alekseyev.

    $n = _any2mpz($$n, 0) // return _array();

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return _array(ZERO) if !Math::GMPz::Rmpz_sgn($n);
        return _array();
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {    # XXX: MPU 0.73 leaks memory
        return _array([map { bless \$_ } Math::Prime::Util::inverse_totient(Math::GMPz::Rmpz_get_ui($n))]);
    }

    my $result = _dynamic_preimage($n, _cook_euler_phi($n));
    _array([map { bless \$_ } sort { $a <=> $b } @$result]);
}

*inverse_phi       = \&inverse_totient;
*phi_inverse       = \&inverse_totient;
*inverse_euler_phi = \&inverse_totient;

sub inverse_totient_len {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ONE if !Math::GMPz::Rmpz_sgn($n);
        return ZERO;
    }

    my $r;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $r = scalar Math::Prime::Util::inverse_totient(Math::GMPz::Rmpz_get_ui($n));
    }
    else {
        $r = _dynamic_preimage_len($n, _cook_euler_phi($n));
    }

    _set_int($r);
}

*phi_inverse_len       = \&inverse_totient_len;
*inverse_phi_len       = \&inverse_totient_len;
*inverse_euler_phi_len = \&inverse_totient_len;

sub inverse_euler_phi_min {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_euler_phi($n), min => 1) // return undef;
    bless \$r;
}

*phi_inverse_min     = \&inverse_euler_phi_min;
*inverse_phi_min     = \&inverse_euler_phi_min;
*inverse_totient_min = \&inverse_euler_phi_min;

sub inverse_euler_phi_max {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_euler_phi($n), min => 0) // return undef;
    bless \$r;
}

*phi_inverse_max     = \&inverse_euler_phi_max;
*inverse_phi_max     = \&inverse_euler_phi_max;
*inverse_totient_max = \&inverse_euler_phi_max;

sub _cook_dedekind_psi {
    my ($N, $k) = @_;

    my $p = Math::GMPz::Rmpz_init();
    my $v = Math::GMPz::Rmpz_init();

    my %L;
    my @D = _divisors($N);

    foreach my $d (@D) {

        Math::Prime::Util::GMP::is_prime(Math::Prime::Util::GMP::subint($d, 1)) || next;

        (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($p, $d)
          : Math::GMPz::Rmpz_set_str($p, $d, 10);

        Math::GMPz::Rmpz_sub_ui($p, $p, 1);

        my $t = Math::GMPz::Rmpz_remove($v, $N, $p);

        push @{$L{$p}}, map {

            # [(p+1)*p^(k-1), p^k]

            my $x = Math::GMPz::Rmpz_init();
            my $y = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_pow_ui($v, $p, $_ - 1);
            Math::GMPz::Rmpz_pow_ui($y, $p, $_);

            Math::GMPz::Rmpz_add_ui($x, $p, 1);
            Math::GMPz::Rmpz_mul($x, $x, $v);

            [$x, $y]
        } 1 .. $t + 1;
    }

    ([values %L], \@D);
}

sub inverse_dedekind_psi {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return _array(ZERO) if !Math::GMPz::Rmpz_sgn($n);
        return _array();
    }

    my $result = _dynamic_preimage($n, _cook_dedekind_psi($n));
    _array([map { bless \$_ } sort { $a <=> $b } @$result]);
}

*psi_inverse = \&inverse_dedekind_psi;
*inverse_psi = \&inverse_dedekind_psi;

sub inverse_dedekind_psi_len {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ONE if !Math::GMPz::Rmpz_sgn($n);
        return ZERO;
    }

    my $r = _dynamic_preimage_len($n, _cook_dedekind_psi($n));
    _set_int($r);
}

*psi_inverse_len = \&inverse_dedekind_psi_len;
*inverse_psi_len = \&inverse_dedekind_psi_len;

sub inverse_dedekind_psi_min {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_dedekind_psi($n), min => 1) // return undef;
    bless \$r;
}

*psi_inverse_min = \&inverse_dedekind_psi_min;
*inverse_psi_min = \&inverse_dedekind_psi_min;

sub inverse_dedekind_psi_max {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return undef;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_dedekind_psi($n), min => 0) // return undef;
    bless \$r;
}

*psi_inverse_max = \&inverse_dedekind_psi_max;
*inverse_psi_max = \&inverse_dedekind_psi_max;

sub _cook_usigma {
    my ($N) = @_;

    my %L;
    my @D = _divisors($N);

    foreach my $d (@D) {

        Math::Prime::Util::GMP::is_prime_power(Math::Prime::Util::GMP::subint($d, 1)) || next;

        my $u = Math::GMPz::Rmpz_init();
        my $v = Math::GMPz::Rmpz_init();

        (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($u, $d)
          : Math::GMPz::Rmpz_set_str($u, $d, 10);

        Math::GMPz::Rmpz_sub_ui($v, $u, 1);

        push @{$L{$v}}, [$u, $v];
    }

    ([values %L], \@D);
}

sub inverse_usigma {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return _array(ZERO) if !Math::GMPz::Rmpz_sgn($n);
        return _array();
    }

    my $result = _dynamic_preimage($n, _cook_usigma($n), unitary => 1);
    _array([map { bless \$_ } sort { $a <=> $b } @$result]);
}

*usigma_inverse = \&inverse_usigma;

sub _cook_uphi {
    my ($N) = @_;

    my %L;
    my @D = _divisors($N);

    foreach my $d (@D) {

        Math::Prime::Util::GMP::is_prime_power(Math::Prime::Util::GMP::addint($d, 1)) || next;

        my $u = Math::GMPz::Rmpz_init();
        my $v = Math::GMPz::Rmpz_init();

        (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($u, $d)
          : Math::GMPz::Rmpz_set_str($u, $d, 10);

        Math::GMPz::Rmpz_add_ui($v, $u, 1);

        push @{$L{$v}}, [$u, $v];
    }

    ([values %L], \@D);
}

sub inverse_uphi {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return _array(ZERO) if !Math::GMPz::Rmpz_sgn($n);
        return _array();
    }

    my $result = _dynamic_preimage($n, _cook_uphi($n), unitary => 1);
    _array([map { bless \$_ } sort { $a <=> $b } @$result]);
}

*uphi_inverse = \&inverse_uphi;

sub _cook_sigma {
    my ($N, $k) = @_;

    # Based on invphi.gp ver. 2.1 by Max Alekseyev.

    my $q = Math::GMPz::Rmpz_init();
    my $s = Math::GMPz::Rmpz_init();
    my $u = Math::GMPz::Rmpz_init();
    my $v = Math::GMPz::Rmpz_init();

    my %L;
    my @D = _divisors($N);

    foreach my $d (@D) {

        next if ($d == 1);

        (FAST_MODE and $d < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($u, $d)
          : Math::GMPz::Rmpz_set_str($u, $d, 10);

        foreach my $p (map { $_->[0] } _factor_exp(Math::Prime::Util::GMP::subint($d, 1))) {

            (FAST_MODE and $p < ULONG_MAX)
              ? Math::GMPz::Rmpz_set_ui($s, $p)
              : Math::GMPz::Rmpz_set_str($s, $p, 10);

            Math::GMPz::Rmpz_set($q, $s);
            Math::GMPz::Rmpz_pow_ui($q, $q, $k) if ($k > 1);

            # q = d*(p^k - 1) + 1
            Math::GMPz::Rmpz_sub_ui($q, $q, 1);
            Math::GMPz::Rmpz_mul($q, $q, $u);
            Math::GMPz::Rmpz_add_ui($q, $q, 1);

            my $t = Math::GMPz::Rmpz_remove($v, $q, $s);

            next if (($t <= $k) || ($t % $k));

            Math::GMPz::Rmpz_pow_ui($v, $s, $t);
            Math::GMPz::Rmpz_cmp($v, $q) == 0 or next;

            if ($k == 1) {
                Math::GMPz::Rmpz_divexact($v, $v, $s);
            }
            else {
                Math::GMPz::Rmpz_pow_ui($v, $s, Math::Prime::Util::GMP::divint($t, $k) - 1);
            }

            push @{$L{$p}}, [Math::GMPz::Rmpz_init_set($u), Math::GMPz::Rmpz_init_set($v)];
        }
    }

    ([values %L], \@D);
}

sub inverse_sigma {
    my ($n, $k) = @_;

    # Algorithm "invsigma" from invphi.gp ver. 2.1 by Max Alekseyev.

    $n = _any2mpz($$n, 0) // return _array();
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return _array() } : 1;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return _array(ZERO) if !Math::GMPz::Rmpz_sgn($n);
        return _array();
    }

    my $result = _dynamic_preimage($n, _cook_sigma($n, $k));
    _array([map { bless \$_ } sort { $a <=> $b } @$result]);
}

*sigma_inverse = \&inverse_sigma;

sub inverse_sigma_len {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 0) // return ZERO;
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return ZERO } : 1;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ONE if !Math::GMPz::Rmpz_sgn($n);
        return ZERO;
    }

    my $r = _dynamic_preimage_len($n, _cook_sigma($n, $k));
    _set_int($r);
}

*sigma_inverse_len = \&inverse_sigma_len;

sub inverse_sigma_min {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 0) // return undef;
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return undef } : 1;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_sigma($n, $k), min => 1) // return undef;
    bless \$r;
}

*sigma_inverse_min = \&inverse_sigma_min;

sub inverse_sigma_max {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 0) // return undef;
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // return undef } : 1;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO if !Math::GMPz::Rmpz_sgn($n);
        return undef;
    }

    my $r = _dynamic_preimage_minmax($n, _cook_sigma($n, $k), min => 0) // return undef;
    bless \$r;
}

*sigma_inverse_max = \&inverse_sigma_max;

sub jordan_totient {
    my ($n, $k) = @_;

    if (!defined($k)) {
        return $n->euler_phi;
    }

    # Multiplicative with:
    #   a(p^e, k) = p^(k*e) - p^(k*e - k)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 1) {
        return $n->euler_phi;
    }

    $n = $$n;
    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    if (CORE::length($n) < SPECIAL_FACTORS_MIN) {
        return _set_int(Math::Prime::Util::GMP::jordan_totient($k, $n));
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if ($e == 1) {

            if (FAST_MODE and $p < ULONG_MAX) {

                Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
                Math::GMPz::Rmpz_sub_ui($t, $t, 1);

                if (Math::GMPz::Rmpz_fits_ulong_p($t)) {
                    push @terms, Math::GMPz::Rmpz_get_ui($t);
                    next;
                }
            }
            else {
                Math::GMPz::Rmpz_set_str($t, "$p", 10);
                Math::GMPz::Rmpz_pow_ui($t, $t, $k) if ($k > 1);
                Math::GMPz::Rmpz_sub_ui($t, $t, 1);
            }

            push @terms, Math::GMPz::Rmpz_init_set($t);
            next;
        }

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
            Math::GMPz::Rmpz_pow_ui($u, $t, $e - 1);
            Math::GMPz::Rmpz_mul($t, $t, $u);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_set($u, $t);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k);
            Math::GMPz::Rmpz_pow_ui($u, $t, $e - 1);
            Math::GMPz::Rmpz_mul($t, $t, $u);
        }

        Math::GMPz::Rmpz_sub($t, $t, $u);
        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

*JordanTotient = \&jordan_totient;

sub dedekind_psi {
    my ($n, $k) = @_;

    # Multiplicative with:
    #   a(p^e, k) = p^(k*e) + p^(k*e - k)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    return $n->usigma0 if ($k == 0);

    $n = $$n;
    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if ($e == 1) {

            if (FAST_MODE and $p < ULONG_MAX) {

                if ($k == 1) {
                    push @terms, $p + 1;
                    next;
                }

                Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
                Math::GMPz::Rmpz_add_ui($t, $t, 1);

                if (Math::GMPz::Rmpz_fits_ulong_p($t)) {
                    push @terms, Math::GMPz::Rmpz_get_ui($t);
                    next;
                }
            }
            else {
                Math::GMPz::Rmpz_set_str($t, "$p", 10);
                Math::GMPz::Rmpz_pow_ui($t, $t, $k) if ($k > 1);
                Math::GMPz::Rmpz_add_ui($t, $t, 1);
            }

            push @terms, Math::GMPz::Rmpz_init_set($t);
            next;
        }

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
            Math::GMPz::Rmpz_pow_ui($u, $t, $e - 1);
            Math::GMPz::Rmpz_mul($t, $t, $u);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_set($u, $t);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k);
            Math::GMPz::Rmpz_pow_ui($u, $t, $e - 1);
            Math::GMPz::Rmpz_mul($t, $t, $u);
        }

        Math::GMPz::Rmpz_add($t, $t, $u);
        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

*psi         = \&dedekind_psi;
*DedekindPsi = \&dedekind_psi;

sub dedekind_psi_sum {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    my $k_obj = bless \$k;

    my $f = sub { $_[0]->is_squarefree ? 1 : 0 };
    my $g = sub { $_[0]->ipow($k_obj) };

    my $F = sub { $_[0]->squarefree_count };
    my $G = sub { $_[0]->faulhaber_sum($k_obj) };

    if ($k == 0) {
        $g = sub { 1 };
        $G = sub { $_[0] };
    }
    elsif ($k == 1) {
        $g = sub { $_[0] };
    }
    elsif ($k == 2) {
        $g = sub { $_[0]->sqr };
    }

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

*psi_sum = \&dedekind_psi_sum;

sub carmichael_lambda {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n) and $n >= 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::carmichael_lambda($n)
          : Math::Prime::Util::GMP::carmichael_lambda($n);
        return bless \$r;
    }

    $n = _big2uistr($n) // goto &nan;

    if (CORE::length($n) >= SPECIAL_FACTORS_MIN) {
        my @lambdas;
        foreach my $pp (_factor_exp($n)) {
            my ($p, $e) = @$pp;
            if ($e == 1) {
                push @lambdas, ((FAST_MODE and $p < ULONG_MAX) ? ($p - 1) : Math::Prime::Util::GMP::subint($p, 1));
            }
            else {
                push @lambdas, Math::Prime::Util::GMP::carmichael_lambda(Math::Prime::Util::GMP::powint($p, $e));
            }
        }
        return _set_int(Math::Prime::Util::GMP::lcm(@lambdas));
    }

    _set_int(Math::Prime::Util::GMP::carmichael_lambda($n));
}

*lambda          = \&carmichael_lambda;
*reduced_totient = \&carmichael_lambda;

sub liouville {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n) and $n > 0) {
        my $r =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::liouville($n)
          : Math::Prime::Util::GMP::liouville($n);
        return bless \$r;
    }

    my $str = _big2uistr($n) // goto &nan;

    if ($str eq '0') {
        return ZERO;
    }

    Math::Prime::Util::GMP::liouville($str) eq '1' ? ONE : MONE;
}

*Liouville        = \&liouville;
*liouville_lambda = \&liouville;

sub bigomega {
    my ($n, $m) = @_;

    $m = defined($m) ? do { _valid(\$m); _any2ui($$m) // goto &nan } : 0;

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }

    $n eq '0' and return ZERO;

    if ($m == 0) {

        my $r;
        if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            $r = Math::Prime::Util::prime_bigomega($n);
        }
        elsif (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            $r = scalar Math::Prime::Util::factor($n);
        }
        else {
            my @factors = _factor($n);
            $r = scalar @factors;
        }

        return bless \$r;
    }

    # Ω_m(n) = Sum_{p^k|n} Sum_{j=1..k} n^m / p^(j*m)
    #        = Sum_{p^k|n} n^m * (p^(m*k) - 1) / (p^m - 1) / p^(m*k)

    my $t  = Math::GMPz::Rmpz_init();
    my $u  = Math::GMPz::Rmpz_init();
    my $nm = Math::GMPz::Rmpz_init_set_str($n, 10);

    Math::GMPz::Rmpz_pow_ui($nm, $nm, $m) if $m > 1;

    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pp (_factor_exp($n)) {
        my ($p, $k) = @$pp;

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($u, $p, $m);    # u = p^m
        }
        else {
            Math::GMPz::Rmpz_set_str($u, $p, 10);
            Math::GMPz::Rmpz_pow_ui($u, $u, $m);       # u = p^m
        }

        Math::GMPz::Rmpz_pow_ui($t, $u, $k);      # t = (p^m)^k = p^(m*k)
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);       # u = p^m - 1
        Math::GMPz::Rmpz_mul($u, $u, $t);         # u = (p^m - 1) * p^(m*k)
        Math::GMPz::Rmpz_sub_ui($t, $t, 1);       # t = p^(m*k) - 1
        Math::GMPz::Rmpz_mul($t, $t, $nm);        # t = n^m * (p^(m*k) - 1)
        Math::GMPz::Rmpz_divexact($t, $t, $u);    # t = (n^m * (p^(m*k) - 1)) / ((p^m - 1) * p^(m*k))

        Math::GMPz::Rmpz_add($sum, $sum, $t);
    }

    bless \$sum;
}

*Ω     = \&bigomega;
*Omega = \&bigomega;

sub bigomega_sum {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 0;

    my $k_obj = bless \$k;

    my $f = sub { $_[0]->ipow($k_obj) };
    my $g = sub { (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime_power(${$_[0]}) : Math::Prime::Util::GMP::is_prime_power(${$_[0]})) ? 1 : 0 };

    my $F = sub { $_[0]->faulhaber_sum($k_obj) };
    my $G = sub { HAS_PRIME_UTIL ? Math::Prime::Util::prime_power_count(${$_[0]}) : $_[0]->prime_power_count };

    if ($k == 1) {
        $f = sub { $_[0] };
    }
    elsif ($k == 2) {
        $f = sub { $_[0]->sqr };
    }

    if ($k == 0) {
        $f = sub { 1 };
        $F = sub { $_[0] };
    }

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

sub prime_power_sigma0 {
    $_[0]->bigomega;
}

sub collatz {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // goto &nan;
    $n = Math::GMPz::Rmpz_init_set($n);    # copy

    state $two = Math::GMPz::Rmpz_init_set_ui(2);
    my $count = Math::GMPz::Rmpz_even_p($n) ? Math::GMPz::Rmpz_remove($n, $n, $two) : 0;

    while (Math::GMPz::Rmpz_cmp_ui($n, 1) > 0) {

        Math::GMPz::Rmpz_mul_ui($n, $n, 3);
        Math::GMPz::Rmpz_add_ui($n, $n, 1);

        $count += 1 + Math::GMPz::Rmpz_remove($n, $n, $two);
    }

    _set_int($count);
}

sub omega {
    my ($n, $m) = @_;

    $m = defined($m) ? do { _valid(\$m); _any2ui($$m) // goto &nan } : 0;
    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }

    $n eq '0' and return ZERO;

    if ($m == 0) {

        my $r;
        if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            $r = Math::Prime::Util::prime_omega($n);
        }
        elsif (HAS_PRIME_UTIL and $n < ULONG_MAX) {
            $r = scalar Math::Prime::Util::factor_exp($n);
        }
        else {
            $r = scalar _factor_exp($n);
        }

        return bless \$r;
    }

    # omega_m(n) = n^m * Sum_{p|n} 1/p^m

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $nm = Math::GMPz::Rmpz_init_set_str($n, 10);

    Math::GMPz::Rmpz_pow_ui($nm, $nm, $m) if ($m > 1);

    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pe (_factor_exp($n)) {

        my $p = $pe->[0];

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $m);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $m);
        }

        Math::GMPz::Rmpz_divexact($t, $nm, $t);
        Math::GMPz::Rmpz_add($sum, $sum, $t);
    }

    bless \$sum;
}

*ω = \&omega;

sub omega_sum {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 0;

    my $k_obj = bless \$k;

    my $f = sub { $_[0]->ipow($k_obj) };
    my $g = sub { (HAS_PRIME_UTIL ? Math::Prime::Util::is_prime(${$_[0]}) : Math::Prime::Util::GMP::is_prime(${$_[0]})) ? 1 : 0 };

    my $F = sub { $_[0]->faulhaber_sum($k_obj) };
    my $G = sub { HAS_PRIME_UTIL ? Math::Prime::Util::prime_count(${$_[0]}) : $_[0]->prime_count };

    if ($k == 1) {
        $f = sub { $_[0] };
    }
    elsif ($k == 2) {
        $f = sub { $_[0]->sqr };
    }

    if ($k == 0) {
        $f = sub { 1 };
        $F = sub { $_[0] };
    }

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

sub prime_sigma0 {
    $_[0]->omega;
}

sub prime_power_usigma0 {
    $_[0]->omega;
}

sub usigma0 {
    my ($n) = @_;

    # Identity:
    #   usigma0(n) = 2^omega(n)

    $n = $$n;
    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    if (HAS_PRIME_UTIL and $n < ULONG_MAX) {
        return _set_int(1 << (Math::Prime::Util::prime_omega($n)));
    }

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_ui($r, 0);
    Math::GMPz::Rmpz_setbit($r, scalar _factor_exp($n));
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub usigma {
    my ($n, $k) = @_;

    # Interesting identity:
    #   usigma(n, k) = sigma(n^(2*k) / rad(n)) / sigma(n^k / rad(n))

    # Multiplicative with:
    #   usigma(p^e, k) = p^(k*e) + 1

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &usigma0;
    }

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {

            if ($e == 1 and $k == 1) {    # optimization
                push @terms, $p + 1;
                next;
            }

            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k * $e);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k * $e);
        }

        Math::GMPz::Rmpz_add_ui($t, $t, 1);

        push @terms, (Math::GMPz::Rmpz_fits_ulong_p($t) ? Math::GMPz::Rmpz_get_ui($t) : Math::GMPz::Rmpz_init_set($t));
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub nisigma0 {    # A348341: count non-infinitary divisors of n
    my ($n) = @_;
    $n->sigma0->sub($n->isigma0);
}

sub nisigma {    # A348271: sum of non-infinitary divisors of n
    my ($n, $k) = @_;
    $n->sigma($k)->sub($n->isigma($k));
}

sub nusigma0 {    # A048105: count of non-unitary divisors of n
    my ($n) = @_;
    $n->sigma0->sub($n->usigma0);
}

sub nusigma {    # A048146: sum of non-unitary divisors of n
    my ($n, $k) = @_;
    $n->sigma($k)->sub($n->usigma($k));
}

sub bsigma0 {    # A286324: count of bi-unitary divisors of n.
    my ($n) = @_;

    # Multiplicative with:
    #   a(p^e) = e + (e mod 2)

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;
    _set_int(Math::Prime::Util::GMP::vecprod(map { $_->[1] + ($_->[1] % 2) } _factor_exp($n)));
}

*biusigma0 = \&bsigma0;

sub bsigma {    # A188999: Bi-unitary sigma: sum of the bi-unitary divisors of n
    my ($n, $k) = @_;

    # Multiplicative with:
    #   bsigma(p^e, k) = (p^(k*(e+1)) - 1)/(p^k - 1)                   if e is odd
    #   bsigma(p^e, k) = (p^(k*(e+1)) - 1)/(p^k - 1) - p^(k*(e/2))     if e is even

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &bsigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($u, $p, $k);
            Math::GMPz::Rmpz_pow_ui($t, $u, $e + 1);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($u, $t, $k);
            Math::GMPz::Rmpz_pow_ui($t, $u, $e + 1);
        }

        Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);
        Math::GMPz::Rmpz_divexact($t, $t, $u);

        if ($e % 2 == 0) {
            Math::GMPz::Rmpz_add_ui($u, $u, 1);
            Math::GMPz::Rmpz_pow_ui($u, $u, $e >> 1);
            Math::GMPz::Rmpz_sub($t, $t, $u);
        }

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

*biusigma = \&bsigma;

sub nbsigma0 {    # Axxxxxx: count of non-bi-unitary divisors of n
    my ($n) = @_;
    $n->sigma0->sub($n->bsigma0);
}

sub nbsigma {    # A319072: sum of non-bi-unitary divisors of n.
    my ($n, $k) = @_;
    $n->sigma($k)->sub($n->bsigma($k));
}

sub isigma0 {    # A037445: count of infinitary divisors (or i-divisors) of n
    my ($n) = @_;

    # Multiplicative with:
    #   a(p^e) = 2^hammingweight(e)

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;
    _set_int(
             Math::Prime::Util::GMP::vecprod(
                      map { 1 << (HAS_PRIME_UTIL ? Math::Prime::Util::hammingweight($_->[1]) : Math::Prime::Util::GMP::hammingweight($_->[1])) } _factor_exp($n)
             )
            );
}

sub isigma {    # A049417: sum of infinitary divisors of n
    my ($n, $k) = @_;

    # Multiplicative with:
    #   If e = Sum_{k >= 0} d_k 2^k (binary representation of e), then
    #   isigma(p^e, r) = Product_{k >= 0} (p^(r*2^k*{d_k+1}) - 1)/(p^(r*2^k) - 1)

    # Simplified formula, where d_k is odd in the binary representation of e (ignore even d_k):
    #   isigma(p^e, r) = Product_{k >= 0} (p^(r * 2^k) + 1)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &isigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($t, $p)
          : Math::GMPz::Rmpz_set_str($t, "$p", 10);

        my $j = 0;
        do {
            if ($e % 2 == 1) {
                Math::GMPz::Rmpz_pow_ui($u, $t, (1 << $j) * $k);
                Math::GMPz::Rmpz_add_ui($u, $u, 1);
                push @terms,
                  (
                      Math::GMPz::Rmpz_fits_ulong_p($u)
                    ? Math::GMPz::Rmpz_get_ui($u)
                    : Math::GMPz::Rmpz_init_set($u)
                  );
            }
            ++$j;
        } while ($e >>= 1);
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub esigma0 {    # A049419: count of exponential divisors (or e-divisors) of n.
    my ($n) = @_;

    # Multiplicative with:
    #   a(p^e) = tau(e)

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    my @terms;
    foreach my $pp (_factor_exp($n)) {
        if ($pp->[1] > 1) {
            push @terms,
              (
                HAS_PRIME_UTIL
                ? Math::Prime::Util::divisor_sum($pp->[1], 0)
                : Math::Prime::Util::GMP::sigma($pp->[1], 0)
              );
        }
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub esigma {    # A051377: sum of exponential divisors (or e-divisors) of n
    my ($n, $k) = @_;

    # Multiplicative with:
    #   a(p^e) = Sum_{d|e} p^d

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &esigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $w = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        Math::GMPz::Rmpz_set_ui($t, 1);

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($t, $p)
          : Math::GMPz::Rmpz_set_str($t, $p, 10);

        my @e_divisors =
          ($e > 1)
          ? (
             HAS_PRIME_UTIL
             ? Math::Prime::Util::divisors($e)
             : _divisors($e)
            )
          : (1);

        Math::GMPz::Rmpz_set_ui($u, 0);

        foreach my $d (@e_divisors) {
            Math::GMPz::Rmpz_pow_ui($w, $t, $k * $d);
            Math::GMPz::Rmpz_add($u, $u, $w);
        }

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($u)
            ? Math::GMPz::Rmpz_get_ui($u)
            : Math::GMPz::Rmpz_init_set($u)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub nesigma0 {    # A160097: count of non-exponential divisors of n
    my ($n) = @_;
    $n->sigma0->sub($n->esigma0);
}

sub nesigma {    # A160135: sum of non-exponential divisors of n
    my ($n, $k) = @_;
    $n->sigma($k)->sub($n->esigma($k));
}

sub uphi {    # OEIS: A047994
    my ($n, $k) = @_;

    # Multiplicative with:
    #   uphi(p^e, k) = p^(e*k) - 1

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {

            if ($e == 1 and $k == 1) {    # optimization
                push @terms, $p - 1;
                next;
            }

            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e * $k);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $e * $k);
        }

        Math::GMPz::Rmpz_sub_ui($t, $t, 1);

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub nuphi {    # OEIS: A254503
    my ($n) = @_;

    # TODO: generalize for k > 1.

    # Multiplicative with:
    #   a(p) = p
    #   a(p^e) = phi(p^e) = (p-1) * p^(e-1), for e > 1.

    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {

            if ($e == 1) {
                push @terms, $p;
                next;
            }

            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e - 1);
            Math::GMPz::Rmpz_mul_ui($t, $t, $p - 1);
        }
        elsif ($e == 1) {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
        }
        else {
            Math::GMPz::Rmpz_set_str($u, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $u, $e - 1);
            Math::GMPz::Rmpz_sub_ui($u, $u, 1);
            Math::GMPz::Rmpz_mul($t, $t, $u);
        }

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub iphi {    # OEIS: A091732 -- infinitary analog of Euler's phi function
    my ($n, $k) = @_;

    # Let e = Sum_{k >= 0} d_k 2^k (binary representation of e).

    # Multiplicative with, where d_k is odd in the binary representation of e (ignore even d_k):
    #   iphi(p^e, r) = Product_{k >= 0} (p^(r * 2^k) - 1)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $n = $$n;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {
        my ($p, $e) = @$pe;

        (FAST_MODE and $p < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($t, $p)
          : Math::GMPz::Rmpz_set_str($t, "$p", 10);

        my $j = 0;
        do {
            if ($e % 2 == 1) {
                Math::GMPz::Rmpz_pow_ui($u, $t, (1 << $j) * $k);
                Math::GMPz::Rmpz_sub_ui($u, $u, 1);
                push @terms,
                  (
                      Math::GMPz::Rmpz_fits_ulong_p($u)
                    ? Math::GMPz::Rmpz_get_ui($u)
                    : Math::GMPz::Rmpz_init_set($u)
                  );
            }
            ++$j;
        } while ($e >>= 1);
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub bphi {    # OEIS: A116550 -- bi-unitary analog of Euler's totient function of n.
    my ($n) = @_;

    # Formula:
    #   a(n) = Sum{d|n, d is unitary} (-1)^omega(d) * Sum{k|d, k is squarefree, k <= n/d} mu(k) * floor(n/(d*k)).

    my @final_terms;

    foreach my $v (@{$n->udivisors}) {
        my $sign = (-1)**(scalar _factor_exp($$v));
        my $x    = $n->idiv($v);
        my $nod  = "$$x";

        if ($nod eq '1') {
            push @final_terms, $sign;
            next;
        }

        my @terms;
        foreach my $u (@{$v->squarefree_divisors}) {
            my $k   = $$u;
            my $div = Math::Prime::Util::GMP::divint($nod, $k);
            last if ($div eq '0');
            my $mu =
              (HAS_PRIME_UTIL and !ref($k))
              ? Math::Prime::Util::moebius($k)
              : Math::Prime::Util::GMP::moebius($k);
            push @terms, Math::Prime::Util::GMP::mulint($mu, $div);
        }

        push @final_terms, Math::Prime::Util::GMP::mulint($sign, ((scalar(@terms) == 1) ? $terms[0] : Math::Prime::Util::GMP::vecsum(@terms)));
    }

    @final_terms || return ZERO;
    _set_int(Math::Prime::Util::GMP::vecsum(@final_terms));
}

sub pillai {    # OEIS: A018804 -- Pillai's arithmetical function: Sum_{k=1..n} gcd(k, n).
    my ($n, $k) = @_;

    # Multiplicative with:
    #   a(p^e) = p^(e-1)*((p-1)*e + p) = p^(e-1) * ((1+e)*p - e)

    # Multiplicative formula for Sum_{k=1..n} gcd(n,k)^m:
    #   a(p^e) = p^(e-1)*(p^((m-1)*e+m) - p^((m-1)*e) - p + 1)/(p^(m-1)-1)

    # Multiplicative formula for Sum_{1 <= x_1, x_2, ..., x_k <= n} gcd(x_1, x_2, ..., x_k, n)^k:
    #   a(p^e) = (e - e/p^k + 1) * p^(k*e) = p^((e - 1) * k) * (p^k + e*(p^k - 1))

    $n = $$n;
    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $v = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_set_ui($u, $p);
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k * ($e - 1));
        }
        else {
            Math::GMPz::Rmpz_set_str($u, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $u, $k * ($e - 1));
        }

        Math::GMPz::Rmpz_pow_ui($u, $u, $k) if ($k > 1);
        Math::GMPz::Rmpz_sub_ui($v, $u, 1);
        Math::GMPz::Rmpz_mul_ui($v, $v, $e);
        Math::GMPz::Rmpz_add($u, $u, $v);
        Math::GMPz::Rmpz_mul($t, $t, $u);

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub pillai_sum {
    my ($n, $k) = @_;

    # TODO: try to compute the sum, based on the Dirichlet convolution:
    #   pillai(n,k) = Sum_{d|n} mu(n/d) * d^k * tau(d)

    # See also:
    #   https://github.com/trizen/sidef-scripts/blob/master/Math/partial_sums_of_gcd-sum_function_fast.sf

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    my $k_obj = bless \$k;

    my $f = sub { $_[0]->ipow($k_obj)->mul($_[0]->tau) };
    my $g = sub { HAS_PRIME_UTIL ? Math::Prime::Util::moebius(${$_[0]}) : Math::Prime::Util::GMP::moebius(${$_[0]}) };

    my $F = sub { $_[0]->sigma_sum(ZERO, $k_obj) };
    my $G = sub { $_[0]->mertens };

    if ($k == 1) {
        $f = sub { $_[0]->mul($_[0]->tau) };
    }

#<<<
    # Alternative formula (slightly slower)
    #~ my $f = sub { $_[0]->jordan_totient($k_obj) };
    #~ my $g = sub { $_[0]->ipow($k_obj) };

    #~ my $F = sub { $_[0]->totient_sum($k_obj) };
    #~ my $G = sub { $_[0]->faulhaber_sum($k_obj) };

    #~ if ($k == 1) {
        #~ $g = sub { $_[0] };
        #~ $f = sub { $_[0]->euler_phi };
        #~ $F = sub { $_[0]->totient_sum };
    #~ }
#>>>

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

sub prime_power_sigma {
    my ($n, $k) = @_;

    # Additive with:
    #   a(p^e, k) = (p^(k*(e+1)) - p^k) / (p^k - 1)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &prime_power_sigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my $r = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {
            ($k == 1)
              ? Math::GMPz::Rmpz_set_ui($u, $p)
              : Math::GMPz::Rmpz_ui_pow_ui($u, $p, $k);
        }
        else {
            Math::GMPz::Rmpz_set_str($u, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($u, $u, $k) if ($k > 1);
        }

        Math::GMPz::Rmpz_pow_ui($t, $u, $e + 1);
        Math::GMPz::Rmpz_sub($t, $t, $u);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);
        Math::GMPz::Rmpz_divexact($t, $t, $u);
        Math::GMPz::Rmpz_add($r, $r, $t);
    }

    bless \$r;
}

sub prime_power_usigma {
    my ($n, $k) = @_;

    # Additive with:
    #   a(p^e, k) = p^(e*k)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &prime_power_usigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $r = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k * $e);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k * $e);
        }

        Math::GMPz::Rmpz_add($r, $r, $t);
    }

    bless \$r;
}

sub squarefree_usigma0 {
    (TWO)->powerfree_usigma0($_[0]);
}

sub cubefree_usigma0 {
    (THREE)->powerfree_usigma0($_[0]);
}

sub squarefree_usigma {
    (TWO)->powerfree_usigma($_[0], $_[1]);
}

sub cubefree_usigma {
    (THREE)->powerfree_usigma($_[0], $_[1]);
}

*squarefree_sigma0 = \&usigma0;

sub cubefree_sigma0 {
    (THREE)->powerfree_sigma0($_[0]);
}

sub squarefree_sigma {
    (TWO)->powerfree_sigma($_[0], $_[1]);
}

sub cubefree_sigma {
    (THREE)->powerfree_sigma($_[0], $_[1]);
}

sub power_sigma0 {
    my ($k, $n) = @_;

    # Multiplicative with:
    #   a(p^e) = floor(e/k) + 1

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k)    // goto &nan } : 1;
    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);

    $k > 0 or return ZERO;
    $n eq '0' and return ZERO;

    _set_int(
             Math::Prime::Util::GMP::vecprod(map  { Math::Prime::Util::GMP::divint($_->[1], $k) + 1 }
                                             grep { $_->[1] >= $k } _factor_exp($n))
            );
}

sub power_sigma {
    my ($k, $n, $j) = @_;

    # Multiplicative with:
    #   a(p^e) = (p^(j*k*(1+floor(e/k))) - 1) / (p^(j*k) - 1), where e >= k.

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $j = defined($j) ? do { _valid(\$j); _any2ui($$j) // goto &nan } : 1;

    $k > 0 or return ZERO;

    if ($j == 0) {
        goto &power_sigma0;
    }

    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        next if ($e < $k);

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k * $j);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k * $j);
        }

        Math::GMPz::Rmpz_pow_ui($u, $t, 1 + Math::Prime::Util::GMP::divint($e, $k));
        Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);
        Math::GMPz::Rmpz_divexact($u, $u, $t);

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($u)
            ? Math::GMPz::Rmpz_get_ui($u)
            : Math::GMPz::Rmpz_init_set($u)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub square_sigma0 {
    (TWO)->power_sigma0($_[0]);
}

sub square_sigma {
    (TWO)->power_sigma($_[0], $_[1]);
}

sub cube_sigma0 {
    (THREE)->power_sigma0($_[0]);
}

sub cube_sigma {
    (THREE)->power_sigma($_[0], $_[1]);
}

sub power_usigma0 {
    my ($k, $n) = @_;

    # Multiplicative with:
    #   a(p^e) = 2             if e == 0 (mod k)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k)    // goto &nan } : 1;
    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);

    $k > 0 or return ZERO;
    $n eq '0' and return ZERO;

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_ui($r, 0);
    Math::GMPz::Rmpz_setbit($r, scalar grep { $_->[1] % $k == 0 } _factor_exp($n));
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub power_usigma {
    my ($k, $n, $j) = @_;

    # Multiplicative with:
    #   a(p^e) = p^(e*j) + 1, where e == 0 (mod k).

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $j = defined($j) ? do { _valid(\$j); _any2ui($$j) // goto &nan } : 1;

    $k > 0 or return ZERO;

    if ($j == 0) {
        goto &power_usigma0;
    }

    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        $e % $k == 0 or next;

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e * $j);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$p", 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $e * $j);
        }

        Math::GMPz::Rmpz_add_ui($t, $t, 1);
        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub square_usigma0 {
    (TWO)->power_usigma0($_[0]);
}

sub square_usigma {
    (TWO)->power_usigma($_[0], $_[1]);
}

sub cube_usigma0 {
    (THREE)->power_usigma0($_[0]);
}

sub cube_usigma {
    (THREE)->power_usigma($_[0], $_[1]);
}

sub powerfree_sigma0 {
    my ($k, $n) = @_;

    # Multiplicative with:
    #   a(p^e) = min(e, k-1) + 1

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k)    // goto &nan } : 1;
    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);

    $k > 0 or return ZERO;
    $n eq '0' and return ZERO;

    _set_int(Math::Prime::Util::GMP::vecprod(map { ($_->[1] < $k) ? ($_->[1] + 1) : $k } _factor_exp($n)));
}

sub powerfree_sigma {
    my ($k, $n, $j) = @_;

    # Multiplicative with:
    #   a(p^e) = (p^(j*(e+1)) - 1)/(p^j - 1), where e = min(e, k-1)

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $j = defined($j) ? do { _valid(\$j); _any2ui($$j) // goto &nan } : 1;

    $k > 0 or return ZERO;

    if ($j == 0) {
        goto &powerfree_sigma0;
    }

    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    my @terms;

    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        $e = $k - 1 if ($e >= $k);

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $j);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, $p, 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $j);
        }

        Math::GMPz::Rmpz_pow_ui($u, $t, $e + 1);
        Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);
        Math::GMPz::Rmpz_divexact($u, $u, $t);

        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($u)
            ? Math::GMPz::Rmpz_get_ui($u)
            : Math::GMPz::Rmpz_init_set($u)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub powerfree_usigma0 {
    my ($k, $n) = @_;

    # Multiplicative with:
    #   a(p^e) = 2          # for e < k
    #   a(p^e) = 1          # for e >= k

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k)    // goto &nan } : 1;
    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);

    $k > 0 or return ZERO;
    $n eq '0' and return ZERO;

    state $r = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_set_ui($r, 0);
    Math::GMPz::Rmpz_setbit($r, scalar grep { $_->[1] < $k } _factor_exp($n));
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub powerfree_usigma {
    my ($k, $n, $j) = @_;

    # Multiplicative with:
    #   a(p^e) = p^(e*j) + 1      # for e < k
    #   a(p^e) = 1                # for e >= k

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $j = defined($j) ? do { _valid(\$j); _any2ui($$j) // goto &nan } : 1;

    $k > 0 or return ZERO;

    if ($j == 0) {
        goto &powerfree_usigma0;
    }

    $n = defined($n) ? do { _valid(\$n); _big2uistr($$n) // goto &nan } : (goto &nan);
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @terms;
    foreach my $pe (_factor_exp($n)) {

        my ($p, $e) = @$pe;

        $e < $k or next;

        if (FAST_MODE and $p < ULONG_MAX) {

            if ($e == 1 and $j == 1) {    # optimization
                push @terms, $p + 1;
                next;
            }

            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e * $j);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, $p, 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $e * $j);
        }

        Math::GMPz::Rmpz_add_ui($t, $t, 1);
        push @terms,
          (
              Math::GMPz::Rmpz_fits_ulong_p($t)
            ? Math::GMPz::Rmpz_get_ui($t)
            : Math::GMPz::Rmpz_init_set($t)
          );
    }

    @terms || return ONE;
    bless \_binsplit(\@terms, \&__mul__);
}

sub prime_sigma {
    my ($n, $k) = @_;

    # Additive with:
    #   a(p^e, k) = p^k

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &prime_sigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();

    my $r = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pe (_factor_exp($n)) {

        my $p = $pe->[0];

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, $p, 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k);
        }

        Math::GMPz::Rmpz_add($r, $r, $t);
    }

    bless \$r;
}

sub prime_usigma0 {
    my ($n) = @_;
    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;
    _set_int(scalar grep { $_->[1] == 1 } _factor_exp($n));
}

sub prime_usigma {
    my ($n, $k) = @_;

    # Additive with:
    #   a(p,   k) = p^k
    #   a(p^e, k) = 0 for e>1

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;

    if ($k == 0) {
        goto &prime_usigma0;
    }

    $n = _big2uistr($$n) // goto &nan;
    $n eq '0' and return ZERO;

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $r = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $pe (grep { $_->[1] == 1 } _factor_exp($n)) {

        my $p = $pe->[0];

        if (FAST_MODE and $p < ULONG_MAX) {
            Math::GMPz::Rmpz_ui_pow_ui($t, $p, $k);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, $p, 10);
            Math::GMPz::Rmpz_pow_ui($t, $t, $k);
        }

        Math::GMPz::Rmpz_add($r, $r, $t);
    }

    bless \$r;
}

sub sigma0 {
    my ($n) = @_;

    $n = _sanitize_mpz($$n, 0) // goto &nan;

    (Math::GMPz::Rmpz_sgn($n) || return ZERO) > 0 or goto &nan;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return _set_int(Math::Prime::Util::divisor_sum(Math::GMPz::Rmpz_get_ui($n), 0));
    }

    $n = Math::GMPz::Rmpz_get_str($n, 10);

    if (CORE::length($n) < SPECIAL_FACTORS_MIN) {
        return _set_int(Math::Prime::Util::GMP::sigma($n, 0));
    }

    # Multiplicative with a(p^e) = e+1
    _set_int(Math::Prime::Util::GMP::vecprod(map { $_->[1] + 1 } _factor_exp($n)));
}

*d             = \&sigma0;
*divisor_count = \&sigma0;

sub sigma {
    my ($n, $k) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2si($$k) // goto &nan } : 1;

    if ($k == 0) {
        return $n->sigma0;
    }

    $n = $$n;

    if (FAST_MODE and HAS_PRIME_UTIL and $k == 1 and !ref($n) and $n > 0 and $n < (ULONG_MAX >> 3)) {
        return _set_int(Math::Prime::Util::divisor_sum($n));
    }

    $n = _sanitize_mpz($n, 0) // goto &nan;
    (Math::GMPz::Rmpz_sgn($n) || return ZERO) > 0 or goto &nan;

    my $r;

    $n = Math::GMPz::Rmpz_get_str($n, 10);

    if (FAST_MODE and HAS_PRIME_UTIL and CORE::abs($k) == 1 and $n < (ULONG_MAX >> 3)) {
        $r = Math::Prime::Util::divisor_sum($n);
    }
    elsif (FAST_MODE and CORE::length($n) < SPECIAL_FACTORS_MIN) {
        $r = Math::Prime::Util::GMP::sigma($n, CORE::abs($k));
    }
    else {

        # Multiplicative with sigma_k(p^e) = ((p^((e+1)*k))-1)/(p^k-1)

        state $t = Math::GMPz::Rmpz_init_nobless();
        state $u = Math::GMPz::Rmpz_init_nobless();

        my @terms;

        foreach my $pp (_factor_exp($n)) {
            my ($p, $e) = @$pp;

            if (FAST_MODE and $p < ULONG_MAX) {

                if ($e == 1 and CORE::abs($k) == 1) {    # optimization
                    push @terms, $p + 1;
                    next;
                }

                Math::GMPz::Rmpz_ui_pow_ui($u, $p, CORE::abs($k));
            }
            else {
                Math::GMPz::Rmpz_set_str($u, "$p", 10);
                Math::GMPz::Rmpz_pow_ui($u, $u, CORE::abs($k)) if (CORE::abs($k) != 1);
            }

            Math::GMPz::Rmpz_pow_ui($t, $u, $e + 1);
            Math::GMPz::Rmpz_sub_ui($u, $u, 1);
            Math::GMPz::Rmpz_sub_ui($t, $t, 1);
            Math::GMPz::Rmpz_divexact($t, $t, $u);

            push @terms,
              (
                  Math::GMPz::Rmpz_fits_ulong_p($t)
                ? Math::GMPz::Rmpz_get_ui($t)
                : Math::GMPz::Rmpz_init_set($t)
              );
        }

        $r = (@terms ? _binsplit(\@terms, \&__mul__) : 1);
    }

    if ($k < 0) {    # Sum_{d|n} 1/d^k = sigma_k(n)/n^k
        return _set_int($r)->div(_set_int(Math::Prime::Util::GMP::powint($n, CORE::abs($k))));
    }

    _set_int($r);
}

*σ = \&sigma;

sub sigma_sum {
    my ($n, $k, $j) = @_;

    $k = defined($k) ? do { _valid(\$k); _any2ui($$k) // goto &nan } : 1;
    $j = defined($j) ? do { _valid(\$j); _any2ui($$j) // goto &nan } : 0;

    $k += $j;

    my $k_obj = bless \$k;
    my $j_obj = bless \$j;

    my $f = sub { $_[0]->ipow($k_obj) };
    my $g = sub { $_[0]->ipow($j_obj) };

    my $F = sub { $_[0]->faulhaber_sum($k_obj) };
    my $G = sub { $_[0]->faulhaber_sum($j_obj) };

    if ($k == 0) {
        $f = sub { 1 };
        $g = sub { 1 };
        $F = sub { $_[0] };
        $G = sub { $_[0] };
    }
    elsif ($k == 1) {
        $f = sub { $_[0] };
    }
    elsif ($k == 2) {
        $f = sub { $_[0]->sqr };
    }

    if ($j == 0) {
        $g = sub { 1 };
        $G = sub { $_[0] };
    }
    elsif ($j == 1) {
        $g = sub { $_[0] };
    }
    elsif ($j == 2) {
        $g = sub { $_[0]->sqr };
    }

    $n->dirichlet_hyperbola($f, $g, $F, $G);
}

sub tau_sum {
    $_[0]->sigma_sum(ZERO);
}

*sigma0_sum = \&tau_sum;

sub antidivisor_count {    # OEIS: A066272
    my ($n) = @_;

    $n = $$n;

    if (__cmp__($n, 2) <= 0) {
        return ZERO;
    }

    my $n2 = __add__($n, $n);

    my $w = (bless \__dec__($n2))->sigma0;
    my $x = (bless \__inc__($n2))->sigma0;
    my $y = (bless \$n)->remove(TWO)->sigma0;

    bless \__sub__(__add__(__add__($$w, $$x), $$y), 5);
}

*antidivisor_sigma0 = \&antidivisor_count;

sub antidivisor_sigma {    # OEIS: A066417
    my ($n) = @_;

    # TODO: find a formula for computing:
    #   antidivisors(n).sum {|d| d**k }, for k > 1

    $n = $$n;

    if (__cmp__($n, 2) <= 0) {
        return ZERO;
    }

    my $n2 = __add__($n, $n);

    my $v = (bless \$n)->valuation(TWO);
    my $x = (bless \__inc__($n2))->sigma;
    my $y = (bless \__dec__($n2))->sigma;
    my $z = (bless \$n)->remove(TWO)->sigma->shift_left(bless \__inc__($$v));

    bless \__sub__(__sub__(__add__(__add__($$x, $$y), $$z), __mul__($n, 6)), 2);
}

*antidivisor_sum = \&antidivisor_sigma;

sub aliquot {
    my ($n, $k) = @_;

    $n = $$n;

    if (FAST_MODE and HAS_NEW_PRIME_UTIL and !defined($k) and !ref($n) and $n < (ULONG_MAX >> 3)) {
        return _set_int(Math::Prime::Util::aliquot_sum($n));
    }

    $n = _sanitize_mpz($n, 0) // goto &nan;
    (Math::GMPz::Rmpz_sgn($n) || return ZERO) > 0 or goto &nan;

    if (defined($k)) {
        return ((bless \$n)->sigma($k)->sub((bless \$n)->pow($k)));
    }

    my $s =
      (FAST_MODE and HAS_PRIME_UTIL and Math::GMPz::Rmpz_cmp_ui($n, ULONG_MAX >> 3) <= 0)
      ? Math::Prime::Util::divisor_sum(Math::GMPz::Rmpz_get_ui($n))
      : ${_set_int($n)->sigma};

    $s = Math::Prime::Util::GMP::subint($s, $n);

    _set_int($s);
}

*aliquot_sum  = \&aliquot;
*proper_sigma = \&aliquot;

sub proper_divisor_count {
    my ($n) = @_;
    $n->sigma0->dec->max(ZERO);
}

*proper_sigma0 = \&proper_divisor_count;

sub proper_divisors {
    my ($n) = @_;
    my $divisors = $n->divisors;
    $divisors->pop;
    $divisors;
}

sub abundancy_index {
    my ($n) = @_;
    $n->sigma->div($n);
}

*abundancy = \&abundancy_index;

sub is_deficient {
    my ($n) = @_;
    (__is_int__($$n) && __cmp__(${$n->sigma}, __mul__($$n, 2)) < 0) ? $TRUE : $FALSE;
}

sub is_abundant {
    my ($n) = @_;
    (__is_int__($$n) && __cmp__(${$n->sigma}, __mul__($$n, 2)) > 0) ? $TRUE : $FALSE;
}

sub is_primitive_abundant {
    my ($n) = @_;

    $n->is_abundant || return $FALSE;
    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        foreach my $pp (_factor_exp($n)) {
            my $t =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::divint($n, $pp->[0])
              : Math::Prime::Util::GMP::divint($n, $pp->[0]);
            (bless \$t)->is_abundant && return $FALSE;
        }
        return $TRUE;
    }

    $n = _any2mpz($n, 0) // return $FALSE;

    my $t = Math::GMPz::Rmpz_init();
    foreach my $pp (_factor_exp($n)) {
        if (FAST_MODE and $pp->[0] < ULONG_MAX) {
            Math::GMPz::Rmpz_divexact_ui($t, $n, $pp->[0]);
        }
        else {
            Math::GMPz::Rmpz_set_str($t, "$pp->[0]", 10);
            Math::GMPz::Rmpz_divexact($t, $n, $t);
        }
        (bless \$t)->is_abundant && return $FALSE;
    }

    return $TRUE;
}

sub is_amicable {
    my ($n, $m) = @_;
    _valid(\$m);
    if ($n->eq($m)) {
        return $FALSE;
    }
    (__is_int__($$n) && __is_int__($$m) && $n->aliquot->eq($m) && $m->aliquot->eq($n)) ? $TRUE : $FALSE;
}

sub sopf {    # OEIS: A008472
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and HAS_NEW_PRIME_UTIL and !ref($n) and $n >= 0) {
        my $r = Math::Prime::Util::sopf($n);
        return bless \$r;
    }

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    my $s = Math::Prime::Util::GMP::vecsum(map { $_->[0] } _factor_exp($n));
    _set_int($s);
}

sub sopfr {    # OEIS: A001414
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and HAS_NEW_PRIME_UTIL and !ref($n) and $n >= 0) {
        my $r = Math::Prime::Util::sopfr($n);
        return bless \$r;
    }

    if (ref($n)) {
        $n = _big2uistr($n) // goto &nan;
    }
    else {
        $n < 0 and goto &nan;
    }
    my $s = Math::Prime::Util::GMP::vecsum(_factor($n));
    _set_int($s);
}

sub sopf_sum {
    my ($self) = @_;

    my $n_gmp = _any2mpz($$self) // return ZERO;

    if (Math::GMPz::Rmpz_cmp_ui($n_gmp, 2) < 0) {
        return ZERO;
    }

    if (FAST_MODE and HAS_PRIME_UTIL and $n_gmp < PRIMESUM_MIN) {

        my $f = sub { 1 };
        my $g = sub { Math::Prime::Util::is_prime(${$_[0]}) ? $_[0] : 0 };

        my $F = sub { $_[0] };
        my $G = sub { Math::Prime::Util::sum_primes(${$_[0]}) };

        return ((bless \$n_gmp)->dirichlet_hyperbola($f, $g, $F, $G));
    }

    my $m_gmp = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($m_gmp, $n_gmp);
    my $m = Math::GMPz::Rmpz_get_ui($m_gmp);

    my @S_small;
    my @S_large;

    my $v_gmp = Math::GMPz::Rmpz_init();
    for my $v (1 .. $m) {
        my $res = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_set_ui($v_gmp, $v);
        Math::GMPz::Rmpz_add_ui($res, $v_gmp, 1);
        Math::GMPz::Rmpz_mul($res, $res, $v_gmp);
        Math::GMPz::Rmpz_tdiv_q_2exp($res, $res, 1);
        Math::GMPz::Rmpz_sub_ui($res, $res, 1);
        $S_small[$v] = $res;
    }

    for my $k (1 .. $m) {
        my $res = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_tdiv_q_ui($v_gmp, $n_gmp, $k);
        Math::GMPz::Rmpz_add_ui($res, $v_gmp, 1);
        Math::GMPz::Rmpz_mul($res, $res, $v_gmp);
        Math::GMPz::Rmpz_tdiv_q_2exp($res, $res, 1);
        Math::GMPz::Rmpz_sub_ui($res, $res, 1);
        $S_large[$k] = $res;
    }

    my $scratch   = Math::GMPz::Rmpz_init();
    my $k_max_gmp = Math::GMPz::Rmpz_init();
    my $p2        = Math::GMPz::Rmpz_init();

    foreach my $p_ui (@{_primes(2, $m)}) {
        Math::GMPz::Rmpz_set_ui($p2, $p_ui);
        Math::GMPz::Rmpz_mul_ui($p2, $p2, $p_ui);

        Math::GMPz::Rmpz_tdiv_q($k_max_gmp, $n_gmp, $p2);
        my $k_max = Math::GMPz::Rmpz_get_ui($k_max_gmp);
        $k_max = $m if $k_max > $m;

        my $sp_minus_1 = $S_small[$p_ui - 1];
        my $kp         = $p_ui;

        foreach my $k (1 .. $k_max) {
            my $sw;
            if ($kp <= $m) {
                $sw = $S_large[$kp];
            }
            else {
                Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $kp);
                $sw = $S_small[Math::GMPz::Rmpz_get_ui($scratch)];
            }

            Math::GMPz::Rmpz_sub($scratch, $sw, $sp_minus_1);
            Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
            Math::GMPz::Rmpz_sub($S_large[$k], $S_large[$k], $scratch);
            $kp += $p_ui;
        }

        for (my $v = $m ; $v >= $p2 ; $v--) {
            my $sw = $S_small[CORE::int($v / $p_ui)];
            Math::GMPz::Rmpz_sub($scratch, $sw, $sp_minus_1);
            Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
            Math::GMPz::Rmpz_sub($S_small[$v], $S_small[$v], $scratch);
        }
    }

    my $sum = Math::GMPz::Rmpz_init_set_ui(0);
    for my $k (1 .. $m) {
        Math::GMPz::Rmpz_add($sum, $sum, $S_large[$k]);
    }

    Math::GMPz::Rmpz_mul_ui($scratch, $S_large[$m], $m);
    Math::GMPz::Rmpz_sub($sum, $sum, $scratch);

    Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $m);
    my $limit = Math::GMPz::Rmpz_get_ui($scratch);

    foreach my $p_ui (@{_primes(2, $limit)}) {
        Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $p_ui);
        Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
        Math::GMPz::Rmpz_add($sum, $sum, $scratch);
    }

    bless \$sum;
}

sub sopfr_sum {
    my ($self) = @_;

    my $n_gmp = _any2mpz($$self) // return ZERO;

    if (Math::GMPz::Rmpz_cmp_ui($n_gmp, 2) < 0) {
        return ZERO;
    }

    my $m_gmp = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_sqrt($m_gmp, $n_gmp);
    my $m = Math::GMPz::Rmpz_get_ui($m_gmp);

    my @S_small;
    my @S_large;

    my $v_gmp = Math::GMPz::Rmpz_init();
    for my $v (1 .. $m) {
        my $res = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_set_ui($v_gmp, $v);
        Math::GMPz::Rmpz_add_ui($res, $v_gmp, 1);
        Math::GMPz::Rmpz_mul($res, $res, $v_gmp);
        Math::GMPz::Rmpz_tdiv_q_2exp($res, $res, 1);
        Math::GMPz::Rmpz_sub_ui($res, $res, 1);
        $S_small[$v] = $res;
    }

    for my $k (1 .. $m) {
        my $res = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_tdiv_q_ui($v_gmp, $n_gmp, $k);
        Math::GMPz::Rmpz_add_ui($res, $v_gmp, 1);
        Math::GMPz::Rmpz_mul($res, $res, $v_gmp);
        Math::GMPz::Rmpz_tdiv_q_2exp($res, $res, 1);
        Math::GMPz::Rmpz_sub_ui($res, $res, 1);
        $S_large[$k] = $res;
    }

    my $scratch   = Math::GMPz::Rmpz_init();
    my $k_max_gmp = Math::GMPz::Rmpz_init();
    my $p2        = Math::GMPz::Rmpz_init();

    my $m_primes = _primes(2, $m);

    foreach my $p_ui (@$m_primes) {
        Math::GMPz::Rmpz_set_ui($p2, $p_ui);
        Math::GMPz::Rmpz_mul_ui($p2, $p2, $p_ui);

        Math::GMPz::Rmpz_tdiv_q($k_max_gmp, $n_gmp, $p2);
        my $k_max = Math::GMPz::Rmpz_get_ui($k_max_gmp);
        $k_max = $m if $k_max > $m;

        my $sp_minus_1 = $S_small[$p_ui - 1];
        my $kp         = $p_ui;

        foreach my $k (1 .. $k_max) {
            my $sw;
            if ($kp <= $m) {
                $sw = $S_large[$kp];
            }
            else {
                Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $kp);
                $sw = $S_small[Math::GMPz::Rmpz_get_ui($scratch)];
            }

            Math::GMPz::Rmpz_sub($scratch, $sw, $sp_minus_1);
            Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
            Math::GMPz::Rmpz_sub($S_large[$k], $S_large[$k], $scratch);
            $kp += $p_ui;
        }

        for (my $v = $m ; $v >= $p2 ; $v--) {
            my $sw = $S_small[CORE::int($v / $p_ui)];
            Math::GMPz::Rmpz_sub($scratch, $sw, $sp_minus_1);
            Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
            Math::GMPz::Rmpz_sub($S_small[$v], $S_small[$v], $scratch);
        }
    }

    my $sum = Math::GMPz::Rmpz_init_set_ui(0);
    for my $k (1 .. $m) {
        Math::GMPz::Rmpz_add($sum, $sum, $S_large[$k]);
    }

    Math::GMPz::Rmpz_mul_ui($scratch, $S_large[$m], $m);
    Math::GMPz::Rmpz_sub($sum, $sum, $scratch);

    Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $m);
    my $limit = Math::GMPz::Rmpz_get_ui($scratch);

    foreach my $p_ui (@{_primes(2, $limit)}) {
        Math::GMPz::Rmpz_tdiv_q_ui($scratch, $n_gmp, $p_ui);
        Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
        Math::GMPz::Rmpz_add($sum, $sum, $scratch);
    }

    my $p_pow = Math::GMPz::Rmpz_init();
    foreach my $p_ui (@$m_primes) {

        Math::GMPz::Rmpz_set_ui($p_pow, $p_ui);
        Math::GMPz::Rmpz_mul_ui($p_pow, $p_pow, $p_ui);

        while (Math::GMPz::Rmpz_cmp($p_pow, $n_gmp) <= 0) {
            Math::GMPz::Rmpz_tdiv_q($scratch, $n_gmp, $p_pow);
            Math::GMPz::Rmpz_mul_ui($scratch, $scratch, $p_ui);
            Math::GMPz::Rmpz_add($sum, $sum, $scratch);
            Math::GMPz::Rmpz_mul_ui($p_pow, $p_pow, $p_ui);
        }
    }

    bless \$sum;
}

sub factor_map {
    my ($n, $block) = @_;

    $n = _big2pistr($$n) // return _array();

    my @array;
    foreach my $pk (_factor_exp($n)) {

        my ($p, $k) = @$pk;

        $p = _set_int($p);
        $k = _set_int($k);

        push @array, $block->run($p, $k);
    }

    _array(\@array);
}

sub _divisors_lazy {
    my ($n, $callback) = @_;

    $n = $$n;

    if (ref($n)) {
        $n = _big2pistr($n) // return;
    }
    else {
        $n >= 1 or return;
    }

    my @factors = _factor_exp($n);

    if (!@factors) {
        $callback->(ONE);
        return;
    }

    my @primes    = map { $_->[0] } @factors;
    my @exps      = map { $_->[1] } @factors;
    my $max_depth = scalar(@primes);

    if (FAST_MODE and $n < ULONG_MAX) {    # native divisors
        return sub {
            my ($idx, $prod) = @_;
            if ($idx == $max_depth) {
                $callback->(bless \$prod);
                return;
            }
            my $p = $primes[$idx];
            my $e = $exps[$idx];
            my $m = 1;
            foreach my $i (1 .. $e + 1) {
                __SUB__->($idx + 1, $prod * $m);
                $m *= $p;
            }
          }
          ->(0, 1);
    }

    @primes = map { Math::GMPz::Rmpz_init_set_str($_, 10) } @primes;

    sub {
        my ($idx, $prod) = @_;
        if ($idx == $max_depth) {
            my $r = Math::GMPz::Rmpz_init_set($prod);
            $callback->(bless \$r);
            return;
        }
        my $p = $primes[$idx];
        my $e = $exps[$idx];
        my $m = Math::GMPz::Rmpz_init_set_ui(1);
        my $t = Math::GMPz::Rmpz_init();
        foreach my $i (1 .. $e + 1) {
            Math::GMPz::Rmpz_mul($t, $prod, $m);
            __SUB__->($idx + 1, $t);
            Math::GMPz::Rmpz_mul($m, $m, $p);
        }
      }
      ->(0, Math::GMPz::Rmpz_init_set_ui(1));
}

sub divisor_each {
    my ($n, $block) = @_;
    _divisors_lazy($n, $block);
}

*divisors_each = \&divisor_each;

sub divisor_map {
    my ($n, $block) = @_;

    $n = _big2pistr($$n) // return _array();

    my @array;
    foreach my $divisor (_divisors($n)) {
        push @array, $block->run(_set_int($divisor));
    }

    _array(\@array);
}

*divisors_map = \&divisor_map;

sub divisor_sum {
    my ($n, $block) = @_;
    $block // return $n->sigma;
    $n->divisor_map($block)->sum;
}

*divisors_sum = \&divisor_sum;

sub divisor_prod {
    my ($n, $block) = @_;
    $block // return $n->divisors->prod;
    $n->divisor_map($block)->prod;
}

*divisors_prod = \&divisor_prod;

sub factor_sum {
    my ($n, $block) = @_;
    $block // return $n->sopfr;
    $n->factor_map($block)->sum;
}

*factors_sum = \&factor_sum;

sub factor_prod {
    my ($n, $block) = @_;
    $block // return $n;
    $n->factor_map($block)->prod;
}

*factors_prod = \&factor_prod;

sub partition_count {
    my ($n) = @_;

    $n = _big2uistr($$n) // goto &nan;

    if ($n >= 1e4 and $USE_PARI_GP) {
        if (my $res = _execute_pari_gp("numbpart($n)")) {
            return _set_int($res);
        }
    }

    _set_int(Math::Prime::Util::GMP::partitions($n));
}

*partition_number = \&partition_count;

sub is_primitive_root {
    my ($x, $y) = @_;
    _valid(\$y);

    $x = $$x;
    $y = $$y;

    (__is_int__($x) && __is_int__($y) && Math::Prime::Util::GMP::is_primitive_root(_big2uistr($x) // (return $FALSE), _big2uistr($y) // (return $FALSE)))
      ? $TRUE
      : $FALSE;
}

sub _sieve_powerful {
    my ($from, $to, $k) = @_;

    my @powerful;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($to)) {

        $from = Math::GMPz::Rmpz_get_ui($from);
        $to   = Math::GMPz::Rmpz_get_ui($to);

        sub {
            my ($m, $r) = @_;

            my $lo = 1;
            my $hi = Math::Prime::Util::rootint(
                                                (
                                                 HAS_PRIME_UTIL
                                                 ? Math::Prime::Util::divint($to, $m)
                                                 : Math::Prime::Util::GMP::divint($to, $m)
                                                ),
                                                $r
                                               );

            if ($r <= $k) {

                if ($from > $m) {
                    my $t =
                      HAS_PRIME_UTIL
                      ? Math::Prime::Util::divint($from, $m)
                      : Math::Prime::Util::GMP::divint($from, $m);
                    $t++ if ($from % $m != 0);
                    $lo = Math::Prime::Util::rootint($t, $r);
                    $lo++
                      if (
                          (
                           HAS_PRIME_UTIL
                           ? Math::Prime::Util::powint($lo, $r)
                           : Math::Prime::Util::GMP::powint($lo, $r)
                          ) != $t
                         );
                }

                foreach my $v ($lo .. $hi) {
                    push @powerful,
                      $m * (
                            HAS_PRIME_UTIL
                            ? Math::Prime::Util::powint($v, $r)
                            : Math::Prime::Util::GMP::powint($v, $r)
                           );
                }

                return;
            }

            foreach my $v ($lo .. $hi) {

                Math::Prime::Util::is_square_free($v) or next;
                Math::Prime::Util::gcd($m, $v) == 1   or next;

                __SUB__->(
                          $m * (
                                HAS_PRIME_UTIL
                                ? Math::Prime::Util::powint($v, $r)
                                : Math::Prime::Util::GMP::powint($v, $r)
                               ),
                          $r - 1
                         );
            }
          }
          ->(1, 2 * $k - 1);
    }
    else {
        state $t = Math::GMPz::Rmpz_init_nobless();

        sub {
            my ($m, $r) = @_;

            Math::GMPz::Rmpz_div($t, $to, $m);
            Math::GMPz::Rmpz_root($t, $t, $r);

            Math::GMPz::Rmpz_fits_ulong_p($t) || die "Too large value!";

            my $lo = 1;
            my $hi = Math::GMPz::Rmpz_get_ui($t);

            if ($r <= $k) {

                if (Math::GMPz::Rmpz_cmp($from, $m) > 0) {
                    Math::GMPz::Rmpz_cdiv_q($t, $from, $m);
                    my $exact = Math::GMPz::Rmpz_root($t, $t, $r);
                    $lo = Math::GMPz::Rmpz_get_ui($t) + ($exact ? 0 : 1);
                }

                foreach my $v ($lo .. $hi) {
                    Math::GMPz::Rmpz_ui_pow_ui($t, $v, $r);
                    Math::GMPz::Rmpz_mul($t, $t, $m);
                    push @powerful,
                      (
                          Math::GMPz::Rmpz_fits_ulong_p($t)
                        ? Math::GMPz::Rmpz_get_ui($t)
                        : Math::GMPz::Rmpz_init_set($t)
                      );
                }

                return;
            }

            foreach my $v ($lo .. $hi) {

                (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($v) : Math::Prime::Util::GMP::moebius($v)) or next;
                Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $v) == 1                                        or next;

                Math::GMPz::Rmpz_ui_pow_ui($t, $v, $r);
                Math::GMPz::Rmpz_mul($t, $t, $m);

                __SUB__->(Math::GMPz::Rmpz_init_set($t), $r - 1);
            }
          }
          ->($ONE, 2 * $k - 1);
    }

    @powerful = sort { $a <=> $b } @powerful;
    return \@powerful;
}

# Array of k-powerful numbers in the range [from, to]
sub powerful {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @powerful = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_powerful($from, $to, $k)};
#>>>

    _array(\@powerful);
}

sub squarefull {
    (TWO)->powerful(@_);
}

sub cubefull {
    (THREE)->powerful(@_);
}

sub powerful_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    my $step_value = $TEN**(6 * $k);    # TODO: improve this value
    _generic_each($from, $to, $block, sub { $step_value }, sub { _sieve_powerful($_[0], $_[1], $k) });
}

*each_powerful = \&powerful_each;

sub squarefull_each {
    (TWO)->powerful_each(@_);
}

*each_squarefull = \&squarefull_each;

sub cubefull_each {
    (THREE)->powerful_each(@_);
}

*each_cubefull = \&cubefull_each;

sub nth_powerful {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 1 or goto &nan;
    }
    else {
        $k = 2;
    }

    my $k_obj = bless \$k;
    my $n_obj = $n;

    $n = _any2mpz($$n, 0) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) > 0 or do {
        return ZERO if (Math::GMPz::Rmpz_sgn($n) == 0);    # not k-powerful, but...
        goto &nan;
    };

    if ($k == 1) {
        return _set_int($n);
    }

    my $min = Math::GMPz::Rmpz_init_set_ui(1);
    my $max = Math::GMPz::Rmpz_init_set($n);

    Math::GMPz::Rmpz_mul($max, $max, $n);

    while (__cmp__(${$k_obj->powerful_count(bless \$max)}, $n) < 0) {
        Math::GMPz::Rmpz_set($min, $max);
        Math::GMPz::Rmpz_mul_ui($max, $max, $k);
    }

    my $v     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init();

    while (1) {
        Math::GMPz::Rmpz_add($v, $min, $max);
        Math::GMPz::Rmpz_div_2exp($v, $v, 1);

        $count =
          (HAS_PRIME_UTIL && Math::GMPz::Rmpz_fits_ulong_p($v))
          ? Math::GMPz::Rmpz_init_set_ui(Math::Prime::Util::powerful_count(Math::GMPz::Rmpz_get_ui($v), $k))
          : ${$k_obj->powerful_count(bless \$v)};

        my $cmp = __cmp__($count, $n);

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($max, $v, 1);
        }
        elsif ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($min, $v, 1);
        }
        else {
            last;
        }
    }

    $k_obj->powerful((bless \$min), (bless \$v))->last;
}

sub nth_squarefull {
    my ($n) = @_;
    $n->nth_powerful(TWO);
}

sub nth_cubefull {
    my ($n) = @_;
    $n->nth_powerful(THREE);
}

sub next_powerful {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
    }
    else {
        $k = TWO;
    }

    $k->powerful_count($n)->inc->nth_powerful($k);
}

sub next_squarefull {
    my ($n) = @_;
    $n->next_powerful(TWO);
}

sub next_cubefull {
    my ($n) = @_;
    $n->next_powerful(THREE);
}

sub prev_powerful {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
    }
    else {
        $k = TWO;
    }

    my $count = $k->powerful_count($n);
    $count = $count->dec if $n->is_powerful($k);
    $count->nth_powerful($k);
}

sub prev_squarefull {
    my ($n) = @_;
    $n->prev_powerful(TWO);
}

sub prev_cubefull {
    my ($n) = @_;
    $n->prev_powerful(THREE);
}

# =============================================================================
# Helper: Strong Fermat condition check for a prime (power) p.
#
# Returns true iff:
#   v2(p-1) > k_exp   AND
#   base^((p-1) >> (v2(p-1) - k_exp))  ≡  congr  (mod p)
#
# Dispatches to Math::Prime::Util or Math::Prime::Util::GMP as available.
# p must fit in a native unsigned long (always the case when called from the
# sieve generator, where p is iterated with next_prime from a small lower
# bound).
# =============================================================================
sub _strong_fermat_check {
    my ($p, $base, $k_exp, $congr) = @_;

    my $val =
      HAS_PRIME_UTIL
      ? Math::Prime::Util::valuation($p - 1, 2)
      : Math::Prime::Util::GMP::valuation($p - 1, 2);

    return 0 unless $val > $k_exp;

    my $pm =
      HAS_PRIME_UTIL
      ? Math::Prime::Util::powmod($base, ($p - 1) >> ($val - $k_exp), $p)
      : Math::Prime::Util::GMP::powmod($base, ($p - 1) >> ($val - $k_exp), $p);

    $pm == ($congr % $p);
}

# =============================================================================
# Brute-force sieve for k-omega primes in [$from, $to].
# Used when the interval is so small that iterating every integer is faster
# than the recursive generator: |B - A| < B^(1/(k+1)).
# =============================================================================
sub _sieve_small_range {
    my ($from, $to, $k, $fermat, $strong) = @_;

    my $v        = Math::GMPz::Rmpz_init_set($from);
    my $o        = bless \$v;                          # Sidef::Types::Number::Number
    my $k_obj    = bless \$k;
    my $base_obj = bless \$fermat;                     # only used when $fermat is truthy

    my @arr;
    for (; Math::GMPz::Rmpz_cmp($v, $to) <= 0 ; Math::GMPz::Rmpz_add_ui($v, $v, 1)) {
        if ($fermat) {
            next unless $strong
              ? $o->is_strong_fermat_psp($base_obj)
              : $o->is_fermat_psp($base_obj);
        }
        if ($k == 1) {
            $o->is_prime_power || next;
        }
        else {
            $o->is_omega_prime($k_obj) || next;
        }
        push @arr, Math::GMPz::Rmpz_init_set($v);
    }
    return \@arr;
}

# =============================================================================
# Sieve for k-omega (Fermat) primes using native unsigned-long arithmetic.
# Pre-conditions: HAS_PRIME_UTIL is true AND $to fits in an unsigned long.
# =============================================================================
sub _sieve_omega_ulong {
    my ($from, $to, $k, $fermat, $strong) = @_;

    my $A = Math::GMPz::Rmpz_get_ui($from);
    my $B = Math::GMPz::Rmpz_get_ui($to);

    # Raise A to at least the k-th primorial so the smallest possible
    # product of k distinct primes is always in range.
    $A = Math::Prime::Util::vecmax($A, Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    return [] if $A > $B;

    my @omega_primes;
    my %znorder;    # memoised multiplicative orders: p -> ord_{p}(fermat)

    my $generator;
    $generator = sub {
        my ($m, $lo, $j, %args) = @_;

        my $hi = Math::Prime::Util::rootint(Math::Prime::Util::divint($B, $m), $j,);
        return if $lo > $hi;

        # ------------------------------------------------------------------
        # Base case j == 1: choose the final prime factor.
        # ------------------------------------------------------------------
        if ($j == 1) {
            my $lambda = $args{lambda};

            # Fast path: no Fermat filter, or lambda == 1 (no congruence
            # constraint on p beyond being prime).
            if (!$fermat or $lambda == 1) {
                for (my $p = Math::Prime::Util::next_prime($lo - 1) ; $p <= $hi ; $p = Math::Prime::Util::next_prime($p)) {
                    next if ($fermat and $fermat % $p == 0);
                    next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

                    # When m==1 and we have a Fermat filter, start from p^2
                    # so every candidate n = p^e has at least two prime
                    # factors counted with multiplicity.
                    my $v = ($fermat and $m == 1) ? $p * $p : $m * $p;
                    for (; $v - 1 < $B ; $v *= $p) {
                        next unless $v >= $A;
                        if ($fermat) {
                            last unless Math::Prime::Util::powmod($fermat, $v - 1, $v) == 1;
                        }
                        push @omega_primes, $v;
                    }
                }
                return;
            }

            # Fermat path with lambda > 1: valid prime powers must satisfy
            # p ≡ m^{-1} (mod lambda).
            my $t = Math::Prime::Util::invmod($m, $lambda);
            return                                                        if $t > $hi;
            $t += Math::Prime::Util::cdivint($lo - $t, $lambda) * $lambda if $t < $lo;
            return                                                        if $t > $hi;

            for (my $p = $t ; $p <= $hi ; $p += $lambda) {
                next
                  unless Math::Prime::Util::is_prime_power($p)
                  && Math::Prime::Util::gcd($m,      $p) == 1
                  && Math::Prime::Util::gcd($fermat, $p) == 1;
                next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

                my $v = $m * $p;
                next unless $v >= $A;

                # k==1 composites that happen to be prime are excluded.
                next if ($k == 1 and Math::Prime::Util::is_prime($v));
                next unless (($v - 1) % ($znorder{$p} //= Math::Prime::Util::znorder($fermat, $p)) == 0);
                push @omega_primes, $v;
            }
            return;
        }

        # ------------------------------------------------------------------
        # Recursive case j > 1: choose one prime factor and recurse.
        # ------------------------------------------------------------------
        my $lambda = $args{lambda};

        for (my ($p, $r, $L) = ($lo) ; $p <= $hi ; $p = $r) {
            $r = Math::Prime::Util::next_prime($p);
            next if ($fermat and $fermat % $p == 0);
            next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

            my $z;
            if ($fermat) {
                $z = ($znorder{$p} //= Math::Prime::Util::znorder($fermat, $p));
                next unless Math::Prime::Util::gcd($m, $z) == 1;
                $L = Math::Prime::Util::lcm($lambda, $z);
            }

            # Iterate over prime powers of p: p, p^2, p^3, ...
            for (my ($pk, $v) = ($p, $m * $p) ; $v - 1 < $B ; ($pk, $v) = ($pk * $p, $v * $p)) {

                # For p^e with e >= 2, verify the Fermat order condition
                # extends to the higher power before recursing.
                last if ($fermat and $pk > $p and Math::Prime::Util::powmod($fermat, $z, $pk) != 1);

                if ($v * $r - 1 < $B) {
                    $generator->($v, $r, $j - 1, %args, (defined $L ? (lambda => $L) : ()));
                }
            }
        }
    };

    # ------------------------------------------------------------------
    # Kick off the generator.
    # ------------------------------------------------------------------
    if ($strong) {

        # Case where base^d ≡ 1 (mod p), d = odd part of p-1.
        $generator->(1, 2, $k, lambda => 1, k_exp => 0, congr => 1);

        # Cases where base^(d·2^v) ≡ -1 (mod p), for v = 0 .. floor(log2 B).
        for my $v (0 .. Math::Prime::Util::logint($B, 2)) {
            $generator->(1, 2, $k, lambda => 1, k_exp => $v, congr => -1);
        }
    }
    else {
        $generator->(1, 2, $k, ($fermat ? (lambda => 1) : ()));
    }

    undef %znorder;
    undef $generator;    # break circular reference

    @omega_primes = sort { $a <=> $b } @omega_primes;

    $fermat
      ? [List::Util::uniq(@omega_primes)]
      : \@omega_primes;
}

# =============================================================================
# Sieve for k-omega (Fermat) primes using GMP big-integer arithmetic.
# Used when $to does not fit in an unsigned long.
#
# Design notes on GMPz allocation strategy
# -----------------------------------------
# * $su / $sv are scratch variables captured by the closure and reused across
#   every j==1 base-case call.  This is safe because j==1 never recurses.
# * The j>1 recursive case allocates its own per-frame $lv / $lu / $lcm so
#   that recursive frames do not alias each other's loop variables.
# * $lcm is passed to the recursive call by reference (no copy).  The callee
#   returns before the caller overwrites $lcm for the next prime, so there is
#   no aliasing hazard in single-threaded Perl.
# =============================================================================
sub _sieve_omega_gmp {
    my ($from, $to, $k, $fermat, $strong) = @_;

    my $A = Math::GMPz::Rmpz_init_set($from);
    my $B = Math::GMPz::Rmpz_init_set($to);

    # Raise A to at least the k-th primorial.
    {
        my $primorial = _cached_pn_primorial($k);
        if (Math::GMPz::Rmpz_cmp($primorial, $A) > 0) {
            Math::GMPz::Rmpz_set($A, $primorial);
        }
    }
    return [] if Math::GMPz::Rmpz_cmp($A, $B) > 0;

    # Closure-captured scratch variables (j==1 base case only; never aliased
    # by recursive j>1 frames).
    my $su = Math::GMPz::Rmpz_init();
    my $sv = Math::GMPz::Rmpz_init();
    my $sw = Math::GMPz::Rmpz_init_set_ui($fermat // 0);    # Fermat base

    my @omega_primes;
    my %seen;                                               # dedup guard (used only when $fermat is set)
    my %znorder;                                            # memoised multiplicative orders: p -> ord_{p}(fermat)

    # Hoist dispatch outside the closure.
    my $next_prime =
      HAS_PRIME_UTIL
      ? \&Math::Prime::Util::next_prime
      : \&Math::Prime::Util::GMP::next_prime;

    my $znorder_fn =
      HAS_PRIME_UTIL
      ? \&Math::Prime::Util::znorder
      : \&Math::Prime::Util::GMP::znorder;

    my $is_prime_power_fn =
      HAS_PRIME_UTIL
      ? \&Math::Prime::Util::is_prime_power
      : \&Math::Prime::Util::GMP::is_prime_power;

    my $gcd_fn =
      HAS_PRIME_UTIL
      ? \&Math::Prime::Util::gcd
      : \&Math::Prime::Util::GMP::gcd;

    my $powmod_fn =
      HAS_PRIME_UTIL
      ? \&Math::Prime::Util::powmod
      : \&Math::Prime::Util::GMP::powmod;

    my $generator;
    $generator = sub {
        my ($m, $lo, $j, %args) = @_;

        # hi = floor( floor(B / m)^(1/j) )
        Math::GMPz::Rmpz_tdiv_q($su, $B, $m);
        Math::GMPz::Rmpz_root($su, $su, $j);
        Math::GMPz::Rmpz_fits_ulong_p($su) || die "Too large value!";
        my $hi = Math::GMPz::Rmpz_get_ui($su);

        return if $lo > $hi;

        # ------------------------------------------------------------------
        # Base case j == 1: choose the final prime factor.
        # ------------------------------------------------------------------
        if ($j == 1) {
            my $L = $args{lambda};

            # Fast path: no Fermat filter, or lambda == 1.
            if (!$fermat or Math::GMPz::Rmpz_cmp_ui($L, 1) == 0) {

                for (my $p = $next_prime->($lo - 1) ; $p <= $hi ; $p = $next_prime->($p)) {
                    next if ($fermat and $fermat % $p == 0);
                    next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

                    # Starting value: p^2 when (fermat && m==1), else m*p.
                    if ($fermat and Math::GMPz::Rmpz_cmp_ui($m, 1) == 0) {
                        Math::GMPz::Rmpz_set_ui($sv, $p);
                    }
                    else {
                        Math::GMPz::Rmpz_set($sv, $m);
                    }

                    for (Math::GMPz::Rmpz_mul_ui($sv, $sv, $p) ; Math::GMPz::Rmpz_cmp($sv, $B) <= 0 ; Math::GMPz::Rmpz_mul_ui($sv, $sv, $p)) {
                        next unless Math::GMPz::Rmpz_cmp($sv, $A) >= 0;

                        if ($fermat) {

                            # Check: base^(v-1) ≡ 1 (mod v)
                            Math::GMPz::Rmpz_sub_ui($su, $sv, 1);
                            Math::GMPz::Rmpz_powm($su, $sw, $su, $sv);
                            last unless Math::GMPz::Rmpz_cmp_ui($su, 1) == 0;
                        }

                        my $val =
                            Math::GMPz::Rmpz_fits_ulong_p($sv)
                          ? Math::GMPz::Rmpz_get_ui($sv)
                          : Math::GMPz::Rmpz_init_set($sv);

                        push @omega_primes, $val
                          unless $fermat && $seen{$val}++;
                    }
                }
                return;
            }

            # Fermat path with lambda > 1: candidates satisfy
            # p ≡ m^{-1} (mod lambda).
            Math::GMPz::Rmpz_invert($sv, $m, $L);
            return if Math::GMPz::Rmpz_cmp_ui($sv, $hi) > 0;

            Math::GMPz::Rmpz_fits_ulong_p($L)  || die "Too large value!";
            Math::GMPz::Rmpz_fits_ulong_p($sv) || die "Too large value!";
            my $step = Math::GMPz::Rmpz_get_ui($L);
            my $t    = Math::GMPz::Rmpz_get_ui($sv);
            return if $t > $hi;
            $t += $step while $t < $lo;
            return if $t > $hi;

            for (my $p = $t ; $p <= $hi ; $p += $step) {
                next
                  unless $is_prime_power_fn->($p)
                  && Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $p) == 1
                  && $gcd_fn->($fermat, $p) == 1;
                next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

                Math::GMPz::Rmpz_mul_ui($sv, $m, $p);

                if ($k == 1 and _is_prob_prime($p) and Math::GMPz::Rmpz_cmp_ui($m, 1) == 0) {

                    # Single-prime case handled by outer k==1 logic; skip.
                }
                elsif (Math::GMPz::Rmpz_cmp($sv, $A) >= 0) {
                    Math::GMPz::Rmpz_sub_ui($su, $sv, 1);
                    my $z = ($znorder{$p} //= $znorder_fn->($fermat, $p));
                    if (Math::GMPz::Rmpz_divisible_ui_p($su, $z)) {
                        my $val =
                            Math::GMPz::Rmpz_fits_ulong_p($sv)
                          ? Math::GMPz::Rmpz_get_ui($sv)
                          : Math::GMPz::Rmpz_init_set($sv);
                        push @omega_primes, $val unless $seen{$val}++;
                    }
                }
            }
            return;
        }

        # ------------------------------------------------------------------
        # Recursive case j > 1: choose one prime factor and recurse.
        # Per-frame GMPz objects prevent aliasing across recursive calls.
        # ------------------------------------------------------------------
        my $lambda = $args{lambda};
        my $lv     = Math::GMPz::Rmpz_init();    # m * p^e  (loop variable)
        my ($lu, $lcm);
        if ($fermat) {
            $lu  = Math::GMPz::Rmpz_init();      # p^e  (for Fermat power check)
            $lcm = Math::GMPz::Rmpz_init();
        }

        for (my ($p, $r) = ($lo) ; $p <= $hi ; $p = $r) {
            $r = $next_prime->($p);
            next if ($fermat and $fermat % $p == 0);
            next if ($strong and !_strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr}));

            my $z;
            if ($fermat) {
                Math::GMPz::Rmpz_set_ui($lu, $p);
                $z = ($znorder{$p} //= $znorder_fn->($fermat, $p));
                next unless Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $z) == 1;
                Math::GMPz::Rmpz_lcm_ui($lcm, $lambda, $z);
            }

            for (Math::GMPz::Rmpz_mul_ui($lv, $m, $p) ; Math::GMPz::Rmpz_cmp($lv, $B) <= 0 ; Math::GMPz::Rmpz_mul_ui($lv, $lv, $p)) {
                $generator->($lv, $r, $j - 1, %args, (defined $lcm ? (lambda => $lcm) : ()),);

                if ($fermat) {
                    Math::GMPz::Rmpz_mul_ui($lu, $lu, $p);
                    last unless $powmod_fn->($fermat, $z, $lu) == 1;
                }
            }
        }
    };

    # ------------------------------------------------------------------
    # Kick off the generator.
    # ------------------------------------------------------------------
    if ($strong) {
        $generator->(
                     Math::GMPz::Rmpz_init_set_ui(1), 2, $k,
                     lambda => Math::GMPz::Rmpz_init_set_ui(1),
                     k_exp  => 0,
                     congr  => 1,
                    );
        for my $v (0 .. Math::Prime::Util::GMP::logint($B, 2)) {
            $generator->(
                         Math::GMPz::Rmpz_init_set_ui(1), 2, $k,
                         lambda => Math::GMPz::Rmpz_init_set_ui(1),
                         k_exp  => $v,
                         congr  => -1,
                        );
        }
    }
    else {
        $generator->(Math::GMPz::Rmpz_init_set_ui(1), 2, $k, ($fermat ? (lambda => Math::GMPz::Rmpz_init_set_ui(1)) : ()),);
    }

    undef %znorder;
    undef $generator;    # break circular reference

    @omega_primes = sort { $a <=> $b } @omega_primes;
    return \@omega_primes;
}

# =============================================================================
# Main entry point: sieve for k-omega primes in the interval [$from, $to].
#
# Strategy selection:
#   (a) Trivial base cases.
#   (b) Math::Prime::Util::prime_powers  — k==1, no Fermat, ulong range.
#   (c) _sieve_small_range               — |B-A| < B^(1/(k+1)).
#   (d) Math::Prime::Util::omega_primes  — no Fermat, ulong range.
#   (e) _sieve_omega_ulong               — Fermat, ulong range.
#   (f) _sieve_omega_gmp                 — any, bignum range.
# =============================================================================
sub _sieve_omega_primes {
    my ($from, $to, $k, %opt) = @_;

    my $fermat = $opt{fermat};
    my $strong = $opt{strong};

    # ------------------------------------------------------------------
    # 1. Base cases
    # ------------------------------------------------------------------
    return []  if $fermat and $k < 1;
    return [1] if $k == 0 and $to >= 1 and $from <= 1;
    return []  if $k == 0;

    # Computed once; used by both the fast path and the dispatch below.
    my $fits_ulong = Math::GMPz::Rmpz_fits_ulong_p($to);

    # ------------------------------------------------------------------
    # 2. Fast path: prime powers via Math::Prime::Util
    # ------------------------------------------------------------------
    if (HAS_PRIME_UTIL and $k == 1 and !$fermat and $fits_ulong) {
        return scalar Math::Prime::Util::prime_powers(Math::GMPz::Rmpz_get_ui($from), Math::GMPz::Rmpz_get_ui($to),);
    }

    # ------------------------------------------------------------------
    # 3. Small-range optimisation: brute-force when |B-A| < B^(1/(k+1))
    # ------------------------------------------------------------------
    {
        state $dt = Math::GMPz::Rmpz_init_nobless();
        state $du = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_sub($dt, $to, $from);
        Math::GMPz::Rmpz_root($du, $to, $k + 1);
        if (Math::GMPz::Rmpz_cmp($dt, $du) < 0) {
            $VERBOSE && say STDERR "omega_primes: small range: ($from, $to) with k = $k";
            return _sieve_small_range($from, $to, $k, $fermat, $strong);
        }
    }

    # ------------------------------------------------------------------
    # 4. Dispatch to the appropriate sieve
    # ------------------------------------------------------------------
    if (HAS_PRIME_UTIL and $fits_ulong) {
        unless ($fermat) {

            # Direct library call for the common case.
            return Math::Prime::Util::omega_primes($k, Math::GMPz::Rmpz_get_ui($from), Math::GMPz::Rmpz_get_ui($to),);
        }
        return _sieve_omega_ulong($from, $to, $k, $fermat, $strong);
    }

    return _sieve_omega_gmp($from, $to, $k, $fermat, $strong);
}

sub omega_prime_divisors {
    my ($n, $k) = @_;

    if (!defined($k)) {
        my @list;

        foreach my $k (0 .. $n->omega) {
            push @list, $n->omega_prime_divisors(_set_int($k));
        }

        return _array(\@list);
    }

    _valid(\$k);

    $k = _any2ui($$k) // return _array();
    my $z = _any2mpz($$n, 0) // return _array();

    if ($k == 0) {
        return _array([ONE]);
    }

    if ($k == 1) {
        return $_[0]->prime_power_divisors;
    }

    my @factor_exp = _factor_exp($z);

    if ($k > scalar(@factor_exp)) {
        return _array();
    }

    my %valuations  = map { @$_ } @factor_exp;
    my @factors     = map { ($_ < ULONG_MAX) ? $_ : Math::GMPz::Rmpz_init_set_str("$_", 10) } map { $_->[0] } @factor_exp;
    my $factors_end = $#factors;

    my $t = Math::GMPz::Rmpz_init();

    my @list;

    sub {
        my ($m, $k, $i) = @_;

        Math::GMPz::Rmpz_div($t, $z, $m);
        Math::GMPz::Rmpz_root($t, $t, $k);

        my $L =
            Math::GMPz::Rmpz_fits_ulong_p($t)
          ? Math::GMPz::Rmpz_get_ui($t)
          : Math::GMPz::Rmpz_init_set($t);

        foreach my $j ($i .. $factors_end) {

            my $q = $factors[$j];

            if (($k > 1 and $j == $factors_end) or ($q > $L)) {
                last;
            }

            my $v = $m * $q;

            foreach (1 .. $valuations{$q}) {

                if ($k == 1) {
                    push @list, $v;
                }
                else {
                    __SUB__->($v, $k - 1, $j + 1);
                }

                $v *= $q;
            }
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), $k, 0);

    _array([map { bless \$_ } sort { $a <=> $b } @list]);
}

sub omega_primes {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @omega_primes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_omega_primes($from, $to, $k)};
#>>>

    _array(\@omega_primes);
}

sub omega_inverse {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->omega_primes($from, $to);
}

sub fermat_psp {
    my ($k, $base, $from, $to) = @_;

    _valid(\$base, \$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $base = _any2ui($$base) // return _array();
    $k    = _any2ui($$k)    // return _array();

    if ($base <= 1) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @fermat_pseudoprimes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_omega_primes($from, $to, $k, fermat => $base)};
#>>>

    _array(\@fermat_pseudoprimes);
}

sub strong_fermat_psp {
    my ($k, $base, $from, $to) = @_;

    _valid(\$base, \$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $base = _any2ui($$base) // return _array();
    $k    = _any2ui($$k)    // return _array();

    if ($base <= 1) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @fermat_pseudoprimes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_omega_primes($from, $to, $k, fermat => $base, strong => 1)};
#>>>

    _array(\@fermat_pseudoprimes);
}

sub prime_powers {
    (ONE)->omega_primes(@_);
}

sub omega_primes_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    my $step = ($k > 8) ? Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10) : 1e7;

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_omega_primes($_[0], $_[1], $k) });
}

*each_omega_prime = \&omega_primes_each;

sub prime_powers_each {
    (ONE)->omega_primes_each(@_);
}

*each_prime_power = \&prime_powers_each;

# =============================================================================
# Note: _strong_fermat_check is defined in _sieve_omega_primes's companion
# refactoring and is shared across both sieves.  Signature:
#   _strong_fermat_check($p, $base, $k_exp, $congr)
# Returns true iff v2(p-1) > k_exp  AND
#   base^((p-1) >> (v2(p-1)-k_exp)) ≡ congr (mod p).
# =============================================================================

# =============================================================================
# Brute-force sieve for k-almost primes in [$from, $to].
# Used when the interval is small enough that iterating every integer is faster
# than the recursive generator: |B - A| < B^(1/k).
# =============================================================================
sub _almost_prime_small_range {
    my ($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree) = @_;

    my $v        = Math::GMPz::Rmpz_init_set($from);
    my $o        = bless \$v;                          # Sidef::Types::Number::Number
    my $base_obj = bless \$fermat;
    my $k_obj    = bless \$k;

    my @arr;
    for (; Math::GMPz::Rmpz_cmp($v, $to) <= 0 ; Math::GMPz::Rmpz_add_ui($v, $v, 1)) {

        if ($fermat) {
            next unless $strong
              ? $o->is_strong_fermat_psp($base_obj)
              : $o->is_fermat_psp($base_obj);
        }
        next unless !$carmichael       || $o->is_carmichael;
        next unless !$lucas_carmichael || $o->is_lucas_carmichael;

        if ($squarefree) {
            if    ($k == 1) { next unless $o->is_prime }
            elsif ($k == 2) { next unless $o->is_squarefree_semiprime }
            else            { next unless $o->is_squarefree_almost_prime($k_obj) }
        }
        else {
            if    ($k == 1) { next unless $o->is_prime }
            elsif ($k == 2) { next unless $o->is_semiprime }
            else            { next unless $o->is_almost_prime($k_obj) }
        }
        push @arr, Math::GMPz::Rmpz_init_set($v);
    }
    return \@arr;
}

# =============================================================================
# Sieve for k-almost primes using native unsigned-long arithmetic.
# Pre-conditions: HAS_PRIME_UTIL is true AND $to fits in an unsigned long.
# =============================================================================
sub _almost_prime_ulong {
    my ($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree) = @_;

    my $A = Math::GMPz::Rmpz_get_ui($from);
    my $B = Math::GMPz::Rmpz_get_ui($to);

    # Compute the effective lower bound A and the optional ceiling on the
    # largest prime factor (max_p), depending on which filter is active.
    my $max_p = undef;

    if ($squarefree) {
        if ($carmichael or $lucas_carmichael) {
            $A = Math::Prime::Util::vecmax($A, Math::Prime::Util::GMP::divint(Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k + 1), 10), 2),);
            if ($carmichael) {

                # For n = p1*…*pk, the largest factor satisfies
                # p_k ≤ (1 + sqrt(8n+1)) / 4.
                $max_p = (1 + Math::Prime::Util::GMP::sqrtint(Math::Prime::Util::GMP::addint(Math::Prime::Util::GMP::mulint(8, $B), 1))) >> 2;
            }
            elsif ($lucas_carmichael) {
                $max_p = Math::Prime::Util::sqrtint($B);
            }
        }
        else {
            $A = Math::Prime::Util::vecmax($A, Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10),);
        }
    }
    else {
        # Smallest k-almost prime is 2^k.
        $A = Math::Prime::Util::vecmax($A, Math::Prime::Util::GMP::powint(2, $k));
    }

    return [] if $A > $B;

    my @almost_primes;
    my %znorder;    # memoised multiplicative orders: p -> ord_p(fermat)

    my $generator;
    $generator = sub {
        my ($m, $p, $k, %args) = @_;
        my $lambda = $args{lambda};

        # ------------------------------------------------------------------
        # Base case k == 1: choose the final prime factor.
        # ------------------------------------------------------------------
        if ($k == 1) {
            my $lo = $args{from};
            my $hi = $args{upto};

            # Carmichael constraint: the largest prime factor p_k must be ≤ m
            # (the product of all smaller factors), otherwise (p_k - 1) cannot
            # divide (n - 1).
            $hi = $m     if ($carmichael     && $m < $hi);
            $hi = $max_p if (defined($max_p) && $max_p < $hi);

            return if $lo > $hi;

            if ($carmichael) {

                # Valid primes satisfy p ≡ m^{-1} (mod lambda) so that
                # (m*p - 1) ≡ 0 (mod (p - 1)).
                my $L = $args{lambda};
                my $t = Math::Prime::Util::invmod($m, $L);
                return                                              if $t > $hi;
                $t += Math::Prime::Util::cdivint($lo - $t, $L) * $L if $t < $lo;
                return                                              if $t > $hi;

                for (my $p = $t ; $p <= $hi ; $p += $L) {
                    my $n = $m * $p;
                    if (($n - 1) % ($p - 1) == 0 && Math::Prime::Util::is_prime($p)) {
                        if ($strong) {
                            next unless _strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr});
                        }
                        push @almost_primes, $n;
                    }
                }
            }
            elsif ($lucas_carmichael) {

                # Valid primes satisfy p ≡ -m^{-1} (mod lambda) so that
                # (m*p + 1) ≡ 0 (mod (p + 1)).
                my $L = $args{lambda};
                my $t = $L - Math::Prime::Util::invmod($m, $L);
                return                                              if $t > $hi;
                $t += Math::Prime::Util::cdivint($lo - $t, $L) * $L if $t < $lo;
                return                                              if $t > $hi;

                for (my $p = $t ; $p <= $hi ; $p += $L) {
                    my $n = $m * $p;
                    push(@almost_primes, $n)
                      if (($n + 1) % ($p + 1) == 0 && Math::Prime::Util::is_prime($p));
                }
            }
            elsif ($fermat) {

                # Valid primes satisfy p ≡ m^{-1} (mod lambda) so that
                # ord_p(base) | (m*p - 1).
                my $L = $args{lambda};
                my $t = Math::Prime::Util::invmod($m, $L);
                return                                              if $t > $hi;
                $t += Math::Prime::Util::cdivint($lo - $t, $L) * $L if $t < $lo;
                return                                              if $t > $hi;

                for (my $p = $t ; $p <= $hi ; $p += $L) {
                    next unless Math::Prime::Util::is_prime($p);
                    next if $fermat % $p == 0;

                    my $n = $m * $p;
                    if (($n - 1) % ($znorder{$p} //= Math::Prime::Util::znorder($fermat, $p)) == 0) {
                        if ($strong) {
                            next unless _strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr});
                        }
                        push @almost_primes, $n;
                    }
                }
            }
            else {
                # Plain almost primes: any prime in [lo, hi].
                Math::Prime::Util::forprimes(sub { push @almost_primes, $m * $_ }, $lo, $hi);
            }

            return;
        }

        # ------------------------------------------------------------------
        # Recursive case k > 1: choose one prime factor and recurse.
        # ------------------------------------------------------------------
        my $hi = Math::Prime::Util::rootint(Math::Prime::Util::divint($B, $m), $k);
        my $L;

        for (my $r ; $p <= $hi ; $p = $r) {
            $r = Math::Prime::Util::next_prime($p);

            # Strong Fermat filter on each chosen intermediate prime.
            if ($strong && $fermat) {
                next if $fermat % $p == 0;
                next unless _strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr});
            }

            # Accumulate lambda (LCM of group orders seen so far) so the
            # final prime can be found by an arithmetic-progression sieve.
            if ($carmichael) {
                next unless Math::Prime::Util::gcd($m, $p >> 1) == 1;
                $L = Math::Prime::Util::lcm($lambda, $p - 1);
            }
            elsif ($lucas_carmichael) {
                next unless Math::Prime::Util::gcd($m, ($p >> 1) + 1) == 1;
                $L = Math::Prime::Util::lcm($lambda, $p + 1);
            }
            elsif ($fermat) {
                next if $fermat % $p == 0;
                my $z = ($znorder{$p} //= Math::Prime::Util::znorder($fermat, $p));
                next unless Math::Prime::Util::gcd($m, $z) == 1;
                $L = Math::Prime::Util::lcm($lambda, $z);
            }

            my $t = $m * $p;
            my $u = Math::Prime::Util::divint($A, $t);
            my $v = Math::Prime::Util::divint($B, $t);
            ++$u if $t * $u < $A;

            unless ($u > $v) {
                $p = $r if $squarefree;          # enforce strictly increasing primes
                $u = $p if $k == 2 && $p > $u;
                $generator->($t, $p, $k - 1, %args, ($k == 2 ? (from => $u, upto => $v) : ()), (defined($L) ? (lambda => $L) : ()),);
            }
        }
    };

    # ------------------------------------------------------------------
    # Kick off the generator.
    # ------------------------------------------------------------------
    if ($strong) {

        # Case where base^d ≡ 1 (mod p), d = odd part of p-1.
        $generator->(1, 2, $k, lambda => 1, k_exp => 0, congr => 1);

        # Cases where base^(d·2^v) ≡ -1 (mod p), for v = 0 .. floor(log2 B).
        for my $v (0 .. Math::Prime::Util::logint($B, 2)) {
            $generator->(1, 2, $k, lambda => 1, k_exp => $v, congr => -1);
        }
    }
    else {
        $generator->(1, ($carmichael || $lucas_carmichael) ? 3 : 2, $k, lambda => 1,);
    }

    undef %znorder;
    undef $generator;    # break circular reference

    @almost_primes = sort { $a <=> $b } @almost_primes;
    return \@almost_primes;
}

# =============================================================================
# Sieve for k-almost primes using GMP big-integer arithmetic.
# Used when $to does not fit in an unsigned long.
#
# Allocation strategy
# -------------------
# $su / $sv: shared scratch captured by the closure.  Safe because:
#   * The k==1 base case never recurses.
#   * The k>1 recursive case extracts plain-scalar values from $su/$sv with
#     Rmpz_get_ui / Rmpz_get_str before each recursive call, so recursive
#     frames cannot alias the caller's live values.
# $x / $L:  allocated fresh per k>1 frame; $L is passed to the callee by
#            reference (no copy) — safe because the callee returns before the
#            caller overwrites $L for the next prime.
# =============================================================================
sub _almost_prime_gmp {
    my ($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree) = @_;

    my $A = Math::GMPz::Rmpz_init_set($from);
    my $B = Math::GMPz::Rmpz_init_set($to);

    # Closure-captured scratch variables (see allocation strategy above).
    my $su = Math::GMPz::Rmpz_init();
    my $sv = Math::GMPz::Rmpz_init();

    my $max_p = undef;

    if ($squarefree) {
        if ($carmichael or $lucas_carmichael) {
            Math::GMPz::Rmpz_set_str($su, Math::Prime::Util::GMP::divint(Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k + 1), 10), 2), 10,);

            if ($carmichael) {

                # max_p = floor((1 + sqrt(8*B + 1)) / 4)
                $max_p = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_mul_2exp($max_p, $B, 3);
                Math::GMPz::Rmpz_add_ui($max_p, $max_p, 1);
                Math::GMPz::Rmpz_sqrt($max_p, $max_p);
                Math::GMPz::Rmpz_add_ui($max_p, $max_p, 1);
                Math::GMPz::Rmpz_div_2exp($max_p, $max_p, 2);
                $max_p = Math::GMPz::Rmpz_get_ui($max_p)
                  if Math::GMPz::Rmpz_fits_ulong_p($max_p);
            }
            elsif ($lucas_carmichael) {
                $max_p = Math::GMPz::Rmpz_init();
                Math::GMPz::Rmpz_sqrt($max_p, $B);
                $max_p = Math::GMPz::Rmpz_get_ui($max_p)
                  if Math::GMPz::Rmpz_fits_ulong_p($max_p);
            }
        }
        else {
            Math::GMPz::Rmpz_set($su, _cached_pn_primorial($k));
        }
    }
    else {
        # su = 2^k  (smallest k-almost prime)
        Math::GMPz::Rmpz_set_ui($su, 0);
        Math::GMPz::Rmpz_setbit($su, $k);
    }

    # A = max(A, su)
    Math::GMPz::Rmpz_set($A, $su) if Math::GMPz::Rmpz_cmp($su, $A) > 0;
    return []                     if Math::GMPz::Rmpz_cmp($A,  $B) > 0;

    my @almost_primes;
    my %znorder;    # memoised multiplicative orders: p -> ord_p(fermat)

    # Hoist dispatch outside the closure.
    my $next_prime  = HAS_PRIME_UTIL ? \&Math::Prime::Util::next_prime : \&Math::Prime::Util::GMP::next_prime;
    my $is_prime_fn = HAS_PRIME_UTIL ? \&Math::Prime::Util::is_prime   : \&Math::Prime::Util::GMP::is_prime;
    my $znorder_fn  = HAS_PRIME_UTIL ? \&Math::Prime::Util::znorder    : \&Math::Prime::Util::GMP::znorder;
    my $cdivint     = HAS_PRIME_UTIL ? \&Math::Prime::Util::cdivint    : \&Math::Prime::Util::GMP::cdivint;

    my $generator;
    $generator = sub {
        my ($m, $lo, $k, %args) = @_;
        my $lambda = $args{lambda};

        # ------------------------------------------------------------------
        # Base case k == 1: choose the final prime factor.
        # ------------------------------------------------------------------
        if ($k == 1) {

            if ($carmichael || $fermat || $lucas_carmichael) {

                my $from_p = $args{from};
                my $to_p   = $args{upto};

                # Carmichael: the largest prime factor must not exceed m.
                $to_p = Math::GMPz::Rmpz_get_ui($m)
                  if ($carmichael && Math::GMPz::Rmpz_cmp_ui($m, $to_p) < 0);

                $to_p = $max_p if defined($max_p) && $max_p < $to_p;

                return if $from_p > $to_p;

                # Starting point of the arithmetic progression.
                # Carmichael / Fermat: p ≡  m^{-1}  (mod lambda)
                # Lucas-Carmichael:    p ≡ -m^{-1}  (mod lambda)
                Math::GMPz::Rmpz_invert($sv, $m, $lambda);
                Math::GMPz::Rmpz_sub($sv, $lambda, $sv) if $lucas_carmichael;

                return if Math::GMPz::Rmpz_cmp_ui($sv, $to_p) > 0;

                Math::GMPz::Rmpz_fits_ulong_p($lambda) || die "Too large value!";
                Math::GMPz::Rmpz_fits_ulong_p($sv)     || die "Too large value!";

                my $step = Math::GMPz::Rmpz_get_ui($lambda);
                my $t    = Math::GMPz::Rmpz_get_ui($sv);
                $t += $cdivint->($from_p - $t, $step) * $step if $t < $from_p;
                return                                        if $t > $to_p;

                for (my $p = $t ; $p <= $to_p ; $p += $step) {
                    next unless $is_prime_fn->($p);
                    next if ($fermat && $fermat % $p == 0);

                    if ($strong && $fermat) {
                        next unless _strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr});
                    }

                    # Compute n = m * p and n ± 1 for the divisibility test.
                    Math::GMPz::Rmpz_mul_ui($sv, $m, $p);
                    $lucas_carmichael
                      ? Math::GMPz::Rmpz_add_ui($su, $sv, 1)
                      : Math::GMPz::Rmpz_sub_ui($su, $sv, 1);

                    my $ok =
                        $lucas_carmichael ? Math::GMPz::Rmpz_divisible_ui_p($su, $p + 1)
                      : $carmichael       ? Math::GMPz::Rmpz_divisible_ui_p($su, $p - 1)
                      : do {
                        my $ord = ($znorder{$p} //= $znorder_fn->($fermat, $p));
                        Math::GMPz::Rmpz_divisible_ui_p($su, $ord);
                      };

                    if ($ok) {
                        push @almost_primes, Math::GMPz::Rmpz_fits_ulong_p($sv)
                          ? Math::GMPz::Rmpz_get_ui($sv)
                          : Math::GMPz::Rmpz_init_set($sv);
                    }
                }
            }
            else {
                # Plain almost primes: any prime q in [from, upto], push m*q.
                for my $q (@{_primes($args{from}, $args{upto})}) {
                    Math::GMPz::Rmpz_mul_ui($su, $m, $q);
                    push @almost_primes, Math::GMPz::Rmpz_fits_ulong_p($su)
                      ? Math::GMPz::Rmpz_get_ui($su)
                      : Math::GMPz::Rmpz_init_set($su);
                }
            }

            return;
        }

        # ------------------------------------------------------------------
        # Recursive case k > 1: choose one prime factor and recurse.
        # $su is repurposed here for rootint scratch (safe: written before
        # each read, and no interleaving with k==1 branch).
        # ------------------------------------------------------------------
        Math::GMPz::Rmpz_tdiv_q($su, $B, $m);
        Math::GMPz::Rmpz_root($su, $su, $k);

        my $hi =
            Math::GMPz::Rmpz_fits_ulong_p($su)
          ? Math::GMPz::Rmpz_get_ui($su)
          : Math::GMPz::Rmpz_get_str($su, 10);

        return if $lo > $hi;

        # Per-frame allocations prevent aliasing across recursive calls.
        my $x = Math::GMPz::Rmpz_init();
        my $L =
          ($carmichael || $lucas_carmichael || $fermat)
          ? Math::GMPz::Rmpz_init()
          : undef;

        for (my ($p, $r) = ($lo) ; $p <= $hi ; $p = $r) {
            $r = $next_prime->($p);

            if ($strong && $fermat) {
                next if $fermat % $p == 0;
                next unless _strong_fermat_check($p, $fermat, $args{k_exp}, $args{congr});
            }

            if ($carmichael) {
                next unless Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $p >> 1) == 1;
                Math::GMPz::Rmpz_lcm_ui($L, $lambda, $p - 1);
            }
            elsif ($lucas_carmichael) {
                next unless Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, ($p >> 1) + 1) == 1;
                Math::GMPz::Rmpz_lcm_ui($L, $lambda, $p + 1);
            }
            elsif ($fermat) {
                next if $fermat % $p == 0;
                my $ord = ($znorder{$p} //= $znorder_fn->($fermat, $p));
                next unless Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $ord) == 1;
                Math::GMPz::Rmpz_lcm_ui($L, $lambda, $ord);
            }

            # x = m * p; su = ceil(A/x); sv = floor(B/x).
            Math::GMPz::Rmpz_mul_ui($x, $m, $p);
            Math::GMPz::Rmpz_cdiv_q($su, $A, $x);
            Math::GMPz::Rmpz_div($sv, $B, $x);

            if (Math::GMPz::Rmpz_cmp($su, $sv) <= 0) {

                $p = $r if $squarefree;    # enforce strictly increasing primes

                # For k==2, raise the lower bound for the next prime to at
                # least p (prevents double-counting pairs).
                if ($k == 2 && Math::GMPz::Rmpz_cmp_ui($su, $p) < 0) {
                    Math::GMPz::Rmpz_set_ui($su, $p);
                }

                # Extract plain scalars before the recursive call so that
                # $su/$sv can be safely overwritten inside the callee.
                $generator->(
                             $x, $p,
                             $k - 1,
                             %args,
                             (
                              $k == 2
                              ? (
                                 from => (
                                            Math::GMPz::Rmpz_fits_ulong_p($su)
                                          ? Math::GMPz::Rmpz_get_ui($su)
                                          : Math::GMPz::Rmpz_get_str($su, 10)
                                         ),
                                 upto => (
                                            Math::GMPz::Rmpz_fits_ulong_p($sv)
                                          ? Math::GMPz::Rmpz_get_ui($sv)
                                          : Math::GMPz::Rmpz_get_str($sv, 10)
                                         ),
                                )
                              : ()
                             ),
                             (defined($L) ? (lambda => $L) : ()),
                            );
            }
        }
    };

    # ------------------------------------------------------------------
    # Kick off the generator.
    # ------------------------------------------------------------------
    if ($strong && $fermat) {

        # Case where base^d ≡ 1 (mod p), d = odd part of p-1.
        $generator->(
                     Math::GMPz::Rmpz_init_set_ui(1), 2, $k,
                     lambda => Math::GMPz::Rmpz_init_set_ui(1),
                     k_exp  => 0,
                     congr  => 1,
                    );

        # Cases where base^(d·2^v) ≡ -1 (mod p), for v = 0 .. floor(log2 B).
        for my $v (0 .. Math::Prime::Util::GMP::logint($B, 2)) {
            $generator->(
                         Math::GMPz::Rmpz_init_set_ui(1), 2, $k,
                         lambda => Math::GMPz::Rmpz_init_set_ui(1),
                         k_exp  => $v,
                         congr  => -1,
                        );
        }
    }
    else {
        $generator->(
                     Math::GMPz::Rmpz_init_set_ui(1),
                     ($carmichael || $lucas_carmichael) ? 3 : 2,
                     $k,
                     (
                        ($carmichael || $lucas_carmichael || $fermat)
                      ? (lambda => Math::GMPz::Rmpz_init_set_ui(1))
                      : ()
                     ),
                    );
    }

    undef %znorder;
    undef $generator;    # break circular reference

    @almost_primes = sort { $a <=> $b } @almost_primes;
    return \@almost_primes;
}

# =============================================================================
# Main entry point: sieve for k-almost primes in the interval [$from, $to].
#
# Strategy selection:
#   (a) Trivial base cases.
#   (b) Primes (k==1) — direct library sieve.
#   (c) _almost_prime_small_range  — |B-A| < B^(1/k).
#   (d) Math::Prime::Util::almost_primes — no filters, ulong range.
#   (e) Math::Prime::Util::semi_primes  — k==2, no Fermat, ulong range.
#   (f) _almost_prime_ulong  — general, ulong range.
#   (g) _almost_prime_gmp    — general, bignum range.
# =============================================================================
sub _sieve_almost_primes {
    my ($from, $to, $k, %opt) = @_;

    my $strong           = $opt{strong};
    my $fermat           = $opt{fermat};
    my $carmichael       = $opt{carmichael};
    my $lucas_carmichael = $opt{lucas_carmichael};

    # ------------------------------------------------------------------
    # 1. Base cases
    # ------------------------------------------------------------------
    return []  if (($carmichael || $lucas_carmichael) && $k < 3);
    return []  if ($fermat                            && $k < 2);
    return [1] if ($k == 0                            && $to >= 1 && $from <= 1);
    return []  if ($k == 0);

    # ------------------------------------------------------------------
    # 2. k == 1: return primes directly — no almost-prime sieve needed.
    # ------------------------------------------------------------------
    if ($k == 1) {
        return _primes($from, $to);
    }

    my $squarefree = $opt{squarefree};

    # Computed once; referenced in both the small-range check and the dispatch.
    my $fits_ulong = Math::GMPz::Rmpz_fits_ulong_p($to);

    # ------------------------------------------------------------------
    # 3. Small-range optimisation: brute-force when |B-A| < B^(1/k).
    # ------------------------------------------------------------------
    {
        state $dt = Math::GMPz::Rmpz_init_nobless();
        state $du = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_sub($dt, $to, $from);
        Math::GMPz::Rmpz_root($du, $to, $k);
        if (Math::GMPz::Rmpz_cmp($dt, $du) < 0) {
            $VERBOSE and say STDERR "almost_primes: small range: ($from, $to) with k = $k";
            return _almost_prime_small_range($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree,);
        }
    }

    # ------------------------------------------------------------------
    # 4. Library fast paths (ulong range only).
    # ------------------------------------------------------------------
    if (HAS_PRIME_UTIL && $fits_ulong) {

        # Cleanest case: no filter at all — library handles everything.
        if (!$squarefree && !$fermat && !$carmichael && !$lucas_carmichael) {
            return Math::Prime::Util::almost_primes($k, Math::GMPz::Rmpz_get_ui($from), Math::GMPz::Rmpz_get_ui($to),);
        }

        # k==2 without Fermat/Carmichael: semi_primes is fastest.
        # (carmichael/lucas with k==2 already returned [] above.)
        if ($k == 2 && !$fermat && !$carmichael && !$lucas_carmichael) {
            my $A   = Math::GMPz::Rmpz_get_ui($from);
            my $B   = Math::GMPz::Rmpz_get_ui($to);
            my $arr = Math::Prime::Util::semi_primes($A, $B);

            # Squarefree semiprimes = semiprimes that are not perfect squares.
            @$arr = grep { !Math::Prime::Util::is_square($_) } @$arr if $squarefree;
            return $arr;
        }
    }

    # ------------------------------------------------------------------
    # 5. Full recursive sieve.
    # ------------------------------------------------------------------
    if (HAS_PRIME_UTIL && $fits_ulong) {
        return _almost_prime_ulong($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree,);
    }
    return _almost_prime_gmp($from, $to, $k, $fermat, $strong, $carmichael, $lucas_carmichael, $squarefree,);
}

sub almost_prime_divisors {
    my ($n, $k) = @_;

    if (!defined($k)) {
        my @list;

        foreach my $k (0 .. $n->bigomega) {
            push @list, $n->almost_prime_divisors(_set_int($k));
        }

        return _array(\@list);
    }

    _valid(\$k);

    $k = _any2ui($$k) // return _array();
    my $z = _any2mpz($$n, 0) // return _array();

    if ($k == 0) {
        return _array([ONE]);
    }

    my @factor_exp  = _factor_exp($z);
    my %valuations  = map { @$_ } @factor_exp;
    my @factors     = map { ($_ < ULONG_MAX) ? $_ : Math::GMPz::Rmpz_init_set_str("$_", 10) } map { $_->[0] } @factor_exp;
    my $factors_end = $#factors;

    if ($k == 1) {
        return _array([map { _set_int($_) } @factors]);
    }

    my $bigomega = 0;

    foreach my $pp (@factor_exp) {
        $bigomega += $pp->[1];
    }

    if ($k > $bigomega) {
        return _array();
    }

    my $t = Math::GMPz::Rmpz_init();

    my @list;

    sub {
        my ($m, $k, $i) = @_;

        Math::GMPz::Rmpz_div($t, $z, $m);

        if ($k == 1) {

            my $L =
                Math::GMPz::Rmpz_fits_ulong_p($t)
              ? Math::GMPz::Rmpz_get_ui($t)
              : Math::GMPz::Rmpz_init_set($t);

            foreach my $j ($i .. $factors_end) {

                my $q = $factors[$j];
                $q > $L and last;

                my $v = ref($q) ? Math::GMPz::Rmpz_remove($t, $m, $q) : do {
                    Math::GMPz::Rmpz_set_ui($t, $q);
                    Math::GMPz::Rmpz_remove($t, $m, $t);
                };

                if ($v < $valuations{$q}) {
                    push @list, $m * $q;
                }
            }

            return;
        }

        Math::GMPz::Rmpz_root($t, $t, $k);

        my $L =
            Math::GMPz::Rmpz_fits_ulong_p($t)
          ? Math::GMPz::Rmpz_get_ui($t)
          : Math::GMPz::Rmpz_init_set($t);

        foreach my $j ($i .. $factors_end) {

            my $q = $factors[$j];
            $q > $L and last;

            my $v = ref($q) ? Math::GMPz::Rmpz_remove($t, $m, $q) : do {
                Math::GMPz::Rmpz_set_ui($t, $q);
                Math::GMPz::Rmpz_remove($t, $m, $t);
            };

            if ($v < $valuations{$q}) {
                __SUB__->($m * $q, $k - 1, $j);
            }
        }
      }
      ->(Math::GMPz::Rmpz_init_set_ui(1), $k, 0);

    _array([map { bless \$_ } sort { $a <=> $b } @list]);
}

sub almost_primes {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @almost_primes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k)};
#>>>

    _array(\@almost_primes);
}

sub bigomega_inverse {
    my ($from, $to, $k) = @_;
    _valid(\$to, \$k);
    $k->almost_primes($from, $to);
}

sub _sieve_prime_sig_numbers {
    my ($from, $to, $prime_signature) = @_;

    my $A = Math::GMPz::Rmpz_init_set($from);
    my $B = Math::GMPz::Rmpz_init_set($to);

    my $k = scalar(@$prime_signature);

    # Handle empty prime signature
    if ($k == 0) {
        return ($A <= 1 and 1 <= $B) ? [1] : [];
    }

    my $primorial = _cached_pn_primorial($k);
    if (Math::GMPz::Rmpz_cmp($primorial, $A) > 0) {
        Math::GMPz::Rmpz_set($A, $primorial);
    }

    # Early exit if A is out of bounds
    if (Math::GMPz::Rmpz_cmp($A, $B) > 0) {
        return [];
    }

    my $sum_e = List::Util::sum(@$prime_signature) || return [];

    if ($sum_e >= Math::GMPz::Rmpz_sizeinbase($B, 2)) {
        return [];
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    my @results;

    sub {
        my ($m, $lo, $rem_sig, $rem_sum) = @_;

        Math::GMPz::Rmpz_tdiv_q($t, $B, $m);
        Math::GMPz::Rmpz_root($t, $t, $rem_sum) if ($rem_sum > 1);

        if (Math::GMPz::Rmpz_cmp_ui($t, $lo) < 0) {
            return;
        }

        Math::GMPz::Rmpz_fits_ulong_p($t) || die "Too large value!";

        my $k  = scalar(@$rem_sig);
        my $hi = Math::GMPz::Rmpz_get_ui($t);

        my %seen;
        foreach my $i (0 .. $k - 1) {
            my $e = $rem_sig->[$i];

            next if $seen{$e}++;

            my @new_sig = @$rem_sig;
            splice(@new_sig, $i, 1);

            # Base case for recursion
            if ($k == 1) {

                Math::GMPz::Rmpz_cdiv_q($t, $A, $m);
                my $exact_root = Math::GMPz::Rmpz_root($t, $t, $e);
                Math::GMPz::Rmpz_add_ui($t, $t, 1) if !$exact_root;

                if (Math::GMPz::Rmpz_cmp_ui($t, $lo) > 0) {
                    $lo = Math::GMPz::Rmpz_get_ui($t);
                }

                foreach my $p (@{_primes($lo, $hi)}) {

                    if ($e == 1) {
                        Math::GMPz::Rmpz_mul_ui($t, $m, $p);
                    }
                    else {
                        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e);
                        Math::GMPz::Rmpz_mul($t, $m, $t);
                    }
                    push @results,
                      (
                          Math::GMPz::Rmpz_fits_ulong_p($t)
                        ? Math::GMPz::Rmpz_get_ui($t)
                        : Math::GMPz::Rmpz_init_set($t)
                      );
                }

                return;
            }
            else {

                my $u       = Math::GMPz::Rmpz_init();
                my $new_sum = $rem_sum - $e;

                for (my $p = $lo ; $p <= $hi ;) {

                    if ($e == 1) {
                        Math::GMPz::Rmpz_mul_ui($u, $m, $p);
                    }
                    else {
                        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e);
                        Math::GMPz::Rmpz_mul($u, $m, $t);
                    }

                    my $r = HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p);
                    __SUB__->($u, $r, \@new_sig, $new_sum);
                    $p = $r;
                }
            }
        }
      }
      ->(
        do {
            state $m = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_set_ui($m, 1);
            $m;
        },
        2,
        $prime_signature,
        $sum_e
      );

    return \@results;
}

sub prime_signature_numbers {
    my ($from, $to, $signature) = @_;

    Scalar::Util::reftype($signature) eq 'ARRAY'
      or die "usage: prime_signature_numbers(A, B, [...])";

    my @sig = map { my $v = "$_"; $v <= 0 and die "invalid prime signature (entry: $v)"; $v } @$signature;

    _valid(\$to);

    $from = _any2mpz($$from, 0) // return _array();
    $to   = _any2mpz($$to,   1) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

    my @numbers = map { bless \$_ } sort { $a <=> $b } @{_sieve_prime_sig_numbers($from, $to, \@sig)};
    _array(\@numbers);
}

*prime_signature_inverse = \&prime_signature_numbers;
*inverse_prime_signature = \&prime_signature_numbers;

sub _prime_sig_count {
    my ($n, $prime_signature) = @_;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return 0;

    my $k = scalar(@$prime_signature);

    if ($k == 0) {
        return 1;
    }

    if (Math::GMPz::Rmpz_sizeinbase($n, 2) <= $k) {
        return 0;
    }

    if (Math::GMPz::Rmpz_cmp($n, _cached_pn_primorial($k)) < 0) {
        return 0;
    }

    my $sum_e = List::Util::sum(@$prime_signature) || return 0;

    if ($sum_e >= Math::GMPz::Rmpz_sizeinbase($n, 2)) {
        return 0;
    }

    state $t     = Math::GMPz::Rmpz_init_nobless();
    state $count = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($count, 0);

    sub {
        my ($m, $lo, $rem_sig, $rem_sum, $j) = @_;

        Math::GMPz::Rmpz_tdiv_q($t, $n, $m);
        Math::GMPz::Rmpz_root($t, $t, $rem_sum) if ($rem_sum > 1);

        if (Math::GMPz::Rmpz_cmp_ui($t, $lo) < 0) {
            return;
        }

        Math::GMPz::Rmpz_fits_ulong_p($t) || die "Too large value!";

        my $k  = scalar(@$rem_sig);
        my $hi = Math::GMPz::Rmpz_get_ui($t);

        if ($k == 1) {

            my $pi =
              (HAS_PRIME_UTIL and $hi < PRIMECOUNT_MIN)
              ? Math::Prime::Util::prime_count($hi)
              : _prime_count($hi);

            if (FAST_MODE and $pi < ULONG_MAX) {
                Math::GMPz::Rmpz_add_ui($count, $count, $pi - $j);
            }
            else {
                Math::GMPz::Rmpz_set_str($t, "$pi", 10);
                Math::GMPz::Rmpz_sub_ui($t, $t, $j);
                Math::GMPz::Rmpz_add($count, $count, $t);
            }

            return;
        }

        my %seen;
        for my $i (0 .. $k - 1) {

            my $e = $rem_sig->[$i];
            next if $seen{$e}++;

            my $local_j = $j;
            my @new_sig = @$rem_sig;
            splice(@new_sig, $i, 1);

            if ($k == 2) {
                my $e2 = $new_sig[0];

                foreach my $p (@{_primes($lo, $hi)}) {

                    if ($e == 1) {
                        Math::GMPz::Rmpz_mul_ui($t, $m, $p);
                    }
                    else {
                        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e);
                        Math::GMPz::Rmpz_mul($t, $t, $m);
                    }

                    Math::GMPz::Rmpz_tdiv_q($t, $n, $t);
                    Math::GMPz::Rmpz_root($t, $t, $e2) if ($e2 > 1);

                    my $pi =
                      (HAS_PRIME_UTIL and Math::GMPz::Rmpz_cmp_ui($t, PRIMECOUNT_MIN) < 0)
                      ? Math::Prime::Util::prime_count(Math::GMPz::Rmpz_get_ui($t))
                      : _prime_count(Math::GMPz::Rmpz_get_str($t, 10));

                    if (FAST_MODE and $pi < ULONG_MAX) {
                        Math::GMPz::Rmpz_add_ui($count, $count, $pi - ++$local_j);
                    }
                    else {
                        Math::GMPz::Rmpz_set_str($t, "$pi", 10);
                        Math::GMPz::Rmpz_sub_ui($t, $t, ++$local_j);
                        Math::GMPz::Rmpz_add($count, $count, $t);
                    }
                }
            }
            else {

                my $u       = Math::GMPz::Rmpz_init();
                my $new_sum = $rem_sum - $e;

                for (my $p = $lo ; $p <= $hi ;) {
                    if ($e == 1) {
                        Math::GMPz::Rmpz_mul_ui($u, $m, $p);
                    }
                    else {
                        Math::GMPz::Rmpz_ui_pow_ui($t, $p, $e);
                        Math::GMPz::Rmpz_mul($u, $t, $m);
                    }
                    my $r = (HAS_PRIME_UTIL ? Math::Prime::Util::next_prime($p) : Math::Prime::Util::GMP::next_prime($p));
                    __SUB__->($u, $r, \@new_sig, $new_sum, ++$local_j);
                    $p = $r;
                }
            }
        }
      }
      ->(
        do {
            state $m = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_set_ui($m, 1);
            $m;
        },
        2,
        $prime_signature,
        $sum_e
      );

    Math::GMPz::Rmpz_fits_ulong_p($count)
      ? Math::GMPz::Rmpz_get_ui($count)
      : Math::GMPz::Rmpz_init_set($count);
}

sub prime_signature_count {
    my ($from, $to, $signature) = @_;

    Scalar::Util::reftype($signature) eq 'ARRAY'
      or die "usage: prime_signature_count(A, B, [...])";

    my @sig = map { my $v = "$_"; $v <= 0 and die "invalid prime signature (entry: $v)"; $v } @$signature;

    _valid(\$to);

    return ZERO if $to->lt($from);

    if ($from->gt(ONE)) {
        return ((ONE)->prime_signature_count($to, $signature)->sub((ONE)->prime_signature_count($from->dec, $signature)));
    }

    my $n = _any2mpz($$to) // return ZERO;
    my $r = _prime_sig_count($n, \@sig);

    bless \$r;
}

*prime_signature_inverse_len = \&prime_signature_count;
*inverse_prime_signature_len = \&prime_signature_count;

sub _tau_partitions {
    my ($n, $max_sum_e) = @_;

    my @results;
    my @divs = _divisors($n);

    shift(@divs);    # remove divisor '1'

    my $end = $#divs;
    my @path;

    sub {
        my ($target, $min_idx, $curr_sum_e) = @_;

        if ($target == 1) {
            push @results, [@path];
            return;
        }

        for my $i ($min_idx .. $end) {
            my $d = $divs[$i];
            my $e = $d - 1;

            last if $d > $target;
            last if ($curr_sum_e + $e > $max_sum_e);

            if ($target % $d == 0) {
                push @path, $e;
                my $t =
                  HAS_PRIME_UTIL
                  ? Math::Prime::Util::divint($target, $d)
                  : Math::Prime::Util::GMP::divint($target, $d);
                __SUB__->($t, $i, $curr_sum_e + $e);
                pop @path;
            }
        }
      }
      ->($n, 0, 0);

    return @results;
}

sub tau_inverse {
    my ($from, $to, $k) = @_;

    _valid(\$to, \$k);

    $k = _any2ui($$k) // return _array();

    $from = _any2mpz($$from, 0) // return _array();
    $to   = _any2mpz($$to,   1) // return _array();

    my @signatures = _tau_partitions($k, Math::GMPz::Rmpz_sizeinbase($to, 2) - 1);

    my @list;
    foreach my $sig (@signatures) {
        push @list, @{_sieve_prime_sig_numbers($from, $to, $sig)};
    }

    @list = map { bless \$_ } sort { $a <=> $b } @list;
    _array(\@list);
}

*inverse_tau = \&tau_inverse;

sub tau_inverse_len {
    my ($from, $to, $k) = @_;

    _valid(\$to, \$k);

    return ZERO if $to->lt($from);

    if ($from->gt(ONE)) {
        return ((ONE)->tau_inverse_len($to, $k)->sub((ONE)->tau_inverse_len($from->dec, $k)));
    }

    $k = _any2ui($$k) // return ZERO;
    my $n = _any2mpz($$to, 0) // return ZERO;
    my @signatures = _tau_partitions($k, Math::GMPz::Rmpz_sizeinbase($n, 2) - 1);

    my @list;
    foreach my $sig (@signatures) {
        push @list, _prime_sig_count($n, $sig);
    }

    _set_int(Math::Prime::Util::GMP::vecsum(@list));
}

*inverse_tau_len = \&tau_inverse_len;

sub nth_tau_inverse {
    my ($n, $k) = @_;

    _valid(\$k);

    my $n_z = _any2mpz($$n) // goto &nan;
    my $k_z = _any2mpz($$k) // goto &nan;

    if (   Math::GMPz::Rmpz_sgn($n_z) <= 0
        or Math::GMPz::Rmpz_sgn($k_z) <= 0) {
        goto &nan;
    }

    if (Math::GMPz::Rmpz_cmp_ui($k_z, 1) == 0 and Math::GMPz::Rmpz_cmp_ui($n_z, 1) > 0) {
        goto &nan;
    }

    $n->inverse_count(sub { Sidef::Types::Number::Number::tau_inverse_len(ONE, $_[0], $k) }, 'bsearch_min');
}

sub next_tau_inverse {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_zero && return ONE;

    my $N = _big2uistr($$n) // goto &nan;
    my $K = _big2uistr($$k) // goto &nan;

    foreach my $i (1 .. Math::Prime::Util::GMP::sigma($K, 0)**2) {
        my $v = Math::Prime::Util::GMP::addint($N, $i);
        my $tau =
          (HAS_PRIME_UTIL and $v < ULONG_MAX)
          ? Math::Prime::Util::divisor_sum($v, 0)
          : Math::Prime::Util::GMP::sigma($v, 0);
        if ($tau == $K) {
            return _set_int($v);
        }
    }

    my $count = Sidef::Types::Number::Number::tau_inverse_len(ONE, $n, $k);
    $count = $count->inc;
    $count->nth_tau_inverse($k);
}

sub prev_tau_inverse {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->le(ONE) && goto &nan;

    my $N = _big2uistr($$n) // goto &nan;
    my $K = _big2uistr($$k) // goto &nan;

    foreach my $i (1 .. Math::Prime::Util::GMP::sigma($K, 0)**2) {
        my $v = Math::Prime::Util::GMP::subint($N, $i) || last;
        my $tau =
          (HAS_PRIME_UTIL and $v < ULONG_MAX)
          ? Math::Prime::Util::divisor_sum($v, 0)
          : Math::Prime::Util::GMP::sigma($v, 0);
        if ($tau == $K) {
            return _set_int($v);
        }
    }

    my $count = Sidef::Types::Number::Number::tau_inverse_len(ONE, $n, $k);
    $count = $count->dec if $n->tau->eq($k);
    $count->nth_tau_inverse($k);
}

sub semiprimes {
    (TWO)->almost_primes(@_);
}

sub carmichael {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @carmichael_numbers = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, carmichael => 1, squarefree => 1)};
#>>>

    _array(\@carmichael_numbers);
}

sub strong_fermat_carmichael {
    my ($k, $base, $from, $to) = @_;

    _valid(\$base, \$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $base = _any2ui($$base) // return _array();
    $k    = _any2ui($$k)    // return _array();

    if ($base <= 1) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @carmichael_numbers = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, fermat => $base, carmichael => 1, squarefree => 1, strong => 1)};
#>>>

    _array(\@carmichael_numbers);
}

*carmichael_strong_fermat     = \&strong_fermat_carmichael;
*carmichael_strong_fermat_psp = \&strong_fermat_carmichael;
*carmichael_strong_psp        = \&strong_fermat_carmichael;

sub lucas_carmichael {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @lucas_carmichael_numbers = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, lucas_carmichael => 1, squarefree => 1)};
#>>>

    _array(\@lucas_carmichael_numbers);
}

sub squarefree_fermat_psp {
    my ($k, $base, $from, $to) = @_;

    _valid(\$base, \$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $base = _any2ui($$base) // return _array();
    $k    = _any2ui($$k)    // return _array();

    if ($base <= 1) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @fermat_pseudoprimes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) :  _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, fermat => $base, squarefree => 1)};
#>>>

    _array(\@fermat_pseudoprimes);
}

sub squarefree_strong_fermat_psp {
    my ($k, $base, $from, $to) = @_;

    _valid(\$base, \$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $base = _any2ui($$base) // return _array();
    $k    = _any2ui($$k)    // return _array();

    if ($base <= 1) {
        return _array();
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @fermat_pseudoprimes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) :  _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, fermat => $base, squarefree => 1, strong => 1)};
#>>>

    _array(\@fermat_pseudoprimes);
}

sub almost_primes_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_cmp_ui($from, $k) <= 0 and $k > 0) {
        $from = Math::GMPz::Rmpz_init_set_ui(0);
        Math::GMPz::Rmpz_setbit($from, $k);    # from = 2**k
    }

#<<<
    _generic_each($from, $to, $block, sub {
            my ($from) = @_;

            my $lo   = Math::GMPz::Rmpz_get_d($from);
            my $step = ($k > 0) ? (_nth_almost_prime_lower($lo + CORE::log($lo) * 5e4, $k) - _nth_almost_prime_lower($lo, $k)) : 0;

            if ($step <= 0 or $step > 1e14) {
                $step = 100_000_000 * $k;
            }

            $step;
        }, sub { _sieve_almost_primes($_[0], $_[1], $k) });
#>>>
}

*each_almost_prime = \&almost_primes_each;

sub semiprimes_each {
    (TWO)->almost_primes_each(@_);
}

*each_semiprime = \&semiprimes_each;

sub nth_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k >= 1 or goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->nth_prime;
    }
    elsif ($k == 2) {
        return $n->nth_semiprime;
    }

    my $k_obj = _set_int($k);
    my $n_obj = $n;

    $n = _any2mpz($$n) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) > 0 or do {
        return ONE if (Math::GMPz::Rmpz_sgn($n) == 0);    # not k-almost prime, but...
        goto &nan;
    };

    my $min = Math::GMPz::Rmpz_init();
    my $max = Math::GMPz::Rmpz_init_set($n);

    Math::GMPz::Rmpz_setbit($min, $k);
    Math::GMPz::Rmpz_mul_ui($max, $max, $k + 1);

    while (__cmp__(${$k_obj->almost_prime_count(bless \$max)}, $n) < 0) {
        Math::GMPz::Rmpz_set($min, $max);
        Math::GMPz::Rmpz_mul_ui($max, $max, $k);
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($max)) {
        my $r = eval { Math::Prime::Util::nth_almost_prime("$k", "$n") };
        if ($r) {    # workaround for danaj/Math-Prime-Util #71
            return _set_int("$r");
        }
    }

    my $v     = Math::GMPz::Rmpz_init();
    my $count = Math::GMPz::Rmpz_init();

    while (1) {
        Math::GMPz::Rmpz_add($v, $min, $max);
        Math::GMPz::Rmpz_div_2exp($v, $v, 1);

        $count =
          (HAS_PRIME_UTIL && Math::GMPz::Rmpz_fits_ulong_p($v))
          ? Math::GMPz::Rmpz_init_set_ui(Math::Prime::Util::almost_prime_count($k, Math::GMPz::Rmpz_get_ui($v)))
          : ${$k_obj->almost_prime_count(bless \$v)};

        my $cmp = __cmp__($count, $n);

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($max, $v, 1);
        }
        elsif ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($min, $v, 1);
        }
        else {
            last;
        }
    }

    $k_obj->almost_primes((bless \$min), (bless \$v))->last;
}

sub _almost_prime_count_cost {
    my ($n, $k) = @_;

    sub {
        my ($m, $k) = @_;

        my $s = Math::Prime::Util::GMP::rootint(Math::Prime::Util::GMP::divint($n, $m), $k);

        if ($k == 2) {
            return Math::Prime::Util::GMP::mulint(Math::Prime::Util::GMP::rootint($s, (HAS_PRIME_UTIL ? 3 : 2)), Math::Prime::Util::GMP::prime_count_upper($s));
        }

        Math::Prime::Util::GMP::mulint(Math::Prime::Util::GMP::prime_count_upper($s), __SUB__->(Math::Prime::Util::GMP::mulint($m, $s), $k - 1));
      }
      ->(1, $k);
}

sub next_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->next_prime;
    }
    elsif ($k == 2) {
        return $n->next_semiprime;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set_ui(0);
    Math::GMPz::Rmpz_setbit($r, $k);

    # Smallest k-almost prime is 2^k
    if (Math::GMPz::Rmpz_cmp($n, $r) < 0) {
        return bless \$r;
    }

    if ($k <= 23) {

        # When the cost is too large, do a linear search for the next k-almost prime
        if ($k <= 20 or _almost_prime_count_cost($n, $k) >= 1e7) {

            # Optimization for native integers
            if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_slong_p($n)) {
                $n = Math::GMPz::Rmpz_get_ui($n) + 1;
                until (Math::Prime::Util::is_almost_prime($k, $n)) {
                    ++$n;
                }
                return bless \$n;
            }

            Math::GMPz::Rmpz_add_ui($r, $n, 1);

            my $r_obj = bless \$r;

            until ($r_obj->is_almost_prime($k_obj)) {
                Math::GMPz::Rmpz_add_ui($r, $r, 1);
            }

            return $r_obj;
        }
    }

    $k_obj->almost_prime_count($n_obj)->inc->nth_almost_prime($k_obj);
}

sub prev_almost_prime {
    my ($n, $k) = @_;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) || goto &nan;
    }
    else {
        $k = 2;
    }

    if ($k == 1) {
        return $n->prev_prime;
    }
    elsif ($k == 2) {
        return $n->prev_semiprime;
    }

    my $n_obj = $n;
    my $k_obj = bless \$k;

    $n = _any2mpz($$n) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n) < 0) {
        goto &nan;
    }

    my $r = Math::GMPz::Rmpz_init_set_ui(0);
    Math::GMPz::Rmpz_setbit($r, $k);

    # Smallest k-almost prime is 2^k
    if (Math::GMPz::Rmpz_cmp($n, $r) <= 0) {
        goto &nan;
    }

    if ($k <= 23) {

        # When the cost is too large, do a linear search for the next k-almost prime
        if ($k <= 20 or _almost_prime_count_cost($n, $k) >= 1e7) {

            # Optimization for native integers
            if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
                $n = Math::GMPz::Rmpz_get_ui($n) - 1;
                until (Math::Prime::Util::is_almost_prime($k, $n)) {
                    --$n;
                }
                return bless \$n;
            }

            Math::GMPz::Rmpz_sub_ui($r, $n, 1);

            my $r_obj = bless \$r;

            until ($r_obj->is_almost_prime($k_obj)) {
                Math::GMPz::Rmpz_sub_ui($r, $r, 1);
            }

            return $r_obj;
        }
    }

    my $count = $k_obj->almost_prime_count($n_obj);
    $count = $count->dec if $n_obj->is_almost_prime($k_obj);
    $count->nth_almost_prime($k_obj);
}

sub squarefree_almost_primes {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from, 0) // return _array();
        $to   = _any2mpz($$to,   1) // return _array();
    }
    else {
        $to   = _any2mpz($$from, 0) // return _array();
        $from = $ONE;
    }

    $k = _any2ui($$k) // return _array();

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @squarefree_almost_primes = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_almost_primes($from, $to, $k, squarefree => 1)};
#>>>

    _array(\@squarefree_almost_primes);
}

sub squarefree_semiprimes {
    (TWO)->squarefree_almost_primes(@_);
}

sub sphenic {
    (THREE)->squarefree_almost_primes(@_);
}

*sphenic_numbers = \&sphenic;

sub sphenic_count {
    (THREE)->squarefree_almost_prime_count(@_);
}

sub squarefree_almost_primes_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    my $step = ($k > 8) ? Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10) : 1e7;

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1) });
}

*each_squarefree_almost_prime = \&squarefree_almost_primes_each;

sub squarefree_semiprimes_each {
    (TWO)->squarefree_almost_primes_each(@_);
}

*each_squarefree_semiprime = \&squarefree_semiprimes_each;

sub sphenic_each {
    (THREE)->squarefree_almost_primes_each(@_);
}

sub carmichael_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 3) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k) x (($k >> 1) + 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($k == 4) {
        $step = 5e7;
    }

    if ($k >= 5 and $step < 1e8) {
        $step = '1' . ('0' x 8);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1, carmichael => 1) });
}

*each_carmichael = \&carmichael_each;

sub carmichael_strong_fermat_each {
    my ($k, $base, $from, $to, $block) = @_;

    _valid(\$base, \$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k    = _any2ui($$k)    // return ZERO;
    $base = _any2ui($$base) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 3) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k) x ($k + 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e8) {
        $step = '1' . ('0' x 8);
    }

    if ($k == 4) {
        $step = '1' . ('0' x 9);
    }

    if ($k >= 5 and $step < 1e10) {
        $step = '1' . ('0' x 10);
    }

    if ($k >= 6 and $step < 1e11) {
        $step = '1' . ('0' x 11);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block,
                  sub { $step },
                  sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1, carmichael => 1, strong => 1, fermat => $base) });
}

*carmichael_strong_psp_each        = \&carmichael_strong_fermat_each;
*each_carmichael_strong_psp        = \&carmichael_strong_fermat_each;
*each_carmichael_strong_fermat     = \&carmichael_strong_fermat_each;
*each_carmichael_strong_fermat_psp = \&carmichael_strong_fermat_each;
*each_strong_fermat_carmichael     = \&carmichael_strong_fermat_each;
*strong_fermat_carmichael_each     = \&carmichael_strong_fermat_each;

sub lucas_carmichael_each {
    my ($k, $from, $to, $block) = @_;

    _valid(\$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 3) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k) x (($k >> 1) + 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e7) {
        $step = 1e7;
    }

    if ($k == 4) {
        $step = 5e7;
    }

    if ($k >= 5 and $step < 1e8) {
        $step = '1' . ('0' x 8);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1, lucas_carmichael => 1) });
}

*each_lucas_carmichael = \&lucas_carmichael_each;

sub squarefree_fermat_psp_each {
    my ($k, $base, $from, $to, $block) = @_;

    _valid(\$base, \$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k    = _any2ui($$k)    // return ZERO;
    $base = _any2ui($$base) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 2) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k + 1) x (($k + 1) >> 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($k == 4) {
        $step = 5e6;
    }

    if ($k >= 5 and $step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1, fermat => $base) });
}

*each_squarefree_fermat_psp = \&squarefree_fermat_psp_each;

sub squarefree_strong_fermat_psp_each {
    my ($k, $base, $from, $to, $block) = @_;

    _valid(\$base, \$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k    = _any2ui($$k)    // return ZERO;
    $base = _any2ui($$base) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 2) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k) x ($k + 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($k == 2) {
        $step = '1' . ('0' x 6);
    }

    if ($k == 4) {
        $step = '5' . ('0' x 8);
    }

    if ($k >= 5 and $step < 5e9) {
        $step = '5' . ('0' x 9);
    }

    if ($k >= 6 and $step < 2e11) {
        $step = '2' . ('0' x 11);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_almost_primes($_[0], $_[1], $k, squarefree => 1, strong => 1, fermat => $base) });
}

*each_squarefree_strong_fermat_psp = \&squarefree_strong_fermat_psp_each;

sub fermat_psp_each {
    my ($k, $base, $from, $to, $block) = @_;

    _valid(\$base, \$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k    = _any2ui($$k)    // return ZERO;
    $base = _any2ui($$base) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 1) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k + 1) x (($k + 1) >> 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($k == 4) {
        $step = '5' . ('0' x 6);
    }

    if ($k >= 5 and $step < 1e7) {
        $step = '1' . ('0' x 7);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_omega_primes($_[0], $_[1], $k, fermat => $base) });
}

*each_fermat_psp = \&fermat_psp_each;

sub strong_fermat_psp_each {
    my ($k, $base, $from, $to, $block) = @_;

    _valid(\$base, \$from);

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    $k    = _any2ui($$k)    // return ZERO;
    $base = _any2ui($$base) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if ($k < 1) {
        return ZERO;
    }

    # TODO: tweak the step value for better performance
    my $step = Math::Prime::Util::GMP::vecprod(($k) x ($k + 1), Math::GMPz::Rmpz_get_str(_cached_pn_primorial($k), 10));

    if ($step < 1e6) {
        $step = '1' . ('0' x 6);
    }

    if ($k == 4) {
        $step = '1' . ('0' x 7);
    }

    if ($k >= 5 and $step < 5e8) {
        $step = '5' . ('0' x 8);
    }

    if ($step > ULONG_MAX) {
        $step = Math::GMPz::Rmpz_init_set_str("$step", 10);
    }

    _generic_each($from, $to, $block, sub { $step }, sub { _sieve_omega_primes($_[0], $_[1], $k, fermat => $base, strong => 1) });
}

*each_strong_fermat_psp = \&strong_fermat_psp_each;

sub _sieve_squarefree {
    my ($from, $to) = @_;

    my @squarefree;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($to)) {
        Math::Prime::Util::forsquarefree(    # XXX: leaks memory in MPU 0.73
           sub {
               push @squarefree, $_;
           },
           Math::GMPz::Rmpz_get_ui($from),
           Math::GMPz::Rmpz_get_ui($to)
        );
    }
    elsif (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($to)) {

        $from = Math::GMPz::Rmpz_get_ui($from);
        $to   = Math::GMPz::Rmpz_get_ui($to);

        my @mu = Math::Prime::Util::moebius($from, $to);
        for (my $i = -1 ; $from < $to ; ++$from) {
            push(@squarefree, $from) if $mu[++$i];
        }
        push(@squarefree, $to) if $mu[-1];
    }
    else {

        my $t  = Math::GMPz::Rmpz_init_set($from);
        my @mu = Math::Prime::Util::GMP::moebius($from, $to);

        for (my $i = -1 ; ; Math::GMPz::Rmpz_add_ui($t, $t, 1)) {
            push(@squarefree, Math::GMPz::Rmpz_get_str($t, 10)) if ($mu[++$i] // last);
        }
    }

    return \@squarefree;
}

sub squarefree {
    my ($from, $to) = @_;

    if (defined($to)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return _array();
        $to   = _any2mpz($$to)   // return _array();
    }
    else {
        $to   = _any2mpz($$from) // return _array();
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($to) < 0) {
        $to = $ZERO;
    }

#<<<
    my @squarefree = map {
        (ref($_) or $_ < ULONG_MAX) ? (bless \$_) : _set_int($_)
    } @{_sieve_squarefree($from, $to)};
#>>>

    _array(\@squarefree);
}

sub squarefree_each {
    my ($from, $to, $block) = @_;

    if (defined($block)) {
        _valid(\$to);
        $from = _any2mpz($$from) // return ZERO;
        $to   = _any2mpz($$to)   // return ZERO;
    }
    else {
        $block = $to;
        $to    = _any2mpz($$from) // return ZERO;
        $from  = $ONE;
    }

    if (Math::GMPz::Rmpz_sgn($from) <= 0) {
        $from = $ONE;
    }

    _generic_each($from, $to, $block, sub { 1e4 }, sub { _sieve_squarefree($_[0], $_[1]) });
}

*each_squarefree = \&squarefree_each;

sub is_squarefree {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 0
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_square_free($n)
                        : Math::Prime::Util::GMP::moebius($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    my $size = Math::GMPz::Rmpz_sizeinbase($n, 2);

    if ($size > SMALL_NUMBER_MAX_BITS) {
        state $lim_small = _set_int(1e4);
        (bless \$n)->is_prob_squarefree($lim_small) || return $FALSE;

        if ($size > MEDIUM_NUMBER_MAX_BITS) {
            state $lim_large = _set_int(1e6);
            (bless \$n)->is_prob_squarefree($lim_large) || return $FALSE;
        }
    }

    $n = _big2uistr($n) // return $FALSE;

    _is_squarefree($n) ? $TRUE : $FALSE;
}

*is_square_free = \&is_squarefree;

sub is_powerfree {
    my ($n, $k) = @_;

    if (!defined($k)) {    # default to k = 2
        return $n->is_squarefree;
    }

    _valid(\$k);

    $k = _any2ui($$k) || return $FALSE;
    $n = $$n;

    if (FAST_MODE and HAS_PRIME_UTIL and !ref($n)) {
        return (($n > 0 and Math::Prime::Util::is_powerfree($n, $k)) ? $TRUE : $FALSE);
    }

    if (FAST_MODE and $k == 2 and !ref($n)) {
        return (
                (
                 $n > 0
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_square_free($n)
                        : Math::Prime::Util::GMP::moebius($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    if ($k == 2) {
        return ((bless \$n)->is_squarefree);
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return $FALSE;
    }

    if ($k == 1) {
        return $TRUE if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0);
        return $FALSE;
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return (Math::Prime::Util::is_powerfree(Math::GMPz::Rmpz_get_ui($n), $k) ? $TRUE : $FALSE);
    }

    # Optimization for large n
    if (Math::GMPz::Rmpz_sizeinbase($n, 2) > SMALL_NUMBER_MAX_BITS) {
        my ($rem, @f) = _adaptive_trial_factor($n);

        my %factors;
        ++$factors{$_} for @f;

        foreach my $e (values %factors) {
            $e < $k
              or return $FALSE;
        }

        if (Math::GMPz::Rmpz_cmp_ui($rem, 1) == 0) {
            return $TRUE;
        }

        $n = $rem;
    }

    foreach my $pp (_factor_exp($n)) {
        $pp->[1] < $k
          or return $FALSE;
    }

    return $TRUE;
}

sub is_totient {    # OEIS: A002202
    my ($x) = @_;
    $x = $$x;
    __is_int__($x)
      && Math::Prime::Util::GMP::is_totient(_big2uistr($x) // return $FALSE) ? $TRUE : $FALSE;
}

sub is_practical {    # OEIS: A005153
    my ($x) = @_;

    $x = $$x;

    # XXX: requires the GitHub version of Math::Prime::Util::GMP.
    __is_int__($x)
      && Math::Prime::Util::GMP::is_practical(_big2uistr($x) // return $FALSE) ? $TRUE : $FALSE;
}

sub is_fibonacci {    # OEIS: A010056
    my ($n) = @_;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;

    state $t = Math::GMPz::Rmpz_init_nobless();

    # 5*n^2 +/- 4 must be a perfect square

    Math::GMPz::Rmpz_mul($t, $n, $n);
    Math::GMPz::Rmpz_mul_ui($t, $t, 5);

    Math::GMPz::Rmpz_sub_ui($t, $t, 4);
    Math::GMPz::Rmpz_perfect_square_p($t)
      && return $TRUE;

    Math::GMPz::Rmpz_add_ui($t, $t, 8);
    Math::GMPz::Rmpz_perfect_square_p($t)
      && return $TRUE;

    $FALSE;
}

*is_fib = \&is_fibonacci;

sub is_lucas {    # OEIS: A102460
    my ($n) = @_;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 1) >= 0
      or return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 2) <= 0
      and return $TRUE;

    if (Math::GMPz::Rmpz_fits_ulong_p($n)) {
        $n = Math::GMPz::Rmpz_get_ui($n);

        state $lookup = {'3' => undef};
        state $x      = 1;
        state $y      = 3;

        while ($y < $n) {
            ($x, $y) = ($y, $x + $y);
            undef $lookup->{$y};
        }
        if (exists $lookup->{$n}) {
            return $TRUE;
        }
        return $FALSE;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();

    if (Math::GMPz::Rmpz_sizeinbase($n, 10) <= 1000) {

        state $lookup = {'3' => undef};
        state $x      = Math::GMPz::Rmpz_init_set_ui_nobless(1);
        state $y      = Math::GMPz::Rmpz_init_set_ui_nobless(3);

        while (Math::GMPz::Rmpz_cmp($y, $n) < 0) {
            Math::GMPz::Rmpz_set($t, $y);
            Math::GMPz::Rmpz_add($y, $x, $y);
            Math::GMPz::Rmpz_set($x, $t);
            undef $lookup->{Math::GMPz::Rmpz_get_str($y, 10)};
        }

        if (exists $lookup->{Math::GMPz::Rmpz_get_str($n, 10)}) {
            return $TRUE;
        }
        return $FALSE;
    }

    state $log_phi = do {
        my $t = Math::MPFR::Rmpfr_init2_nobless(64);
        my $r = ${__PACKAGE__->phi};
        Math::MPFR::Rmpfr_log($t, $r, $ROUND);
        $t;
    };

    my $f = _any2mpfr($n) // return $FALSE;

    Math::MPFR::Rmpfr_log($f, $f, $ROUND);
    Math::MPFR::Rmpfr_div($f, $f, $log_phi, $ROUND);
    Math::MPFR::Rmpfr_round($f, $f);

    Math::GMPz::Rmpz_lucnum_ui($t, Math::MPFR::Rmpfr_get_ui($f, $ROUND));

    (Math::GMPz::Rmpz_cmp($t, $n) == 0) ? $TRUE : $FALSE;
}

sub is_cyclic {    # OEIS: A003277
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {

        $n <= 0 and return $FALSE;

        if ($n <= 3) {    # 1,2,3 are terms
            return $TRUE;
        }

        if (
            !($n & 1)                                                                 # 2 only even
            || !($n % 9)   || !($n % 25) || !($n % 49)                                # not square-free
            || !($n % 21)  || !($n % 39) || !($n % 55) || !($n % 57) || !($n % 93)    # q = 1 mod p
            || !($n % 121) || !($n % 169)                                             # not square-free
            || !($n % 111) || !($n % 129) || !($n % 155) || !($n % 183)               # q = 1 mod p
          ) {
            return $FALSE;
        }

        if (
            (
             HAS_PRIME_UTIL
             ? Math::Prime::Util::gcd(Math::Prime::Util::euler_phi($n), $n)
             : Math::Prime::Util::GMP::gcd(Math::Prime::Util::GMP::totient($n), $n)
            ) == 1
          ) {
            return $TRUE;
        }

        return $FALSE;
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return $FALSE;

    if (!Math::GMPz::Rmpz_odd_p($n)) {
        if (Math::GMPz::Rmpz_cmp_ui($n, 2) == 0) {
            return $TRUE;
        }
        return $FALSE;
    }

    foreach my $k (9, 25, 49, 21, 39, 55, 57, 93, 121, 169, 111, 129, 155, 183) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $k)) {
            return $FALSE;
        }
    }

    my $nstr = Math::GMPz::Rmpz_get_str($n, 10);

    if (Math::GMPz::Rmpz_sizeinbase($n, 10) >= 40) {
        my ($r, @factors) = _adaptive_trial_factor($n);

        my %seen;
        foreach my $p (@factors) {
            if ($seen{$p}++) {    # n must be squarefree
                return $FALSE;
            }
            if (Math::Prime::Util::GMP::gcd($nstr, $p >> 1) != 1) {
                return $FALSE;
            }
        }

        $nstr = Math::GMPz::Rmpz_get_str($r, 10);
    }

    (Math::Prime::Util::GMP::gcd($nstr, Math::Prime::Util::GMP::totient($nstr)) eq '1') ? $TRUE : $FALSE;
}

sub _verify_small_factors {
    my ($remainder_ref, $omega_ref, $check_conditions, $from, $left, $right) = @_;

    $from  //= 3;
    $left  //= 1e5;
    $right //= 1e6;

    my ($r, @factors) = _adaptive_trial_factor($$remainder_ref, $from, $left, $right);

    if (@factors) {
        $check_conditions->(@factors) || return 0;

        if (Math::GMPz::Rmpz_cmp_ui($r, 1) == 0) {
            return 1;
        }
        elsif (Math::GMPz::Rmpz_sizeinbase($r, 2) <= SMALL_NUMBER_MAX_BITS) {
            return $check_conditions->(_factor($r)) ? 1 : 0;
        }

        $$omega_ref += scalar(@factors);
        $$remainder_ref = $r;
    }

    return undef;    # inconclusive
}

# Shared logic for the final factorization and omega count verification
sub _verify_heavy_factors {
    my ($remainder, $factors_ref, $omega_ref, $check_conditions, $factor_methods) = @_;

    my @factors = @$factors_ref;

    if (scalar(@factors) == 1 and Math::GMPz::Rmpz_sizeinbase($remainder, 10) > SPECIAL_FACTORS_MIN) {
        @factors = map { $$_ } @{_set_int($remainder)->special_factors};
    }

    if (scalar(@factors) > 1) {
        my %seen;
        my @composites;

        foreach my $f (@factors) {
            return 0 if $seen{$f}++;    # not squarefree

            if (_is_prob_prime($f, 1)) {
                $check_conditions->($f) || return 0;
                ++$$omega_ref;
            }
            else {
                push @composites, $f;
            }
        }

        return 1 unless @composites;
        @factors = @composites;
    }

    # Factorize remaining composites
    foreach my $method (@$factor_methods, sub { _factor($_[0]) }) {
        @factors = map { _is_prob_prime($_, 1) ? $_ : $method->($_) } @factors;
    }

    $$omega_ref += scalar(@factors);

    ($$omega_ref >= 3 and $check_conditions->(@factors)) ? 1 : 0;
}

sub is_carmichael {    # OEIS: A002997
    my ($n) = @_;

    $n = $$n;

    # Fast path for native integers
    if (FAST_MODE and !ref($n)) {
        return (
                HAS_PRIME_UTIL
                ? Math::Prime::Util::is_carmichael($n)
                : Math::Prime::Util::GMP::is_carmichael($n)
               ) ? $TRUE : $FALSE;
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    # Small or even
    Math::GMPz::Rmpz_odd_p($n)            or return $FALSE;
    Math::GMPz::Rmpz_cmp_ui($n, 561) >= 0 or return $FALSE;

    state $nm1 = Math::GMPz::Rmpz_init_nobless();
    state $pm1 = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_sub_ui($nm1, $n, 1);

    # Divisible by a small square
    foreach my $k (@SMALL_PRIMES_20) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $k)) {
            return $FALSE if Math::GMPz::Rmpz_divisible_ui_p($n, $k * $k);
            return $FALSE unless Math::GMPz::Rmpz_divisible_ui_p($nm1, $k - 1);
        }
    }

    my $check_conditions = sub {
        my %seen;
        foreach my $p (@_) {
            return 0 if $seen{$p}++;    # not squarefree

            if (ref($p) eq 'Math::GMPz') {
                Math::GMPz::Rmpz_sub_ui($pm1, $p, 1);
                Math::GMPz::Rmpz_divisible_p($nm1, $pm1) || return 0;
            }
            elsif (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_divisible_ui_p($nm1, $p - 1) || return 0;
            }
            else {
                Math::GMPz::Rmpz_set_str($pm1, "$p", 10);
                Math::GMPz::Rmpz_sub_ui($pm1, $pm1, 1);
                Math::GMPz::Rmpz_divisible_p($nm1, $pm1) || return 0;
            }
        }
        return 1;
    };

    my $omega     = 0;
    my $remainder = $n;

    if (Math::GMPz::Rmpz_sizeinbase($remainder, 10) >= 200) {

        my $conclusion = _verify_small_factors(\$remainder, \$omega, $check_conditions);

        if (defined($conclusion)) {
            return ($conclusion ? $TRUE : $FALSE);
        }
    }

    # Check: p^(n-1) == 1 (mod n) for random primes
    foreach my $k (1 .. 7) {
        my $p = _random_prime(CORE::int(CORE::sqrt(ULONG_MAX)));
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $p)) {
            return $FALSE if Math::GMPz::Rmpz_divisible_ui_p($n, $p * $p);
            return $FALSE unless Math::GMPz::Rmpz_divisible_ui_p($nm1, $p - 1);
        }
        else {
            Math::GMPz::Rmpz_set_ui($pm1, $p);
            Math::GMPz::Rmpz_powm($pm1, $pm1, $nm1, $n);
            return $FALSE unless Math::GMPz::Rmpz_cmp_ui($pm1, 1) == 0;
        }
    }

    # Fermat pseudoprime to base 2
    Math::GMPz::Rmpz_powm($pm1, $TWO, $nm1, $n);
    return $FALSE unless Math::GMPz::Rmpz_cmp_ui($pm1, 1) == 0;

    # Final heavy factorization via shared helper
    my @factors = _miller_factor($n);

    my @factor_methods =
      (sub { my ($n) = @_; _miller_factor(_any2mpz($n), 4 * CORE::length("$n")) }, sub { my ($n) = @_; _lucas_factor(_any2mpz($n), -1, CORE::length("$n")) },);

    _verify_heavy_factors($remainder, \@factors, \$omega, $check_conditions, \@factor_methods) ? $TRUE : $FALSE;
}

sub is_absolute_euler_psp {    # OEIS: A033181
    my ($n) = @_;

    $n = $$n;

    # Fast path for native integers
    if (FAST_MODE and !ref($n)) {
        (
         HAS_PRIME_UTIL
         ? (Math::Prime::Util::is_carmichael($n) && Math::Prime::Util::is_euler_pseudoprime($n, 2))
         : (Math::Prime::Util::GMP::is_carmichael($n) && Math::Prime::Util::GMP::is_euler_pseudoprime($n, 2))
        )
          || return $FALSE;
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    Math::GMPz::Rmpz_odd_p($n)             or return $FALSE;
    Math::GMPz::Rmpz_cmp_ui($n, 1729) >= 0 or return $FALSE;

    state $nm1   = Math::GMPz::Rmpz_init_nobless();
    state $nm1d2 = Math::GMPz::Rmpz_init_nobless();
    state $pm1   = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($nm1, $n, 1);
    Math::GMPz::Rmpz_div_2exp($nm1d2, $nm1, 1);
    return $FALSE unless Math::GMPz::Rmpz_even_p($nm1d2);

    # Divisible by a small square
    foreach my $k (@SMALL_PRIMES_20) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $k)) {
            return $FALSE if Math::GMPz::Rmpz_divisible_ui_p($n, $k * $k);
            return $FALSE unless Math::GMPz::Rmpz_divisible_ui_p($nm1d2, $k - 1);
        }
    }

    my $check_conditions = sub {
        my %seen;
        foreach my $p (@_) {
            return 0 if $seen{$p}++;    # not squarefree

            if (ref($p) eq 'Math::GMPz') {
                Math::GMPz::Rmpz_sub_ui($pm1, $p, 1);
                Math::GMPz::Rmpz_divisible_p($nm1d2, $pm1) || return 0;
            }
            elsif (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_divisible_ui_p($nm1d2, $p - 1) || return 0;
            }
            else {
                Math::GMPz::Rmpz_set_str($pm1, "$p", 10);
                Math::GMPz::Rmpz_sub_ui($pm1, $pm1, 1);
                Math::GMPz::Rmpz_divisible_p($nm1d2, $pm1) || return 0;
            }
        }
        return 1;
    };

    my $omega     = 0;
    my $remainder = $n;

    if (Math::GMPz::Rmpz_sizeinbase($remainder, 10) >= 200) {

        my $conclusion = _verify_small_factors(\$remainder, \$omega, $check_conditions);

        if (defined($conclusion)) {
            return ($conclusion ? $TRUE : $FALSE);
        }
    }

    # Random primes check
    foreach my $k (1 .. 7) {
        my $p = _random_prime(CORE::int(CORE::sqrt(ULONG_MAX)));
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $p)) {
            return $FALSE if Math::GMPz::Rmpz_divisible_ui_p($n, $p * $p);
            return $FALSE unless Math::GMPz::Rmpz_divisible_ui_p($nm1d2, $p - 1);
        }
        else {
            Math::GMPz::Rmpz_set_ui($pm1, $p);
            Math::GMPz::Rmpz_powm($pm1, $pm1, $nm1d2, $n);
            return $FALSE unless (Math::GMPz::Rmpz_cmp_ui($pm1, 1) == 0 or Math::GMPz::Rmpz_cmp($pm1, $nm1) == 0);
        }
    }

    # Euler pseudoprime to base 2
    if (!Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return $FALSE unless Math::Prime::Util::GMP::is_euler_pseudoprime(Math::GMPz::Rmpz_get_str($n, 10), 2);
    }

    # Final heavy factorization via shared helper
    my @factors = _miller_factor($n);

    my @factor_methods =
      (sub { my ($n) = @_; _miller_factor(_any2mpz($n), 4 * CORE::length("$n")) }, sub { my ($n) = @_; _lucas_factor(_any2mpz($n), -1, CORE::length("$n")) },);

    _verify_heavy_factors($remainder, \@factors, \$omega, $check_conditions, \@factor_methods) ? $TRUE : $FALSE;
}

*is_abs_euler_psp = \&is_absolute_euler_psp;

sub is_imprimitive_carmichael {    # OEIS: A328935
    my ($n) = @_;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_cmp_ui($n, 294409) >= 0 or return $FALSE;

    if (HAS_PRIME_UTIL && Math::GMPz::Rmpz_fits_ulong_p($n)) {

        $n = Math::GMPz::Rmpz_get_ui($n);
        Math::Prime::Util::is_carmichael($n) || return $FALSE;
        my @factors = map { $_ - 1 } Math::Prime::Util::factor($n);

        my $gcd = Math::Prime::Util::gcd(@factors);
        my $lcm = Math::Prime::Util::lcm(@factors);

        return ((($gcd * $gcd) > $lcm) ? $TRUE : $FALSE);
    }

    (bless \$n)->is_carmichael() || return $FALSE;

    my @factors = map { Math::Prime::Util::GMP::subint($_, 1) } map { _factor($_) } _miller_factor($n);

    my $gcd = Math::Prime::Util::GMP::gcd(@factors);
    my $lcm = Math::Prime::Util::GMP::lcm(@factors);

    state $x = Math::GMPz::Rmpz_init_nobless();
    state $y = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_str($x, $gcd, 10);
    Math::GMPz::Rmpz_set_str($y, $lcm, 10);

    Math::GMPz::Rmpz_mul($x, $x, $x);
    Math::GMPz::Rmpz_cmp($x, $y) > 0 ? $TRUE : $FALSE;
}

sub is_lucas_carmichael {    # OEIS: A006972
    my ($n) = @_;

    $n = $$n;

    # ---------------------------------------------------------
    # Fast path for native integers
    # ---------------------------------------------------------
    if (FAST_MODE and !ref($n) and $n < ULONG_MAX) {

        return $FALSE unless $n >= 399 && $n % 2 != 0;

        foreach my $p (@SMALL_PRIMES_20) {
            if ($n % $p == 0) {
                return $FALSE if $n % ($p * $p) == 0;
                return $FALSE unless ($n + 1) % ($p + 1) == 0;
            }
        }

        my @factors =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::factor($n)
          : Math::Prime::Util::GMP::factor($n);

        return $FALSE if scalar(@factors) <= 2;

        my %seen;
        foreach my $p (@factors) {
            return $FALSE if $seen{$p}++;
            return $FALSE unless (($n + 1) % ($p + 1) == 0);
        }

        return $TRUE;
    }

    # ---------------------------------------------------------
    # GMPz Initialization & Base Checks
    # ---------------------------------------------------------
    $n = _sanitize_mpz($n, 0) // return $FALSE;

    Math::GMPz::Rmpz_odd_p($n)            or return $FALSE;
    Math::GMPz::Rmpz_cmp_ui($n, 399) >= 0 or return $FALSE;

    state $np1 = Math::GMPz::Rmpz_init_nobless();
    state $pp1 = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_add_ui($np1, $n, 1);

    # Divisible by a small square
    foreach my $p (@SMALL_PRIMES_20) {
        if (Math::GMPz::Rmpz_divisible_ui_p($n, $p)) {
            return $FALSE if Math::GMPz::Rmpz_divisible_ui_p($n, $p * $p);
            return $FALSE unless Math::GMPz::Rmpz_divisible_ui_p($np1, $p + 1);
        }
    }

    # my $nstr = Math::GMPz::Rmpz_get_str($n, 10);

    # No Lucas-Carmichael number is known that is also a Carmichael number or a Fermat base-2 pseudoprime.
    # However, it is conjectured that infinitely many such numbers exist.

    # If there exists a squarefree composite number N such that p-1 | N-1 and
    # p+1 | N+1 for every p|N, then N must have an odd number ≥ 5 of prime factors.
    # See: https://www.sciencedirect.com/science/article/pii/S0022314X14002108

    # if (Math::Prime::Util::GMP::is_pseudoprime($nstr, 2)) {
    #     return $FALSE;     # no counter-example is known
    # }

    # Closure for Lucas-Korselt criterion: p+1 | n+1
    my $check_conditions = sub {
        my %seen;
        foreach my $p (@_) {
            return 0 if $seen{$p}++;    # not squarefree

            if (ref($p) eq 'Math::GMPz') {
                Math::GMPz::Rmpz_add_ui($pp1, $p, 1);
                Math::GMPz::Rmpz_divisible_p($np1, $pp1) || return 0;
            }
            elsif (FAST_MODE and $p < ULONG_MAX) {
                Math::GMPz::Rmpz_divisible_ui_p($np1, $p + 1) || return 0;
            }
            else {
                Math::GMPz::Rmpz_set_str($pp1, "$p", 10);
                Math::GMPz::Rmpz_add_ui($pp1, $pp1, 1);
                Math::GMPz::Rmpz_divisible_p($np1, $pp1) || return 0;
            }
        }
        return 1;
    };

    my $omega     = 0;
    my $remainder = $n;

    # ---------------------------------------------------------
    # Adaptive Trial Division
    # ---------------------------------------------------------
    if (!Math::GMPz::Rmpz_fits_ulong_p($remainder)) {

        my $conclusion = _verify_small_factors(\$remainder, \$omega, $check_conditions);

        if (defined($conclusion)) {
            return ($conclusion ? $TRUE : $FALSE);
        }
    }

    # ---------------------------------------------------------
    # Final Lucas Factorization & Verification
    # ---------------------------------------------------------
    my @factors = _lucas_factor($remainder);

    my @factor_methods = (sub { my ($n) = @_; _lucas_factor(_any2mpz($n), undef, CORE::length("$n")) },);

    _verify_heavy_factors($remainder, \@factors, \$omega, $check_conditions, \@factor_methods) ? $TRUE : $FALSE;
}

sub is_fundamental {
    my ($x) = @_;
    $x = $$x;
    __is_int__($x)
      && Math::Prime::Util::GMP::is_fundamental(_big2uistr($x) // return $FALSE) ? $TRUE : $FALSE;
}

sub is_smooth_over_prod {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _sanitize_mpz($$n, 0) // return $FALSE;
    $k = _sanitize_mpz($$k, 1) // return $FALSE;

    return $FALSE if Math::GMPz::Rmpz_sgn($n) <= 0;
    return $FALSE if Math::GMPz::Rmpz_sgn($k) <= 0;
    return $TRUE  if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;

    state $g = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_gcd($g, $n, $k);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
        return $FALSE;
    }

    my $t = Math::GMPz::Rmpz_init_set($n);

    while (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
        Math::GMPz::Rmpz_remove($t, $t, $g);
        return $TRUE if Math::GMPz::Rmpz_cmp_ui($t, 1) == 0;
        Math::GMPz::Rmpz_gcd($g, $t, $g);
    }

    return $FALSE;
}

sub is_smooth {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _sanitize_mpz($$n, 0) // return $FALSE;
    $k = _any2ui($$k);

    if (!defined($k) or $k > 1e8) {
        return $_[0]->gpf->le($_[1]);
    }

    return $FALSE if Math::GMPz::Rmpz_sgn($n) <= 0;
    return $TRUE  if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;
    return $FALSE if $k <= 1;

    my $B = _cached_primorial($k);

    state $g = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($g, $n, $B);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
        return $FALSE;
    }

    my $t = Math::GMPz::Rmpz_init_set($n);

    while (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
        Math::GMPz::Rmpz_remove($t, $t, $g);
        return $TRUE if Math::GMPz::Rmpz_cmp_ui($t, 1) == 0;
        Math::GMPz::Rmpz_gcd($g, $t, $g);
    }

    return $FALSE;
}

sub is_rough {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _sanitize_mpz($$n, 0) // return $FALSE;
    $k = _any2ui($$k);

    if (!defined($k) or $k > 1e8) {
        return $_[0]->lpf->ge($_[1]);
    }

    --$k;

    return $FALSE if Math::GMPz::Rmpz_sgn($n) <= 0;
    return $TRUE  if $k <= 1;
    return $TRUE  if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;

    my $B = _cached_primorial($k);

    state $g = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($g, $n, $B);

    (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) ? return $FALSE : return $TRUE;
}

sub smooth_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->smooth_count($to)->sub($k->smooth_count($from->dec));
    }

    my $n = _any2mpz($$from) // return ZERO;
    $k = _any2ui($$k) // return ZERO;

    if ($k < 2 or Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, $k) <= 0) {
        return _set_int($n);
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        my $r = Math::Prime::Util::smooth_count(Math::GMPz::Rmpz_get_ui($n), $k);
        return bless \$r;
    }

    # Array of all primes up to $k
    my @P = @{_primes(2, $k)};

    # ========================================================================
    # TIER 1 – native (UV) arithmetic
    # psi_small($x, $i): count of P[$i]-smooth numbers ≤ $x,  $x a plain UV
    # ========================================================================
    my $psi_small = sub {
        my ($x, $i) = @_;    # $x is a native UV

        if (HAS_PRIME_UTIL) {
            return Math::Prime::Util::smooth_count($x, $P[$i]);
        }

        $x || return 0;

        # Count powers of 2 in [1..$x] = number of bits in $x.
        $i || return 1 + Math::Prime::Util::GMP::logint($x, 2);

        ($x <= $P[$i])
          ? $x
          : (__SUB__->($x, $i - 1) + __SUB__->(Math::Prime::Util::GMP::divint($x, $P[$i]), $i));
    };

    # ========================================================================
    # TIER 2 – Math::GMPz arithmetic
    # psi_big($x, $i): same, but $x is a Math::GMPz (x > ULONG_MAX)
    # ========================================================================
    my $count = sub {
        my ($x, $i) = @_;    # $x is a Math::GMPz

        # Hand off to the fast native tier as soon as possible
        if (FAST_MODE and $x < ULONG_MAX) {
            return $psi_small->($x, $i);
        }

        $x || return 0;

        # floor(log2 $x) + 1 = # of 2-smooth numbers ≤ $x.
        $i || return 1 + Math::Prime::Util::GMP::logint($x, 2);

        Math::Prime::Util::GMP::addint(__SUB__->($x, $i - 1), __SUB__->(Math::Prime::Util::GMP::divint($x, $P[$i]), $i));
      }
      ->(Math::GMPz::Rmpz_get_str($n, 10), $#P);

    _set_int($count);
}

# Finds the smallest m >= 0 such that count_func(m) >= target
sub inverse_count {
    my ($n, $count_func, $bsearch_method) = @_;
    $bsearch_method //= 'bsearch_min';

    goto &nan if $n->is_neg;

    my $lo = _set_int(0);
    my $hi = _set_int(1);

    # Exponential search to find a sufficient upper bound
    while ($count_func->($hi)->lt($n)) {
        $lo = $hi;
        $hi = $hi->mul(TWO);
    }

    $lo->$bsearch_method(
        $hi,
        Sidef::Types::Block::Block->new(
            code => sub {
                $count_func->($_[0])->cmp($n);
            }
        )
    );
}

sub rough_count {
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->rough_count($to)->sub($k->rough_count($from->dec));
    }

    my $n = _any2mpz($$from) // return ZERO;
    $k = _any2ui($$k) // return ZERO;

    if (Math::GMPz::Rmpz_sgn($n) <= 0) {
        return ZERO;
    }

    if ($k <= 2) {
        return _set_int($n);
    }

    if (Math::GMPz::Rmpz_cmp_ui($n, $k) < 0) {
        return TWO if (Math::GMPz::Rmpz_cmp_ui($n, $k) == 0);
        return ONE;
    }

    if ($k > 50 and $USE_PRIMECOUNT and Math::GMPz::Rmpz_sizeinbase($n, 2) <= 63 and $n > PRIMECOUNT_MIN) {
        my $c     = _prime_count($k - 1);
        my $count = `primecount --phi $n $c`;

        if ($? == 0 and defined($count)) {
            chomp $count;
            if ($count) {    # make sure count is not zero
                return _set_int($count);
            }
        }
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        my $r = Math::Prime::Util::legendre_phi(Math::GMPz::Rmpz_get_ui($n), Math::Prime::Util::prime_count($k - 1));
        return bless \$r;
    }

    my %cache;
    my @P = @{_primes(2, $k - 1)};

    my $result = sub {
        my ($n, $x) = @_;

        # Optimization for native n
        if (FAST_MODE and (!ref($n) or Math::GMPz::Rmpz_fits_slong_p($n))) {

            $n = Math::GMPz::Rmpz_get_ui($n) if ref($n);

            if (HAS_PRIME_UTIL) {
                return Math::Prime::Util::legendre_phi($n, $x);
            }

            return (
                sub {
                    my ($n, $x) = @_;

                    # Meissel-Lehmer truncation (The Sublinear Secret)
                    if ($P[$x - 1]**2 > $n) {
                        return _prime_count($n) - $x + 1;
                    }

                    return $cache{$x}{$n}
                      if exists $cache{$x}{$n};

                    # Initial count: odd numbers ≤ n
                    my $count = $n - ($n >> 1);

                    # Inclusion-Exclusion principle
                    for my $j (1 .. $x - 1) {
                        last if ($P[$j] > $n);
                        $count -= __SUB__->(
                                            (
                                             HAS_PRIME_UTIL
                                             ? Math::Prime::Util::divint($n, $P[$j])
                                             : Math::Prime::Util::GMP::divint($n, $P[$j])
                                            ),
                                            $j
                                           );
                    }

                    $cache{$x}{$n} = $count;
                  }
                  ->($n, $x)
            );
        }

        my $n_str = Math::GMPz::Rmpz_get_str($n, 10);

        return $cache{$x}{$n_str}
          if exists $cache{$x}{$n_str};

        # Initial count: odd numbers ≤ n
        my $count = Math::GMPz::Rmpz_init_set($n);
        Math::GMPz::Rmpz_div_2exp($count, $count, 1);
        Math::GMPz::Rmpz_sub($count, $n, $count);

        my $t = Math::GMPz::Rmpz_init();

        # Inclusion-Exclusion principle
        foreach my $j (1 .. $x - 1) {

            Math::GMPz::Rmpz_div_ui($t, $n, $P[$j]);

            my $r = __SUB__->($t, $j);

            ref($r)
              ? Math::GMPz::Rmpz_sub($count, $count, $r)
              : Math::GMPz::Rmpz_sub_ui($count, $count, $r);
        }

        $cache{$x}{$n_str} =
            Math::GMPz::Rmpz_fits_ulong_p($count)
          ? Math::GMPz::Rmpz_get_ui($count)
          : $count;
      }
      ->($n, scalar @P);

    if (ref($result)) {
        return bless \$result;
    }

    _set_int($result);
}

sub nth_smooth {
    my ($n, $k) = @_;
    _valid(\$k);

    my $n_z = _any2mpz($$n) // goto &nan;
    my $k_z = _any2mpz($$k) // goto &nan;

    if (   Math::GMPz::Rmpz_sgn($n_z) <= 0
        or Math::GMPz::Rmpz_cmp_ui($k_z, 1) <= 0) {
        goto &nan;
    }

    $n->inverse_count(sub { $k->smooth_count($_[0]) }, 'bsearch_min');
}

sub nth_rough {
    my ($n, $k) = @_;
    _valid(\$k);

    my $n_z = _any2mpz($$n) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($n_z) <= 0) {
        goto &nan;
    }

    $n->inverse_count(sub { $k->rough_count($_[0]) }, 'bsearch_min');
}

sub next_smooth {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_zero && return ONE;
    my $count = $k->smooth_count($n);
    $count = $count->inc;
    $count->nth_smooth($k);
}

sub next_rough {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->is_zero && return ONE;
    my $count = $k->rough_count($n);
    $count = $count->inc;
    $count->nth_rough($k);
}

sub prev_smooth {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->le(ONE) && goto &nan;
    my $count = $k->smooth_count($n);
    $count = $count->dec if $n->is_smooth($k);
    $count->nth_smooth($k);
}

sub prev_rough {
    my ($n, $k) = @_;
    _valid(\$k);
    $n->le(ONE) && goto &nan;
    my $count = $k->rough_count($n);
    $count = $count->dec if $n->is_rough($k);
    $count->nth_rough($k);
}

sub legendre_phi {
    my ($n, $k) = @_;
    _valid(\$k);
    $k->inc->nth_prime->rough_count($n);
}

sub smooth_part {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _sanitize_mpz($$n, 0) // goto &nan;
    $k = _any2ui($$k)          // goto &nan;

    return ZERO if Math::GMPz::Rmpz_sgn($n) <= 0;
    return ONE  if $k <= 1;
    return ONE  if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;

    my $B = _cached_primorial($k);

    state $g = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($g, $n, $B);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
        return ONE;
    }

    my $t = Math::GMPz::Rmpz_init_set($n);

    while (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
        Math::GMPz::Rmpz_remove($t, $t, $g);
        return _set_int($n) if Math::GMPz::Rmpz_cmp_ui($t, 1) == 0;
        Math::GMPz::Rmpz_gcd($g, $t, $g);
    }

    Math::GMPz::Rmpz_divexact($t, $n, $t);
    bless \$t;
}

sub smooth_divisors {
    my ($k, $n) = @_;
    $k->smooth_part($n)->divisors;
}

sub rough_part {
    my ($k, $n) = @_;

    _valid(\$n);

    $n = _sanitize_mpz($$n, 0) // goto &nan;
    $k = _any2ui($$k)          // goto &nan;

    --$k;

    return ZERO         if Math::GMPz::Rmpz_sgn($n) <= 0;
    return _set_int($n) if $k <= 1;
    return ONE          if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;

    my $B = _cached_primorial($k);

    state $g = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_gcd($g, $n, $B);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) == 0) {
        return _set_int($n);
    }

    my $t = Math::GMPz::Rmpz_init_set($n);

    while (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {
        Math::GMPz::Rmpz_remove($t, $t, $g);
        return ONE if Math::GMPz::Rmpz_cmp_ui($t, 1) == 0;
        Math::GMPz::Rmpz_gcd($g, $t, $g);
    }

    bless \$t;
}

sub rough_divisors {
    my ($k, $n) = @_;
    $k->rough_part($n)->divisors;
}

sub is_prob_squarefree {
    my ($n, $k) = @_;

    if (!defined($k)) {
        state %cache;
        foreach my $k (2 .. 7) {

            $n->is_prob_squarefree(_set_int(10**$k))
              || return $FALSE;

            my $t = (
                $cache{$k} //= do {
                    my $z = Math::GMPz::Rmpz_init();
                    Math::GMPz::Rmpz_ui_pow_ui($z, 10, 3 * $k);
                    $z;
                }
            );

            __cmp__($$n, $t) < 0 and return $TRUE;
        }

        return $TRUE;
    }

    _valid(\$k);
    __is_int__($$n) || return $FALSE;

    $n = _any2mpz($$n, 0) // return $FALSE;
    $k = _any2ui($$k)     // return $FALSE;

    return $FALSE if Math::GMPz::Rmpz_sgn($n) <= 0;
    return $FALSE if $k <= 0;
    return $TRUE  if Math::GMPz::Rmpz_cmp_ui($n, 1) == 0;
    return $FALSE if Math::GMPz::Rmpz_perfect_power_p($n);

    my $B = _cached_primorial($k);

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $g = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_gcd($g, $n, $B);

    if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

        return $TRUE if Math::GMPz::Rmpz_cmp($g, $n) == 0;

        Math::GMPz::Rmpz_divexact($t, $n, $g);
        Math::GMPz::Rmpz_gcd($g, $t, $g);

        # Divisible by a small square
        return $FALSE if Math::GMPz::Rmpz_cmp_ui($g, 1) > 0;

        # k-rough part is a perfect power
        return $FALSE if Math::GMPz::Rmpz_perfect_power_p($t);
    }

    return $TRUE;
}

sub is_cubefree {
    $_[0]->is_powerfree(THREE);
}

sub is_cubefull {
    $_[0]->is_powerful(THREE);
}

sub is_squarefull {
    my ($n) = @_;
    $n->is_powerful;
}

sub __is_power__ {
    my ($n, $k) = @_;

    # $n is a Math::GMPz object
    # $k is a native signed integer

    # Everything is a first power
    $k == 1 and return 1;

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        return 1;
    }

    # Return true when `n` is -1 and `k` is odd
    if ($k % 2 and Math::GMPz::Rmpz_cmp_si($n, -1) == 0) {
        return 1;
    }

    # Don't accept a non-positive power
    # Also, when `n` is negative and `k` is even, return faster
    if ($k <= 0 or ($k % 2 == 0 and Math::GMPz::Rmpz_sgn($n) < 0)) {
        return 0;
    }

    # Optimization for perfect squares (thanks to Dana Jacobsen)
    $k == 2 and return Math::GMPz::Rmpz_perfect_square_p($n);

    # Return faster if not a perfect power
    Math::GMPz::Rmpz_perfect_power_p($n) || return 0;

    # Check if n = a^k, for some integer `a`, by taking the k-th root of `n`
    state $t = Math::GMPz::Rmpz_init_nobless();
    !!Math::GMPz::Rmpz_root($t, $n, $k);
}

sub is_power {
    my ($n, $k) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {

        if (defined($k)) {
            _valid(\$k);
            $k = _any2si($$k) // return $FALSE;

            if ($k == 1) {
                return $TRUE;
            }

            if ($n == 1) {
                return $TRUE;
            }

            if ($k % 2 and $n == -1) {
                return $TRUE;
            }

            if ($k <= 0 or ($k % 2 == 0 and $n < 0)) {
                return $FALSE;
            }
        }

        if ($n == 0 or $n == 1 or $n == -1) {
            return $TRUE;
        }

        my $res;
        if (defined($k) and $k == 2) {
            $res =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::is_square($n)
              : Math::Prime::Util::GMP::is_square($n);
        }
        elsif (defined($k)) {
            $res =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::is_power($n, $k)
              : Math::Prime::Util::GMP::is_power($n, $k);
        }
        else {
            $res =
              HAS_PRIME_UTIL
              ? Math::Prime::Util::is_power($n)
              : Math::Prime::Util::GMP::is_power($n);
        }

        return ($res ? $TRUE : $FALSE);
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    if (defined($k)) {
        _valid(\$k);

        $k = _any2si($$k) // return undef;

        return (__is_power__($n, $k) ? $TRUE : $FALSE);
    }

    Math::GMPz::Rmpz_perfect_power_p($n) ? $TRUE : $FALSE;
}

*is_pp            = \&is_power;
*is_pow           = \&is_power;
*is_perfect_power = \&is_power;

sub is_prime_power {
    my ($n) = @_;

    $n = $$n;

    if (FAST_MODE and !ref($n)) {
        return (
                (
                 $n > 1
                   and (
                        HAS_PRIME_UTIL
                        ? Math::Prime::Util::is_prime_power($n)
                        : Math::Prime::Util::GMP::is_prime_power($n)
                       )
                ) ? $TRUE : $FALSE
               );
    }

    (__is_int__($n) && _prime_power_factor($n)) ? $TRUE : $FALSE;
}

sub is_square {
    my ($n, $k) = @_;

    if (FAST_MODE and !ref($$n)) {
        $n = $$n;
        $n < 0 and return $FALSE;
        my $r = (
                 HAS_PRIME_UTIL
                 ? Math::Prime::Util::is_square($n)
                 : Math::Prime::Util::GMP::is_square($n)
                );
        return ($r ? $TRUE : $FALSE);
    }

    $n->is_power(TWO);
}

*is_sqr            = \&is_square;
*is_perfect_square = \&is_square;

sub is_cube {
    $_[0]->is_power(THREE);
}

sub perfect_power_count {    # OEIS: A069623
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 0) // return ZERO;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k == 0 and return ONE;
        state $r = Math::GMPz::Rmpz_init_nobless();
        Math::GMPz::Rmpz_root($r, $n, $k);
        my $r2 =
            Math::GMPz::Rmpz_fits_ulong_p($r)
          ? Math::GMPz::Rmpz_get_ui($r)
          : Math::GMPz::Rmpz_init_set($r);
        return bless \$r2;
    }

    # Formula:
    #   a(n) = n - Sum_{k=1..floor(log_2(n))} μ(k) * (floor(n^(1/k)) - 1)
    #        = 1 - Sum_{k=2..floor(log_2(n))} μ(k) * (floor(n^(1/k)) - 1)

    state $r = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_set_ui($r, 0);

    foreach my $k (2 .. __ilog__($n, 2)) {
        my $mu = (HAS_PRIME_UTIL ? Math::Prime::Util::moebius($k) : Math::Prime::Util::GMP::moebius($k)) || next;
        Math::GMPz::Rmpz_root($t, $n, $k);
        Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        ($mu == 1) ? Math::GMPz::Rmpz_add($r, $r, $t) : Math::GMPz::Rmpz_sub($r, $r, $t);
    }

    Math::GMPz::Rmpz_ui_sub($r, 1, $r);
    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub perfect_power_sum {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 5) // return ZERO;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // goto &nan;
        $k == 0 and return ONE;
        my $r = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_root($r, $n, $k);
        return ((bless \$r)->faulhaber_sum(bless \$k));
    }

    # Formula:
    #   a(n) = faulhaber(n,1) - Sum_{k=1..floor(log_2(n))} μ(k) * (faulhaber(floor(n^(1/k)),k) - 1)
    #        = 1 - Sum_{k=2..floor(log_2(n))} μ(k) * (faulhaber(floor(n^(1/k)),k) - 1)

    my $t = Math::GMPz::Rmpz_init();
    my $r = Math::GMPz::Rmpz_init_set_ui(0);

    foreach my $k (2 .. __ilog__($n, 2)) {
        my $mu = (HAS_PRIME_UTIL ? Math::Prime::Util::moebius($k) : Math::Prime::Util::GMP::moebius($k)) || next;
        Math::GMPz::Rmpz_root($t, $n, $k);
        my $f = ${(bless \$t)->faulhaber_sum(bless \$k)} - 1;
        if (ref($f)) {
            ($mu == 1)
              ? Math::GMPz::Rmpz_add($r, $r, $f)
              : Math::GMPz::Rmpz_sub($r, $r, $f);
        }
        else {
            ($mu == 1)
              ? Math::GMPz::Rmpz_add_ui($r, $r, $f)
              : Math::GMPz::Rmpz_sub_ui($r, $r, $f);
        }
    }

    Math::GMPz::Rmpz_ui_sub($r, 1, $r);
    $r = Math::GMPz::Rmpz_get_ui($r) if Math::GMPz::Rmpz_fits_ulong_p($r);
    bless \$r;
}

sub nth_perfect_power {
    my ($n, $k) = @_;

    if (defined($k)) {
        return $n->ipow($k);
    }

    my $z = _any2mpz($$n) // goto &nan;

    if (Math::GMPz::Rmpz_sgn($z) < 0) {
        goto &nan;
    }

    # Asymptotic Formulae for the n-th Perfect Power
    # https://www.emis.de/journals/JIS/VOL15/Jakimczuk/jak29.html

    bsearch_min(
        $n,
        $n->sqr,
        Sidef::Types::Block::Block->new(
            code => sub {
                $_[0]->perfect_power_count->cmp($n);
            }
        )
    );
}

sub next_perfect_power {
    my ($n, $k) = @_;

    if (defined($k)) {
        return $n->iroot($k)->inc->ipow($k);
    }

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) >= 0 or goto &nan;

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_sqrt($r, $n);
    Math::GMPz::Rmpz_add_ui($r, $r, 1);
    Math::GMPz::Rmpz_mul($r, $r, $r);

    foreach my $k (3 .. __ilog__($n, 2) + 1) {
        Math::GMPz::Rmpz_root($t, $n, $k);
        Math::GMPz::Rmpz_add_ui($t, $t, 1);
        Math::GMPz::Rmpz_pow_ui($t, $t, $k);
        if (Math::GMPz::Rmpz_cmp($t, $r) < 0) {
            Math::GMPz::Rmpz_set($r, $t);
        }
    }

    bless \$r;
}

sub prev_perfect_power {
    my ($n, $k) = @_;

    if (defined($k)) {
        return $n->iroot($k)->dec->ipow($k);
    }

    $n = _any2mpz($$n, 0) // goto &nan;
    Math::GMPz::Rmpz_sgn($n) > 0 or goto &nan;

    state $t = Math::GMPz::Rmpz_init_nobless();
    my $r = Math::GMPz::Rmpz_init();

    if (Math::GMPz::Rmpz_root($r, $n, 2)) {
        Math::GMPz::Rmpz_sub_ui($r, $r, 1);
    }
    Math::GMPz::Rmpz_mul($r, $r, $r);

    foreach my $k (3 .. __ilog__($n, 2)) {
        if (Math::GMPz::Rmpz_root($t, $n, $k)) {
            Math::GMPz::Rmpz_sub_ui($t, $t, 1);
        }
        Math::GMPz::Rmpz_pow_ui($t, $t, $k);
        if (Math::GMPz::Rmpz_cmp($t, $r) > 0) {
            Math::GMPz::Rmpz_set($r, $t);
        }
    }

    bless \$r;
}

sub is_power_of {
    my ($n, $k) = @_;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;
    $k = _sanitize_mpz($$k, 1) // return $FALSE;

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        return $TRUE;
    }

    if (Math::GMPz::Rmpz_cmp_ui($k, 2) == 0) {
        return ((Math::GMPz::Rmpz_popcount($n) == 1) ? $TRUE : $FALSE);
    }

    my $e = __ilog__($n, $k) // return $FALSE;

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_pow_ui($t, $k, $e);

    (Math::GMPz::Rmpz_cmp($t, $n) == 0) ? $TRUE : $FALSE;
}

sub powerful_count {    # count of k-powerful numbers
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->powerful_count($to)->sub($k->powerful_count($from->dec));
    }

    my $n = _any2mpz($$from, 5) // return ZERO;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    $k = _any2ui($$k) // return ZERO;

    if ($k <= 1) {
        return _set_int($n);
    }

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        my $r = Math::Prime::Util::powerful_count(Math::GMPz::Rmpz_get_ui($n), $k);
        return bless \$r;
    }

    _set_int(Math::Prime::Util::GMP::powerful_count(Math::GMPz::Rmpz_get_str($n, 10), $k));
}

sub squarefull_count {
    (TWO)->powerful_count(@_);
}

sub cubefull_count {
    (THREE)->powerful_count(@_);
}

sub powerful_sum {    # sum of k-powerful numbers
    my ($k, $from, $to) = @_;

    _valid(\$from);

    if (defined($to)) {
        _valid(\$to);
        return ZERO if $to->lt($from);
        return $k->powerful_sum($to)->sub($k->powerful_sum($from->dec));
    }

    my $n = _any2mpz($$from, 5) // return ZERO;
    Math::GMPz::Rmpz_sgn($n) > 0 or return ZERO;

    $k = _any2ui($$k) // return ZERO;

    if ($k <= 1) {
        return ((bless \$n)->faulhaber_sum(ONE));
    }

    my $t   = Math::GMPz::Rmpz_init();
    my $sum = Math::GMPz::Rmpz_init_set_ui(0);

    sub {
        my ($m, $r) = @_;

        Math::GMPz::Rmpz_div($t, $n, $m);
        Math::GMPz::Rmpz_root($t, $t, $r);

        if ($r <= $k) {
            my $w = ${(bless \$t)->faulhaber_sum(bless \$r)};
            if (ref($w) eq 'Math::GMPz') {
                Math::GMPz::Rmpz_addmul($sum, $m, $w);
            }
            else {
                Math::GMPz::Rmpz_addmul_ui($sum, $m, $w);
            }
            return;
        }

        my $z = Math::GMPz::Rmpz_init();

        foreach my $v (1 .. Math::GMPz::Rmpz_get_ui($t)) {

            if ($r > $k) {
                (HAS_PRIME_UTIL ? Math::Prime::Util::is_square_free($v) : Math::Prime::Util::GMP::moebius($v)) or next;
                Math::GMPz::Rmpz_gcd_ui($Math::GMPz::NULL, $m, $v) == 1                                        or next;
            }

            Math::GMPz::Rmpz_ui_pow_ui($z, $v, $r);
            Math::GMPz::Rmpz_mul($z, $z, $m);
            __SUB__->($z, $r - 1);
        }
      }
      ->($ONE, 2 * $k - 1);

    $sum = Math::GMPz::Rmpz_get_ui($sum) if Math::GMPz::Rmpz_fits_ulong_p($sum);
    bless \$sum;
}

sub squarefull_sum {
    (TWO)->powerful_sum(@_);
}

sub cubefull_sum {
    (THREE)->powerful_sum(@_);
}

sub is_powerful {
    my ($n, $k) = @_;

    $n = $$n;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2ui($$k) // return $FALSE;
        $k <= 1
          and return (__cmp__($n, 0) > 0 ? $TRUE : $FALSE);
    }
    else {
        $k = 2;
    }

    if (FAST_MODE and !ref($n) and $n < (ULONG_MAX >> 5)) {
        $n > 0 or return $FALSE;

        if (HAS_PRIME_UTIL) {
            return (Math::Prime::Util::is_powerful($n, $k) ? $TRUE : $FALSE);
        }

        foreach my $p (2, 3, 5, 7, 11, 13) {
            if ($n % $p == 0) {
                if ($n % ($p * $p) != 0) {
                    return $FALSE;
                }
                if ($k > 2) {
                    my $v =
                      HAS_PRIME_UTIL
                      ? Math::Prime::Util::valuation($n, $p)
                      : Math::Prime::Util::GMP::valuation($n, $p);
                    if ($v < $k) {
                        return $FALSE;
                    }
                }
            }
        }

        foreach my $pp (_factor_exp($n)) {
            $pp->[1] < $k
              and return $FALSE;
        }
        return $TRUE;
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return $FALSE;

    Math::GMPz::Rmpz_divisible_2exp_p($n, 1)
      and !Math::GMPz::Rmpz_divisible_2exp_p($n, $k)
      and return $FALSE;

    foreach my $p (3, 5, 7, 11, 13) {
        Math::GMPz::Rmpz_divisible_ui_p($n, $p)
          and !Math::GMPz::Rmpz_divisible_ui_p($n, $p * $p)
          and return $FALSE;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_root($t, $n, 2 * $k + 1);

    my $trial_limit = 1e6;
    if (Math::GMPz::Rmpz_fits_ulong_p($t)) {
        $trial_limit = Math::GMPz::Rmpz_get_ui($t);
        $trial_limit = 10**(1 + CORE::int(CORE::log($trial_limit) / CORE::log(10)));
        $trial_limit = 1e2 if ($trial_limit < 1e2);
        $trial_limit = 1e6 if ($trial_limit > 1e6);
    }

    my ($rem, @f) = _primorial_trial_factor($n, $trial_limit);

    my %factors;
    ++$factors{$_} for @f;

    foreach my $e (values %factors) {
        $e < $k and return $FALSE;
    }

    if (Math::GMPz::Rmpz_cmp_ui($rem, 1) == 0) {
        return $TRUE;
    }

    if (my $exp = _power_factor($rem)) {
        return (($exp >= $k) ? $TRUE : $FALSE);
    }

    if (Math::GMPz::Rmpz_cmp_ui($t, $trial_limit) < 0) {
        return $FALSE;
    }

    foreach my $pe (_factor_exp($rem)) {
        $pe->[1] < $k
          and return $FALSE;
    }

    return $TRUE;
}

sub is_achilles {
    my ($n) = @_;
    $n->is_powerful || return $FALSE;
    $n->is_power && return $FALSE;
    return $TRUE;
}

sub is_perfect {
    my ($n) = @_;

    $n = _sanitize_mpz($$n, 0) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) > 0
      or return $FALSE;

    if (Math::GMPz::Rmpz_odd_p($n)) {    # odd case

        # References:
        #   https://www.lirmm.fr/~ochem/opn/
        #   https://en.wikipedia.org/wiki/Perfect_number#Odd_perfect_numbers

        Math::GMPz::Rmpz_sizeinbase($n, 10) > 2190
          or return $FALSE;

        Math::GMPz::Rmpz_divisible_ui_p($n, 105)
          and return $FALSE;

             Math::GMPz::Rmpz_congruent_ui_p($n, 1, 12)
          or Math::GMPz::Rmpz_congruent_ui_p($n, 117, 468)
          or Math::GMPz::Rmpz_congruent_ui_p($n, 81,  324)
          or return $FALSE;

        my $t = _set_int($n)->sigma;

        return ((__cmp__($$t, 2 * $n) == 0) ? $TRUE : $FALSE);
    }

    # Here n is even
    state $m = Math::GMPz::Rmpz_init_nobless();

    my $v     = Math::GMPz::Rmpz_remove($m, $n, $TWO);
    my $scan0 = Math::GMPz::Rmpz_scan0($m, 0);

    $scan0 == $v + 1
      or return $FALSE;

    # n must have the form: 2^(k-1)*(2^k - 1)
    $scan0 == Math::GMPz::Rmpz_popcount($m)
      or return $FALSE;

    _set_int($v + 1)->is_mersenne_prime;
}

sub _power_factor {
    my ($n) = @_;

    $n = _any2mpz($n);
    Math::GMPz::Rmpz_sgn($n) < 0 and return 0;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return Math::Prime::Util::is_power(Math::GMPz::Rmpz_get_ui($n));
    }

    my $e = 0;
    if (Math::GMPz::Rmpz_sizeinbase($n, 2) > 1e4) {

        $n = Math::GMPz::Rmpz_init_set($n);    # copy

        foreach my $i (1 .. 26) {

            my $B = _cached_primorial(1 << $i);

            state $g = Math::GMPz::Rmpz_init_nobless();
            Math::GMPz::Rmpz_gcd($g, $n, $B);

            if (Math::GMPz::Rmpz_cmp_ui($g, 1) > 0) {

                local $USE_YAFU        = 0;
                local $USE_FACTORDB    = 0;
                local $SPECIAL_FACTORS = 0;

                my @factors = _factor(Math::GMPz::Rmpz_get_str($g, 10));
                my @exponents;

                foreach my $f (@factors) {
                    Math::GMPz::Rmpz_set_ui($g, $f);
                    push @exponents, Math::GMPz::Rmpz_remove($n, $n, $g);
                }

                $e =
                  HAS_PRIME_UTIL
                  ? (Math::Prime::Util::gcd($e, @exponents))
                  : (Math::Prime::Util::GMP::gcd($e, @exponents));

                $e == 1 and return 0;

                if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
                    return $e;
                }

                if (Math::GMPz::Rmpz_sizeinbase($n, 2) < 1e4) {
                    last;
                }
            }

            last if (Math::GMPz::Rmpz_cmp($B, $n) > 0);
        }
    }

    my $e2 = Math::Prime::Util::GMP::is_power(Math::GMPz::Rmpz_get_str($n, 10));
    $e2 = Math::Prime::Util::GMP::gcd($e2, $e) if $e > 0;
    return $e2;
}

sub _prime_power_factor {
    my ($n) = @_;

    $n = _any2mpz($n);
    Math::GMPz::Rmpz_sgn($n) < 0 and return 0;

    if (HAS_PRIME_UTIL and Math::GMPz::Rmpz_fits_ulong_p($n)) {
        return Math::Prime::Util::is_prime_power(Math::GMPz::Rmpz_get_ui($n));
    }

    if (Math::GMPz::Rmpz_even_p($n)) {
        my $k = Math::GMPz::Rmpz_scan1($n, 0);
        if ($k + 1 == Math::GMPz::Rmpz_sizeinbase($n, 2)) {
            return $k;
        }
        return 0;
    }

    if (_is_prob_prime($n, 1)) {
        return 1;
    }

    my $pow = _power_factor($n) || return 0;
    state $t = Math::GMPz::Rmpz_init_nobless();
    Math::GMPz::Rmpz_root($t, $n, $pow);
    if (_is_prob_prime(Math::GMPz::Rmpz_get_str($t, 10), 1)) {
        return $pow;
    }
    return 0;
}

sub prime_root {
    my ($n) = @_;

    my $t   = _any2mpz($$n) // return $n;
    my $pow = _prime_power_factor($t) || return $n;

    $pow == 1 and return $n;

    my $r = Math::GMPz::Rmpz_init();

    $pow == 2
      ? Math::GMPz::Rmpz_sqrt($r, $t)
      : Math::GMPz::Rmpz_root($r, $t, $pow);

    bless \$r;
}

sub prime_power {
    my ($n) = @_;
    my $pow = _prime_power_factor($$n) || 1;
    $pow eq '1' ? ONE : (bless \$pow);
}

sub perfect_root {
    my ($n) = @_;

    my $t = _any2mpz($$n) // return $n;
    Math::GMPz::Rmpz_perfect_power_p($t) || return $n;

    my $pow = _power_factor($t) || return $n;
    my $r   = Math::GMPz::Rmpz_init();

    $pow == 2
      ? Math::GMPz::Rmpz_sqrt($r, $t)
      : Math::GMPz::Rmpz_root($r, $t, $pow);

    bless \$r;
}

sub perfect_power {
    my ($n) = @_;
    my $pow = _power_factor($$n) || return ONE;
    bless \$pow;
}

sub next_pow {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpz($$x, 0) // goto &nan;
    $y = _any2mpz($$y, 1) // goto &nan;

    Math::GMPz::Rmpz_sgn($x) <= 0 and return ONE;

    my $log = 1 + (__ilog__($x, $y) // goto &nan);

    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_fits_ulong_p($y)
      ? Math::GMPz::Rmpz_ui_pow_ui($r, Math::GMPz::Rmpz_get_ui($y), $log)
      : Math::GMPz::Rmpz_pow_ui($r, $y, $log);

    bless \$r;
}

*next_power = \&next_pow;

sub prev_pow {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = _any2mpz($$x, 0) // goto &nan;
    $y = _any2mpz($$y, 1) // goto &nan;

    Math::GMPz::Rmpz_sgn($x) <= 0 and goto &nan;

    my $log = (__ilog__($x, $y) // goto &nan);

    my $r = Math::GMPz::Rmpz_init();

    Math::GMPz::Rmpz_fits_ulong_p($y)
      ? Math::GMPz::Rmpz_ui_pow_ui($r, Math::GMPz::Rmpz_get_ui($y), $log)
      : Math::GMPz::Rmpz_pow_ui($r, $y, $log);

    if (Math::GMPz::Rmpz_cmp($r, $x) == 0) {

        if ($log == 0) {
            goto &nan;
        }

        Math::GMPz::Rmpz_divexact($r, $r, $y);
    }

    bless \$r;
}

*prev_power = \&prev_pow;

#
## Is a polygonal number?
#

sub __is_polygonal__ {
    my ($n, $k, $second) = @_;

    # $n is a Math::GMPz object
    # $k is a Math::GMPz object
    # $second is a boolean

    Math::GMPz::Rmpz_sgn($n) || return 1;

    # polygonal_root(n, k)
    #   = ((k - 4) ± sqrt(8 * (k - 2) * n + (k - 4)^2)) / (2 * (k - 2))

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($u, $k, 2);      # u = k-2
    Math::GMPz::Rmpz_mul($t, $n, $u);        # t = n*u
    Math::GMPz::Rmpz_mul_2exp($t, $t, 3);    # t = t*8

    Math::GMPz::Rmpz_sub_ui($u, $u, 2);      # u = u-2
    Math::GMPz::Rmpz_mul($u, $u, $u);        # u = u^2

    Math::GMPz::Rmpz_add($t, $t, $u);        # t = t+u
    Math::GMPz::Rmpz_perfect_square_p($t) || return 0;
    Math::GMPz::Rmpz_sqrt($t, $t);           # t = sqrt(t)

    Math::GMPz::Rmpz_sub_ui($u, $k, 4);      # u = k-4

    $second
      ? Math::GMPz::Rmpz_sub($t, $u, $t)     # t = t-u
      : Math::GMPz::Rmpz_add($t, $t, $u);    # t = t+u

    Math::GMPz::Rmpz_add_ui($u, $u, 2);      # u = u+2
    Math::GMPz::Rmpz_mul_2exp($u, $u, 1);    # u = u*2

    Math::GMPz::Rmpz_divisible_p($t, $u);    # true iff u|t
}

sub is_polygonal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = $$n;
    $k = $$k;

    if (FAST_MODE and !ref($n) and !ref($k) and $k >= 3) {
        return (
                (
                 HAS_PRIME_UTIL
                 ? Math::Prime::Util::is_polygonal($n, $k)
                 : Math::Prime::Util::GMP::is_polygonal($n, $k)
                ) ? $TRUE : $FALSE
               );
    }

    $n = _sanitize_mpz($n, 0) // return $FALSE;
    $k = _sanitize_mpz($k, 1) // return $FALSE;

    __is_polygonal__($n, $k) ? $TRUE : $FALSE;
}

sub is_polygonal2 {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _sanitize_mpz($$n, 0) // return $FALSE;
    $k = _sanitize_mpz($$k, 1) // return $FALSE;

    __is_polygonal__($n, $k, 1) ? $TRUE : $FALSE;
}

#
## Integer polygonal root
#

sub __ipolygonal_root__ {
    my ($n, $k, $second) = @_;

    # $n is a Math::GMPz object
    # $k is a Math::GMPz object
    # $second is a boolean

    # polygonal_root(n, k)
    #   = ((k - 4) ± sqrt(8 * (k - 2) * n + (k - 4)^2)) / (2 * (k - 2))

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($u, $k, 2);      # u = k-2
    Math::GMPz::Rmpz_mul($t, $n, $u);        # t = n*u
    Math::GMPz::Rmpz_mul_2exp($t, $t, 3);    # t = t*8

    Math::GMPz::Rmpz_sub_ui($u, $u, 2);      # u = u-2
    Math::GMPz::Rmpz_mul($u, $u, $u);        # u = u^2
    Math::GMPz::Rmpz_add($t, $t, $u);        # t = t+u

    Math::GMPz::Rmpz_sgn($t) < 0 && goto &_nan;    # `t` is negative

    Math::GMPz::Rmpz_sqrt($t, $t);                 # t = sqrt(t)
    Math::GMPz::Rmpz_sub_ui($u, $k, 4);            # u = k-4

    $second
      ? Math::GMPz::Rmpz_sub($t, $u, $t)           # t = u-t
      : Math::GMPz::Rmpz_add($t, $t, $u);          # t = t+u

    Math::GMPz::Rmpz_add_ui($u, $u, 2);            # u = u+2
    Math::GMPz::Rmpz_mul_2exp($u, $u, 1);          # u = u*2

    Math::GMPz::Rmpz_sgn($u) || return $n;         # `u` is zero

    my $r = Math::GMPz::Rmpz_init();
    Math::GMPz::Rmpz_div($r, $t, $u);              # r = floor(t/u)
    return $r;
}

#
## Integer k-gonal root of `n`
#

sub ipolygonal_root {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    bless \__ipolygonal_root__($n, $k);
}

*polygonal_iroot = \&ipolygonal_root;

#
## Second integer k-gonal root of `n`
#

sub ipolygonal_root2 {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    bless \__ipolygonal_root__($n, $k, 1);
}

*polygonal_iroot2 = \&ipolygonal_root2;

#
## n-th k-gonal number
#

sub polygonal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = $$n;
    $k = $$k;

    if (FAST_MODE and !ref($n) and !ref($k)) {
        my $t = $n * ($n * $k - $k - 2 * $n + 4);
        if (FAST_MODE and $t < ULONG_MAX and $t > LONG_MIN) {
            return
              _set_int(
                       HAS_PRIME_UTIL
                       ? Math::Prime::Util::divint($t, 2)
                       : Math::Prime::Util::GMP::divint($t, 2)
                      );
        }
    }

    $n = _any2mpz($n, 0) // goto &nan;
    $k = _any2mpz($k, 1) // goto &nan;

    # polygonal(n, k) = n * (n*k - k - 2*n + 4) / 2

    state $r = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_mul($r, $n, $k);         # n*k
    Math::GMPz::Rmpz_sub($r, $r, $k);         # n*k - k
    Math::GMPz::Rmpz_submul_ui($r, $n, 2);    # n*k - k - 2*n
    Math::GMPz::Rmpz_add_ui($r, $r, 4);       # n*k - k - 2*n + 4
    Math::GMPz::Rmpz_mul($r, $r, $n);         # n*(n*k - k - 2*n + 4)
    Math::GMPz::Rmpz_div_2exp($r, $r, 1);     # n*(n*k - k - 2*n + 4)/2

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

#
## n-th centered k-gonal number
#

sub centered_polygonal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    # centered_polygonal(n, k) = k*n*(n+1)/2 + 1

    state $r = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_mul($r, $n, $n);        # n*n
    Math::GMPz::Rmpz_add($r, $r, $n);        # n*(n+1)
    Math::GMPz::Rmpz_mul($r, $r, $k);        # k*n*(n+1)
    Math::GMPz::Rmpz_div_2exp($r, $r, 1);    # k*n*(n+1)/2
    Math::GMPz::Rmpz_add_ui($r, $r, 1);      # k*n*(n+1)/2 + 1

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub is_centered_polygonal {
    my ($n, $k) = @_;

    # (2*n + 1)^2 = (8*(n-1)/k + 1)

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    state $t = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_sub_ui($t, $n, 1);
    Math::GMPz::Rmpz_mul_2exp($t, $t, 3);
    Math::GMPz::Rmpz_divisible_p($t, $k) || return $FALSE;

    if (Math::GMPz::Rmpz_sgn($k) == 0) {
        Math::GMPz::Rmpz_cmp_ui($n, 1) == 0 and return $TRUE;
        return $FALSE;
    }

    Math::GMPz::Rmpz_divexact($t, $t, $k);
    Math::GMPz::Rmpz_add_ui($t, $t, 1);
    Math::GMPz::Rmpz_perfect_square_p($t) || return $FALSE;
    Math::GMPz::Rmpz_sqrt($t, $t);
    Math::GMPz::Rmpz_sub_ui($t, $t, 1);
    Math::GMPz::Rmpz_even_p($t) || return $FALSE;

    return $TRUE;
}

sub centered_polygonal_root {
    my ($n, $k) = @_;
    _valid(\$k);

    # r = (sqrt(8*(n-1)/k + 1)-1)/2

    $n = $$n;
    $k = $$k;

    my $t = __inc__(__div__(__mul__(__dec__($n), 8), $k));

    if (!ref($t) and (HAS_PRIME_UTIL ? Math::Prime::Util::is_square($t) : Math::Prime::Util::GMP::is_square($t))) {
        $t =
          HAS_PRIME_UTIL
          ? Math::Prime::Util::sqrtint($t)
          : Math::Prime::Util::GMP::sqrtint($t);
    }
    elsif (ref($t) eq 'Math::GMPz' and Math::GMPz::Rmpz_perfect_square_p($t)) {
        Math::GMPz::Rmpz_sqrt($t, $t);
    }
    else {
        $t = __sqrt__(_any2mpfr_mpc($t));
    }

    bless \__div__(__dec__($t), 2);
}

#
## n-th k-gonal pyramidal number
#

sub pyramidal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    # pyramidal(n, k) = (n*(n+1) * (n*(k-2) - (k-5)))/6

    state $r = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_mul($t, $n, $n);
    Math::GMPz::Rmpz_add($t, $t, $n);         # t = n*(n+1)
    Math::GMPz::Rmpz_mul($r, $n, $k);         # r = n*k
    Math::GMPz::Rmpz_submul_ui($r, $n, 2);    # r = n*(k-2)
    Math::GMPz::Rmpz_sub($r, $r, $k);         # r = n*(k-2) - k
    Math::GMPz::Rmpz_add_ui($r, $r, 5);       # r = n*(k-2) - (k-5)
    Math::GMPz::Rmpz_mul($r, $r, $t);         # r = n*(n+1) * (n*(k-2) - (k-5))
    Math::GMPz::Rmpz_divexact_ui($r, $r, 6);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

#
## n-th k-gonal centered pyramidal number
#

sub centered_pyramidal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    # centered_pyramidal(n, k) = (k-1)*(n-1)*n*(n+1)/6 + n = n*(k*n^2 - k - n^2 + 7)/6

    state $r = Math::GMPz::Rmpz_init_nobless();
    state $t = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_mul($t, $n, $n);
    Math::GMPz::Rmpz_mul($r, $k, $t);           # r = k * n^2
    Math::GMPz::Rmpz_sub($r, $r, $k);           # r = k * n^2 - k
    Math::GMPz::Rmpz_sub($r, $r, $t);           # r = k * n^2 - k - n^2
    Math::GMPz::Rmpz_add_ui($r, $r, 7);         # r = k * n^2 - k - n^2 + 7
    Math::GMPz::Rmpz_mul($r, $r, $n);           # r = n*(k*n^2 - k - n^2 + 7)
    Math::GMPz::Rmpz_divexact_ui($r, $r, 6);    # r = n*(k*n^2 - k - n^2 + 7)/6

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

sub is_pyramidal {
    my ($n, $k) = @_;

    _valid(\$k);

    $n = _any2mpz($$n, 0) // goto &nan;
    $k = _any2mpz($$k, 1) // goto &nan;

    if (Math::GMPz::Rmpz_cmp_ui($n, 1) == 0) {
        return $TRUE;
    }

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $r = Math::GMPz::Rmpz_init_nobless();

    # When k = 2, check if n is a triangular number
    if (Math::GMPz::Rmpz_cmp_ui($k, 2) == 0) {
        return (__is_polygonal__($n, Math::GMPz::Rmpz_init_set_ui(3)) ? $TRUE : $FALSE);
    }

    Math::GMPz::Rmpz_mul_ui($t, $n, 6);
    Math::GMPz::Rmpz_sub_ui($u, $k, 2);
    Math::GMPz::Rmpz_div($u, $t, $u);

    # u = floor(((6*n)/(r-2) + floor(n^(1/3)))^(1/3))
    if (Math::GMPz::Rmpz_cmp_ui($k, 9000) <= 0) {
        Math::GMPz::Rmpz_root($r, $n, 3);
        Math::GMPz::Rmpz_add($u, $u, $r);
        Math::GMPz::Rmpz_root($u, $u, 3);
    }

    # u = round(((6*n)/(r-2))^(1/3))
    else {
        my $f = Math::MPFR::Rmpfr_init2(Math::GMPz::Rmpz_sizeinbase($n, 2));
        Math::MPFR::Rmpfr_set_z($f, $u, $round_z);
        Math::MPFR::Rmpfr_cbrt($f, $f, $round_z);
        Math::MPFR::Rmpfr_round($f, $f);
        Math::MPFR::Rmpfr_get_z($u, $f, $round_z);
    }

    Math::GMPz::Rmpz_mul($t, $u, $u);
    Math::GMPz::Rmpz_add($t, $t, $u);         # t = n*(n+1)
    Math::GMPz::Rmpz_mul($r, $u, $k);         # r = n*k
    Math::GMPz::Rmpz_submul_ui($r, $u, 2);    # r = n*(k-2)
    Math::GMPz::Rmpz_sub($r, $r, $k);         # r = n*(k-2) - k
    Math::GMPz::Rmpz_add_ui($r, $r, 5);       # r = n*(k-2) - (k-5)
    Math::GMPz::Rmpz_mul($r, $r, $t);         # r = n*(n+1) * (n*(k-2) - (k-5))
    Math::GMPz::Rmpz_divexact_ui($r, $r, 6);

    (Math::GMPz::Rmpz_cmp($r, $n) == 0) ? $TRUE : $FALSE;
}

#
## Polygonal inverses for a given number
#

sub polygonal_inverse {
    my ($n) = @_;

    $n = _any2mpz($$n, 0) // return _array();
    Math::GMPz::Rmpz_sgn($n) > 0 or return _array();

    state $t = Math::GMPz::Rmpz_init_nobless();
    state $u = Math::GMPz::Rmpz_init_nobless();
    state $v = Math::GMPz::Rmpz_init_nobless();

    Math::GMPz::Rmpz_mul_2exp($t, $n, 1);

    my @divisors = _divisors(Math::GMPz::Rmpz_get_str($t, 10));

    shift @divisors;
    pop @divisors;

    my @inverses;

    foreach my $divisor (@divisors) {

        (FAST_MODE and $divisor < ULONG_MAX)
          ? Math::GMPz::Rmpz_set_ui($u, $divisor)
          : Math::GMPz::Rmpz_set_str($u, $divisor, 10);

        Math::GMPz::Rmpz_divexact($v, $t, $u);
        Math::GMPz::Rmpz_addmul_ui($v, $u, 2);
        Math::GMPz::Rmpz_sub_ui($v, $v, 4);
        Math::GMPz::Rmpz_sub_ui($u, $u, 1);

        if (Math::GMPz::Rmpz_divisible_p($v, $u)) {

            my $r = Math::GMPz::Rmpz_init();
            my $i = Math::GMPz::Rmpz_init();

            Math::GMPz::Rmpz_add_ui($r, $u, 1);
            Math::GMPz::Rmpz_divexact($i, $v, $u);

            push @inverses, _array([(bless \$r), (bless \$i)]);
        }
    }

    _array(\@inverses);
}

*inverse_polygonal = \&polygonal_inverse;

#
## k-gonal root of `n`
#

sub __polygonal_root__ {
    my ($n, $k, $second) = @_;
    goto(join('__', ref($n), ref($k)) =~ tr/:/_/rs);

    # polygonal_root(n, k)
    #   = ((k - 4) ± sqrt(8 * (k - 2) * n + (k - 4)^2)) / (2 * (k - 2))

  Math_MPFR__Math_MPFR: {
        my $t = Math::MPFR::Rmpfr_init2($PREC);
        my $u = Math::MPFR::Rmpfr_init2($PREC);

        Math::MPFR::Rmpfr_sub_ui($u, $k, 2, $ROUND);     # u = k-2
        Math::MPFR::Rmpfr_mul($t, $n, $u, $ROUND);       # t = n*u
        Math::MPFR::Rmpfr_mul_2ui($t, $t, 3, $ROUND);    # t = t*8

        Math::MPFR::Rmpfr_sub_ui($u, $u, 2, $ROUND);     # u = u-2
        Math::MPFR::Rmpfr_sqr($u, $u, $ROUND);           # u = u^2
        Math::MPFR::Rmpfr_add($t, $t, $u, $ROUND);       # t = t+u

        # Return a complex number for `t < 0`
        if (Math::MPFR::Rmpfr_sgn($t) < 0) {
            $n = _mpfr2mpc($n);
            $k = _mpfr2mpc($k);
            goto Math_MPC__Math_MPC;
        }

        Math::MPFR::Rmpfr_sqrt($t, $t, $ROUND);          # t = sqrt(t)
        Math::MPFR::Rmpfr_sub_ui($u, $k, 4, $ROUND);     # u = k-4

        $second
          ? Math::MPFR::Rmpfr_sub($t, $u, $t, $ROUND)     # t = u-t
          : Math::MPFR::Rmpfr_add($t, $t, $u, $ROUND);    # t = t+u

        Math::MPFR::Rmpfr_add_ui($u, $u, 2, $ROUND);      # u = u+2
        Math::MPFR::Rmpfr_mul_2ui($u, $u, 1, $ROUND);     # u = u*2

        Math::MPFR::Rmpfr_zero_p($u) && return $n;        # `u` is zero
        Math::MPFR::Rmpfr_div($t, $t, $u, $ROUND);        # t = t/u
        return $t;
    }

  Math_MPFR__Math_MPC: {
        $n = _mpfr2mpc($n);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPFR: {
        $k = _mpfr2mpc($k);
        goto Math_MPC__Math_MPC;
    }

  Math_MPC__Math_MPC: {
        my $t = Math::MPC::Rmpc_init2($PREC);
        my $u = Math::MPC::Rmpc_init2($PREC);

        Math::MPC::Rmpc_sub_ui($u, $k, 2, $ROUND);     # u = k-2
        Math::MPC::Rmpc_mul($t, $n, $u, $ROUND);       # t = n*u
        Math::MPC::Rmpc_mul_2ui($t, $t, 3, $ROUND);    # t = t*8

        Math::MPC::Rmpc_sub_ui($u, $u, 2, $ROUND);     # u = u-2
        Math::MPC::Rmpc_sqr($u, $u, $ROUND);           # u = u^2
        Math::MPC::Rmpc_add($t, $t, $u, $ROUND);       # t = t+u

        Math::MPC::Rmpc_sqrt($t, $t, $ROUND);          # t = sqrt(t)
        Math::MPC::Rmpc_sub_ui($u, $k, 4, $ROUND);     # u = k-4

        $second
          ? Math::MPC::Rmpc_sub($t, $u, $t, $ROUND)     # t = u-t
          : Math::MPC::Rmpc_add($t, $t, $u, $ROUND);    # t = t+u

        Math::MPC::Rmpc_add_ui($u, $u, 2, $ROUND);      # u = u+2
        Math::MPC::Rmpc_mul_2ui($u, $u, 1, $ROUND);     # u = u*2

        if (Math::MPC::Rmpc_cmp_si($t, 0) == 0) {       # `u` is zero
            return $n;
        }

        Math::MPC::Rmpc_div($t, $t, $u, $ROUND);        # t = t/u
        return $t;
    }
}

#
## k-gonal root of `n`
#

sub polygonal_root {
    my ($n, $k) = @_;
    _valid(\$k);
    bless \__polygonal_root__(_any2mpfr_mpc($$n), _any2mpfr_mpc($$k));
}

#
## Second k-gonal root of `n`
#

sub polygonal_root2 {
    my ($n, $k) = @_;
    _valid(\$k);
    bless \__polygonal_root__(_any2mpfr_mpc($$n), _any2mpfr_mpc($$k), 1);
}

sub pyramidal_root {
    my ($n, $k) = @_;
    _valid(\$k);

    # cubic_formula(-(1/3 - k/6), 1/2, -(k/6 - 5/6), -n)

    $n = $$n;
    $k = $$k;

    my $k_over_6 = __div__($k, 6);

    my $A = __add__(__div__(-1, 3), $k_over_6);
    my $B = __div__(1, 2);
    my $C = __add__(__neg__($k_over_6), __div__(5, 6));
    my $D = __neg__($n);

    my $root = [(bless \$A)->cubic_formula((bless \$B), (bless \$C), (bless \$D))]->[1];

    if (__is_real__($n) and __cmp__($n, 0) >= 0) {
        $root = $root->real;
    }

    $root;
}

sub centered_pyramidal_root {
    my ($n, $k) = @_;
    _valid(\$k);

    # cubic_formula(k/6 - 1/6, 0, -k/6 + 7/6, -n)

    $n = $$n;
    $k = $$k;

    my $k_over_6 = __div__($k, 6);

    my $A = __sub__($k_over_6, __div__(1, 6));
    my $C = __add__(__neg__($k_over_6), __div__(7, 6));
    my $D = __neg__($n);

    my @root = (bless \$A)->cubic_formula(ZERO, (bless \$C), (bless \$D));

    if (__is_real__($n) and __cmp__($n, 0) >= 0) {
        my @pos = grep { $_->is_pos } map { $_->real } @root;
        @pos and return $pos[0];
    }

    $root[0];
}

sub is_palindrome {
    my ($n, $k) = @_;

    $n = $$n;

    if (FAST_MODE and HAS_NEW_PRIME_UTIL and !ref($n) and $n >= 0) {

        if (!defined($k)) {
            return (Math::Prime::Util::is_palindrome($n) ? $TRUE : $FALSE);
        }

        _valid(\$k);
        if (!ref($$k) and $$k < 2147483647 and $$k >= 2) {
            return (Math::Prime::Util::is_palindrome($n, $$k) ? $TRUE : $FALSE);
        }
    }

    if (defined($k)) {
        _valid(\$k);
        $k = _any2mpz($$k, 6) // return $FALSE;
    }

    $n = _sanitize_mpz($n, 5) // return $FALSE;

    Math::GMPz::Rmpz_sgn($n) >= 0
      or return $FALSE;

    # Optimization for bases <= 62
    if (!defined($k) or Math::GMPz::Rmpz_cmp_ui($k, 62) <= 0) {

        $k = defined($k) ? Math::GMPz::Rmpz_get_ui($k) : 10;
        $k <= 1 and return $FALSE;

        my $str = Math::GMPz::Rmpz_get_str($n, $k);

        return (($str eq CORE::reverse($str)) ? $TRUE : $FALSE);
    }

    my @digits = @{$_[0]->digits($_[1])};
    my $len    = scalar(@digits) - 1;

    foreach my $i (0 .. ($len >> 1)) {
        __cmp__(${$digits[$i]}, ${$digits[$len - $i]})
          && return $FALSE;
    }

    return $TRUE;
}

*is_palindromic = \&is_palindrome;

sub next_palindrome {
    my ($n, $base) = @_;

    $base = defined($base) ? do { _valid(\$base); _any2ui($$base) // goto &nan } : 10;
    $base <= 1 and goto &nan;

    $n = _any2mpz($$n, 5) // goto &nan;

    Math::GMPz::Rmpz_sgn($n) >= 0
      or goto &nan;

    my @d;

    if ($base <= 10) {
        @d = split(//, scalar CORE::reverse Math::GMPz::Rmpz_get_str($n, $base));
    }
    elsif ($base <= 36) {
        @d = @DIGITS_36{split(//, scalar CORE::reverse Math::GMPz::Rmpz_get_str($n, $base))};
    }
    elsif ($base <= 62) {
        @d = @DIGITS_62{split(//, scalar CORE::reverse Math::GMPz::Rmpz_get_str($n, $base))};
    }
    else {
        @d = map { (ref($$_) eq 'Math::GMPz') ? Math::GMPz::Rmpz_get_ui($$_) : $$_ } @{$_[0]->digits($_[1])};
    }

    my $l = $#d;
    my $i = ((scalar(@d) + 1) >> 1) - 1;

    my $is_palindrome = 1;

    foreach my $j (0 .. $i) {
        if ($d[$j] != $d[$l - $j]) {
            $is_palindrome = 0;
            last;
        }
    }

    if (!$is_palindrome) {
        my @copy = @d;

        foreach my $i (0 .. $i) {
            $d[$i] = $d[$l - $i];
        }

        my $is_greater = 1;

        foreach my $j (0 .. $i) {
            my $cmp = $d[$i - $j] <=> $copy[$i - $j];

            if ($cmp > 0) {
                last;
            }
            if ($cmp < 0) {
                $is_greater = 0;
                last;
            }
        }

        if ($is_greater) {
            return _set_int(__digits2num__($base, \@d));
        }
    }

    while ($i >= 0 and $d[$i] == $base - 1) {
        $d[$i] = 0;
        $d[$l - $i] = 0;
        $i--;
    }

    if ($i >= 0) {
        $d[$i]++;
        $d[$l - $i] = $d[$i];
    }
    else {
        @d     = (0) x (scalar(@d) + 1);
        $d[0]  = 1;
        $d[-1] = 1;
    }

    _set_int(__digits2num__($base, \@d));
}

sub reverse {
    my ($n, $k) = @_;

    $n = _any2mpz($$n, 5) // goto &nan;

    if (defined($k)) {
        _valid(\$k);
        $k = _any2mpz($$k, 6) // goto &nan;
    }

    # Optimization for bases <= 62
    if (!defined($k) or Math::GMPz::Rmpz_cmp_ui($k, 62) <= 0) {

        $k = defined($k) ? Math::GMPz::Rmpz_get_ui($k) : 10;
        $k <= 1 and goto &nan;

        my $str = scalar(CORE::reverse(Math::GMPz::Rmpz_get_str($n, $k))) =~ s/^0+//r;

        $str || return ZERO;

        if (substr($str, -1) eq '-') {    # support for negative numbers
            chop($str);
            $str = "-$str";
        }

        if ($k == 10) {
            return _set_int($str);
        }

        return bless \(my $value = Math::GMPz::Rmpz_init_set_str("$str", $k));
    }

    $_[0]->digits($_[1])->flip->digits2num($_[1])->mul($_[0]->sgn);
}

*flip = \&reverse;

sub rotate {
    my ($n, $k, $base) = @_;

    _valid(\$k);
    _valid(\$base) if defined($base);

    $n->digits($base)->rotate($k->neg)->digits2num($base);
}

sub shift_left {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if ($y == 0) {
        return bless \$x;
    }

    if (FAST_MODE and $y >= 0 and !ref($x) and $x >= 0) {

        if ($x == 0) {
            return bless \$x;
        }

        if ($] >= 5.024000 and CORE::log($x) + CORE::log(2) * $y < CORE::log(ULONG_MAX)) {
            my $r = $x << $y;
            return bless \$r;
        }
    }

    $x = _any2mpz($x, 0) // (goto &nan);

    my $r = Math::GMPz::Rmpz_init();

    ($y < 0)
      ? Math::GMPz::Rmpz_div_2exp($r, $x, -$y)
      : Math::GMPz::Rmpz_mul_2exp($r, $x, $y);

    bless \$r;
}

*lsft = \&shift_left;

sub shift_right {
    my ($x, $y) = @_;

    _valid(\$y);

    $x = $$x;
    $y = $$y;

    $y = _any2si($y) // (goto &nan) if ref($y);

    if (FAST_MODE and $y >= 0 and !ref($x) and $x >= 0) {

        if ($y >= $x) {
            return ZERO;
        }

        if ($y == 0) {
            return bless \$x;
        }

        # Fails on (old?) 32-bit perl with use64bitint=undef
        # perl -E 'say (112 >> 32)'   # incorrectly prints 112 instead of 0
        # https://www.cpantesters.org/cpan/report/38630124-4799-11ee-98b0-b3c3213a625c

        # This issue has been fixed in perl-5.24.0
        # https://perldoc.perl.org/5.24.0/perldelta#Integer-shift-(%3C%3C-and-%3E%3E)-now-more-explicitly-defined
        if ($] >= 5.024000 or $y < 32) {
            my $r = $x >> $y;
            return bless \$r;
        }
    }

    $x = _any2mpz($x, 0) // (goto &nan);

    state $r = Math::GMPz::Rmpz_init_nobless();

    ($y < 0)
      ? Math::GMPz::Rmpz_mul_2exp($r, $x, -$y)
      : Math::GMPz::Rmpz_div_2exp($r, $x, $y);

    my $r2 =
        Math::GMPz::Rmpz_fits_ulong_p($r)
      ? Math::GMPz::Rmpz_get_ui($r)
      : Math::GMPz::Rmpz_init_set($r);
    bless \$r2;
}

*rsft = \&shift_right;

#
## Rational specific
#

sub numerator {
    my ($x) = @_;

    my $r = $$x;
    while (1) {

        if (ref($r) eq 'Math::GMPq') {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPq::Rmpq_get_num($z, $r);
            return bless \$z;
        }

        ref($r) eq 'Math::GMPz' and return $x;    # is an integer

        $r = _any2mpq($r) // (goto &nan);
    }
}

*nu = \&numerator;

sub denominator {
    my ($x) = @_;

    my $r = $$x;
    while (1) {

        if (ref($r) eq 'Math::GMPq') {
            my $z = Math::GMPz::Rmpz_init();
            Math::GMPq::Rmpq_get_den($z, $r);
            return bless \$z;
        }

        ref($r) eq 'Math::GMPz' and return ONE;    # is an integer

        $r = _any2mpq($r) // (goto &nan);
    }
}

*de = \&denominator;

sub nude {
    ($_[0]->numerator, $_[0]->denominator);
}

#
## Conversion/Miscellaneous
#

sub chr {
    my ($x) = @_;
    Sidef::Types::String::String->new(CORE::chr(__numify__($$x)));
}

sub __round__ {
    my ($x, $prec) = @_;

    goto((ref($x) || 'Scalar') =~ tr/:/_/rs);

  Scalar: {
        $x = _any2mpz($x);
        goto Math_GMPz;
    }

  Math_MPFR: {
        my $nth = -CORE::int($prec);

        my $p = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        Math::MPFR::Rmpfr_set_str($p, '1e' . CORE::abs($nth), 10, $ROUND);

        my $r = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        if ($nth < 0) {
            Math::MPFR::Rmpfr_div($r, $x, $p, $ROUND);
        }
        else {
            Math::MPFR::Rmpfr_mul($r, $x, $p, $ROUND);
        }

        Math::MPFR::Rmpfr_round($r, $r);

        if ($nth < 0) {
            Math::MPFR::Rmpfr_mul($r, $r, $p, $ROUND);
        }
        else {
            Math::MPFR::Rmpfr_div($r, $r, $p, $ROUND);
        }

        return $r;
    }

  Math_MPC: {
        my $real = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
        my $imag = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

        Math::MPC::RMPC_RE($real, $x);
        Math::MPC::RMPC_IM($imag, $x);

        $real = __SUB__->($real, $prec);
        $imag = __SUB__->($imag, $prec);

        if (Math::MPFR::Rmpfr_zero_p($imag)) {
            return $real;
        }

        my $r = Math::MPC::Rmpc_init2(CORE::int($PREC));
        Math::MPC::Rmpc_set_fr_fr($r, $real, $imag, $ROUND);
        return $r;
    }

  Math_GMPq: {
        my $nth = -CORE::int($prec);

        my $r = Math::GMPq::Rmpq_init();
        Math::GMPq::Rmpq_set($r, $x);

        my $sgn = Math::GMPq::Rmpq_sgn($r);

        if ($sgn < 0) {
            Math::GMPq::Rmpq_neg($r, $r);
        }

        my $p = Math::GMPz::Rmpz_init_set_str('1' . ('0' x CORE::abs($nth)), 10);

        if ($nth < 0) {
            Math::GMPq::Rmpq_div_z($r, $r, $p);
        }
        else {
            Math::GMPq::Rmpq_mul_z($r, $r, $p);
        }

        state $half = do {
            my $q = Math::GMPq::Rmpq_init_nobless();
            Math::GMPq::Rmpq_set_ui($q, 1, 2);
            $q;
        };

        Math::GMPq::Rmpq_add($r, $r, $half);

        my $z = Math::GMPz::Rmpz_init();
        Math::GMPz::Rmpz_set_q($z, $r);

        if (Math::GMPz::Rmpz_odd_p($z) and Math::GMPq::Rmpq_integer_p($r)) {
            Math::GMPz::Rmpz_sub_ui($z, $z, 1);
        }

        Math::GMPq::Rmpq_set_z($r, $z);

        if ($nth < 0) {
            Math::GMPq::Rmpq_mul_z($r, $r, $p);
        }
        else {
            Math::GMPq::Rmpq_div_z($r, $r, $p);
        }

        if ($sgn < 0) {
            Math::GMPq::Rmpq_neg($r, $r);
        }

        if (Math::GMPq::Rmpq_integer_p($r)) {
            Math::GMPz::Rmpz_set_q($z, $r);
            return $z;
        }

        return $r;
    }

  Math_GMPz: {
        return $x if ($prec == 0);
        $x = _mpz2mpq($x);
        goto Math_GMPq;
    }
}

sub round {
    my ($x, $prec) = @_;

    my $nth = (
        defined($prec)
        ? do {
            _valid(\$prec);
            _any2si($$prec) // (goto &nan);
          }
        : 0
    );

    bless \__round__($$x, $nth);
}

*roundf = \&round;

sub to {
    my ($from, $to, $step) = @_;
    Sidef::Types::Range::RangeNumber->new($from, $to, $step // ONE);
}

*upto = \&to;

sub downto {
    my ($from, $to, $step) = @_;
    Sidef::Types::Range::RangeNumber->new($from, $to, defined($step) ? $step->neg : MONE);
}

sub xto {
    my ($from, $to, $step) = @_;

    $to = defined($step) ? $to->sub($step) : $to->dec;

    Sidef::Types::Range::RangeNumber->new($from, $to, $step // ONE);
}

*xupto = \&xto;

sub xdownto {
    my ($from, $to, $step) = @_;

    $from = defined($step) ? $from->sub($step) : $from->dec;

    Sidef::Types::Range::RangeNumber->new($from, $to, defined($step) ? $step->neg : MONE);
}

sub range {
    my ($from, $to, $step) = @_;

    defined($to) ? $from->to($to, $step) : (ZERO)->to($from->dec);
}

{
    my $srand = (HAS_PRIME_UTIL ? Math::Prime::Util::urandomm(ULONG_MAX) : Math::Prime::Util::GMP::urandomm(ULONG_MAX));

    {
        state $state = Math::MPFR::Rmpfr_randinit_mt_nobless();
        Math::MPFR::Rmpfr_randseed_ui($state, $srand);

        sub rand {
            my ($x, $y) = @_;

            my $rand = Math::MPFR::Rmpfr_init2(CORE::int($PREC));

            if (defined($y)) {
                _valid(\$y);
                Math::MPFR::Rmpfr_urandom($rand, $state, $ROUND);
                $rand = __mul__($rand, __sub__($$y, $$x));
                $rand = __add__($rand, $$x);
            }
            else {
                Math::MPFR::Rmpfr_urandom($rand, $state, $ROUND);
                $rand = __mul__($rand, $$x);
            }
            bless \$rand;
        }

        sub seed {
            my ($x) = @_;
            my $z = _any2mpz($$x) // die "[ERROR] Number.seed(): invalid seed value <<$x>> (expected an integer)";
            Math::MPFR::Rmpfr_randseed($state, $z);
            bless \$z;
        }
    }
}

sub of {
    my ($x, $obj, $range) = @_;

    if (defined($range) and ref($obj) eq 'Sidef::Types::Block::Block') {
        return $range->lazy->map($obj)->first($x);
    }

    $x = CORE::int(__numify__($$x));

    if (ref($obj) eq 'Sidef::Types::Block::Block') {
        my @array;
        for (my $i = 0 ; $i < $x ; ++$i) {
            push @array, $obj->run(bless(\(my $o = $i)));
        }
        return _array(\@array);
    }

    _array([($obj) x $x]);
}

sub by {
    my ($x, $block, $range) = @_;

    if (defined($range) and ref($block) eq 'Sidef::Types::Block::Block') {
        return $block->first($x, $range);
    }

    $x = CORE::int(__numify__($$x));

    my @items;
    for (my ($i, $j) = (0, 0) ; $j < $x ; ++$i) {
        my $k = bless(\(my $o = $i));
        if ($block->run($k)) {
            push @items, $k;
            ++$j;
        }
    }

    _array(\@items);
}

sub defs {
    my ($x, $block, $range) = @_;

    if (defined($range) and ref($block) eq 'Sidef::Types::Block::Block') {
        state $defined = Sidef::Types::Block::Block->new(code => sub { defined($_[0]) });
        return $range->lazy->map($block)->grep($defined)->first($x);
    }

    $x = CORE::int(__numify__($$x));

    my @items;
    for (my ($i, $j) = (0, 0) ; $j < $x ; ++$i) {
        push @items, $block->run(bless(\(my $o = $i))) // next;
        ++$j;
    }

    _array(\@items);
}

sub times {
    my ($x, $block) = @_;

    $x = CORE::int(__numify__($$x));

    for (my $i = 0 ; $i < $x ; ++$i) {
        $block->run(bless(\(my $o = $i)));
    }

    return $_[0];
}

sub th {
    my ($n, $block, $range) = @_;

    if (ref($block) ne 'Sidef::Types::Block::Block') {
        return undef;
    }

    $block->nth($n, $range);
}

*st = \&th;
*nd = \&th;
*rd = \&th;

foreach my $name (
                  qw(
                  permutations
                  circular_permutations
                  derangements
                  )
  ) {
    no strict 'refs';
    *{__PACKAGE__ . '::' . $name} = sub {
        my ($n, $block) = @_;
        _array([map { bless \$_ } 0 .. __numify__($$n) - 1])->$name($block);
    };
}

*complete_permutations = \&derangements;

foreach my $name (
                  qw(
                  subsets
                  variations
                  variations_with_repetition
                  combinations
                  combinations_with_repetition
                  )
  ) {
    no strict 'refs';
    *{__PACKAGE__ . '::' . $name} = sub {
        my ($n, $k, $block) = @_;
        _array([map { bless \$_ } 0 .. __numify__($$n) - 1])->$name($k, $block);
    };
}

*tuples                 = \&variations;
*tuples_with_repetition = \&variations_with_repetition;

sub bsearch_inverse {
    my ($min, $max, $block) = @_;

    my $prec = CORE::int($PREC);

    my $left   = Math::MPFR::Rmpfr_init2($prec);
    my $right  = Math::MPFR::Rmpfr_init2($prec);
    my $middle = Math::MPFR::Rmpfr_init2($prec);

    if (defined($block)) {
        _valid(\$max);
        Math::MPFR::Rmpfr_set($left,  (_any2mpfr($$min) // return undef), $ROUND);
        Math::MPFR::Rmpfr_set($right, (_any2mpfr($$max) // return undef), $ROUND);
    }
    else {
        $block = $max;
        Math::MPFR::Rmpfr_set($right, (_any2mpfr($$min) // return undef), $ROUND);
        Math::MPFR::Rmpfr_set_ui($left, 0, $ROUND);
    }

    my $prev;

    while (1) {

        Math::MPFR::Rmpfr_add($middle, $left, $right, $ROUND);
        Math::MPFR::Rmpfr_div_2ui($middle, $middle, 1, $ROUND);

        my $item = Math::MPFR::Rmpfr_init2($prec);
        Math::MPFR::Rmpfr_set($item, $middle, $ROUND);

        my $value = bless(\$item, __PACKAGE__);
        my $cmp   = CORE::int($block->run($value)) || return $value;

        if ($cmp > 0) {
            Math::MPFR::Rmpfr_set($right, $middle, $ROUND);
        }
        elsif ($cmp < 0) {
            Math::MPFR::Rmpfr_set($left, $middle, $ROUND);
        }

        if (Math::MPFR::Rmpfr_cmp($left, $right) >= 0) {
            last;
        }

        # Prevent infinite looping
        if (defined($prev) and Math::MPFR::Rmpfr_cmp($prev, $item) == 0) {
            return $value;
        }

        $prev = $item;
    }

    return undef;
}

*bsearch_solve = \&bsearch_inverse;

sub bsearch {
    my ($left, $right, $block) = @_;

    if (defined($block)) {
        _valid(\$right);
        $left  = Math::GMPz::Rmpz_init_set(_any2mpz($$left)  // return undef);
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$right) // return undef);
    }
    else {
        $block = $right;
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$left) // return undef);
        $left  = Math::GMPz::Rmpz_init_set_ui(0);
    }

    my $middle = Math::GMPz::Rmpz_init();

    while (Math::GMPz::Rmpz_cmp($left, $right) <= 0) {

        Math::GMPz::Rmpz_add($middle, $left, $right);
        Math::GMPz::Rmpz_div_2exp($middle, $middle, 1);

        my $item = bless \(my $value = Math::GMPz::Rmpz_init_set($middle));
        my $cmp  = CORE::int($block->run($item)) || return $item;

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($right, $middle, 1);
        }
        else {
            Math::GMPz::Rmpz_add_ui($left, $middle, 1);
        }
    }

    return undef;
}

sub bsearch_ge {
    my ($left, $right, $block) = @_;

    if (defined($block)) {
        _valid(\$right);
        $left  = Math::GMPz::Rmpz_init_set(_any2mpz($$left)  // return undef);
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$right) // return undef);
    }
    else {
        $block = $right;
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$left) // return undef);
        $left  = Math::GMPz::Rmpz_init_set_ui(0);
    }

    my $middle = Math::GMPz::Rmpz_init();

    while (1) {

        Math::GMPz::Rmpz_add($middle, $left, $right);
        Math::GMPz::Rmpz_div_2exp($middle, $middle, 1);

        my $item = bless \(my $value = Math::GMPz::Rmpz_init_set($middle));
        my $cmp  = CORE::int($block->run($item)) || return $item;

        if ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($left, $middle, 1);

            if (Math::GMPz::Rmpz_cmp($left, $right) > 0) {
                Math::GMPz::Rmpz_add_ui($middle, $middle, 1);
                last;
            }
        }
        else {
            Math::GMPz::Rmpz_sub_ui($right, $middle, 1);
            Math::GMPz::Rmpz_cmp($left, $right) > 0 and last;
        }
    }

    bless \$middle;
}

sub bsearch_le {
    my ($left, $right, $block) = @_;

    if (defined($block)) {
        _valid(\$right);
        $left  = Math::GMPz::Rmpz_init_set(_any2mpz($$left)  // return undef);
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$right) // return undef);
    }
    else {
        $block = $right;
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$left) // return undef);
        $left  = Math::GMPz::Rmpz_init_set_ui(0);
    }

    my $middle = Math::GMPz::Rmpz_init();

    while (1) {

        Math::GMPz::Rmpz_add($middle, $left, $right);
        Math::GMPz::Rmpz_div_2exp($middle, $middle, 1);

        my $item = bless \(my $value = Math::GMPz::Rmpz_init_set($middle));
        my $cmp  = CORE::int($block->run($item)) || return $item;

        if ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($left, $middle, 1);
            Math::GMPz::Rmpz_cmp($left, $right) > 0 and last;
        }
        else {
            Math::GMPz::Rmpz_sub_ui($right, $middle, 1);
            if (Math::GMPz::Rmpz_cmp($left, $right) > 0) {
                Math::GMPz::Rmpz_sub_ui($middle, $middle, 1);
                last;
            }
        }
    }

    bless \$middle;
}

sub bsearch_min {
    my ($left, $right, $block) = @_;

    if (defined($block)) {
        _valid(\$right);
        $left  = Math::GMPz::Rmpz_init_set(_any2mpz($$left)  // return undef);
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$right) // return undef);
    }
    else {
        $block = $right;
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$left) // return undef);
        $left  = Math::GMPz::Rmpz_init_set_ui(0);
    }

    my $middle = Math::GMPz::Rmpz_init();

    while (Math::GMPz::Rmpz_cmp($left, $right) < 0) {

        Math::GMPz::Rmpz_add($middle, $left, $right);
        Math::GMPz::Rmpz_div_2exp($middle, $middle, 1);

        my $item = bless \(my $value = Math::GMPz::Rmpz_init_set($middle));
        my $cmp  = CORE::int($block->run($item));

        if ($cmp < 0) {
            Math::GMPz::Rmpz_add_ui($left, $middle, 1);
        }
        else {
            Math::GMPz::Rmpz_set($right, $middle);
        }
    }

    bless \$left;
}

sub bsearch_max {
    my ($left, $right, $block) = @_;

    if (defined($block)) {
        _valid(\$right);
        $left  = Math::GMPz::Rmpz_init_set(_any2mpz($$left)  // return undef);
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$right) // return undef);
    }
    else {
        $block = $right;
        $right = Math::GMPz::Rmpz_init_set(_any2mpz($$left) // return undef);
        $left  = Math::GMPz::Rmpz_init_set_ui(0);
    }

    my $middle = Math::GMPz::Rmpz_init();

    while (Math::GMPz::Rmpz_cmp($left, $right) < 0) {

        Math::GMPz::Rmpz_add($middle, $left, $right);
        Math::GMPz::Rmpz_div_2exp($middle, $middle, 1);
        Math::GMPz::Rmpz_add_ui($middle, $middle, 1);

        my $item = bless \(my $value = Math::GMPz::Rmpz_init_set($middle));
        my $cmp  = CORE::int($block->run($item));

        if ($cmp > 0) {
            Math::GMPz::Rmpz_sub_ui($right, $middle, 1);
        }
        else {
            Math::GMPz::Rmpz_set($left, $middle);
        }
    }

    bless \$right;
}

sub commify {
    my ($self) = @_;

    my $n = "$self";

    my $x   = $n;
    my $neg = $n =~ s{^-}{};
    $n =~ /\.|$/;

    if ($-[0] > 3) {

        my $l = $-[0] - 3;
        my $i = ($l - 1) % 3 + 1;

        $x = substr($n, 0, $i) . ',';

        while ($i < $l) {
            $x .= substr($n, $i, 3) . ',';
            $i += 3;
        }

        $x .= substr($n, $i);
    }

    Sidef::Types::String::String->new(($neg ? '-' : '') . $x);
}

#
## Conversions
#

sub rad2deg {
    my ($x) = @_;
    my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($f, $ROUND);
    Math::MPFR::Rmpfr_ui_div($f, 180, $f, $ROUND);
    bless \__mul__($f, $$x);
}

sub deg2rad {
    my ($x) = @_;
    my $f = Math::MPFR::Rmpfr_init2(CORE::int($PREC));
    Math::MPFR::Rmpfr_const_pi($f, $ROUND);
    Math::MPFR::Rmpfr_div_ui($f, $f, 180, $ROUND);
    bless \__mul__($f, $$x);
}

# Precomputed pi(n) values — see source for references to OEIS sequences.
%PRIMEPI_LOOKUP = (

    # Powers of 10 — OEIS A006880
    "1000000"                        => "78498",
    "10000000"                       => "664579",
    "100000000"                      => "5761455",
    "1000000000"                     => "50847534",
    "10000000000"                    => "455052511",
    "100000000000"                   => "4118054813",
    "1000000000000"                  => "37607912018",
    "10000000000000"                 => "346065536839",
    "100000000000000"                => "3204941750802",
    "1000000000000000"               => "29844570422669",
    "10000000000000000"              => "279238341033925",
    "100000000000000000"             => "2623557157654233",
    "1000000000000000000"            => "24739954287740860",
    "10000000000000000000"           => "234057667276344607",
    "100000000000000000000"          => "2220819602560918840",
    "1000000000000000000000"         => "21127269486018731928",
    "10000000000000000000000"        => "201467286689315906290",
    "100000000000000000000000"       => "1925320391606803968923",
    "1000000000000000000000000"      => "18435599767349200867866",
    "10000000000000000000000000"     => "176846309399143769411680",
    "100000000000000000000000000"    => "1699246750872437141327603",
    "1000000000000000000000000000"   => "16352460426841680446427399",
    "10000000000000000000000000000"  => "157589269275973410412739598",
    "100000000000000000000000000000" => "1520698109714272166094258063",

    # Floor(sqrt(10^(2n+1))) — OEIS A122121
    "3162277"                     => "227647",
    "31622776"                    => "1951957",
    "316227766"                   => "17082666",
    "3162277660"                  => "151876932",
    "31622776601"                 => "1367199811",
    "316227766016"                => "12431880460",
    "3162277660168"               => "113983535775",
    "31622776601683"              => "1052370166553",
    "316227766016837"             => "9773865306521",
    "3162277660168379"            => "91238789797384",
    "31622776601683793"           => "855502559228365",
    "316227766016837933"          => "8052994747583677",
    "3162277660168379331"         => "76066570954337300",
    "31622776601683793319"        => "720722641159301040",
    "316227766016837933199"       => "6847673381013822597",
    "3162277660168379331998"      => "65223071241820793398",
    "31622776601683793319988"     => "622647095301172021671",
    "316227766016837933199889"    => "5956317545928249075039",
    "3162277660168379331998893"   => "57086403558149290301868",
    "31622776601683793319988935"  => "548074549053620897173483",
    "316227766016837933199889354" => "5270353162790246525701178",

    # pi(n) for n = floor(10^k / m), various small m
    "10309278350515"   => "356392355629",
    "104166666666"     => "4282427463",
    "1041666666666"    => "39114743793",
    "10416666666666"   => "359975632063",
    "104166666666666"  => "3334113665385",
    "10638297872340"   => "367367036747",
    "106382978723404"  => "3402756647584",
    "1063829787234"    => "39915368200",
    "1111111111111111" => "33056584174789",
    "111111111111111"  => "3549047966156",
    "11111111111111"   => "383118399785",
    "1111111111111"    => "41621368073",
    "111111111111"     => "4555800188",
    "11235955056179"   => "387273732609",
    "116279069767441"  => "3708728266177",
    "1162790697674"    => "43483092670",
    "11627906976744"   => "400309533209",
    "12048192771084"   => "414271426137",
    "1219512195121"    => "45522996012",
    "12195121951219"   => "419148610121",
    "121951219512195"  => "3883734854095",
    "1234567901234"    => "46063874709",
    "12345679012345"   => "424144130506",
    "123456790123456"  => "3930144678942",
    "123456790123"     => "5040193472",
    "1250000000000000" => "37058666602970",
    "125000000000000"  => "3977696644164",
    "12500000000000"   => "429262473738",
    "1250000000000"    => "46618036665",
    "125000000000"     => "5100605440",
    "12658227848101"   => "434508183042",
    "133333333333333"  => "4234170485973",
    "13513513513513"   => "462826669200",
    "135135135135135"  => "4289558418919",
    "1351351351351"    => "50251513574",
    "13698630136986"   => "468947991808",
    "137174211248"     => "5576181164",
    "138888888888"     => "5643027425",
    "1388888888888"    => "51594745537",
    "13888888888888"   => "475236412836",
    "138888888888888"  => "4404877383892",
    "14084507042253"   => "481699024983",
    "14285714285"      => "639663667",
    "142857142857142"  => "4526682048776",
    "1428571428571428" => "42185591326481",
    "14285714285714"   => "488343188629",
    "1428571428571"    => "53013308951",
    "142857142857"     => "5797603975",
    "1428571428"       => "71341127",
    "14925373134328"   => "509445517621",
    "156250000000000"  => "4937023458275",
    "15625000000000"   => "532492027113",
    "1562500000000"    => "57790812036",
    "156250000000"     => "6318108621",
    "16129032258064"   => "549074258778",
    "161290322580645"  => "5091169161272",
    "1612903225806"    => "59584931448",
    "16393442622950"   => "557766346667",
    "1666666666666"    => "61496476037",
    "16666666666666"   => "566743254123",
    "166666666666666"  => "5255429125062",
    "1666666666666666" => "48993281778403",
    "166666666666"     => "6721737844",
    "16949152542372"   => "576019453837",
    "169491525423728"  => "5341670076170",
    "17241379310344"   => "585610147276",
    "172413793103448"  => "5430838640194",
    "1724137931034"    => "63537448178",
    "1851851851851"    => "68064427524",
    "185185185185185"  => "5820007824922",
    "18518518518518"   => "627463720453",
    "185185185185"     => "7436940214",
    "18867924528301"   => "638897445082",
    "188679245283018"  => "5926333832509",
    "195312500000"     => "7826876358",
    "2000000000000000" => "58478215681891",
    "200000000000000"  => "6270424651315",
    "20000000000000"   => "675895909271",
    "2000000000000"    => "73301896139",
    "200000000000"     => "8007105059",
    "20000000000"      => "882206716",
    "2000000000"       => "98222287",
    "208333333333333"  => "6523333943109",
    "20833333333333"   => "703087019180",
    "2083333333333"    => "76241905348",
    "208333333333"     => "8327097931",
    "21276595744680"   => "717535853858",
    "212765957446808"  => "6657734355710",
    "2127659574468"    => "77804020175",
    "217391304347826"  => "6797887995201",
    "21739130434782"   => "732602396419",
    "2173913043478"    => "79432841094",
    "222222222222222"  => "6944174236933",
    "22222222222222"   => "748327378590",
    "232558139534883"  => "7256837914986",
    "2325581395348"    => "84765434681",
    "23255813953488"   => "781934640950",
    "2439024390243"    => "88746041012",
    "243902439024390"  => "7599523082981",
    "24390243902439"   => "818764914659",
    "24414062500"      => "1067503007",
    "2500000000000000" => "72623478149504",
    "250000000000000"  => "7783516108362",
    "25000000000000"   => "838538039510",
    "2500000000000"    => "90882915772",
    "250000000000"     => "9920079604",
    "263157894736842"  => "8180096926854",
    "26315789473684"   => "881153649559",
    "2631578947368"    => "95487895305",
    "270270270270270"  => "8394214368915",
    "27027027027027"   => "904160213736",
    "2702702702702"    => "97973697682",
    "277777777777"     => "10976339421",
    "2777777777777"    => "100595137378",
    "277777777777777"  => "8620043221450",
    "27777777777777"   => "928423650342",
    "2941176470588"    => "106292129666",
    "294117647058823"  => "9110926447966",
    "29411764705882"   => "981160027955",
    "312500000000000"  => "9662193791231",
    "31250000000000"   => "1040375711845",
    "3125000000000"    => "112688110354",
    "312500000000"     => "12291069731",
    "3225806451612"    => "116189994198",
    "322580645161290"  => "9964085882898",
    "32258064516129"   => "1072800852668",
    "3333333333"       => "159687131",
    "33333333333"      => "1437873584",
    "333333333333"     => "13077229631",
    "3333333333333"    => "119921146470",
    "33333333333333"   => "1107351740716",
    "333333333333333"  => "10285792709504",
    "3333333333333333" => "96028033831165",
    "3448275862068"    => "123905016177",
    "34482758620689"   => "1144245941771",
    "344827586206896"  => "10629343624801",
    "370370370370"     => "14470326153",
    "3703703703703"    => "132741764431",
    "370370370370370"  => "11391582423328",
    "37037037037037"   => "1226093877740",
    "3846153846153"    => "137660753262",
    "38461538461538"   => "1271660515811",
    "384615384615384"  => "11815989555322",
    "390625000000"     => "15229927577",
    "400000000000000"  => "12273824155491",
    "40000000000000"   => "1320811971702",
    "4000000000000"    => "142966208126",
    "411522633744"     => "16012100612",
    "416666666666"     => "16204404651",
    "4166666666666"    => "148705830761",
    "41666666666666"   => "1373991431074",
    "416666666666666"  => "12769221416219",
    "434782608695"     => "16880968331",
    "4347826086956"    => "154935587256",
    "43478260869565"   => "1431718321829",
    "434782608695652"  => "13307030092080",
    "4545454545454"    => "161721584165",
    "45454545454545"   => "1494605849770",
    "454545454545454"  => "13892973435920",
    "48828125000"      => "2072060851",
    "5000000000000000" => "142377417196364",
    "500000000000000"  => "15237833654620",
    "50000000000000"   => "1638923764567",
    "5000000000000"    => "177291661649",
    "500000000000"     => "19308136142",
    "50000000000"      => "2119654578",
    "5000000000"       => "234954223",
    "5154639175257"    => "182577435554",
    "51546391752577"   => "1687924844082",
    "526315789473"     => "20284110437",
    "5263157894736"    => "186283514910",
    "52631578947368"   => "1722283675178",
    "526315789473684"  => "16014764115918",
    "555555555555"     => "21366409911",
    "5555555555555"    => "196256438549",
    "55555555555555"   => "1814751287143",
    "555555555555555"  => "16876678891443",
    "5617977528089"    => "198383139405",
    "56179775280898"   => "1834471308010",
    "588235294117"     => "22573541239",
    "5882352941176"    => "207381587155",
    "58823529411764"   => "1917915926717",
    "588235294117647"  => "17838418304439",
    "60240963855421"   => "1962605300107",
    "6024096385542"    => "212200329534",
    "625000000000000"  => "18918502148600",
    "62500000000000"   => "2033760752473",
    "6250000000000"    => "219872297126",
    "625000000000"     => "23928619968",
    "62500000000"      => "2624687920",
    "6329113924050"    => "222556880781",
    "63291139240506"   => "2058660902059",
    "66666666666"      => "2792083254",
    "666666666666"     => "25460842735",
    "6666666666666"    => "233998450404",
    "66666666666666"   => "2164793088936",
    "666666666666666"  => "20140348712061",
    "68493150684931"   => "2222149729012",
    "6849315068493"    => "240181159890",
    "7042253521126"    => "246706152157",
    "70422535211267"   => "2282685767114",
    "71428571428571"   => "2314230232867",
    "714285714285714"  => "21534014686327",
    "7142857142857"    => "250106086224",
    "74626865671641"   => "2414421726909",
    "7462686567164"    => "260904088230",
    "769230769230"     => "29217952804",
    "7692307692307"    => "268646892583",
    "76923076923076"   => "2486270966954",
    "769230769230769"  => "23138727712057",
    "76923076923"      => "3202406654",
    "7692307692"       => "354277612",
    "769230769"        => "39643338",
    "781250000000"     => "29656993786",
    "8196721311475"    => "285628623364",
    "81967213114754"   => "2643871089132",
    "833333333333"     => "31556671177",
    "8333333333333"    => "290221685283",
    "83333333333333"   => "2686501525128",
    "833333333333333"  => "25006683974403",
    "83333333333"      => "3457724375",
    "8474576271186"    => "294967792369",
    "84745762711864"   => "2730554048517",
    "9090909090"       => "415488843",
    "909090909090"     => "34312203318",
    "90909090909090"   => "2922521280710",
    "909090909090909"  => "27208902168103",
    "9090909090909"    => "315647597350",
    "90909090909"      => "3758465612",
    "909090909"        => "46450197",
    "9174311926605"    => "318442343729",
    "9345794392523"    => "324185856798",
    "9433962264150"    => "327137540745",
    "94339622641509"   => "3029193708578",
    "9708737864077"    => "336330446255",
    "97656250000"      => "4025476060",
    "9900990099009"    => "342757310840",

    # Other prime counts
    '999999999999989' => '29844570422669',
    '99999999999973'  => '3204941750802',
    '9999999999971'   => '346065536839',
    '2760727302517'   => '100000000000',
    '2474799787573'   => '90000000000',
    '2190026988349'   => '80000000000',
    '1906555030411'   => '70000000000',
    '1624571841097'   => '60000000000',
    '1344326694119'   => '50000000000',
    '1066173339601'   => '40000000000',
    '999999999989'    => '37607912018',
    '928037044463'    => '35000000000',
    '790645490053'    => '30000000000',
    '654124187867'    => '25000000000',
    '518649879439'    => '20000000000',
    '384489816343'    => '15000000000',
    '252097800623'    => '10000000000',
    '225898512559'    => '9000000000',
    '200000000507'    => '8007105083',
    '173862636221'    => '7000000000',
    '148059109201'    => '6000000000',
    '122430513841'    => '5000000000',
    '99999999977'     => '4118054813',
    '97011687217'     => '4000000000',
    '71856445751'     => '3000000000',
    '49999999967'     => '2119654578',
    '47055833459'     => '2000000000',
    '44999999971'     => '1916268743',
    '39999999979'     => '1711955433',
    '34999999999'     => '1506589876',
    '29999999993'     => '1300005926',
    '24999999991'     => '1091987405',
    '22801763489'     => '1000000000',
    '21999999977'     => '966358351',
    '20999999981'     => '924324489',
    '20422213579'     => '900000000',
    '19999999967'     => '882206716',
    '18999999959'     => '840000027',
    '18054236957'     => '800000000',
    '17999999989'     => '797703398',
    '16999999999'     => '755305935',
    '15999999991'     => '712799821',
    '15699342107'     => '700000000',
    '14999999969'     => '670180516',
    '13999999991'     => '627440336',
    '13359555403'     => '600000000',
    '12999999959'     => '584570200',
    '11999999983'     => '541555851',
    '11037271757'     => '500000000',
    '10999999999'     => '498388617',
    '9999999967'      => '455052511',
    '9899999987'      => '450708777',
    '9883692017'      => '450000000',
    '9699999997'      => '442014876',
    '9499999979'      => '433311792',
    '9299999999'      => '424603409',
    '8999999993'      => '411523195',
    '8736028057'      => '400000000',
    '8699999953'      => '398425675',
    '8499999941'      => '389682427',
    '8299999993'      => '380930729',
    '7999999957'      => '367783654',
    '7594955549'      => '350000000',
    '7499999999'      => '345826612',
    '7299999979'      => '337024801',
    '6999999989'      => '323804352',
    '6699999991'      => '310558733',
    '6499999993'      => '301711468',
    '6461335109'      => '300000000',
    '6399999959'      => '297285198',
    '6299999977'      => '292856421',
    '5999999989'      => '279545368',
    '5699999999'      => '266206294',
    '5499999997'      => '257294520',
    '5336500537'      => '250000000',
    '5299999967'      => '248370960',
    '5199999977'      => '243902342',
    '4999999937'      => '234954223',
    '4899999983'      => '230475545',
    '4699999987'      => '221504167',
    '4499999989'      => '212514323',
    '4299999973'      => '203507248',
    '4222234741'      => '200000000',
    '4199999929'      => '198996103',
    '4000000483'      => '189961831',
    '3999999979'      => '189961812',
    '3899999989'      => '185436625',
    '3799999979'      => '180906194',
    '3699999991'      => '176369517',
    '3499999991'      => '167279333',
    '3399999971'      => '162725196',
    '3299999959'      => '158165829',
    '3099999953'      => '149028641',
    '2999999929'      => '144449537',
    '2899999957'      => '139864011',
    '2799999973'      => '135270258',
    '2699999989'      => '130670192',
    '2599999991'      => '126062167',
    '2499999977'      => '121443371',
    '2399999983'      => '116818447',
    '2199999973'      => '107540122',
    '2038074743'      => '100000000',
    '1999999973'      => '98222287',
    '1899999979'      => '93547928',
    '1799999977'      => '88862422',
    '1699999997'      => '84163019',
    '1599999983'      => '79451833',
    '1499999957'      => '74726528',
    '1399999987'      => '69985473',
    '1299999983'      => '65228333',
    '1199999993'      => '60454705',
    '1099999997'      => '55662470',
    '999999937'       => '50847534',
    '982451653'       => '50000000',
    '949999993'       => '48431471',
    '899999963'       => '46009215',
    '849999977'       => '43581966',
    '799999999'       => '41146179',
    '749999989'       => '38703181',
    '699999953'       => '36252931',
    '649999993'       => '33793395',
    '599999971'       => '31324703',
    '549999959'       => '28845356',
    '499999993'       => '26355867',
    '449999993'       => '23853038',
    '399999959'       => '21336326',
    '373587883'       => '20000000',
    '369999979'       => '19818405',
    '359999989'       => '19311288',
    '349999999'       => '18803526',
    '329999987'       => '17785475',
    '299999977'       => '16252325',
    '289999999'       => '15739663',
    '269999993'       => '14711384',
    '249999991'       => '13679318',
    '229999981'       => '12642573',
    '199999991'       => '11078937',
    '189999989'       => '10555473',
    '179424673'       => '10000000',
    '169999967'       => '9503083',
    '159999997'       => '8974458',
    '149999957'       => '8444396',
    '139999991'       => '7912199',
    '119999987'       => '6841648',
    '99999989'        => '5761455',
    '94999951'        => '5489749',
    '89999999'        => '5216954',
    '86028121'        => '5000000',
    '84999979'        => '4943731',
    '79999987'        => '4669382',
    '74999959'        => '4394304',
    '69999989'        => '4118064',
    '64999981'        => '3840554',
    '59999999'        => '3562115',
    '54999943'        => '3282200',
    '49999991'        => '3001134',
    '44999971'        => '2718160',
    '39999983'        => '2433654',
    '34999969'        => '2146775',
    '32452843'        => '2000000',
    '29999999'        => '1857859',
    '24999983'        => '1565927',
    '19999999'        => '1270607',
    '18999997'        => '1211050',
    '17999987'        => '1151367',
    '16999999'        => '1091314',
    '15999989'        => '1031130',
    '15485863'        => '1000000',
    '14999981'        => '970704',
    '13999981'        => '910077',
    '12999997'        => '849252',
    '11999989'        => '788060',
    '10999997'        => '726517',
    '9999991'         => '664579',
    '8999993'         => '602489',
    '7999993'         => '539777',
    '7368787'         => '500000',
    '6999997'         => '476648',
    '5999993'         => '412849',
    '4999999'         => '348513',
    '3999971'         => '283146',
    '3499999'         => '250150',
    '2999999'         => '216816',
    '2750159'         => '200000',
    '2499997'         => '183072',
    '1999993'         => '148933',
    '1299709'         => '100000',
    '1159523'         => '90000',
    '1020379'         => '80000',

    # Powers of 2 — OEIS A007053
    "1048576"                      => "82025",
    "2097152"                      => "155611",
    "4194304"                      => "295947",
    "8388608"                      => "564163",
    "16777216"                     => "1077871",
    "33554432"                     => "2063689",
    "67108864"                     => "3957809",
    "134217728"                    => "7603553",
    "268435456"                    => "14630843",
    "536870912"                    => "28192750",
    "1073741824"                   => "54400028",
    "2147483648"                   => "105097565",
    "4294967296"                   => "203280221",
    "8589934592"                   => "393615806",
    "17179869184"                  => "762939111",
    "34359738368"                  => "1480206279",
    "68719476736"                  => "2874398515",
    "137438953472"                 => "5586502348",
    "274877906944"                 => "10866266172",
    "549755813888"                 => "21151907950",
    "1099511627776"                => "41203088796",
    "2199023255552"                => "80316571436",
    "4398046511104"                => "156661034233",
    "8796093022208"                => "305761713237",
    "17592186044416"               => "597116381732",
    "35184372088832"               => "1166746786182",
    "70368744177664"               => "2280998753949",
    "140737488355328"              => "4461632979717",
    "281474976710656"              => "8731188863470",
    "562949953421312"              => "17094432576778",
    "1125899906842624"             => "33483379603407",
    "2251799813685248"             => "65612899915304",
    "4503599627370496"             => "128625503610475",
    "9007199254740992"             => "252252704148404",
    "18014398509481984"            => "494890204904784",
    "36028797018963968"            => "971269945245201",
    "72057594037927936"            => "1906879381028850",
    "144115188075855872"           => "3745011184713964",
    "288230376151711744"           => "7357400267843990",
    "576460752303423488"           => "14458792895301660",
    "1152921504606846976"          => "28423094496953330",
    "2305843009213693952"          => "55890484045084135",
    "4611686018427387904"          => "109932807585469973",
    "9223372036854775808"          => "216289611853439384",
    "18446744073709551616"         => "425656284035217743",
    "36893488147419103232"         => "837903145466607212",
    "73786976294838206464"         => "1649819700464785589",
    "147573952589676412928"        => "3249254387052557215",
    "295147905179352825856"        => "6400771597544937806",
    "590295810358705651712"        => "12611864618760352880",
    "1180591620717411303424"       => "24855455363362685793",
    "2361183241434822606848"       => "48995571600129458363",
    "4722366482869645213696"       => "96601075195075186855",
    "9444732965739290427392"       => "190499823401327905601",
    "18889465931478580854784"      => "375744164937699609596",
    "37778931862957161709568"      => "741263521140740113483",
    "75557863725914323419136"      => "1462626667154509638735",
    "151115727451828646838272"     => "2886507381056867953916",
    "302231454903657293676544"     => "5697549648954257752872",
    "604462909807314587353088"     => "11248065615133675809379",
    "1208925819614629174706176"    => "22209558889635384205844",
    "2417851639229258349412352"    => "43860397052947409356492",
    "4835703278458516698824704"    => "86631124695994360074872",
    "9671406556917033397649408"    => "171136408646923240987028",
    "19342813113834066795298816"   => "338124238545210097236684",
    "38685626227668133590597632"   => "668150111666935905701562",
    "77371252455336267181195264"   => "1320486952377516565496055",
    "154742504910672534362390528"  => "2610087356951889016077639",
    "309485009821345068724781056"  => "5159830247726102115466054",
    "618970019642690137449562112"  => "10201730804263125133012340",
    "1237940039285380274899124224" => "20172933541156002700963336",
    "2475880078570760549798248448" => "39895115987049029184882256",
    "4951760157141521099596496896" => "78908656317357166866404346",
                  );

{
    no strict 'refs';

    *{__PACKAGE__ . '::' . '/'}   = \&div;
    *{__PACKAGE__ . '::' . '÷'}   = \&div;
    *{__PACKAGE__ . '::' . '*'}   = \&mul;
    *{__PACKAGE__ . '::' . '+'}   = \&add;
    *{__PACKAGE__ . '::' . '-'}   = \&sub;
    *{__PACKAGE__ . '::' . '%'}   = \&mod;
    *{__PACKAGE__ . '::' . '**'}  = \&pow;
    *{__PACKAGE__ . '::' . '++'}  = \&inc;
    *{__PACKAGE__ . '::' . '--'}  = \&dec;
    *{__PACKAGE__ . '::' . '<'}   = \&lt;
    *{__PACKAGE__ . '::' . '>'}   = \&gt;
    *{__PACKAGE__ . '::' . '&'}   = \&and;
    *{__PACKAGE__ . '::' . '|'}   = \&or;
    *{__PACKAGE__ . '::' . '^'}   = \&xor;
    *{__PACKAGE__ . '::' . '<=>'} = \&cmp;
    *{__PACKAGE__ . '::' . '<='}  = \&le;
    *{__PACKAGE__ . '::' . '≤'}   = \&le;
    *{__PACKAGE__ . '::' . '>='}  = \&ge;
    *{__PACKAGE__ . '::' . '≥'}   = \&ge;
    *{__PACKAGE__ . '::' . '=='}  = \&eq;
    *{__PACKAGE__ . '::' . '!='}  = \&ne;
    *{__PACKAGE__ . '::' . '≠'}   = \&ne;
    *{__PACKAGE__ . '::' . '..'}  = \&to;
    *{__PACKAGE__ . '::' . '..^'} = \&xto;
    *{__PACKAGE__ . '::' . '^..'} = \&xdownto;
    *{__PACKAGE__ . '::' . '!'}   = \&factorial;
    *{__PACKAGE__ . '::' . '!!'}  = \&double_factorial;
    *{__PACKAGE__ . '::' . '%%'}  = \&is_div;
    *{__PACKAGE__ . '::' . '>>'}  = \&shift_right;
    *{__PACKAGE__ . '::' . '<<'}  = \&shift_left;
    *{__PACKAGE__ . '::' . '~'}   = \&not;
    *{__PACKAGE__ . '::' . ':'}   = \&pair;
    *{__PACKAGE__ . '::' . '//'}  = \&idiv;
    *{__PACKAGE__ . '::' . '=~='} = \&approx_eq;
    *{__PACKAGE__ . '::' . '≅'}   = \&approx_eq;
    *{__PACKAGE__ . '::' . '<~>'} = \&approx_cmp;
};

1
