package org.bouncycastle.math.ec;

import java.math.BigInteger;
import java.util.Hashtable;

/**
 * base class for points on elliptic curves.
 */
public abstract class ECPoint
{
    protected static ECFieldElement[] EMPTY_ZS = new ECFieldElement[0];

    protected static ECFieldElement[] getInitialZCoords(ECCurve curve)
    {
        // Cope with null curve, most commonly used by implicitlyCa
        int coord = null == curve ? ECCurve.COORD_AFFINE : curve.getCoordinateSystem();

        switch (coord)
        {
        case ECCurve.COORD_AFFINE:
        case ECCurve.COORD_LAMBDA_AFFINE:
            return EMPTY_ZS;
        default:
            break;
        }

        ECFieldElement one = curve.fromBigInteger(ECConstants.ONE);

        switch (coord)
        {
        case ECCurve.COORD_HOMOGENEOUS:
        case ECCurve.COORD_JACOBIAN:
        case ECCurve.COORD_LAMBDA_PROJECTIVE:
            return new ECFieldElement[]{ one };
        case ECCurve.COORD_JACOBIAN_CHUDNOVSKY:
            return new ECFieldElement[]{ one, one, one };
        case ECCurve.COORD_JACOBIAN_MODIFIED:
            return new ECFieldElement[]{ one, curve.getA() };
        default:
            throw new IllegalArgumentException("unknown coordinate system");
        }
    }

    protected ECCurve curve;
    protected ECFieldElement x;
    protected ECFieldElement y;
    protected ECFieldElement[] zs;

    protected boolean withCompression;

    // Hashtable is (String -> PreCompInfo)
    protected Hashtable preCompTable = null;

    protected ECPoint(ECCurve curve, ECFieldElement x, ECFieldElement y)
    {
        this(curve, x, y, getInitialZCoords(curve));
    }

    protected ECPoint(ECCurve curve, ECFieldElement x, ECFieldElement y, ECFieldElement[] zs)
    {
        this.curve = curve;
        this.x = x;
        this.y = y;
        this.zs = zs;
    }

    protected boolean satisfiesCofactor()
    {
        BigInteger h = curve.getCofactor();
        return h == null || h.equals(ECConstants.ONE) || !ECAlgorithms.referenceMultiply(this, h).isInfinity();
    }

    protected abstract boolean satisfiesCurveEquation();

    public final ECPoint getDetachedPoint()
    {
        return normalize().detach();
    }

    public ECCurve getCurve()
    {
        return curve;
    }

    protected abstract ECPoint detach();

    protected int getCurveCoordinateSystem()
    {
        // Cope with null curve, most commonly used by implicitlyCa
        return null == curve ? ECCurve.COORD_AFFINE : curve.getCoordinateSystem();
    }

    /**
     * Normalizes this point, and then returns the affine x-coordinate.
     * 
     * Note: normalization can be expensive, this method is deprecated in favour
     * of caller-controlled normalization.
     * 
     * @deprecated Use getAffineXCoord(), or normalize() and getXCoord(), instead
     */
    public ECFieldElement getX()
    {
        return normalize().getXCoord();
    }


    /**
     * Normalizes this point, and then returns the affine y-coordinate.
     * 
     * Note: normalization can be expensive, this method is deprecated in favour
     * of caller-controlled normalization.
     * 
     * @deprecated Use getAffineYCoord(), or normalize() and getYCoord(), instead
     */
    public ECFieldElement getY()
    {
        return normalize().getYCoord();
    }

    /**
     * Returns the affine x-coordinate after checking that this point is normalized.
     * 
     * @return The affine x-coordinate of this point
     * @throws IllegalStateException if the point is not normalized
     */
    public ECFieldElement getAffineXCoord()
    {
        checkNormalized();
        return getXCoord();
    }

    /**
     * Returns the affine y-coordinate after checking that this point is normalized
     * 
     * @return The affine y-coordinate of this point
     * @throws IllegalStateException if the point is not normalized
     */
    public ECFieldElement getAffineYCoord()
    {
        checkNormalized();
        return getYCoord();
    }

    /**
     * Returns the x-coordinate.
     * 
     * Caution: depending on the curve's coordinate system, this may not be the same value as in an
     * affine coordinate system; use normalize() to get a point where the coordinates have their
     * affine values, or use getAffineXCoord() if you expect the point to already have been
     * normalized.
     * 
     * @return the x-coordinate of this point
     */
    public ECFieldElement getXCoord()
    {
        return x;
    }

    /**
     * Returns the y-coordinate.
     * 
     * Caution: depending on the curve's coordinate system, this may not be the same value as in an
     * affine coordinate system; use normalize() to get a point where the coordinates have their
     * affine values, or use getAffineYCoord() if you expect the point to already have been
     * normalized.
     * 
     * @return the y-coordinate of this point
     */
    public ECFieldElement getYCoord()
    {
        return y;
    }

    public ECFieldElement getZCoord(int index)
    {
        return (index < 0 || index >= zs.length) ? null : zs[index];
    }

    public ECFieldElement[] getZCoords()
    {
        int zsLen = zs.length;
        if (zsLen == 0)
        {
            return EMPTY_ZS;
        }
        ECFieldElement[] copy = new ECFieldElement[zsLen];
        System.arraycopy(zs, 0, copy, 0, zsLen);
        return copy;
    }

    public final ECFieldElement getRawXCoord()
    {
        return x;
    }

    public final ECFieldElement getRawYCoord()
    {
        return y;
    }

    protected final ECFieldElement[] getRawZCoords()
    {
        return zs;
    }

    protected void checkNormalized()
    {
        if (!isNormalized())
        {
            throw new IllegalStateException("point not in normal form");
        }
    }

    public boolean isNormalized()
    {
        int coord = this.getCurveCoordinateSystem();

        return coord == ECCurve.COORD_AFFINE
            || coord == ECCurve.COORD_LAMBDA_AFFINE
            || isInfinity()
            || zs[0].isOne();
    }

    /**
     * Normalization ensures that any projective coordinate is 1, and therefore that the x, y
     * coordinates reflect those of the equivalent point in an affine coordinate system.
     * 
     * @return a new ECPoint instance representing the same point, but with normalized coordinates
     */
    public ECPoint normalize()
    {
        if (this.isInfinity())
        {
            return this;
        }

        switch (this.getCurveCoordinateSystem())
        {
        case ECCurve.COORD_AFFINE:
        case ECCurve.COORD_LAMBDA_AFFINE:
        {
            return this;
        }
        default:
        {
            ECFieldElement Z1 = getZCoord(0);
            if (Z1.isOne())
            {
                return this;
            }

            return normalize(Z1.invert());
        }
        }
    }

    ECPoint normalize(ECFieldElement zInv)
    {
        switch (this.getCurveCoordinateSystem())
        {
        case ECCurve.COORD_HOMOGENEOUS:
        case ECCurve.COORD_LAMBDA_PROJECTIVE:
        {
            return createScaledPoint(zInv, zInv);
        }
        case ECCurve.COORD_JACOBIAN:
        case ECCurve.COORD_JACOBIAN_CHUDNOVSKY:
        case ECCurve.COORD_JACOBIAN_MODIFIED:
        {
            ECFieldElement zInv2 = zInv.square(), zInv3 = zInv2.multiply(zInv);
            return createScaledPoint(zInv2, zInv3);
        }
        default:
        {
            throw new IllegalStateException("not a projective coordinate system");
        }
        }
    }

