<?php

/**
 * Base BigInteger Engine
 *
 * PHP version 8.1+
 *
 * @author    Jim Wigginton <terrafrost@php.net>
 * @copyright 2017-2026 Jim Wigginton
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link      https://phpseclib.com/
 */

declare(strict_types=1);

namespace phpseclib4\Math\BigInteger\Engines;

use phpseclib4\Common\Functions\Strings;
use phpseclib4\Exception\{BadConfigurationException, InvalidArgumentException, ResourceLimitException};

/**
 * Base Engine.
 *
 * @author  Jim Wigginton <terrafrost@php.net>
 */
abstract class Engine implements \JsonSerializable
{
    public const ENGINE_DIR = '';
    public const FAST_BITWISE = false;

    /* final */ protected const PRIMES = [
        3,   5,   7,   11,  13,  17,  19,  23,  29,  31,  37,  41,  43,  47,  53,  59,
        61,  67,  71,  73,  79,  83,  89,  97,  101, 103, 107, 109, 113, 127, 131, 137,
        139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227,
        229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313,
        317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419,
        421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509,
        521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617,
        619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727,
        733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829,
        839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947,
        953, 967, 971, 977, 983, 991, 997,
    ];

    /**
     * BigInteger(0)
     *
     * @var array<class-string<static>, ?static>
     */
    protected static array $zero = [];

    /**
     * BigInteger(1)
     *
     * @var array<class-string<static>, static>
     */
    protected static array $one  = [];

    /**
     * BigInteger(2)
     *
     * @var array<class-string<static>, static>
     */
    protected static array $two = [];

    /**
     * Modular Exponentiation Engine
     *
     * @var array<class-string<static>, class-string<static>>
     */
    protected static array $modexpEngine;

    /**
     * Engine Validity Flag
     *
     * @var array<class-string<static>, bool>
     */
    protected static array $isValidEngine;

    /**
     * Holds the BigInteger's sign
     */
    protected bool $is_negative = false;

    /**
     * Precision
     *
     * @see static::setPrecision()
     */
    protected int $precision = -1;

    /**
     * Precision Bitmask
     *
     * @see static::setPrecision()
     * @var ?static
     */
    protected ?Engine $bitmask = null;

    /**
     * Default constructor
     *
     * $base is ignored if $x is an int or an instance of \GMP
     *
     * @param int|numeric-string|\GMP $x integer Base-10 number or base-$base number if $base set.
     */
    public function __construct(int|string|\GMP $x = '0', int $base = 10)
    {
        if (!array_key_exists(static::class, static::$zero)) {
            static::$zero[static::class] = null; // Placeholder to prevent infinite loop.
            static::$zero[static::class] = new static(0);
            static::$one[static::class] = new static(1);
            static::$two[static::class] = new static(2);
        }

        if (is_int($x) || $x instanceof \GMP) {
            $temp = new static("$x");
            $this->value = $temp->value;
            $this->is_negative = $temp->is_negative;
            return;
        }

        // '0' counts as empty() but when the base is 256 '0' is equal to ord('0') or 48
        // '0' is the only value like this per http://php.net/empty
        if (empty($x) && (abs($base) != 256 || $x !== '0')) {
            return;
        }

        switch ($base) {
            case -256:
            case 256:
                if ($base == -256 && (ord($x[0]) & 0x80)) {
                    $this->value = ~$x;
                    $this->is_negative = true;
                } else {
                    $this->value = $x;
                    $this->is_negative = false;
                }

                $this->initialize($base);

                if ($this->is_negative) {
                    $temp = $this->add(new static('-1'));
                    $this->value = $temp->value;
                }
                break;
            case -16:
            case 16:
                if ($base > 0 && $x[0] == '-') {
                    $this->is_negative = true;
                    $x = substr($x, 1);
                }

                $x = preg_replace('#^(?:0x)?([A-Fa-f0-9]*).*#s', '$1', $x);

                $is_negative = false;
                if ($base < 0 && hexdec($x[0]) >= 8) {
                    $this->is_negative = $is_negative = true;
                    $x = Strings::bin2hex(~Strings::hex2bin($x));
                }

                $this->value = $x;
                $this->initialize($base);

                if ($is_negative) {
                    $temp = $this->add(new static('-1'));
                    $this->value = $temp->value;
                }
                break;
            case -10:
            case 10:
                // (?<!^)(?:-).*: find any -'s that aren't at the beginning and then any characters that follow that
                // (?<=^|-)0*: find any 0's that are preceded by the start of the string or by a - (ie. octals)
                // [^-0-9].*: find any non-numeric characters and then any characters that follow that
                $this->value = preg_replace('#(?<!^)-.*|(?<=^|-)0*|[^-0-9].*#s', '', (string) $x);
                if (!strlen($this->value) || $this->value == '-') {
                    $this->value = '0';
                }
                $this->initialize($base);
                break;
            case -2:
            case 2:
                if ($base > 0 && $x[0] == '-') {
                    $this->is_negative = true;
                    $x = substr($x, 1);
                }

                $x = preg_replace('#^([01]*).*#s', '$1', $x);

                $temp = new static(Strings::bits2bin($x), 128 * $base); // ie. either -16 or +16
                $this->value = $temp->value;
                if ($temp->is_negative) {
                    $this->is_negative = true;
                }

                break;
            default:
                // base not supported, so we'll let $this == 0
        }
    }

