package Crypt::Perl::ECDSA::EC::Point;

use strict;
use warnings;

use Crypt::Perl::BigInt ();

my ($bi1, $bi2, $bi3);

END {
    undef $bi1, $bi2, $bi3;
}

sub new_infinity {
    my ($class) = @_;

    return $class->new( undef, undef );
}

#$curve is ECCurve
#$x and $y are “ECFieldElement”
#$z isa bigint (?)
sub new {
    my ($class, $curve, $x, $y, $z) = @_;

    $bi1 ||= Crypt::Perl::BigInt->new(1);
    $bi2 ||= Crypt::Perl::BigInt->new(2);
    $bi3 ||= Crypt::Perl::BigInt->new(3);

    my $self = {
        curve => $curve,
        x => $x,
        y => $y,
        z => $z || $bi1->copy(),
        zinv => undef,
    };

    return bless $self, $class;
}

sub is_infinity {
    my ($self) = @_;

    return 1 if !defined $self->{'x'} && !defined $self->{'y'};

    return( ($self->{'z'}->is_zero() && !$self->{'y'}->to_bigint()->is_zero()) || 0 );
}

#returns ECFieldElement
sub get_x {
    my ($self) = @_;

    return $self->_get_x_or_y('x');
}

#returns ECFieldElement
#Used in key generation (not signing … ?)
sub get_y {
    my ($self) = @_;

    return $self->_get_x_or_y('y');
}

sub _get_x_or_y {
    my ($self, $to_get) = @_;

    if (!defined $self->{'zinv'}) {
        $self->{'zinv'} = $self->{'z'}->copy()->bmodinv($self->{'curve'}{'q'});
    }

    return $self->{'curve'}->from_bigint(
        $self->{$to_get}->to_bigint()->copy()->bmul($self->{'zinv'})->bmod($self->{'curve'}{'q'})
    );
}

sub negate {
    my ($self) = @_;

    my @args = @{$self}{qw( curve x y z )};
    $args[2] = $args[2]->negate();

    return (ref $self)->new(@args);
}

#Did the quadratic formula explode???
sub twice {
    my ($self) = @_;

    return $self if $self->is_infinity();

    #if ($self->{'y'}->to_bigint()->signum() == 0) {
    #    return $self->{'curve'}->get_infinity();
    #}

    my $x1 = $self->{'x'}->to_bigint();
    my $y1 = $self->{'y'}->to_bigint();

    my $y1z1 = $y1->copy()->bmul($self->{'z'});

    my $y1sqz1 = $y1z1->copy()->bmul($y1)->bmod($self->{'curve'}{'q'});

    my $a = $self->{'curve'}{'a'};

    # w = 3 * x1^2 + a * z1^2
    #var w = x1.square().multiply(THREE);
    my $w = $x1->copy()->bpow($bi2)->bmul($bi3);

    if (!$a->is_zero()) {
        #$w += ($self->{'z'} ** 2) * $a;
        $w->badd( $a->copy()->bmul( $self->{'z'} )->bmul($self->{'z'}) );
    }

    $w->bmod($self->{'curve'}{'q'});

    # x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1)
    #var x3 = w.square().subtract(x1.shiftLeft(3).multiply(y1sqz1)).shiftLeft(1).multiply(y1z1).mod(this.curve.q);

    my $x3 = $w->copy()->bmuladd( $w, $y1sqz1->copy()->bmul($x1)->blsft($bi3)->bneg() )->bmul($bi2)->bmul($y1z1);
    #my $x3 = 2 * $y1z1 * (($w ** 2) - ($x1 << 3) * $y1sqz1);
    #my $x3 = ($w ** 2) - (($x1 << 3) * $y1sqz1);
    #$x3 = $x3 << 1;
    #$x3 *= $y1z1;

    # y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3
    #var y3 = w.multiply(THREE).multiply(x1).subtract(y1sqz1.shiftLeft(1)).shiftLeft(2).multiply(y1sqz1).subtract(w.square().multiply(w)).mod(this.curve.q);
    #my $y3 = 4 * $y1sqz1 * (3 * $w * $x1 - 2 * $y1sqz1) - ($w ** 3);

    my $y3 = $y1sqz1->copy()->blsft($bi2);

    $y3->bmuladd(

        #We don’t need y1sqz1 anymore
        $w->copy()->bmul($bi3)->bmuladd($x1, $y1sqz1->blsft($bi1)->bneg()),

        #Don’t need $w anymore
        $w->bpow($bi3)->bneg(),
    );

    #// z3 = 8 * (y1 * z1)^3
    #var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q);
    #my $z3 = ($y1z1 ** 3) << 3;
    my $z3 = $y1z1->bpow($bi3)->blsft($bi3);  #don’t need y1z1 anymore

    #In original JS logic
    $_->bmod($self->{'curve'}{'q'}) for ($x3, $y3, $z3);

    #return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3);
    return (ref $self)->new(
        $self->{'curve'},
        $self->{'curve'}->from_bigint($x3),
        $self->{'curve'}->from_bigint($y3),
        $z3,
    );
}

#XXX clear
sub dump {
    my ($self, $label) = @_;

    printf "$label.x: %s\n", $self->{'x'}->to_bigint()->as_hex();
    printf "$label.y: %s\n", $self->{'y'}->to_bigint()->as_hex();
    printf "$label.z: %s\n", $self->{'z'}->as_hex();
}

