package org.bouncycastle.math.ec;

import org.bouncycastle.asn1.x9.X9IntegerConverter;

import java.math.BigInteger;

/**
 * base class for points on elliptic curves.
 */
public abstract class ECPoint
{
    ECCurve        curve;
    ECFieldElement x;
    ECFieldElement y;
    
    private static X9IntegerConverter converter = new X9IntegerConverter();

    protected ECPoint(ECCurve curve, ECFieldElement x, ECFieldElement y)
    {
        this.curve = curve;
        this.x = x;
        this.y = y;
    }
    
    public ECCurve getCurve()
    {
        return curve;
    }
    
    public ECFieldElement getX()
    {
        return x;
    }

    public ECFieldElement getY()
    {
        return y;
    }

    public boolean isInfinity()
    {
        return x == null && y == null;
    }
    
    public boolean equals(
        Object  other)
    {
        if (other == this)
        {
            return true;
        }

        if (!(other instanceof ECPoint))
        {
            return false;
        }

        ECPoint o = (ECPoint)other;

        if (this.isInfinity())
        {
            return o.isInfinity();
        }

        return x.equals(o.x) && y.equals(o.y);
    }

    public int hashCode()
    {
        if (this.isInfinity())
        {
            return 0;
        }
        
        return x.hashCode() ^ y.hashCode();
    }

    public abstract byte[] getEncoded();

    public abstract ECPoint add(ECPoint b);
    public abstract ECPoint subtract(ECPoint b);
    public abstract ECPoint twice();
    public abstract ECPoint multiply(BigInteger b);

    /**
     * Elliptic curve points over Fp
     */
    public static class Fp extends ECPoint
    {
        private boolean withCompression;
        
        /**
         * Create a point which encodes with point compression.
         * 
         * @param curve the curve to use
         * @param x affine x co-ordinate
         * @param y affine y co-ordinate
         */
        public Fp(ECCurve curve, ECFieldElement x, ECFieldElement y)
        {
            this(curve, x, y, false);
        }

        /**
         * Create a point that encodes with or without point compresion.
         * 
         * @param curve the curve to use
         * @param x affine x co-ordinate
         * @param y affine y co-ordinate
         * @param withCompression if true encode with point compression
         */
        public Fp(ECCurve curve, ECFieldElement x, ECFieldElement y, boolean withCompression)
        {
            super(curve, x, y);

            if ((x != null && y == null) || (x == null && y != null))
            {
                throw new IllegalArgumentException("Exactly one of the field elements is null");
            }

            this.withCompression = withCompression;
        }
         
        /**
         * return the field element encoded with point compression. (S 4.3.6)
         */
        public byte[] getEncoded()
        {
            int qLength = converter.getByteLength(x);
            
            if (withCompression)
            {
                byte    PC;
    
                if (this.getY().toBigInteger().testBit(0))
                {
                    PC = 0x03;
                }
                else
                {
                    PC = 0x02;
                }
    
                byte[]  X = converter.integerToBytes(this.getX().toBigInteger(), qLength);
                byte[]  PO = new byte[X.length + 1];
    
                PO[0] = PC;
                System.arraycopy(X, 0, PO, 1, X.length);
    
                return PO;
            }
            else
            {
                byte[]  X = converter.integerToBytes(this.getX().toBigInteger(), qLength);
                byte[]  Y = converter.integerToBytes(this.getY().toBigInteger(), qLength);
                byte[]  PO = new byte[X.length + Y.length + 1];
                
                PO[0] = 0x04;
                System.arraycopy(X, 0, PO, 1, X.length);
                System.arraycopy(Y, 0, PO, X.length + 1, Y.length);

                return PO;
            }
        }

        // B.3 pg 62
        public ECPoint add(ECPoint b)
        {
            if (this.isInfinity())
            {
                return b;
            }

            if (b.isInfinity())
            {
                return this;
            }

            // Check if b = this or b = -this
            if (this.x.equals(b.x))
            {
                if (this.y.equals(b.y))
                {
                    // this = b, i.e. this must be doubled
                    return this.twice();
                }

                // this = -b, i.e. the result is the point at infinity
                return this.curve.getInfinity();
            }

            ECFieldElement gamma = b.y.subtract(this.y).divide(b.x.subtract(this.x));

            ECFieldElement x3 = gamma.multiply(gamma).subtract(this.x).subtract(b.x);
            ECFieldElement y3 = gamma.multiply(this.x.subtract(x3)).subtract(this.y);

            return new ECPoint.Fp(curve, x3, y3);
        }