    /**
     * Sets engine type.
     *
     * Throws an exception if the type is invalid
     */
    public static function setModExpEngine(string $engine): void
    {
        $fqengine = '\\phpseclib4\\Math\\BigInteger\\Engines\\' . static::ENGINE_DIR . '\\' . $engine;
        if (!class_exists($fqengine) || !method_exists($fqengine, 'isValidEngine')) {
            throw new BadConfigurationException("$engine is not a valid engine");
        }
        if (!$fqengine::isValidEngine()) {
            throw new BadConfigurationException("$engine is not setup correctly on this system");
        }
        static::$modexpEngine[static::class] = $fqengine;
    }

    /**
     * Converts a BigInteger to a byte string (eg. base-256).
     *
     * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're
     * saved as two's compliment.
     */
    protected function toBytesHelper(): string
    {
        $comparison = $this->compare(new static());
        if ($comparison == 0) {
            return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : '';
        }

        $temp = $comparison < 0 ? $this->add(new static(1)) : $this;
        $bytes = $temp->toBytes();

        if (!strlen($bytes)) { // eg. if the number we're trying to convert is -1
            $bytes = chr(0);
        }

        if (ord($bytes[0]) & 0x80) {
            $bytes = chr(0) . $bytes;
        }

        return $comparison < 0 ? ~$bytes : $bytes;
    }

    /**
     * Converts a BigInteger to a hex string (eg. base-16).
     */
    public function toHex(bool $twos_compliment = false): string
    {
        return Strings::bin2hex($this->toBytes($twos_compliment));
    }

    /**
     * Converts a BigInteger to a bit string (eg. base-2).
     *
     * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're
     * saved as two's compliment.
     */
    public function toBits(bool $twos_compliment = false): string
    {
        $hex = $this->toBytes($twos_compliment);
        $bits = Strings::bin2bits($hex);

        $result = $this->precision > 0 ? substr($bits, -$this->precision) : ltrim($bits, '0');

        if ($twos_compliment && $this->compare(new static()) > 0 && $this->precision <= 0) {
            return '0' . $result;
        }

        return $result;
    }

    /**
     * Calculates modular inverses.
     *
     * Say you have (30 mod 17 * x mod 17) mod 17 == 1.  x can be found using modular inverses.
     *
     * {@internal See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=21 HAC 14.64} for more information.}
     */
    protected function modInverseHelper(Engine $n): ?Engine
    {
        // $x mod -$n == $x mod $n.
        $n = $n->abs();

        if ($this->compare(static::$zero[static::class]) < 0) {
            $temp = $this->abs();
            $temp = $temp->modInverse($n);
            return $this->normalize($n->subtract($temp));
        }

        ['gcd' => $gcd, 'x' => $x] = $this->extendedGCD($n);

        if (!$gcd->equals(static::$one[static::class])) {
            return null;
        }

        $x = $x->compare(static::$zero[static::class]) < 0 ? $x->add($n) : $x;

        return $this->compare(static::$zero[static::class]) < 0 ? $this->normalize($n->subtract($x)) : $this->normalize($x);
    }