    protected ECPoint createScaledPoint(ECFieldElement sx, ECFieldElement sy)
    {
        return this.getCurve().createRawPoint(getRawXCoord().multiply(sx), getRawYCoord().multiply(sy), this.withCompression);
    }

    public boolean isInfinity()
    {
        return x == null || y == null || (zs.length > 0 && zs[0].isZero());
    }

    /**
     * @deprecated per-point compression property will be removed, refer {@link #getEncoded(boolean)}
     */
    public boolean isCompressed()
    {
        return this.withCompression;
    }

    public boolean isValid()
    {
        if (isInfinity())
        {
            return true;
        }

        // TODO Sanity-check the field elements

        ECCurve curve = getCurve();
        if (curve != null)
        {
            if (!satisfiesCurveEquation())
            {
                return false;
            }

            if (!satisfiesCofactor())
            {
                return false;
            }
        }

        return true;
    }

    public ECPoint scaleX(ECFieldElement scale)
    {
        return isInfinity()
            ?   this
            :   getCurve().createRawPoint(getRawXCoord().multiply(scale), getRawYCoord(), getRawZCoords(), this.withCompression);
    }

    public ECPoint scaleY(ECFieldElement scale)
    {
        return isInfinity()
            ?   this
            :   getCurve().createRawPoint(getRawXCoord(), getRawYCoord().multiply(scale), getRawZCoords(), this.withCompression);
    }

    public boolean equals(ECPoint other)
    {
        if (null == other)
        {
            return false;
        }

        ECCurve c1 = this.getCurve(), c2 = other.getCurve();
        boolean n1 = (null == c1), n2 = (null == c2);
        boolean i1 = isInfinity(), i2 = other.isInfinity();

        if (i1 || i2)
        {
            return (i1 && i2) && (n1 || n2 || c1.equals(c2));
        }

        ECPoint p1 = this, p2 = other;
        if (n1 && n2)
        {
            // Points with null curve are in affine form, so already normalized
        }
        else if (n1)
        {
            p2 = p2.normalize();
        }
        else if (n2)
        {
            p1 = p1.normalize();
        }
        else if (!c1.equals(c2))
        {
            return false;
        }
        else
        {
            // TODO Consider just requiring already normalized, to avoid silent performance degradation

            ECPoint[] points = new ECPoint[]{ this, c1.importPoint(p2) };

            // TODO This is a little strong, really only requires coZNormalizeAll to get Zs equal
            c1.normalizeAll(points);

            p1 = points[0];
            p2 = points[1];
        }

        return p1.getXCoord().equals(p2.getXCoord()) && p1.getYCoord().equals(p2.getYCoord());
    }

    public boolean equals(Object other)
    {
        if (other == this)
        {
            return true;
        }

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

        return equals((ECPoint)other);
    }

    public int hashCode()
    {
        ECCurve c = this.getCurve();
        int hc = (null == c) ? 0 : ~c.hashCode();

        if (!this.isInfinity())
        {
            // TODO Consider just requiring already normalized, to avoid silent performance degradation

            ECPoint p = normalize();

            hc ^= p.getXCoord().hashCode() * 17;
            hc ^= p.getYCoord().hashCode() * 257;
        }

        return hc;
    }

    public String toString()
    {
        if (this.isInfinity())
        {
            return "INF";
        }

        StringBuffer sb = new StringBuffer();
        sb.append('(');
        sb.append(getRawXCoord());
        sb.append(',');
        sb.append(getRawYCoord());
        for (int i = 0; i < zs.length; ++i)
        {
            sb.append(',');
            sb.append(zs[i]);
        }
        sb.append(')');
        return sb.toString();
    }

    /**
     * @deprecated per-point compression property will be removed, refer {@link #getEncoded(boolean)}
     */
    public byte[] getEncoded()
    {
        return getEncoded(this.withCompression);
    }

    /**
     * Get an encoding of the point value, optionally in compressed format.
     * 
     * @param compressed whether to generate a compressed point encoding.
     * @return the point encoding
     */
    public byte[] getEncoded(boolean compressed)
    {
        if (this.isInfinity())
        {
            return new byte[1];
        }

        ECPoint normed = normalize();

        byte[] X = normed.getXCoord().getEncoded();

        if (compressed)
        {
            byte[] PO = new byte[X.length + 1];
            PO[0] = (byte)(normed.getCompressionYTilde() ? 0x03 : 0x02);
            System.arraycopy(X, 0, PO, 1, X.length);
            return PO;
        }

        byte[] Y = normed.getYCoord().getEncoded();

        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;
    }

    protected abstract boolean getCompressionYTilde();

    public abstract ECPoint add(ECPoint b);

    public abstract ECPoint negate();

    public abstract ECPoint subtract(ECPoint b);

    public ECPoint timesPow2(int e)
    {
        if (e < 0)
        {
            throw new IllegalArgumentException("'e' cannot be negative");
        }

        ECPoint p = this;
        while (--e >= 0)
        {
            p = p.twice();
        }
        return p;
    }

    public abstract ECPoint twice();

    public ECPoint twicePlus(ECPoint b)
    {
        return twice().add(b);
    }

    public ECPoint threeTimes()
    {
        return twicePlus(this);
    }

    /**
     * Multiplies this <code>ECPoint</code> by the given number.
     * @param k The multiplicator.
     * @return <code>k * this</code>.
     */
    public ECPoint multiply(BigInteger k)
    {
        return this.getCurve().getMultiplier().multiply(this, k);
    }

    public static abstract class AbstractFp extends ECPoint
    {
        protected AbstractFp(ECCurve curve, ECFieldElement x, ECFieldElement y)
        {
            super(curve, x, y);
        }

        protected AbstractFp(ECCurve curve, ECFieldElement x, ECFieldElement y, ECFieldElement[] zs)
        {
            super(curve, x, y, zs);
        }

        protected boolean getCompressionYTilde()
        {
            return this.getAffineYCoord().testBitZero();
        }

        protected boolean satisfiesCurveEquation()
        {
            ECFieldElement X = this.x, Y = this.y, A = curve.getA(), B = curve.getB();
            ECFieldElement lhs = Y.square();

            switch (this.getCurveCoordinateSystem())
            {
            case ECCurve.COORD_AFFINE:
                break;
            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Z = this.zs[0];
                if (!Z.isOne())
                {
                    ECFieldElement Z2 = Z.square(), Z3 = Z.multiply(Z2);
                    lhs = lhs.multiply(Z);
                    A = A.multiply(Z2);
                    B = B.multiply(Z3);
                }
                break;
            }
            case ECCurve.COORD_JACOBIAN:
            case ECCurve.COORD_JACOBIAN_CHUDNOVSKY:
            case ECCurve.COORD_JACOBIAN_MODIFIED:
            {
                ECFieldElement Z = this.zs[0];
                if (!Z.isOne())
                {
                    ECFieldElement Z2 = Z.square(), Z4 = Z2.square(), Z6 = Z2.multiply(Z4);
                    A = A.multiply(Z4);
                    B = B.multiply(Z6);
                }
                break;
            }
            default:
                throw new IllegalStateException("unsupported coordinate system");
            }