#yowza ...
sub multiply {
    my ($self, $k) = @_;
#print "in mult\n";
#$self->dump('self');

    if ($self->is_infinity()) {
        return $self;
    }

    #TODO
    #if ($k->signum() == 0) {
    #    return $self->{'curve'}->getInfinity();     #should this happen?
    #}

#print "here2\n";
    my $e = $k;
    my $h = $e->copy()->bmul($bi3);
#print "h: " . $h->as_hex() . $/;

    my $neg = $self->negate();
#$neg->dump('neg');
    my $R = $self;

#print "start loop: " . (_MBI_bit_length($h) - 2) . $/;
    for my $i ( reverse( 1 .. ($h->bit_length() - 2) ) ) {
        $R = $R->twice();
#$R->dump("R, loop $i");

        my $hbit = $h->test_bit($i);
        my $ebit = $e->test_bit($i);
#print "hbit [$hbit], ebit [$ebit]\n";

        if ($hbit != $ebit) {
            $R = $R->add( $hbit ? $self : $neg );
        }
#$R->dump("R, end of loop");
    }

    return $R;
}

#$b isa ECPoint
sub add {
    my ($self, $b) = @_;
#$b->dump('$b');

    #if(this.isInfinity()) return b;
    #if(b.isInfinity()) return this;

    return $b if $self->is_infinity();
    return $self if $b->is_infinity();

    #// u = Y2 * Z1 - Y1 * Z2
    #var u = b.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(b.z)).mod(this.curve.q);
    my $u = $b->{'y'}->to_bigint()->copy()->bmuladd(
        $self->{'z'},
        $self->{'y'}->to_bigint()->copy()->bneg()->bmul($b->{'z'}),
    );
# $b->{'z'};

    #// v = X2 * Z1 - X1 * Z2
    #var v = b.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(b.z)).mod(this.curve.q);
    my $v = $b->{'x'}->to_bigint()->copy()->bmuladd(
        $self->{'z'},
        $self->{'x'}->to_bigint()->copy()->bneg()->bmul($b->{'z'}),
    );


    $_->bmod($self->{'curve'}{'q'}) for ($u, $v);
#print "u: " . $u->as_hex() . $/;
#print "v: " . $v->as_hex() . $/;
    #if(BigInteger.ZERO.equals(v)) {
    #    if(BigInteger.ZERO.equals(u)) {
    #        return this.twice(); // this == b, so double
    #    }
	#return this.curve.getInfinity(); // this = -b, so infinity
    #}
    if ($v->is_zero()) {
        if ($u->is_zero()) {
            return $self->twice();
        }

        return $self->{'curve'}->get_infinity();
    }

    #var THREE = new BigInteger("3");
    #var x1 = this.x.toBigInteger();
    #var y1 = this.y.toBigInteger();
    #var x2 = b.x.toBigInteger();
    #var y2 = b.y.toBigInteger();
    my ($x1, $y1, $z1) = @{$self}{ qw( x y z ) };
    my ($x2, $y2, $z2) = @{$b}{ qw( x y z ) };

    $_ = $_->to_bigint() for ($x1, $y1, $x2, $y2);

    #var v2 = v.square();
    #var v3 = v2.multiply(v);
    #var x1v2 = x1.multiply(v2);
    #var zu2 = u.square().multiply(this.z);

    my $v2 = $v->copy()->bmul($v);
    my $v3 = $v->copy()->bmul($v2);

    my $x1v2 = $x1->copy()->bmul($v2);
    my $zu2 = $u->copy()->bmul($u)->bmul($self->{'z'});
#use Data::Dumper;
#print Dumper( map { $_->as_hex() } $u, $v, $x1, $y1, $z1, $x2, $y2, $z2, $v2, $v3, $x1v2, $zu2 );

    #// x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3)
    #var x3 = zu2.subtract(x1v2.shiftLeft(1)).multiply(b.z).subtract(v3).multiply(v).mod(this.curve.q);
    #my $x3 = $v * ($z2 * ($z1 * ($u ** 2) - 2 * $x1 * ($v ** 2)) - ($v ** 3));
    my $x3 = $u->copy()->bmul($u);
    $x3->bmuladd( $z1, $x1->copy()->blsft($bi1)->bneg()->bmul($v)->bmul($v) );
    $x3->bmuladd( $z2, $v->copy()->bpow($bi3)->bneg() );
    $x3->bmul($v);

    #// y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3
    #var y3 = x1v2.multiply(THREE).multiply(u).subtract(y1.multiply(v3)).subtract(zu2.multiply(u)).multiply(b.z).add(u.multiply(v3)).mod(this.curve.q);
    #my $y3 = $z2 * (3 * $x1 * $u * $v2 - $y1 * $v3 - $z1 * ($u ** 3)) + $u * $v3;
    my $y3 = $u->copy()->bmul($bi3)->bmul($x1);
    $y3->bmuladd($v2, $y1->bmul($v3)->bneg());      #no more y1 after this
    $y3->bsub( $u->copy()->bpow($bi3)->bmul($z1) );

    $y3->bmuladd( $z2, $u->bmul($v3) );             #we don’t need $u anymore

    #// z3 = v^3 * z1 * z2
    #var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q);
    my $z3 = $v3->bmul($z1)->bmul($z2);

    $_->bmod($self->{'curve'}{'q'}) for ($x3, $y3, $z3);

    return (ref $self)->new(
        $self->{'curve'},
        $self->{'curve'}->from_bigint($x3),
        $self->{'curve'}->from_bigint($y3),
        $z3,
    );
}

1;