    /**
     *  __serialize() magic method
     *
     * __sleep / __wakeup were depreciated in PHP 8.5
     * Will be called, automatically, when serialize() is called on a Math_BigInteger object.
     *
     * @see self::__unserialize()
     */
    public function __serialize(): array
    {
        $result = ['hex' => $this->toHex(true)];
        if ($this->precision > 0) {
            $result['precision'] = $this->precision;
        }
        return $result;
    }

    /**
     *  __unserialize() magic method
     *
     * __sleep / __wakeup were depreciated in PHP 8.5
     * Will be called, automatically, when unserialize() is called on a Math_BigInteger object.
     *
     * @see self::__serialize()
     */
    public function __unserialize(array $data): void
    {
        $temp = new static($data['hex'], -16);
        $this->value = $temp->value;
        $this->is_negative = $temp->is_negative;
        if (isset($data['precision']) && $data['precision'] > 0) {
            // recalculate $this->bitmask
            $this->setPrecision($data['precision']);
        }
    }

    /**
     * JSON Serialize
     *
     * Will be called, automatically, when json_encode() is called on a BigInteger object.
     *
     * @return array{hex: string, precision?: int]
     */
    public function jsonSerialize(): mixed
    {
        $result = ['hex' => $this->toHex(true)];
        if ($this->precision > 0) {
            $result['precision'] = $this->precision;
        }
        return $result;
    }

    /**
     * Converts a BigInteger to a base-10 number.
     */
    public function __toString(): string
    {
        return $this->toString();
    }

    /**
     *  __debugInfo() magic method
     *
     * Will be called, automatically, when print_r() or var_dump() are called
     */
    public function __debugInfo(): array
    {
        $result = [
            'value' => '0x' . $this->toHex(true),
            'engine' => basename(static::class),
        ];
        return $this->precision > 0 ? $result + ['precision' => $this->precision] : $result;
    }

    /**
     * Set Precision
     *
     * Some bitwise operations give different results depending on the precision being used.  Examples include left
     * shift, not, and rotates.
     */
    public function setPrecision(int $bits): void
    {
        if ($bits < 1) {
            $this->precision = -1;
            $this->bitmask = null;

            return;
        }
        $this->precision = $bits;
        $this->bitmask = static::setBitmask($bits);

        $temp = $this->normalize($this);
        $this->value = $temp->value;
    }

    /**
     * Get Precision
     *
     * Returns the precision if it exists, -1 if it doesn't
     */
    public function getPrecision(): int
    {
        return $this->precision;
    }

    /**
     * Set Bitmask
     *
     * @return static
     * @see self::setPrecision()
     */
    protected static function setBitmask(int $bits): Engine
    {
        return new static(chr((1 << ($bits & 0x7)) - 1) . str_repeat(chr(0xFF), $bits >> 3), 256);
    }

    /**
     * Logical Not
     */
    public function bitwise_not(): Engine
    {
        // calculuate "not" without regard to $this->precision
        // (will always result in a smaller number.  ie. ~1 isn't 1111 1110 - it's 0)
        $temp = $this->toBytes();
        if ($temp == '') {
            return $this->normalize(static::$zero[static::class]);
        }
        $pre_msb = decbin(ord($temp[0]));
        $temp = ~$temp;
        $msb = decbin(ord($temp[0]));
        if (strlen($msb) == 8) {
            $msb = substr($msb, strpos($msb, '0'));
        }
        $temp[0] = chr(bindec($msb));

        // see if we need to add extra leading 1's
        $current_bits = strlen($pre_msb) + 8 * strlen($temp) - 8;
        $new_bits = $this->precision - $current_bits;
        if ($new_bits <= 0) {
            return $this->normalize(new static($temp, 256));
        }

        // generate as many leading 1's as we need to.
        $leading_ones = chr((1 << ($new_bits & 0x7)) - 1) . str_repeat(chr(0xFF), $new_bits >> 3);

        self::base256_lshift($leading_ones, $current_bits);

        $temp = str_pad($temp, strlen($leading_ones), chr(0), STR_PAD_LEFT);

        return $this->normalize(new static($leading_ones | $temp, 256));
    }