            ECFieldElement rhs = X.square().add(A).multiply(X).add(B);
            return lhs.equals(rhs);
        }

        public ECPoint subtract(ECPoint b)
        {
            if (b.isInfinity())
            {
                return this;
            }

            // Add -b
            return this.add(b.negate());
        }
    }

    /**
     * Elliptic curve points over Fp
     */
    public static class Fp extends AbstractFp
    {
        /**
         * Create a point which encodes without point compression.
         * 
         * @param curve the curve to use
         * @param x affine x co-ordinate
         * @param y affine y co-ordinate
         * 
         * @deprecated Use ECCurve.createPoint to construct points
         */
        public Fp(ECCurve curve, ECFieldElement x, ECFieldElement y)
        {
            this(curve, x, y, false);
        }

        /**
         * Create a point that encodes with or without point compression.
         * 
         * @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
         * 
         * @deprecated per-point compression property will be removed, refer {@link #getEncoded(boolean)}
         */
        public Fp(ECCurve curve, ECFieldElement x, ECFieldElement y, boolean withCompression)
        {
            super(curve, x, y);

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

            this.withCompression = withCompression;
        }

        Fp(ECCurve curve, ECFieldElement x, ECFieldElement y, ECFieldElement[] zs, boolean withCompression)
        {
            super(curve, x, y, zs);

            this.withCompression = withCompression;
        }

        protected ECPoint detach()
        {
            return new ECPoint.Fp(null, this.getAffineXCoord(), this.getAffineYCoord());
        }

        public ECFieldElement getZCoord(int index)
        {
            if (index == 1 && ECCurve.COORD_JACOBIAN_MODIFIED == this.getCurveCoordinateSystem())
            {
                return getJacobianModifiedW();
            }

            return super.getZCoord(index);
        }

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

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            ECFieldElement X1 = this.x, Y1 = this.y;
            ECFieldElement X2 = b.x, Y2 = b.y;

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement dx = X2.subtract(X1), dy = Y2.subtract(Y1);

                if (dx.isZero())
                {
                    if (dy.isZero())
                    {
                        // this == b, i.e. this must be doubled
                        return twice();
                    }

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

                ECFieldElement gamma = dy.divide(dx);
                ECFieldElement X3 = gamma.square().subtract(X1).subtract(X2);
                ECFieldElement Y3 = gamma.multiply(X1.subtract(X3)).subtract(Y1);

                return new ECPoint.Fp(curve, X3, Y3, this.withCompression);
            }

            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Z1 = this.zs[0];
                ECFieldElement Z2 = b.zs[0];

                boolean Z1IsOne = Z1.isOne();
                boolean Z2IsOne = Z2.isOne();

                ECFieldElement u1 = Z1IsOne ? Y2 : Y2.multiply(Z1);
                ECFieldElement u2 = Z2IsOne ? Y1 : Y1.multiply(Z2);
                ECFieldElement u = u1.subtract(u2);
                ECFieldElement v1 = Z1IsOne ? X2 : X2.multiply(Z1);
                ECFieldElement v2 = Z2IsOne ? X1 : X1.multiply(Z2);
                ECFieldElement v = v1.subtract(v2);

                // Check if b == this or b == -this
                if (v.isZero())
                {
                    if (u.isZero())
                    {
                        // this == b, i.e. this must be doubled
                        return this.twice();
                    }

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

                // TODO Optimize for when w == 1
                ECFieldElement w = Z1IsOne ? Z2 : Z2IsOne ? Z1 : Z1.multiply(Z2);
                ECFieldElement vSquared = v.square();
                ECFieldElement vCubed = vSquared.multiply(v);
                ECFieldElement vSquaredV2 = vSquared.multiply(v2);
                ECFieldElement A = u.square().multiply(w).subtract(vCubed).subtract(two(vSquaredV2));

                ECFieldElement X3 = v.multiply(A);
                ECFieldElement Y3 = vSquaredV2.subtract(A).multiplyMinusProduct(u, u2, vCubed);
                ECFieldElement Z3 = vCubed.multiply(w);

                return new ECPoint.Fp(curve, X3, Y3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }

            case ECCurve.COORD_JACOBIAN:
            case ECCurve.COORD_JACOBIAN_MODIFIED:
            {
                ECFieldElement Z1 = this.zs[0];
                ECFieldElement Z2 = b.zs[0];

                boolean Z1IsOne = Z1.isOne();

                ECFieldElement X3, Y3, Z3, Z3Squared = null;

                if (!Z1IsOne && Z1.equals(Z2))
                {
                    // TODO Make this available as public method coZAdd?

                    ECFieldElement dx = X1.subtract(X2), dy = Y1.subtract(Y2);
                    if (dx.isZero())
                    {
                        if (dy.isZero())
                        {
                            return twice();
                        }
                        return curve.getInfinity();
                    }

                    ECFieldElement C = dx.square();
                    ECFieldElement W1 = X1.multiply(C), W2 = X2.multiply(C);
                    ECFieldElement A1 = W1.subtract(W2).multiply(Y1);

                    X3 = dy.square().subtract(W1).subtract(W2);
                    Y3 = W1.subtract(X3).multiply(dy).subtract(A1);
                    Z3 = dx;

                    Z3 = Z3.multiply(Z1);
                }
                else
                {
                    ECFieldElement Z1Squared, U2, S2;
                    if (Z1IsOne)
                    {
                        Z1Squared = Z1; U2 = X2; S2 = Y2;
                    }
                    else
                    {
                        Z1Squared = Z1.square();
                        U2 = Z1Squared.multiply(X2);
                        ECFieldElement Z1Cubed = Z1Squared.multiply(Z1);
                        S2 = Z1Cubed.multiply(Y2);
                    }

                    boolean Z2IsOne = Z2.isOne();
                    ECFieldElement Z2Squared, U1, S1;
                    if (Z2IsOne)
                    {
                        Z2Squared = Z2; U1 = X1; S1 = Y1;
                    }
                    else
                    {
                        Z2Squared = Z2.square();
                        U1 = Z2Squared.multiply(X1); 
                        ECFieldElement Z2Cubed = Z2Squared.multiply(Z2);
                        S1 = Z2Cubed.multiply(Y1);
                    }

                    ECFieldElement H = U1.subtract(U2);
                    ECFieldElement R = S1.subtract(S2);
    
                    // Check if b == this or b == -this
                    if (H.isZero())
                    {
                        if (R.isZero())
                        {
                            // this == b, i.e. this must be doubled
                            return this.twice();
                        }
    
                        // this == -b, i.e. the result is the point at infinity
                        return curve.getInfinity();
                    }
    
                    ECFieldElement HSquared = H.square();
                    ECFieldElement G = HSquared.multiply(H);
                    ECFieldElement V = HSquared.multiply(U1);
    
                    X3 = R.square().add(G).subtract(two(V));
                    Y3 = V.subtract(X3).multiplyMinusProduct(R, G, S1);

                    Z3 = H;
                    if (!Z1IsOne)
                    {
                        Z3 = Z3.multiply(Z1);
                    }
                    if (!Z2IsOne)
                    {
                        Z3 = Z3.multiply(Z2);
                    }
    
                    // Alternative calculation of Z3 using fast square
    //                X3 = four(X3);
    //                Y3 = eight(Y3);
    //                Z3 = doubleProductFromSquares(Z1, Z2, Z1Squared, Z2Squared).multiply(H);
                    
                    if (Z3 == H)
                    {
                        Z3Squared = HSquared;
                    }
                }

                ECFieldElement[] zs;
                if (coord == ECCurve.COORD_JACOBIAN_MODIFIED)
                {
                    // TODO If the result will only be used in a subsequent addition, we don't need W3
                    ECFieldElement W3 = calculateJacobianModifiedW(Z3, Z3Squared);

                    zs = new ECFieldElement[]{ Z3, W3 };
                }
                else
                {
                    zs = new ECFieldElement[]{ Z3 };
                }

                return new ECPoint.Fp(curve, X3, Y3, zs, this.withCompression);
            }

            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }

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

            ECCurve curve = this.getCurve();

            ECFieldElement Y1 = this.y;
            if (Y1.isZero()) 
            {
                return curve.getInfinity();
            }

            int coord = curve.getCoordinateSystem();

            ECFieldElement X1 = this.x;

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement X1Squared = X1.square();
                ECFieldElement gamma = three(X1Squared).add(this.getCurve().getA()).divide(two(Y1));
                ECFieldElement X3 = gamma.square().subtract(two(X1));
                ECFieldElement Y3 = gamma.multiply(X1.subtract(X3)).subtract(Y1);
    
                return new ECPoint.Fp(curve, X3, Y3, this.withCompression);
            }

            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Z1 = this.zs[0];

                boolean Z1IsOne = Z1.isOne();

                // TODO Optimize for small negative a4 and -3
                ECFieldElement w = curve.getA();
                if (!w.isZero() && !Z1IsOne)
                {
                    w = w.multiply(Z1.square());
                }
                w = w.add(three(X1.square()));
                
                ECFieldElement s = Z1IsOne ? Y1 : Y1.multiply(Z1);
                ECFieldElement t = Z1IsOne ? Y1.square() : s.multiply(Y1);
                ECFieldElement B = X1.multiply(t);
                ECFieldElement _4B = four(B);
                ECFieldElement h = w.square().subtract(two(_4B));

                ECFieldElement _2s = two(s);
                ECFieldElement X3 = h.multiply(_2s);
                ECFieldElement _2t = two(t);
                ECFieldElement Y3 = _4B.subtract(h).multiply(w).subtract(two(_2t.square()));
                ECFieldElement _4sSquared = Z1IsOne ? two(_2t) : _2s.square();
                ECFieldElement Z3 = two(_4sSquared).multiply(s);

                return new ECPoint.Fp(curve, X3, Y3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }

            case ECCurve.COORD_JACOBIAN:
            {
                ECFieldElement Z1 = this.zs[0];

                boolean Z1IsOne = Z1.isOne();

                ECFieldElement Y1Squared = Y1.square();
                ECFieldElement T = Y1Squared.square();

                ECFieldElement a4 = curve.getA();
                ECFieldElement a4Neg = a4.negate();

                ECFieldElement M, S;
                if (a4Neg.toBigInteger().equals(BigInteger.valueOf(3)))
                {
                    ECFieldElement Z1Squared = Z1IsOne ? Z1 : Z1.square();
                    M = three(X1.add(Z1Squared).multiply(X1.subtract(Z1Squared)));
                    S = four(Y1Squared.multiply(X1));
                }
                else
                {
                    ECFieldElement X1Squared = X1.square();
                    M = three(X1Squared);
                    if (Z1IsOne)
                    {
                        M = M.add(a4);
                    }
                    else if (!a4.isZero())
                    {
                        ECFieldElement Z1Squared = Z1.square();
                        ECFieldElement Z1Pow4 = Z1Squared.square();
                        if (a4Neg.bitLength() < a4.bitLength())
                        {
                            M = M.subtract(Z1Pow4.multiply(a4Neg));
                        }
                        else
                        {
                            M = M.add(Z1Pow4.multiply(a4));
                        }
                    }
//                  S = two(doubleProductFromSquares(X1, Y1Squared, X1Squared, T));
                    S = four(X1.multiply(Y1Squared));
                }

                ECFieldElement X3 = M.square().subtract(two(S));
                ECFieldElement Y3 = S.subtract(X3).multiply(M).subtract(eight(T));

                ECFieldElement Z3 = two(Y1);
                if (!Z1IsOne)
                {
                    Z3 = Z3.multiply(Z1);
                }

                // Alternative calculation of Z3 using fast square
//                ECFieldElement Z3 = doubleProductFromSquares(Y1, Z1, Y1Squared, Z1Squared);

                return new ECPoint.Fp(curve, X3, Y3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }

            case ECCurve.COORD_JACOBIAN_MODIFIED:
            {
                return twiceJacobianModified(true);
            }

            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }

        public ECPoint twicePlus(ECPoint b)
        {
            if (this == b)
            {
                return threeTimes();
            }
            if (this.isInfinity())
            {
                return b;
            }
            if (b.isInfinity())
            {
                return twice();
            }

            ECFieldElement Y1 = this.y;
            if (Y1.isZero()) 
            {
                return b;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement X1 = this.x;
                ECFieldElement X2 = b.x, Y2 = b.y;

                ECFieldElement dx = X2.subtract(X1), dy = Y2.subtract(Y1);

                if (dx.isZero())
                {
                    if (dy.isZero())
                    {
                        // this == b i.e. the result is 3P
                        return threeTimes();
                    }

                    // this == -b, i.e. the result is P
                    return this;
                }

                /*
                 * Optimized calculation of 2P + Q, as described in "Trading Inversions for
                 * Multiplications in Elliptic Curve Cryptography", by Ciet, Joye, Lauter, Montgomery.
                 */

                ECFieldElement X = dx.square(), Y = dy.square();
                ECFieldElement d = X.multiply(two(X1).add(X2)).subtract(Y);
                if (d.isZero())
                {
                    return curve.getInfinity();
                }

                ECFieldElement D = d.multiply(dx);
                ECFieldElement I = D.invert();
                ECFieldElement L1 = d.multiply(I).multiply(dy);
                ECFieldElement L2 = two(Y1).multiply(X).multiply(dx).multiply(I).subtract(L1);
                ECFieldElement X4 = (L2.subtract(L1)).multiply(L1.add(L2)).add(X2);
                ECFieldElement Y4 = (X1.subtract(X4)).multiply(L2).subtract(Y1);

                return new ECPoint.Fp(curve, X4, Y4, this.withCompression);
            }
            case ECCurve.COORD_JACOBIAN_MODIFIED:
            {
                return twiceJacobianModified(false).add(b);
            }
            default:
            {
                return twice().add(b);
            }
            }
        }

        public ECPoint threeTimes()
        {
            if (this.isInfinity())
            {
                return this;
            }

            ECFieldElement Y1 = this.y;
            if (Y1.isZero())
            {
                return this;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement X1 = this.x;

                ECFieldElement _2Y1 = two(Y1); 
                ECFieldElement X = _2Y1.square();
                ECFieldElement Z = three(X1.square()).add(this.getCurve().getA());
                ECFieldElement Y = Z.square();

                ECFieldElement d = three(X1).multiply(X).subtract(Y);
                if (d.isZero())
                {
                    return this.getCurve().getInfinity();
                }

                ECFieldElement D = d.multiply(_2Y1); 
                ECFieldElement I = D.invert();
                ECFieldElement L1 = d.multiply(I).multiply(Z);
                ECFieldElement L2 = X.square().multiply(I).subtract(L1);

                ECFieldElement X4 = (L2.subtract(L1)).multiply(L1.add(L2)).add(X1);
                ECFieldElement Y4 = (X1.subtract(X4)).multiply(L2).subtract(Y1); 
                return new ECPoint.Fp(curve, X4, Y4, this.withCompression);
            }
            case ECCurve.COORD_JACOBIAN_MODIFIED:
            {
                return twiceJacobianModified(false).add(this);
            }
            default:
            {
                // NOTE: Be careful about recursions between twicePlus and threeTimes
                return twice().add(this);
            }
            }
        }

        public ECPoint timesPow2(int e)
        {
            if (e < 0)
            {
                throw new IllegalArgumentException("'e' cannot be negative");
            }
            if (e == 0 || this.isInfinity())
            {
                return this;
            }
            if (e == 1)
            {
                return twice();
            }

            ECCurve curve = this.getCurve();

            ECFieldElement Y1 = this.y;
            if (Y1.isZero()) 
            {
                return curve.getInfinity();
            }

            int coord = curve.getCoordinateSystem();

            ECFieldElement W1 = curve.getA();
            ECFieldElement X1 = this.x;
            ECFieldElement Z1 = this.zs.length < 1 ? curve.fromBigInteger(ECConstants.ONE) : this.zs[0];

            if (!Z1.isOne())
            {
                switch (coord)
                {
                case ECCurve.COORD_AFFINE:
                    break;
                case ECCurve.COORD_HOMOGENEOUS:
                    ECFieldElement Z1Sq = Z1.square();
                    X1 = X1.multiply(Z1);
                    Y1 = Y1.multiply(Z1Sq);
                    W1 = calculateJacobianModifiedW(Z1, Z1Sq);
                    break;
                case ECCurve.COORD_JACOBIAN:
                    W1 = calculateJacobianModifiedW(Z1, null);
                    break;
                case ECCurve.COORD_JACOBIAN_MODIFIED:
                    W1 = getJacobianModifiedW();
                    break;
                default:
                    throw new IllegalStateException("unsupported coordinate system");
                }
            }

            for (int i = 0; i < e; ++i)
            {
                if (Y1.isZero()) 
                {
                    return curve.getInfinity();
                }

                ECFieldElement X1Squared = X1.square();
                ECFieldElement M = three(X1Squared);
                ECFieldElement _2Y1 = two(Y1);
                ECFieldElement _2Y1Squared = _2Y1.multiply(Y1);
                ECFieldElement S = two(X1.multiply(_2Y1Squared));
                ECFieldElement _4T = _2Y1Squared.square();
                ECFieldElement _8T = two(_4T);

                if (!W1.isZero())
                {
                    M = M.add(W1);
                    W1 = two(_8T.multiply(W1));
                }

                X1 = M.square().subtract(two(S));
                Y1 = M.multiply(S.subtract(X1)).subtract(_8T);
                Z1 = Z1.isOne() ? _2Y1 : _2Y1.multiply(Z1);
            }

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
                ECFieldElement zInv = Z1.invert(), zInv2 = zInv.square(), zInv3 = zInv2.multiply(zInv);
                return new Fp(curve, X1.multiply(zInv2), Y1.multiply(zInv3), this.withCompression);
            case ECCurve.COORD_HOMOGENEOUS:
                X1 = X1.multiply(Z1);
                Z1 = Z1.multiply(Z1.square());
                return new Fp(curve, X1, Y1, new ECFieldElement[]{ Z1 }, this.withCompression);
            case ECCurve.COORD_JACOBIAN:
                return new Fp(curve, X1, Y1, new ECFieldElement[]{ Z1 }, this.withCompression);
            case ECCurve.COORD_JACOBIAN_MODIFIED:
                return new Fp(curve, X1, Y1, new ECFieldElement[]{ Z1, W1 }, this.withCompression);
            default:
                throw new IllegalStateException("unsupported coordinate system");
            }
        }

        protected ECFieldElement two(ECFieldElement x)
        {
            return x.add(x);
        }

        protected ECFieldElement three(ECFieldElement x)
        {
            return two(x).add(x);
        }

        protected ECFieldElement four(ECFieldElement x)
        {
            return two(two(x));
        }

        protected ECFieldElement eight(ECFieldElement x)
        {
            return four(two(x));
        }

        protected ECFieldElement doubleProductFromSquares(ECFieldElement a, ECFieldElement b,
            ECFieldElement aSquared, ECFieldElement bSquared)
        {
            /*
             * NOTE: If squaring in the field is faster than multiplication, then this is a quicker
             * way to calculate 2.A.B, if A^2 and B^2 are already known.
             */
            return a.add(b).square().subtract(aSquared).subtract(bSquared);
        }

        public ECPoint negate()
        {
            if (this.isInfinity())
            {
                return this;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            if (ECCurve.COORD_AFFINE != coord)
            {
                return new ECPoint.Fp(curve, this.x, this.y.negate(), this.zs, this.withCompression);
            }

            return new ECPoint.Fp(curve, this.x, this.y.negate(), this.withCompression);
        }

        protected ECFieldElement calculateJacobianModifiedW(ECFieldElement Z, ECFieldElement ZSquared)
        {
            ECFieldElement a4 = this.getCurve().getA();
            if (a4.isZero() || Z.isOne())
            {
                return a4;
            }

            if (ZSquared == null)
            {
                ZSquared = Z.square();
            }

            ECFieldElement W = ZSquared.square();
            ECFieldElement a4Neg = a4.negate();
            if (a4Neg.bitLength() < a4.bitLength())
            {
                W = W.multiply(a4Neg).negate();
            }
            else
            {
                W = W.multiply(a4);
            }
            return W;
        }

        protected ECFieldElement getJacobianModifiedW()
        {
            ECFieldElement W = this.zs[1];
            if (W == null)
            {
                // NOTE: Rarely, twicePlus will result in the need for a lazy W1 calculation here
                this.zs[1] = W = calculateJacobianModifiedW(this.zs[0], null);
            }
            return W;
        }

        protected ECPoint.Fp twiceJacobianModified(boolean calculateW)
        {
            ECFieldElement X1 = this.x, Y1 = this.y, Z1 = this.zs[0], W1 = getJacobianModifiedW();

            ECFieldElement X1Squared = X1.square();
            ECFieldElement M = three(X1Squared).add(W1);
            ECFieldElement _2Y1 = two(Y1);
            ECFieldElement _2Y1Squared = _2Y1.multiply(Y1);
            ECFieldElement S = two(X1.multiply(_2Y1Squared));
            ECFieldElement X3 = M.square().subtract(two(S));
            ECFieldElement _4T = _2Y1Squared.square();
            ECFieldElement _8T = two(_4T);
            ECFieldElement Y3 = M.multiply(S.subtract(X3)).subtract(_8T);
            ECFieldElement W3 = calculateW ? two(_8T.multiply(W1)) : null;
            ECFieldElement Z3 = Z1.isOne() ? _2Y1 : _2Y1.multiply(Z1);

            return new ECPoint.Fp(this.getCurve(), X3, Y3, new ECFieldElement[]{ Z3, W3 }, this.withCompression);
        }
    }

    public static abstract class AbstractF2m extends ECPoint
    {
        protected AbstractF2m(ECCurve curve, ECFieldElement x, ECFieldElement y)
        {
            super(curve, x, y);
        }

        protected AbstractF2m(ECCurve curve, ECFieldElement x, ECFieldElement y, ECFieldElement[] zs)
        {
            super(curve, x, y, zs);
        }

        protected boolean satisfiesCurveEquation()
        {
            ECCurve curve = this.getCurve();
            ECFieldElement X = this.x, A = curve.getA(), B = curve.getB();
            
            int coord = curve.getCoordinateSystem();
            if (coord == ECCurve.COORD_LAMBDA_PROJECTIVE)
            {
                ECFieldElement Z = this.zs[0];
                boolean ZIsOne = Z.isOne();

                if (X.isZero())
                {
                    // NOTE: For x == 0, we expect the affine-y instead of the lambda-y 
                    ECFieldElement Y = this.y;
                    ECFieldElement lhs = Y.square(), rhs = B;
                    if (!ZIsOne)
                    {
                        rhs = rhs.multiply(Z.square());
                    }
                    return lhs.equals(rhs);
                }

                ECFieldElement L = this.y, X2 = X.square();
                ECFieldElement lhs, rhs;
                if (ZIsOne)
                {
                    lhs = L.square().add(L).add(A);
                    rhs = X2.square().add(B);
                }
                else
                {
                    ECFieldElement Z2 = Z.square(), Z4 = Z2.square();
                    lhs = L.add(Z).multiplyPlusProduct(L, A, Z2);
                    // TODO If sqrt(b) is precomputed this can be simplified to a single square
                    rhs = X2.squarePlusProduct(B, Z4);
                }
                lhs = lhs.multiply(X2);
                return lhs.equals(rhs);
            }

            ECFieldElement Y = this.y;
            ECFieldElement lhs = Y.add(X).multiply(Y);

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
                break;
            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Z = this.zs[0];
                if (!Z.isOne())
                {
                    ECFieldElement Z2 = Z.square(), Z3 = Z.multiply(Z2);
                    lhs = lhs.multiply(Z);
                    A = A.multiply(Z);
                    B = B.multiply(Z3);
                }
                break;
            }
            default:
                throw new IllegalStateException("unsupported coordinate system");
            }

            ECFieldElement rhs = X.add(A).multiply(X.square()).add(B);
            return lhs.equals(rhs);
        }

        public ECPoint scaleX(ECFieldElement scale)
        {
            if (this.isInfinity())
            {
                return this;
            }

            int coord = this.getCurveCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_LAMBDA_AFFINE:
            {
                // Y is actually Lambda (X + Y/X) here
                ECFieldElement X = this.getRawXCoord(), L = this.getRawYCoord(); // earlier JDK

                ECFieldElement X2 = X.multiply(scale);
                ECFieldElement L2 = L.add(X).divide(scale).add(X2);

                return this.getCurve().createRawPoint(X, L2, this.getRawZCoords(), this.withCompression); // earlier JDK
            }
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                // Y is actually Lambda (X + Y/X) here
                ECFieldElement X = this.getRawXCoord(), L = this.getRawYCoord(), Z = this.getRawZCoords()[0]; // earlier JDK

                // We scale the Z coordinate also, to avoid an inversion
                ECFieldElement X2 = X.multiply(scale.square());
                ECFieldElement L2 = L.add(X).add(X2);
                ECFieldElement Z2 = Z.multiply(scale);

                return this.getCurve().createRawPoint(X2, L2, new ECFieldElement[]{ Z2 }, this.withCompression); // earlier JDK
            }
            default:
            {
                return super.scaleX(scale);
            }
            }
        }

        public ECPoint scaleY(ECFieldElement scale)
        {
            if (this.isInfinity())
            {
                return this;
            }

            int coord = this.getCurveCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_LAMBDA_AFFINE:
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                ECFieldElement X = this.getRawXCoord(), L = this.getRawYCoord(); // earlier JDK

                // Y is actually Lambda (X + Y/X) here
                ECFieldElement L2 = L.add(X).multiply(scale).add(X);

                return this.getCurve().createRawPoint(X, L2, this.getRawZCoords(), this.withCompression); // earlier JDK
            }
            default:
            {
                return super.scaleY(scale);
            }
            }
        }

        public ECPoint subtract(ECPoint b)
        {
            if (b.isInfinity())
            {
                return this;
            }

            // Add -b
            return this.add(b.negate());
        }

        public ECPoint.AbstractF2m tau()
        {
            if (this.isInfinity())
            {
                return this;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            ECFieldElement X1 = this.x;

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            case ECCurve.COORD_LAMBDA_AFFINE:
            {
                ECFieldElement Y1 = this.y;
                return (ECPoint.AbstractF2m)curve.createRawPoint(X1.square(), Y1.square(), this.withCompression);
            }
            case ECCurve.COORD_HOMOGENEOUS:
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                ECFieldElement Y1 = this.y, Z1 = this.zs[0];
                return (ECPoint.AbstractF2m)curve.createRawPoint(X1.square(), Y1.square(),
                    new ECFieldElement[]{ Z1.square() }, this.withCompression);
            }
            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }

        public ECPoint.AbstractF2m tauPow(int pow)
        {
            if (this.isInfinity())
            {
                return this;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            ECFieldElement X1 = this.x;

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            case ECCurve.COORD_LAMBDA_AFFINE:
            {
                ECFieldElement Y1 = this.y;
                return (ECPoint.AbstractF2m)curve.createRawPoint(X1.squarePow(pow), Y1.squarePow(pow), this.withCompression);
            }
            case ECCurve.COORD_HOMOGENEOUS:
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                ECFieldElement Y1 = this.y, Z1 = this.zs[0];
                return (ECPoint.AbstractF2m)curve.createRawPoint(X1.squarePow(pow), Y1.squarePow(pow),
                    new ECFieldElement[]{ Z1.squarePow(pow) }, this.withCompression);
            }
            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }
    }

    /**
     * Elliptic curve points over F2m
     */
    public static class F2m extends AbstractF2m
    {
        /**
         * @param curve base curve
         * @param x x point
         * @param y y point
         * 
         * @deprecated Use ECCurve.createPoint to construct points
         */
        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.
         * 
         * @deprecated per-point compression property will be removed, refer {@link #getEncoded(boolean)}
         */
        public F2m(ECCurve curve, ECFieldElement x, ECFieldElement y, boolean withCompression)
        {
            super(curve, x, y);

            if ((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;

//            checkCurveEquation();
        }

        F2m(ECCurve curve, ECFieldElement x, ECFieldElement y, ECFieldElement[] zs, boolean withCompression)
        {
            super(curve, x, y, zs);

            this.withCompression = withCompression;

//            checkCurveEquation();
        }

        protected ECPoint detach()
        {
            return new ECPoint.F2m(null, this.getAffineXCoord(), this.getAffineYCoord()); // earlier JDK
        }

        public ECFieldElement getYCoord()
        {
            int coord = this.getCurveCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_LAMBDA_AFFINE:
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                ECFieldElement X = x, L = y;

                if (this.isInfinity() || X.isZero())
                {
                    return L;
                }

                // Y is actually Lambda (X + Y/X) here; convert to affine value on the fly
                ECFieldElement Y = L.add(X).multiply(X);
                if (ECCurve.COORD_LAMBDA_PROJECTIVE == coord)
                {
                    ECFieldElement Z = zs[0];
                    if (!Z.isOne())
                    {
                        Y = Y.divide(Z);
                    }
                }
                return Y;
            }
            default:
            {
                return y;
            }
            }
        }

        protected boolean getCompressionYTilde()
        {
            ECFieldElement X = this.getRawXCoord();
            if (X.isZero())
            {
                return false;
            }

            ECFieldElement Y = this.getRawYCoord();

            switch (this.getCurveCoordinateSystem())
            {
            case ECCurve.COORD_LAMBDA_AFFINE:
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                // Y is actually Lambda (X + Y/X) here
                return Y.testBitZero() != X.testBitZero();
            }
            default:
            {
                return Y.divide(X).testBitZero();
            }
            }
        }

        public ECPoint add(ECPoint b)
        {
            if (this.isInfinity())
            {
                return b;
            }
            if (b.isInfinity())
            {
                return this;
            }

            ECCurve curve = this.getCurve();
            int coord = curve.getCoordinateSystem();

            ECFieldElement X1 = this.x;
            ECFieldElement X2 = b.x;

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement Y1 = this.y;
                ECFieldElement Y2 = b.y;

                ECFieldElement dx = X1.add(X2), dy = Y1.add(Y2);
                if (dx.isZero())
                {
                    if (dy.isZero())
                    {
                        return twice();
                    }

                    return curve.getInfinity();
                }

                ECFieldElement L = dy.divide(dx);

                ECFieldElement X3 = L.square().add(L).add(dx).add(curve.getA());
                ECFieldElement Y3 = L.multiply(X1.add(X3)).add(X3).add(Y1);

                return new ECPoint.F2m(curve, X3, Y3, this.withCompression);
            }
            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Y1 = this.y, Z1 = this.zs[0];
                ECFieldElement Y2 = b.y, Z2 = b.zs[0];

                boolean Z2IsOne = Z2.isOne();

                ECFieldElement U1 = Z1.multiply(Y2);
                ECFieldElement U2 = Z2IsOne ? Y1 : Y1.multiply(Z2);
                ECFieldElement U = U1.add(U2);
                ECFieldElement V1 = Z1.multiply(X2);
                ECFieldElement V2 = Z2IsOne ? X1 : X1.multiply(Z2);
                ECFieldElement V = V1.add(V2);

                if (V.isZero())
                {
                    if (U.isZero())
                    {
                        return twice();
                    }

                    return curve.getInfinity();
                }

                ECFieldElement VSq = V.square();
                ECFieldElement VCu = VSq.multiply(V);
                ECFieldElement W = Z2IsOne ? Z1 : Z1.multiply(Z2);
                ECFieldElement uv = U.add(V);
                ECFieldElement A = uv.multiplyPlusProduct(U, VSq, curve.getA()).multiply(W).add(VCu);

                ECFieldElement X3 = V.multiply(A);
                ECFieldElement VSqZ2 = Z2IsOne ? VSq : VSq.multiply(Z2);
                ECFieldElement Y3 = U.multiplyPlusProduct(X1, V, Y1).multiplyPlusProduct(VSqZ2, uv, A);
                ECFieldElement Z3 = VCu.multiply(W);

                return new ECPoint.F2m(curve, X3, Y3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                if (X1.isZero())
                {
                    if (X2.isZero())
                    {
                        return curve.getInfinity();
                    }

                    return b.add(this);
                }

                ECFieldElement L1 = this.y, Z1 = this.zs[0];
                ECFieldElement L2 = b.y, Z2 = b.zs[0];

                boolean Z1IsOne = Z1.isOne();
                ECFieldElement U2 = X2, S2 = L2;
                if (!Z1IsOne)
                {
                    U2 = U2.multiply(Z1);
                    S2 = S2.multiply(Z1);
                }

                boolean Z2IsOne = Z2.isOne();
                ECFieldElement U1 = X1, S1 = L1;
                if (!Z2IsOne)
                {
                    U1 = U1.multiply(Z2);
                    S1 = S1.multiply(Z2);
                }

                ECFieldElement A = S1.add(S2);
                ECFieldElement B = U1.add(U2);

                if (B.isZero())
                {
                    if (A.isZero())
                    {
                        return twice();
                    }

                    return curve.getInfinity();
                }

                ECFieldElement X3, L3, Z3;
                if (X2.isZero())
                {
                    // TODO This can probably be optimized quite a bit
                    ECPoint p = this.normalize();
                    X1 = p.getXCoord();
                    ECFieldElement Y1 = p.getYCoord();

                    ECFieldElement Y2 = L2;
                    ECFieldElement L = Y1.add(Y2).divide(X1);

                    X3 = L.square().add(L).add(X1).add(curve.getA());
                    if (X3.isZero())
                    {
                        return new ECPoint.F2m(curve, X3, curve.getB().sqrt(), this.withCompression);
                    }

                    ECFieldElement Y3 = L.multiply(X1.add(X3)).add(X3).add(Y1);
                    L3 = Y3.divide(X3).add(X3);
                    Z3 = curve.fromBigInteger(ECConstants.ONE);
                }
                else
                {
                    B = B.square();
    
                    ECFieldElement AU1 = A.multiply(U1);
                    ECFieldElement AU2 = A.multiply(U2);

                    X3 = AU1.multiply(AU2);
                    if (X3.isZero())
                    {
                        return new ECPoint.F2m(curve, X3, curve.getB().sqrt(), this.withCompression);
                    }

                    ECFieldElement ABZ2 = A.multiply(B);
                    if (!Z2IsOne)
                    {
                        ABZ2 = ABZ2.multiply(Z2);
                    }

                    L3 = AU2.add(B).squarePlusProduct(ABZ2, L1.add(Z1));

                    Z3 = ABZ2;
                    if (!Z1IsOne)
                    {
                        Z3 = Z3.multiply(Z1);
                    }
                }

                return new ECPoint.F2m(curve, X3, L3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }
            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }

        public ECPoint twice()
        {
            if (this.isInfinity()) 
            {
                return this;
            }

            ECCurve curve = this.getCurve();

            ECFieldElement X1 = this.x;
            if (X1.isZero()) 
            {
                // A point with X == 0 is it's own additive inverse
                return curve.getInfinity();
            }

            int coord = curve.getCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement Y1 = this.y;

                ECFieldElement L1 = Y1.divide(X1).add(X1);

                ECFieldElement X3 = L1.square().add(L1).add(curve.getA());
                ECFieldElement Y3 = X1.squarePlusProduct(X3, L1.addOne());

                return new ECPoint.F2m(curve, X3, Y3, this.withCompression);
            }
            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Y1 = this.y, Z1 = this.zs[0];

                boolean Z1IsOne = Z1.isOne();
                ECFieldElement X1Z1 = Z1IsOne ? X1 : X1.multiply(Z1);
                ECFieldElement Y1Z1 = Z1IsOne ? Y1 : Y1.multiply(Z1);

                ECFieldElement X1Sq = X1.square();
                ECFieldElement S = X1Sq.add(Y1Z1);
                ECFieldElement V = X1Z1;
                ECFieldElement vSquared = V.square();
                ECFieldElement sv = S.add(V);
                ECFieldElement h = sv.multiplyPlusProduct(S, vSquared, curve.getA());

                ECFieldElement X3 = V.multiply(h);
                ECFieldElement Y3 = X1Sq.square().multiplyPlusProduct(V, h, sv);
                ECFieldElement Z3 = V.multiply(vSquared);    

                return new ECPoint.F2m(curve, X3, Y3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                ECFieldElement L1 = this.y, Z1 = this.zs[0];

                boolean Z1IsOne = Z1.isOne();
                ECFieldElement L1Z1 = Z1IsOne ? L1 : L1.multiply(Z1);
                ECFieldElement Z1Sq = Z1IsOne ? Z1 : Z1.square();
                ECFieldElement a = curve.getA();
                ECFieldElement aZ1Sq = Z1IsOne ? a : a.multiply(Z1Sq);
                ECFieldElement T = L1.square().add(L1Z1).add(aZ1Sq);
                if (T.isZero())
                {
                    return new ECPoint.F2m(curve, T, curve.getB().sqrt(), withCompression);
                }

                ECFieldElement X3 = T.square();
                ECFieldElement Z3 = Z1IsOne ? T : T.multiply(Z1Sq);

                ECFieldElement b = curve.getB();
                ECFieldElement L3;
                if (b.bitLength() < (curve.getFieldSize() >> 1))
                {
                    ECFieldElement t1 = L1.add(X1).square();
                    ECFieldElement t2;
                    if (b.isOne())
                    {
                        t2 = aZ1Sq.add(Z1Sq).square();
                    }
                    else
                    {
                        // TODO Can be calculated with one square if we pre-compute sqrt(b)
                        t2 = aZ1Sq.squarePlusProduct(b, Z1Sq.square());
                    }
                    L3 = t1.add(T).add(Z1Sq).multiply(t1).add(t2).add(X3);
                    if (a.isZero())
                    {
                        L3 = L3.add(Z3);
                    }
                    else if (!a.isOne())
                    {
                        L3 = L3.add(a.addOne().multiply(Z3));
                    }
                }
                else
                {
                    ECFieldElement X1Z1 = Z1IsOne ? X1 : X1.multiply(Z1);
                    L3 = X1Z1.squarePlusProduct(T, L1Z1).add(X3).add(Z3);
                }

                return new ECPoint.F2m(curve, X3, L3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }
            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }

        public ECPoint twicePlus(ECPoint b)
        {
            if (this.isInfinity()) 
            {
                return b;
            }
            if (b.isInfinity())
            {
                return twice();
            }

            ECCurve curve = this.getCurve();

            ECFieldElement X1 = this.x;
            if (X1.isZero()) 
            {
                // A point with X == 0 is it's own additive inverse
                return b;
            }

            int coord = curve.getCoordinateSystem();

            switch (coord)
            {
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                // NOTE: twicePlus() only optimized for lambda-affine argument
                ECFieldElement X2 = b.x, Z2 = b.zs[0];
                if (X2.isZero() || !Z2.isOne())
                {
                    return twice().add(b);
                }

                ECFieldElement L1 = this.y, Z1 = this.zs[0];
                ECFieldElement L2 = b.y;

                ECFieldElement X1Sq = X1.square();
                ECFieldElement L1Sq = L1.square();
                ECFieldElement Z1Sq = Z1.square();
                ECFieldElement L1Z1 = L1.multiply(Z1);

                ECFieldElement T = curve.getA().multiply(Z1Sq).add(L1Sq).add(L1Z1);
                ECFieldElement L2plus1 = L2.addOne();
                ECFieldElement A = curve.getA().add(L2plus1).multiply(Z1Sq).add(L1Sq).multiplyPlusProduct(T, X1Sq, Z1Sq);
                ECFieldElement X2Z1Sq = X2.multiply(Z1Sq);
                ECFieldElement B = X2Z1Sq.add(T).square();

                if (B.isZero())
                {
                    if (A.isZero())
                    {
                        return b.twice();
                    }

                    return curve.getInfinity();
                }

                if (A.isZero())
                {
                    return new ECPoint.F2m(curve, A, curve.getB().sqrt(), withCompression);
                }

                ECFieldElement X3 = A.square().multiply(X2Z1Sq);
                ECFieldElement Z3 = A.multiply(B).multiply(Z1Sq);
                ECFieldElement L3 = A.add(B).square().multiplyPlusProduct(T, L2plus1, Z3);

                return new ECPoint.F2m(curve, X3, L3, new ECFieldElement[]{ Z3 }, this.withCompression);
            }
            default:
            {
                return twice().add(b);
            }
            }
        }

        public ECPoint negate()
        {
            if (this.isInfinity())
            {
                return this;
            }

            ECFieldElement X = this.x;
            if (X.isZero())
            {
                return this;
            }

            switch (this.getCurveCoordinateSystem())
            {
            case ECCurve.COORD_AFFINE:
            {
                ECFieldElement Y = this.y;
                return new ECPoint.F2m(curve, X, Y.add(X), this.withCompression);
            }
            case ECCurve.COORD_HOMOGENEOUS:
            {
                ECFieldElement Y = this.y, Z = this.zs[0];
                return new ECPoint.F2m(curve, X, Y.add(X), new ECFieldElement[]{ Z }, this.withCompression);
            }
            case ECCurve.COORD_LAMBDA_AFFINE:
            {
                ECFieldElement L = this.y;
                return new ECPoint.F2m(curve, X, L.addOne(), this.withCompression);
            }
            case ECCurve.COORD_LAMBDA_PROJECTIVE:
            {
                // L is actually Lambda (X + Y/X) here
                ECFieldElement L = this.y, Z = this.zs[0];
                return new ECPoint.F2m(curve, X, L.add(Z), new ECFieldElement[]{ Z }, this.withCompression);
            }
            default:
            {
                throw new IllegalStateException("unsupported coordinate system");
            }
            }
        }
    }
}
