/*
    Copyright 2018 0kims association.

    This file is part of zksnark JavaScript library.

    zksnark JavaScript library is a free software: you can redistribute it and/or 
    modify it under the terms of the GNU General Public License as published by the 
    Free Software Foundation, either version 3 of the License, or (at your option) 
    any later version.

    zksnark JavaScript library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
    or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 
    more details.

    You should have received a copy of the GNU General Public License along with 
    zksnark JavaScript library. If not, see <https://www.gnu.org/licenses/>.
*/

const fUtils = require("./futils.js");

class GCurve {

    constructor(F, g) {
        this.F = F;
        this.g = [F.copy(g[0]), F.copy(g[1])];
        if (this.g.length == 2) this.g[2] = this.F.one;
        this.zero = [this.F.zero, this.F.one, this.F.zero];
    }

    isZero(p) {
        return this.F.isZero(p[2]);
    }

    add(p1, p2) {

        if (this.isZero(p1)) return p2;
        if (this.isZero(p2)) return p1;

        const res = new Array(3);

        const Z1Z1 = this.F.square( p1[2] );
        const Z2Z2 = this.F.square( p2[2] );

        const U1 = this.F.mul( p1[0] , Z2Z2 );     // U1 = X1  * Z2Z2
        const U2 = this.F.mul( p2[0] , Z1Z1 );     // U2 = X2  * Z1Z1

        const Z1_cubed = this.F.mul( p1[2] , Z1Z1);
        const Z2_cubed = this.F.mul( p2[2] , Z2Z2);

        const S1 = this.F.mul( p1[1] , Z2_cubed);  // S1 = Y1 * Z2 * Z2Z2
        const S2 = this.F.mul( p2[1] , Z1_cubed);  // S2 = Y2 * Z1 * Z1Z1

        if (this.F.equals(U1,U2) && this.F.equals(S1,S2)) {
            return this.double(p1);
        }

        const H = this.F.sub( U2 , U1 );                    // H = U2-U1

        const S2_minus_S1 = this.F.sub( S2 , S1 );

        const I = this.F.square( this.F.add(H,H) );         // I = (2 * H)^2
        const J = this.F.mul( H , I );                      // J = H * I

        const r = this.F.add( S2_minus_S1 , S2_minus_S1 );  // r = 2 * (S2-S1)
        const V = this.F.mul( U1 , I );                     // V = U1 * I

        res[0] =
            this.F.sub(
                this.F.sub( this.F.square(r) , J ),
                this.F.add( V , V ));                       // X3 = r^2 - J - 2 * V

        const S1_J = this.F.mul( S1 , J );

        res[1] =
            this.F.sub(
                this.F.mul( r , this.F.sub(V,res[0])),
                this.F.add( S1_J,S1_J ));                   // Y3 = r * (V-X3)-2 S1 J

        res[2] =
            this.F.mul(
                H,
                this.F.sub(
                    this.F.square( this.F.add(p1[2],p2[2]) ),
                    this.F.add( Z1Z1 , Z2Z2 )));            // Z3 = ((Z1+Z2)^2-Z1Z1-Z2Z2) * H

        return res;
    }

    neg(p) {
        return [p[0], this.F.neg(p[1]), p[2]];
    }

    sub(a, b) {
        return this.add(a, this.neg(b));
    }

    double(p) {
        const res = new Array(3);

        if (this.isZero(p)) return p;

        const A = this.F.square( p[0] );                    // A = X1^2
        const B = this.F.square( p[1] );                    // B = Y1^2
        const C = this.F.square( B );                       // C = B^2

        let D =
            this.F.sub(
                this.F.square( this.F.add(p[0] , B )),
                this.F.add( A , C));
        D = this.F.add(D,D);                    // D = 2 * ((X1 + B)^2 - A - C)

        const E = this.F.add( this.F.add(A,A), A);          // E = 3 * A
        const F = this.F.square( E );                       // F = E^2

        res[0] = this.F.sub( F , this.F.add(D,D) );         // X3 = F - 2 D

        let eightC = this.F.add( C , C );
        eightC = this.F.add( eightC , eightC );
        eightC = this.F.add( eightC , eightC );

        res[1] =
            this.F.sub(
                this.F.mul(
                    E,
                    this.F.sub( D, res[0] )),
                eightC);                                    // Y3 = E * (D - X3) - 8 * C

        const Y1Z1 = this.F.mul( p[1] , p[2] );
        res[2] = this.F.add( Y1Z1 , Y1Z1 );                 // Z3 = 2 * Y1 * Z1

        return res;
    }

    mulScalar(base, e) {
        return fUtils.mulScalar(this, base, e);
    }

    affine(p) {
        if (this.isZero(p)) {
            return this.zero;
        } else {
            const Z_inv = this.F.inverse(p[2]);
            const Z2_inv = this.F.square(Z_inv);
            const Z3_inv = this.F.mul(Z2_inv, Z_inv);

            const res = new Array(3);
            res[0] = this.F.affine( this.F.mul(p[0],Z2_inv));
            res[1] = this.F.affine( this.F.mul(p[1],Z3_inv));
            res[2] = this.F.one;

            return res;
        }
    }

    equals(p1, p2) {
        if (this.isZero(p1)) return this.isZero(p2);
        if (this.isZero(p2)) return this.isZero(p1);

        const Z1Z1 = this.F.square( p1[2] );
        const Z2Z2 = this.F.square( p2[2] );

        const U1 = this.F.mul( p1[0] , Z2Z2 );
        const U2 = this.F.mul( p2[0] , Z1Z1 );

        const Z1_cubed = this.F.mul( p1[2] , Z1Z1);
        const Z2_cubed = this.F.mul( p2[2] , Z2Z2);

        const S1 = this.F.mul( p1[1] , Z2_cubed);
        const S2 = this.F.mul( p2[1] , Z1_cubed);

        return (this.F.equals(U1,U2) && this.F.equals(S1,S2));
    }

    toString(p) {
        const cp = this.affine(p);
        return `[ ${this.F.toString(cp[0])} , ${this.F.toString(cp[1])} ]`;
    }

}

module.exports = GCurve;