    /**
     * Logical Left Shift
     *
     * Shifts binary strings $shift bits, essentially multiplying by 2**$shift.
     */
    protected static function base256_lshift(string &$x, int $shift): void
    {
        if ($shift == 0) {
            return;
        }

        $num_bytes = $shift >> 3; // eg. floor($shift/8)
        $shift &= 7; // eg. $shift % 8

        $carry = 0;
        for ($i = strlen($x) - 1; $i >= 0; --$i) {
            $temp = (ord($x[$i]) << $shift) | $carry;
            $x[$i] = chr($temp & 0xFF);
            $carry = $temp >> 8;
        }
        $carry = ($carry != 0) ? chr($carry) : '';
        $x = $carry . $x . str_repeat(chr(0), $num_bytes);
    }

    /**
     * Logical Left Rotate
     *
     * Instead of the top x bits being dropped they're appended to the shifted bit string.
     */
    public function bitwise_leftRotate(int $shift): Engine
    {
        $bits = $this->toBytes();

        if ($this->precision > 0) {
            $precision = $this->precision;
            if (static::FAST_BITWISE) {
                $mask = $this->bitmask->toBytes();
            } else {
                $mask = $this->bitmask->subtract(new static(1));
                $mask = $mask->toBytes();
            }
        } else {
            $temp = ord($bits[0]);
            for ($i = 0; $temp >> $i; ++$i) {
            }
            $precision = 8 * strlen($bits) - 8 + $i;
            $mask = chr((1 << ($precision & 0x7)) - 1) . str_repeat(chr(0xFF), $precision >> 3);
        }

        if ($shift < 0) {
            $shift += $precision;
        }
        $shift %= $precision;

        if (!$shift) {
            return clone $this;
        }

        $left = $this->bitwise_leftShift($shift);
        $left = $left->bitwise_and(new static($mask, 256));
        $right = $this->bitwise_rightShift($precision - $shift);
        $result = static::FAST_BITWISE ? $left->bitwise_or($right) : $left->add($right);
        return $this->normalize($result);
    }

    /**
     * Logical Right Rotate
     *
     * Instead of the bottom x bits being dropped they're prepended to the shifted bit string.
     */
    public function bitwise_rightRotate(int $shift): Engine
    {
        return $this->bitwise_leftRotate(-$shift);
    }

    /**
     * Returns the smallest and largest n-bit number
     *
     * @return array{min: static, max: static}
     */
    public static function minMaxBits(int $bits): array
    {
        $bytes = $bits >> 3;
        $min = str_repeat(chr(0), $bytes);
        $max = str_repeat(chr(0xFF), $bytes);
        $msb = $bits & 7;
        if ($msb) {
            $min = chr(1 << ($msb - 1)) . $min;
            $max = chr((1 << $msb) - 1) . $max;
        } else {
            $min[0] = chr(0x80);
        }
        return [
            'min' => new static($min, 256),
            'max' => new static($max, 256),
        ];
    }

    /**
     * Return the size of a BigInteger in bits
     */
    public function getLength(): int
    {
        return strlen($this->toBits());
    }

    /**
     * Return the size of a BigInteger in bytes
     */
    public function getLengthInBytes(): int
    {
        return (int) ceil($this->getLength() / 8);
    }

    /**
     * Performs some pre-processing for powMod
     */
    protected function powModOuter(Engine $e, Engine $n): ?Engine
    {
        $n = isset($this->bitmask) && $this->bitmask->compare($n) < 0 ? $this->bitmask : $n->abs();

        if ($e->compare(new static()) < 0) {
            $e = $e->abs();

            $temp = $this->modInverse($n);
            if ($temp === null) {
                return null;
            }

            return $this->normalize($temp->powModInner($e, $n));
        }

        if ($this->compare($n) > 0 || $this->isNegative()) {
            [, $temp] = $this->divide($n);
            return $temp->powModInner($e, $n);
        }

        return $this->powModInner($e, $n);
    }