        // B.3 pg 62
        public ECPoint twice()
        {
            if (this.isInfinity())
            {
                // Twice identity element (point at infinity) is identity
                return this;
            }

            if (this.y.toBigInteger().signum() == 0) 
            {
                // if y1 == 0, then (x1, y1) == (x1, -y1)
                // and hence this = -this and thus 2(x1, y1) == infinity
                return this.curve.getInfinity();
            }

            ECFieldElement TWO = this.curve.fromBigInteger(BigInteger.valueOf(2));
            ECFieldElement THREE = this.curve.fromBigInteger(BigInteger.valueOf(3));
            ECFieldElement gamma = this.x.multiply(this.x).multiply(THREE).add(curve.a).divide(y.multiply(TWO));

            ECFieldElement x3 = gamma.multiply(gamma).subtract(this.x.multiply(TWO));
            ECFieldElement y3 = gamma.multiply(this.x.subtract(x3)).subtract(this.y);
                
            return new ECPoint.Fp(curve, x3, y3, this.withCompression);
        }

        // D.3.2 pg 102 (see Note:)
        public ECPoint subtract(ECPoint b)
        {
            if (b.isInfinity())
            {
                return this;
            }

            // Add -b
            return add(new ECPoint.Fp(this.curve, b.x, b.y.negate(), this.withCompression));
        }

        // D.3.2 pg 101
        public ECPoint multiply(BigInteger k)
        {
            if (this.isInfinity())
            {
                return this;
            }

            if (k.signum() == 0)
            {
                return this.curve.getInfinity();
            }

            // BigInteger e = k.mod(n); // n == order this
            BigInteger e = k;

            BigInteger h = e.multiply(BigInteger.valueOf(3));

            ECPoint R = this;

            for (int i = h.bitLength() - 2; i > 0; i--)
            {             
                R = R.twice();       

                if (h.testBit(i) && !e.testBit(i))
                {                    
                    //System.out.print("+");
                    R = R.add(this);
                }
                else if (!h.testBit(i) && e.testBit(i))
                {
                    //System.out.print("-");
                    R = R.subtract(this);
                }
                // else
                // System.out.print(".");
            }
            // System.out.println();

            return R;
        }
    }
    
    /**
     * Elliptic curve points over F2m
     */
    public static class F2m extends ECPoint
    {
        private boolean withCompression;
        
        /**
         * @param curve base curve
         * @param x x point
         * @param y y point
         */
        public F2m(ECCurve curve, ECFieldElement x, ECFieldElement y)
        {
            this(curve, x, y, false);
        }
        
        /**
         * @param curve base curve
         * @param x x point
         * @param y y point
         * @param withCompression true if encode with point compression.
         */
        public F2m(ECCurve curve, ECFieldElement x, ECFieldElement y, boolean withCompression)
        {
            super(curve, x, y);

            if ((x != null && y == null) || (x == null && y != null))
            {
                throw new IllegalArgumentException("Exactly one of the field elements is null");
            }
            
            if (x != null)
            {
                // Check if x and y are elements of the same field
                ECFieldElement.F2m.checkFieldElements(this.x, this.y);
    
                // Check if x and a are elements of the same field
                if (curve != null)
                {
                    ECFieldElement.F2m.checkFieldElements(this.x, this.curve.getA());
                }
            }
            
            this.withCompression = withCompression;
        }

        /**
         * @deprecated use ECCurve.getInfinity()
         * Constructor for point at infinity
         */
        public F2m(ECCurve curve)
        {
            super(curve, null, null);
        }

