<?php

namespace BN;

use \Exception;
use \BI\BigInteger;

class Red
{
    public static $ASSERT_ENABLED;
    public $m;

    function __construct($m) {
        if( is_string($m) )
            $this->m = Red::primeByName($m);
        else
            $this->m = $m;

        if( !$this->m->gtn(1) )
            throw new Exception("Modulus must be greater than 1");
    }


    public static function primeByName($name)
    {
        switch($name) {
            case "k256":
                return new BN("ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f", 16);
            case "p224":
                return new BN("ffffffff ffffffff ffffffff ffffffff 00000000 00000000 00000001", 16);
            case "p192":
                return new BN("ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff", 16);
            case "p25519":
                return new BN("7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed", 16);
            default:
                throw new Exception("Unknown prime name " . $name);
        }
    }

    public function verify1(BN $num)
    {
        if (Red::$ASSERT_ENABLED) assert(!$num->negative()); //,"red works only with positives");
        assert($num->red); //, "red works only with red numbers");
    }

    public function verify2(BN $a, BN $b)
    {
        if (Red::$ASSERT_ENABLED) assert(!$a->negative() && !$b->negative()); //, "red works only with positives");
        assert($a->red && ($a->red == $b->red)); //, "red works only with red numbers");
    }

    public function imod(BN &$a) {
        return $a->umod($this->m)->_forceRed($this);
    }

    public function neg(BN $a)
    {
        if( $a->isZero() )
            return $a->_clone();
        return $this->m->sub($a)->_forceRed($this);
    }

    public function add(BN $a, BN $b)
    {
        $this->verify2($a, $b);

        $res = $a->add($b);
        if( $res->cmp($this->m) >= 0 )
            $res->isub($this->m);
        return $res->_forceRed($this);
    }

    public function iadd(BN &$a, BN $b)
    {
        $this->verify2($a, $b);

        $a->iadd($b);
        if( $a->cmp($this->m) >= 0 )
            $a->isub($this->m);

        return $a;
    }

    public function sub(BN $a, BN $b)
    {
        $this->verify2($a, $b);

        $res = $a->sub($b);
        if( $res->negative() )
            $res->iadd($this->m);

        return $res->_forceRed($this);
    }

    public function isub(BN &$a, $b)
    {
        $this->verify2($a, $b);

        $a->isub($b);
        if( $a->negative() )
            $a->iadd($this->m);

        return $a;
    }

    public function shl(BN $a, $num) {
        $this->verify1($a);
        return $this->imod($a->ushln($num));
    }

    public function imul(BN &$a, BN $b) {
        $this->verify2($a, $b);
        $res = $a->imul($b);
        return $this->imod($res);
    }

    public function mul(BN $a, BN $b) {
        $this->verify2($a, $b);
        $res = $a->mul($b);
        return $this->imod($res);
    }

    public function sqr(BN $a) {
        $res = $a->_clone();
        return $this->imul($res, $a);
    }

    public function isqr(BN &$a) {
        return $this->imul($a, $a);
    }

    public function sqrt(BN $a) {
        if ($a->isZero()) 
            return $a->_clone();

        $mod3 = $this->m->andln(3);
        assert($mod3 % 2 == 1);

        // Fast case
        if ($mod3 == 3) {
            $pow = $this->m->add(new BN(1))->iushrn(2);
            return $this->pow($a, $pow);
        }

        // Tonelli-Shanks algorithm (Totally unoptimized and slow)
        //
        // Find Q and S, that Q * 2 ^ S = (P - 1)
        $q = $this->m->subn(1);
        $s = 0;
        while (!$q->isZero() && $q->andln(1) == 0) {
            $s++;
            $q->iushrn(1);
        }
        if (Red::$ASSERT_ENABLED) assert(!$q->isZero());

        $one = (new BN(1))->toRed($this);
        $nOne = $one->redNeg();

        // Find quadratic non-residue
        // NOTE: Max is such because of generalized Riemann hypothesis.
        $lpow = $this->m->subn(1)->iushrn(1);
        $z = $this->m->bitLength();
        $z = (new BN(2 * $z * $z))->toRed($this);

        while ($this->pow($z, $lpow)->cmp($nOne) != 0) {
            $z->redIAdd($nOne);
        }

        $c = $this->pow($z, $q);
        $r = $this->pow($a, $q->addn(1)->iushrn(1));
        $t = $this->pow($a, $q);
        $m = $s;
        while ($t->cmp($one) != 0) {
            $tmp = $t;
            for ($i = 0; $tmp->cmp($one) != 0; $i++) {
                $tmp = $tmp->redSqr();
            }
            if ($i >= $m) {
                throw new \Exception("Assertion failed");
            }
            if ($m - $i - 1 > 54) {
                $b = $this->pow($c, (new BN(1))->iushln($m - $i - 1));
            } else {
                $b = clone($c);
                $b->bi = $c->bi->powMod(1 << ($m - $i - 1), $this->m->bi);
            }

            $r = $r->redMul($b);
            $c = $b->redSqr();
            $t = $t->redMul($c);
            $m = $i;
        }

        return $r;    
    }

    public function invm(BN &$a) {
        $res = $a->invm($this->m);
        return $this->imod($res);
    }

    public function pow(BN $a, BN $num) {
        $r = clone($a);
        $r->bi = $a->bi->powMod($num->bi, $this->m->bi);
        return $r;        
    }

    public function convertTo(BN $num) {
        $r = $num->umod($this->m);
        return $r === $num ? $r->_clone() : $r;
    }

    public function convertFrom(BN $num) {
        $res = $num->_clone();
        $res->red = null;
        return $res;
    }
}
Red::$ASSERT_ENABLED = ini_get("zend.assertions") === "1";

?>