    /**
     * Sliding Window k-ary Modular Exponentiation
     *
     * Based on {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=27 HAC 14.85} /
     * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=210 MPM 7.7}.  In a departure from those algorithims,
     * however, this function performs a modular reduction after every multiplication and squaring operation.
     * As such, this function has the same preconditions that the reductions being used do.
     *
     * @template T of Engine
     * @param class-string<T> $class
     * @return T
     */
    protected static function slidingWindow(Engine $x, Engine $e, Engine $n, string $class): Engine
    {
        static $window_ranges = [7, 25, 81, 241, 673, 1793]; // from BigInteger.java's oddModPow function
        //static $window_ranges = [0, 7, 36, 140, 450, 1303, 3529]; // from MPM 7.3.1

        $e_bits = $e->toBits();
        $e_length = strlen($e_bits);

        // calculate the appropriate window size.
        // $window_size == 3 if $window_ranges is between 25 and 81, for example.
        for ($i = 0, $window_size = 1; $i < count($window_ranges) && $e_length > $window_ranges[$i]; ++$window_size, ++$i) {
        }

        $n_value = $n->value;

        if (method_exists(static::class, 'generateCustomReduction')) {
            static::generateCustomReduction($n, $class);
        }

        // precompute $this^0 through $this^$window_size
        $powers = [];
        $powers[1] = static::prepareReduce($x->value, $n_value, $class);
        $powers[2] = static::squareReduce($powers[1], $n_value, $class);

        // we do every other number since substr($e_bits, $i, $j+1) (see below) is supposed to end
        // in a 1.  ie. it's supposed to be odd.
        $temp = 1 << ($window_size - 1);
        for ($i = 1; $i < $temp; ++$i) {
            $i2 = $i << 1;
            $powers[$i2 + 1] = static::multiplyReduce($powers[$i2 - 1], $powers[2], $n_value, $class);
        }

        $result = new $class(1);
        $result = static::prepareReduce($result->value, $n_value, $class);

        for ($i = 0; $i < $e_length;) {
            if (!$e_bits[$i]) {
                $result = static::squareReduce($result, $n_value, $class);
                ++$i;
            } else {
                for ($j = $window_size - 1; $j > 0; --$j) {
                    if (!empty($e_bits[$i + $j])) {
                        break;
                    }
                }

                // eg. the length of substr($e_bits, $i, $j + 1)
                for ($k = 0; $k <= $j; ++$k) {
                    $result = static::squareReduce($result, $n_value, $class);
                }

                $result = static::multiplyReduce($result, $powers[bindec(substr($e_bits, $i, $j + 1))], $n_value, $class);

                $i += $j + 1;
            }
        }

        $temp = new $class();
        /** @psalm-suppress UndefinedPropertyAssignment */
        $temp->value = static::reduce($result, $n_value, $class);

        return $temp;
    }

    /**
     * Generates a random number of a certain size
     *
     * Bit length is equal to $size
     */
    public static function random(int $size): Engine
    {
        ['min' => $min, 'max' => $max] = static::minMaxBits($size);
        return static::randomRange($min, $max);
    }

    /**
     * Generates a random prime number of a certain size
     *
     * Bit length is equal to $size
     */
    public static function randomPrime(int $size): Engine
    {
        ['min' => $min, 'max' => $max] = static::minMaxBits($size);
        return static::randomRangePrime($min, $max);
    }

    /**
     * Performs some pre-processing for randomRangePrime
     */
    protected static function randomRangePrimeOuter(Engine $min, Engine $max): ?Engine
    {
        $compare = $max->compare($min);

        if (!$compare) {
            return $min->isPrime() ? $min : null;
        } elseif ($compare < 0) {
            // if $min is bigger then $max, swap $min and $max
            $temp = $max;
            $max = $min;
            $min = $temp;
        }

        $length = $max->getLength();
        if ($length > 8196) {
            throw new ResourceLimitException("Generation of random prime numbers larger than 8196 has been disabled ($length)");
        }

        $x = static::randomRange($min, $max);

        return static::randomRangePrimeInner($x, $min, $max);
    }