        /* (non-Javadoc)
         * @see org.bouncycastle.math.ec.ECPoint#getEncoded()
         */
        public byte[] getEncoded()
        {
            if (this.isInfinity()) 
            {
                throw new RuntimeException("Point at infinity cannot be encoded");
            }

            int byteCount = converter.getByteLength(this.x);
            byte[] X = converter.integerToBytes(this.getX().toBigInteger(), byteCount);
            byte[] PO;

            if (withCompression)
            {
                // See X9.62 4.3.6 and 4.2.2
                PO = new byte[byteCount + 1];

                PO[0] = 0x02;
                // X9.62 4.2.2 and 4.3.6:
                // if x = 0 then ypTilde := 0, else ypTilde is the rightmost
                // bit of y * x^(-1)
                // if ypTilde = 0, then PC := 02, else PC := 03
                // Note: PC === PO[0]
                if (!(this.getX().toBigInteger().equals(ECConstants.ZERO)))
                {
                    if (this.getY().multiply(this.getX().invert())
                            .toBigInteger().testBit(0))
                    {
                        // ypTilde = 1, hence PC = 03
                        PO[0] = 0x03;
                    }
                }

                System.arraycopy(X, 0, PO, 1, byteCount);
            }
            else
            {
                byte[] Y = converter.integerToBytes(this.getY().toBigInteger(), byteCount);
    
                PO = new byte[byteCount + byteCount + 1];
    
                PO[0] = 0x04;
                System.arraycopy(X, 0, PO, 1, byteCount);
                System.arraycopy(Y, 0, PO, byteCount + 1, byteCount);    
            }

            return PO;
        }

        /* (non-Javadoc)
         * @see org.bouncycastle.math.ec.ECPoint#add(org.bouncycastle.math.ec.ECPoint)
         */
        public ECPoint add(ECPoint b)
        {
            // Check, if points are on the same curve
            if (!(this.curve.equals(b.getCurve())))
            {
                throw new IllegalArgumentException("Only points on the same "
                        + "curve can be added");
            }

            if (this.isInfinity())
            {
                return b;
            }

            if (b.isInfinity())
            {
                return this;
            }

            ECFieldElement.F2m.checkFieldElements(this.x, b.getX());
            ECFieldElement.F2m x2 = (ECFieldElement.F2m)b.getX();
            ECFieldElement.F2m y2 = (ECFieldElement.F2m)b.getY();

            // Check if b = this or b = -this
            if (this.x.equals(x2))
            {
                if (this.y.equals(y2))
                {
                    // this = b, i.e. this must be doubled
                    return this.twice();
                }

                // this = -b, i.e. the result is the point at infinity
                return this.curve.getInfinity();
            }

            ECFieldElement.F2m lambda
                = (ECFieldElement.F2m)(this.y.add(y2)).divide(this.x.add(x2));

            ECFieldElement.F2m x3
                = (ECFieldElement.F2m)lambda.square().add(lambda).add(this.x).add(x2).add(this.curve.getA());

            ECFieldElement.F2m y3
                = (ECFieldElement.F2m)lambda.multiply(this.x.add(x3)).add(x3).add(this.y);

            return new ECPoint.F2m(curve, x3, y3, withCompression);
        }

        /* (non-Javadoc)
         * @see org.bouncycastle.math.ec.ECPoint#subtract(org.bouncycastle.math.ec.ECPoint)
         */
        public ECPoint subtract(ECPoint b)
        {
            if (b.isInfinity())
            {
                return this;
            }

            // Add -b
            ECPoint.F2m minusB
                = new ECPoint.F2m(this.curve, b.x, b.x.add(b.y),
                        this.withCompression);
            return add(minusB);
        }

        /* (non-Javadoc)
         * @see org.bouncycastle.math.ec.ECPoint#twice()
         */
        public ECPoint twice()
        {
            if (this.isInfinity()) 
            {
                // Twice identity element (point at infinity) is identity
                return this;
            }

            if (this.x.toBigInteger().signum() == 0) 
            {
                // if x1 == 0, then (x1, y1) == (x1, x1 + y1)
                // and hence this = -this and thus 2(x1, y1) == infinity
                return this.curve.getInfinity();
            }

            ECFieldElement.F2m lambda
                = (ECFieldElement.F2m)this.x.add(this.y.divide(this.x));

            ECFieldElement.F2m x3
                = (ECFieldElement.F2m)lambda.square().add(lambda).
                    add(this.curve.getA());

            ECFieldElement.F2m y3
                = (ECFieldElement.F2m)this.x.square().add(lambda.multiply(x3)).
                    add(x3);

            return new ECPoint.F2m(this.curve, x3, y3, withCompression);
        }

        public ECPoint multiply(
            BigInteger k)
        {
            ECPoint p = this;
            ECPoint q = this.curve.getInfinity();

            int t = k.bitLength();
            for (int i = 0; i < t; i++) 
            {
                if (k.testBit(i)) 
                {
                    q = q.add(p);
                }
                p = p.twice();
            }
            return q;
        }
    }
}