    /**
     * Generate a random number between a range
     *
     * Returns a random number between $min and $max where $min and $max
     * can be defined using one of the two methods:
     *
     * BigInteger::randomRange($min, $max)
     * BigInteger::randomRange($max, $min)
     */
    protected static function randomRangeHelper(Engine $min, Engine $max): Engine
    {
        $compare = $max->compare($min);

        if (!$compare) {
            return $min;
        } elseif ($compare < 0) {
            // if $min is bigger then $max, swap $min and $max
            $temp = $max;
            $max = $min;
            $min = $temp;
        }

        if (!isset(static::$one[static::class])) {
            static::$one[static::class] = new static(1);
        }

        $max = $max->subtract($min->subtract(static::$one[static::class]));

        $size = strlen(ltrim($max->toBytes(), chr(0)));

        /*
            doing $random % $max doesn't work because some numbers will be more likely to occur than others.
            eg. if $max is 140 and $random's max is 255 then that'd mean both $random = 5 and $random = 145
            would produce 5 whereas the only value of random that could produce 139 would be 139. ie.
            not all numbers would be equally likely. some would be more likely than others.

            creating a whole new random number until you find one that is within the range doesn't work
            because, for sufficiently small ranges, the likelihood that you'd get a number within that range
            would be pretty small. eg. with $random's max being 255 and if your $max being 1 the probability
            would be pretty high that $random would be greater than $max.

            phpseclib works around this using the technique described here:

            http://crypto.stackexchange.com/questions/5708/creating-a-small-number-from-a-cryptographically-secure-random-string
        */
        $random_max = new static(chr(1) . str_repeat("\0", $size), 256);
        $random = new static($size ? random_bytes($size) : '', 256);

        [$max_multiple] = $random_max->divide($max);
        $max_multiple = $max_multiple->multiply($max);

        while ($random->compare($max_multiple) >= 0) {
            $random = $random->subtract($max_multiple);
            $random_max = $random_max->subtract($max_multiple);
            $random = $random->bitwise_leftShift(8);
            $random = $random->add(new static(random_bytes(1), 256));
            $random_max = $random_max->bitwise_leftShift(8);
            [$max_multiple] = $random_max->divide($max);
            $max_multiple = $max_multiple->multiply($max);
        }
        [, $random] = $random->divide($max);

        return $random->add($min);
    }

    /**
     * Performs some post-processing for randomRangePrime
     */
    protected static function randomRangePrimeInner(Engine $x, Engine $min, Engine $max): ?Engine
    {
        if (!isset(static::$two[static::class])) {
            static::$two[static::class] = new static('2');
        }

        $x->make_odd();
        if ($x->compare($max) > 0) {
            // if $x > $max then $max is even and if $min == $max then no prime number exists between the specified range
            if ($min->equals($max)) {
                return null;
            }
            $x = clone $min;
            $x->make_odd();
        }

        $initial_x = clone $x;

        while (true) {
            if ($x->isPrime()) {
                return $x;
            }

            $x = $x->add(static::$two[static::class]);

            if ($x->compare($max) > 0) {
                $x = clone $min;
                if ($x->equals(static::$two[static::class])) {
                    return $x;
                }
                $x->make_odd();
            }

            if ($x->equals($initial_x)) {
                return null;
            }
        }
    }

    /**
     * Sets the $t parameter for primality testing
     */
    protected function setupIsPrime(): int
    {
        $length = $this->getLengthInBytes();

        // see HAC 4.49 "Note (controlling the error probability)"
        return match (true) {
            $length >= 163 =>  2, // floor(1300 / 8)
            $length >= 106 =>  3, // floor( 850 / 8)
            $length >= 81  =>  4, // floor( 650 / 8)
            $length >= 68  =>  5, // floor( 550 / 8)
            $length >= 56  =>  6, // floor( 450 / 8)
            $length >= 50  =>  7, // floor( 400 / 8)
            $length >= 43  =>  8, // floor( 350 / 8)
            $length >= 37  =>  9, // floor( 300 / 8)
            $length >= 31  => 12, // floor( 250 / 8)
            $length >= 25  => 15, // floor( 200 / 8)
            $length >= 18  => 18, // floor( 150 / 8)
            default        => 27,
        };
    }

    /**
     * Tests Primality
     *
     * Uses the {@link http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin primality test}.
     * See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap4.pdf#page=8 HAC 4.24} for more info.
     */
    protected function testPrimality(int $t): bool
    {
        if (!$this->testSmallPrimes()) {
            return false;
        }

        $n   = clone $this;
        $n_1 = $n->subtract(static::$one[static::class]);
        $n_2 = $n->subtract(static::$two[static::class]);

        $r = clone $n_1;
        $s = static::scan1divide($r);

        for ($i = 0; $i < $t; ++$i) {
            $a = static::randomRange(static::$two[static::class], $n_2);
            $y = $a->modPow($r, $n);

            if (!$y->equals(static::$one[static::class]) && !$y->equals($n_1)) {
                for ($j = 1; $j < $s && !$y->equals($n_1); ++$j) {
                    $y = $y->modPow(static::$two[static::class], $n);
                    if ($y->equals(static::$one[static::class])) {
                        return false;
                    }
                }

                if (!$y->equals($n_1)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Checks a numer to see if it's prime
     *
     * Assuming the $t parameter is not set, this function has an error rate of 2**-80.  The main motivation for the
     * $t parameter is distributability.  BigInteger::randomPrime() can be distributed across multiple pageloads
     * on a website instead of just one.
     */
    public function isPrime(?int $t = null): bool
    {
        // OpenSSL limits RSA keys to 16384 bits. The length of an RSA key is equal to the length of the modulo, which is
        // produced by multiplying the primes p and q by one another. The largest number two 8196 bit primes can produce is
        // a 16384 bit number so, basically, 8196 bit primes are the largest OpenSSL will generate and if that's the largest
        // that it'll generate it also stands to reason that that's the largest you'll be able to test primality on
        $length = $this->getLength();
        if ($length > 8196) {
            throw new ResourceLimitException("Primality testing is not supported for numbers larger than 8196 bits ($length)");
        }

        if (!isset($t)) {
            $t = $this->setupIsPrime();
        }
        return $this->testPrimality($t);
    }

    /**
     * Performs a few preliminary checks on root
     */
    protected function rootHelper(int $n): Engine
    {
        if ($n < 1) {
            return clone static::$zero[static::class];
        } // we want positive exponents
        if ($this->compare(static::$one[static::class]) < 0) {
            return clone static::$zero[static::class];
        } // we want positive numbers
        if ($this->compare(static::$two[static::class]) < 0) {
            return clone static::$one[static::class];
        } // n-th root of 1 or 2 is 1

        return $this->rootInner($n);
    }

    /**
     * Calculates the nth root of a biginteger.
     *
     * Returns the nth root of a positive biginteger, where n defaults to 2
     *
     * {@internal This function is based off of {@link http://mathforum.org/library/drmath/view/52605.html this page} and {@link http://stackoverflow.com/questions/11242920/calculating-nth-root-with-bcmath-in-php this stackoverflow question}.}
     */
    protected function rootInner(int $n): Engine
    {
        $n = new static($n);

        // g is our guess number
        $g = static::$two[static::class];
        // while (g^n < num) g=g*2
        while ($g->pow($n)->compare($this) < 0) {
            $g = $g->multiply(static::$two[static::class]);
        }
        // if (g^n==num) num is a power of 2, we're lucky, end of job
        // == 0 bccomp(bcpow($g, $n), $n->value)==0
        if ($g->pow($n)->equals($this) > 0) {
            $root = $g;
            return $this->normalize($root);
        }

        // if we're here num wasn't a power of 2 :(
        $og = $g; // og means original guess and here is our upper bound
        $g = $g->divide(static::$two[static::class])[0]; // g is set to be our lower bound
        $step = $og->subtract($g)->divide(static::$two[static::class])[0]; // step is the half of upper bound - lower bound
        $g = $g->add($step); // we start at lower bound + step , basically in the middle of our interval

        // while step>1

        while ($step->compare(static::$one[static::class]) == 1) {
            $guess = $g->pow($n);
            $step = $step->divide(static::$two[static::class])[0];
            $comp = $guess->compare($this); // compare our guess with real number
            switch ($comp) {
                case -1: // if guess is lower we add the new step
                    $g = $g->add($step);
                    break;
                case 1: // if guess is higher we sub the new step
                    $g = $g->subtract($step);
                    break;
                case 0: // if guess is exactly the num we're done, we return the value
                    break 2;
            }
        }

        if ($comp == 1) {
            $g = $g->subtract($step);
        }

        // whatever happened, g is the closest guess we can make so return it
        $root = $g;

        return $this->normalize($root);
    }

    /**
     * Calculates the nth root of a biginteger.
     */
    public function root(int $n = 2): Engine
    {
        return $this->rootHelper($n);
    }

    /**
     * Return the minimum BigInteger between an arbitrary number of BigIntegers.
     */
    protected static function minHelper(array $nums): Engine
    {
        if (count($nums) == 1) {
            return $nums[0];
        }
        $min = $nums[0];
        for ($i = 1; $i < count($nums); $i++) {
            $min = $min->compare($nums[$i]) > 0 ? $nums[$i] : $min;
        }
        return $min;
    }

    /**
     * Return the minimum BigInteger between an arbitrary number of BigIntegers.
     */
    protected static function maxHelper(array $nums): Engine
    {
        if (count($nums) == 1) {
            return $nums[0];
        }
        $max = $nums[0];
        for ($i = 1; $i < count($nums); $i++) {
            $max = $max->compare($nums[$i]) < 0 ? $nums[$i] : $max;
        }
        return $max;
    }

    /**
     * Create Recurring Modulo Function
     *
     * Sometimes it may be desirable to do repeated modulos with the same number outside of
     * modular exponentiation
     */
    public function createRecurringModuloFunction(): \Closure
    {
        $class = static::class;

        $fqengine = !method_exists(static::$modexpEngine[static::class], 'reduce') ?
            '\\phpseclib4\\Math\\BigInteger\\Engines\\' . static::ENGINE_DIR . '\\DefaultEngine' :
            static::$modexpEngine[static::class];
        if (method_exists($fqengine, 'generateCustomReduction')) {
            $func = $fqengine::generateCustomReduction($this, static::class);
            return eval('return function(' . static::class . ' $x) use ($func, $class) {
                $r = new $class();
                $r->value = $func($x->value);
                return $r;
            };');
        }
        /** @psalm-suppress UndefinedThisPropertyFetch */
        $n = $this->value;
        return eval('return function(' . static::class . ' $x) use ($n, $fqengine, $class) {
            $r = new $class();
            $r->value = $fqengine::reduce($x->value, $n, $class);
            return $r;
        };');
    }

    /**
     * Calculates the greatest common divisor and Bezout's identity.
     *
     * @return array{gcd: Engine, x: Engine, y: Engine}
     */
    protected function extendedGCDHelper(Engine $n): array
    {
        $u = clone $this;
        $v = clone $n;

        $one = new static(1);
        $zero = new static();

        $a = clone $one;
        $b = clone $zero;
        $c = clone $zero;
        $d = clone $one;

        while (!$v->equals($zero)) {
            [$q] = $u->divide($v);

            $temp = $u;
            $u = $v;
            $v = $temp->subtract($v->multiply($q));

            $temp = $a;
            $a = $c;
            $c = $temp->subtract($a->multiply($q));

            $temp = $b;
            $b = $d;
            $d = $temp->subtract($b->multiply($q));
        }

        return [
            'gcd' => $u,
            'x' => $a,
            'y' => $b,
        ];
    }

    /**
     * Bitwise Split
     *
     * Splits BigInteger's into chunks of $split bits
     *
     * @return Engine[]
     */
    public function bitwise_split(int $split): array
    {
        if ($split < 1) {
            throw new InvalidArgumentException('Offset must be greater than 1');
        }

        $mask = static::$one[static::class]->bitwise_leftShift($split)->subtract(static::$one[static::class]);

        $num = clone $this;

        $vals = [];
        while (!$num->equals(static::$zero[static::class])) {
            $vals[] = $num->bitwise_and($mask);
            $num = $num->bitwise_rightShift($split);
        }

        return array_reverse($vals);
    }

    /**
     * Logical And
     */
    protected function bitwiseAndHelper(Engine $x): Engine
    {
        $left = $this->toBytes(true);
        $right = $x->toBytes(true);

        $length = max(strlen($left), strlen($right));

        $left = str_pad($left, $length, chr(0), STR_PAD_LEFT);
        $right = str_pad($right, $length, chr(0), STR_PAD_LEFT);

        return $this->normalize(new static($left & $right, -256));
    }

    /**
     * Logical Or
     */
    protected function bitwiseOrHelper(Engine $x): Engine
    {
        $left = $this->toBytes(true);
        $right = $x->toBytes(true);

        $length = max(strlen($left), strlen($right));

        $left = str_pad($left, $length, chr(0), STR_PAD_LEFT);
        $right = str_pad($right, $length, chr(0), STR_PAD_LEFT);

        return $this->normalize(new static($left | $right, -256));
    }

    /**
     * Logical Exclusive Or
     */
    protected function bitwiseXorHelper(Engine $x): Engine
    {
        $left = $this->toBytes(true);
        $right = $x->toBytes(true);

        $length = max(strlen($left), strlen($right));


        $left = str_pad($left, $length, chr(0), STR_PAD_LEFT);
        $right = str_pad($right, $length, chr(0), STR_PAD_LEFT);
        return $this->normalize(new static($left ^ $right, -256));
    }
}
