/*
 * Copyright (c) 2016 Karol Bucek.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jruby.ext.openssl;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;

import java.security.SecureRandom;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import javax.crypto.KeyAgreement;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DLSequence;

import org.bouncycastle.crypto.params.ECNamedDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.signers.ECDSASigner;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.component.VariableEntry;

import org.jruby.ext.openssl.impl.CipherSpec;
import static org.jruby.ext.openssl.OpenSSL.debug;
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;
import static org.jruby.ext.openssl.PKey._PKey;
import org.jruby.ext.openssl.impl.ECPrivateKeyWithName;
import static org.jruby.ext.openssl.impl.PKey.readECPrivateKey;
import org.jruby.ext.openssl.util.ByteArrayOutputStream;
import org.jruby.ext.openssl.x509store.PEMInputOutput;

/**
 * OpenSSL::PKey::EC implementation.
 *
 * @author kares
 */
public final class PKeyEC extends PKey {

    private static final long serialVersionUID = 1L;

    private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
        public PKeyEC allocate(Ruby runtime, RubyClass klass) { return new PKeyEC(runtime, klass); }
    };

    public static void createPKeyEC(final Ruby runtime, final RubyModule PKey, final RubyClass PKeyPKey) {
        RubyClass EC = PKey.defineClassUnder("EC", PKeyPKey, ALLOCATOR);

        RubyClass PKeyError = PKey.getClass("PKeyError");
        PKey.defineClassUnder("ECError", PKeyError, PKeyError.getAllocator());

        EC.defineAnnotatedMethods(PKeyEC.class);
        EC.setConstant("NAMED_CURVE", runtime.newFixnum(1));

        Point.createPoint(runtime, EC);
        Group.createGroup(runtime, EC);
    }

    static RubyClass _EC(final Ruby runtime) {
        return _PKey(runtime).getClass("EC");
    }

    public static RaiseException newECError(Ruby runtime, String message) {
        return Utils.newError(runtime, _PKey(runtime).getClass("ECError"), message);
    }

    @JRubyMethod(meta = true)
    public static RubyArray builtin_curves(ThreadContext context, IRubyObject self) {
        final Ruby runtime = context.runtime;
        final RubyArray curves = runtime.newArray();

        Enumeration names;

        names = org.bouncycastle.asn1.x9.X962NamedCurves.getNames();
        while ( names.hasMoreElements() ) {
            final String name = (String) names.nextElement();
            RubyString desc;
            if ( name.startsWith("prime") ) {
                desc = RubyString.newString(runtime, "X9.62 curve over a xxx bit prime field");
            }
            else {
                desc = RubyString.newString(runtime, "X9.62 curve over a xxx bit binary field");
            }
            curves.append(RubyArray.newArrayNoCopy(runtime, new IRubyObject[] { RubyString.newString(runtime, name), desc }));
        }

        names = org.bouncycastle.asn1.sec.SECNamedCurves.getNames();
        while ( names.hasMoreElements() ) {
            RubyString name = RubyString.newString(runtime, (String) names.nextElement());
            RubyString desc = RubyString.newString(runtime, "SECG curve over a xxx bit binary field");
            curves.append(RubyArray.newArrayNoCopy(runtime, new IRubyObject[] { name, desc }));
        }

        names = org.bouncycastle.asn1.nist.NISTNamedCurves.getNames();
        while ( names.hasMoreElements() ) {
            RubyString name = RubyString.newString(runtime, (String) names.nextElement());
            IRubyObject[] nameAndDesc = new IRubyObject[] { name, RubyString.newEmptyString(runtime) };
            curves.append(RubyArray.newArrayNoCopy(runtime, nameAndDesc));
        }

        names = org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves.getNames();
        while ( names.hasMoreElements() ) {
            RubyString name = RubyString.newString(runtime, (String) names.nextElement());
            RubyString desc = RubyString.newString(runtime, "RFC 5639 curve over a xxx bit prime field");
            curves.append(RubyArray.newArrayNoCopy(runtime, new IRubyObject[] { name, desc }));
        }

        return curves;
    }

    private static ASN1ObjectIdentifier getCurveOID(final String curveName) {
        ASN1ObjectIdentifier id;
        id = org.bouncycastle.asn1.sec.SECNamedCurves.getOID(curveName);
        if ( id != null ) return id;
        id = org.bouncycastle.asn1.x9.X962NamedCurves.getOID(curveName);
        if ( id != null ) return id;
        id = org.bouncycastle.asn1.nist.NISTNamedCurves.getOID(curveName);
        if ( id != null ) return id;
        id = org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves.getOID(curveName);
        if ( id != null ) return id;
        throw new IllegalStateException("could not identify curve name: " + curveName);
    }

    private static boolean isCurveName(final String curveName) {
        try {
            return getCurveOID(curveName) != null;
        }
        catch (IllegalStateException ex) { return false; }
    }

    private static String getCurveName(final ASN1ObjectIdentifier oid) {
        String name;
        name = org.bouncycastle.asn1.sec.SECNamedCurves.getName(oid);
        if ( name != null ) return name;
        name = org.bouncycastle.asn1.x9.X962NamedCurves.getName(oid);
        if ( name != null ) return name;
        name = org.bouncycastle.asn1.nist.NISTNamedCurves.getName(oid);
        if ( name != null ) return name;
        name = org.bouncycastle.asn1.teletrust.TeleTrusTNamedCurves.getName(oid);
        if ( name != null ) return name;
        throw new IllegalStateException("could not identify curve name from: " + oid);
    }

    public PKeyEC(Ruby runtime, RubyClass type) {
        super(runtime, type);
    }

    PKeyEC(Ruby runtime, PublicKey pubKey) {
        this(runtime, _EC(runtime), null, pubKey);
    }

    PKeyEC(Ruby runtime, RubyClass type, PrivateKey privKey, PublicKey pubKey) {
        super(runtime, type);
        this.privateKey = privKey;
        this.publicKey = (ECPublicKey) pubKey;
    }

    private transient Group group;

    private ECPublicKey publicKey;
    private transient PrivateKey privateKey;

    private String curveName;

    private String getCurveName() { return curveName; }

//    private ECNamedCurveParameterSpec getParameterSpec() {
//        return ECNamedCurveTable.getParameterSpec( getCurveName() );
//    }

    @Override
    public PublicKey getPublicKey() { return publicKey; }

    @Override
    public PrivateKey getPrivateKey() { return privateKey; }

    @Override
    public String getAlgorithm() { return "EC"; }

    @JRubyMethod(rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
        final Ruby runtime = context.runtime;

        privateKey = null; publicKey = null;

        if ( Arity.checkArgumentCount(runtime, args, 0, 2) == 0 ) {
            return this;
        }

        IRubyObject arg = args[0];

        if ( arg instanceof Group ) {
            this.group = (Group) arg;
            this.curveName = this.group.getCurveName();
            return this;
        }

        IRubyObject pass = null;
        if ( args.length > 1 ) pass = args[1];
        final char[] passwd = password(pass);
        final RubyString str = readInitArg(context, arg);
        final String strJava = str.toString();

        if ( isCurveName(strJava) ) {
            this.curveName = strJava;
            return this;
        }

        Object key = null;
        final KeyFactory ecdsaFactory;
        try {
            ecdsaFactory = SecurityHelper.getKeyFactory("ECDSA");
        }
        catch (NoSuchAlgorithmException e) {
            throw runtime.newRuntimeError("unsupported key algorithm (ECDSA)");
        }
        catch (RuntimeException e) {
            throw runtime.newRuntimeError("unsupported key algorithm (ECDSA) " + e);
        }
        // TODO: ugly NoClassDefFoundError catching for no BC env. How can we remove this?
        boolean noClassDef = false;
        if ( key == null && ! noClassDef ) { // PEM_read_bio_DSAPrivateKey
            try {
                key = readPrivateKey(strJava, passwd);
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (PEMInputOutput.PasswordRequiredException retry) {
                if ( ttySTDIN(context) ) {
                    try { key = readPrivateKey(str, passwordPrompt(context)); }
                    catch (Exception e) { debugStackTrace(runtime, e); }
                }
            }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) {
            try {
                key = PEMInputOutput.readECPublicKey(new StringReader(strJava), passwd);
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) {
            try {
                key = PEMInputOutput.readECPubKey(new StringReader(strJava));
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (Exception e) { debugStackTrace(runtime, e); }
        }
        if ( key == null && ! noClassDef ) {
            try {
                key = readECPrivateKey(ecdsaFactory, str.getBytes());
            }
            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
            catch (InvalidKeySpecException e) { debug(runtime, "PKeyEC could not read private key", e); }
            catch (IOException e) { debug(runtime, "PKeyEC could not read private key", e); }
            catch (RuntimeException e) {
                if ( isKeyGenerationFailure(e) ) debug(runtime, "PKeyEC could not read private key", e);
                else debugStackTrace(runtime, e);
            }
        }
//        if ( key == null && ! noClassDef ) {
//            try { // readECParameters
//                ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(str.getBytes());
//                ECNamedCurveParameterSpec paramSpec = ECNamedCurveTable.getParameterSpec(oid.getId());
//
//                // ecdsaFactory.generatePublic(keySpec)
//
//            }
//            catch (NoClassDefFoundError e) { noClassDef = true; debugStackTrace(runtime, e); }
//            catch (InvalidKeySpecException e) { debug(runtime, "PKeyEC could not read public key", e); }
//            catch (IOException e) { debug(runtime, "PKeyEC could not read public key", e); }
//            catch (RuntimeException e) {
//                if ( isKeyGenerationFailure(e) ) debug(runtime, "PKeyEC could not read public key", e);
//                else debugStackTrace(runtime, e);
//            }
//        }

        if ( key == null ) key = tryPKCS8EncodedKey(runtime, ecdsaFactory, str.getBytes());
        if ( key == null ) key = tryX509EncodedKey(runtime, ecdsaFactory, str.getBytes());

        if ( key == null ) throw newECError(runtime, "Neither PUB key nor PRIV key:");

        if ( key instanceof KeyPair ) {
            final PublicKey pubKey = ((KeyPair) key).getPublic();
            final PrivateKey privKey = ((KeyPair) key).getPrivate();
            if ( ! ( privKey instanceof ECPrivateKey ) ) {
                if ( privKey == null ) {
                    throw newECError(runtime, "Neither PUB key nor PRIV key: (private key is null)");
                }
                throw newECError(runtime, "Neither PUB key nor PRIV key: (invalid key type " + privKey.getClass().getName() + ")");
            }
            this.publicKey = (ECPublicKey) pubKey;
            this.privateKey = (ECPrivateKey) privKey;
            unwrapPrivateKeyWithName();
        }
        else if ( key instanceof ECPrivateKey ) {
            this.privateKey = (ECPrivateKey) key;
            unwrapPrivateKeyWithName();
        }
        else if ( key instanceof ECPublicKey ) {
            this.publicKey = (ECPublicKey) key; this.privateKey = null;
        }
        else {
            throw newECError(runtime, "Neither PUB key nor PRIV key: "  + key.getClass().getName());
        }

        if ( publicKey != null ) {
            publicKey.getParams().getCurve();
        }
        // TODO set curveName ?!?!?!?!?!?!?!

        return this;
    }

    private void unwrapPrivateKeyWithName() {
        final ECPrivateKey privKey = (ECPrivateKey) this.privateKey;
        if ( privKey instanceof ECPrivateKeyWithName ) {
            this.privateKey = ((ECPrivateKeyWithName) privKey).unwrap();
            this.curveName = getCurveName( ((ECPrivateKeyWithName) privKey).getCurveNameOID() );
        }
    }

    //private static ECNamedCurveParameterSpec readECParameters(final byte[] input) throws IOException {
    //    ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(input);
    //    return ECNamedCurveTable.getParameterSpec(oid.getId());
    //}

    @JRubyMethod
    public IRubyObject check_key(final ThreadContext context) {
        return context.runtime.getTrue(); // TODO not implemented stub
    }

    @JRubyMethod(name = "generate_key")
    public PKeyEC generate_key(final ThreadContext context) {
        // final ECDomainParameters params = getDomainParameters();
        try {
            ECGenParameterSpec genSpec = new ECGenParameterSpec(getCurveName());
            KeyPairGenerator gen = SecurityHelper.getKeyPairGenerator("ECDSA"); // "BC"
            gen.initialize(genSpec, new SecureRandom());
            KeyPair pair = gen.generateKeyPair();
            this.publicKey = (ECPublicKey) pair.getPublic();
            this.privateKey = pair.getPrivate();
        }
        catch (GeneralSecurityException ex) {
            throw newECError(context.runtime, ex.toString());
        }
        return this;
    }

    @JRubyMethod(name = "dsa_sign_asn1")
    public IRubyObject dsa_sign_asn1(final ThreadContext context, final IRubyObject data) {
        try {
            ECNamedCurveParameterSpec params = ECNamedCurveTable.getParameterSpec(getCurveName());
            ASN1ObjectIdentifier oid = getCurveOID(getCurveName());
            ECNamedDomainParameters domainParams = new ECNamedDomainParameters(oid,
                params.getCurve(), params.getG(), params.getN(), params.getH(), params.getSeed()
            );

            final ECDSASigner signer = new ECDSASigner();
            final ECPrivateKey privKey = (ECPrivateKey) this.privateKey;
            signer.init(true, new ECPrivateKeyParameters(privKey.getS(), domainParams));

            final byte[] message = data.convertToString().getBytes();
            BigInteger[] signature = signer.generateSignature(message); // [r, s]

//            final byte[] r = signature[0].toByteArray();
//            final byte[] s = signature[1].toByteArray();
//            // ASN.1 encode as: 0x30 len 0x02 rlen (r) 0x02 slen (s)
//            final int len = 1 + (1 + r.length) + 1 + (1 + s.length);
//
//            final byte[] encoded = new byte[1 + 1 + len]; int i;
//            encoded[0] = 0x30;
//            encoded[1] = (byte) len;
//            encoded[2] = 0x20;
//            encoded[3] = (byte) r.length;
//            System.arraycopy(r, 0, encoded, i = 4, r.length); i += r.length;
//            encoded[i++] = 0x20;
//            encoded[i++] = (byte) s.length;
//            System.arraycopy(s, 0, encoded, i, s.length);

            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            ASN1OutputStream asn1 = new ASN1OutputStream(bytes);

            ASN1EncodableVector v = new ASN1EncodableVector();
            v.add(new ASN1Integer(signature[0])); // r
            v.add(new ASN1Integer(signature[1])); // s

            asn1.writeObject(new DLSequence(v));

            return StringHelper.newString(context.runtime, bytes.buffer(), bytes.size());
        }
        catch (IOException ex) {
            throw newECError(context.runtime, ex.toString());
        }
        catch (RuntimeException ex) {
            throw newECError(context.runtime, ex.toString());
        }
    }


    @JRubyMethod(name = "dh_compute_key")
    public IRubyObject dh_compute_key(final ThreadContext context, final IRubyObject point) {
        try {
            KeyAgreement agreement = SecurityHelper.getKeyAgreement("ECDH"); // "BC"
            agreement.init(getPrivateKey());
            if ( point.isNil() ) {
                agreement.doPhase(getPublicKey(), true);
            }
            else {
                final ECPoint ecPoint = ((Point) point).asECPoint();
                final String name = getCurveName();

                KeyFactory keyFactory = KeyFactory.getInstance("EC"); // "BC"
                ECParameterSpec spec = getParamSpec(name);
                ECPublicKey ecPublicKey = (ECPublicKey) keyFactory.generatePublic(new ECPublicKeySpec(ecPoint, spec));
                agreement.doPhase(ecPublicKey, true);
            }
            final byte[] secret = agreement.generateSecret();
            return StringHelper.newString(context.runtime, secret);
        }
        catch (NoSuchAlgorithmException ex) {
            throw newECError(context.runtime, ex.toString());
        }
        catch (InvalidKeyException ex) {
            throw newECError(context.runtime, ex.toString());
        }
        catch (GeneralSecurityException ex) {
            throw newECError(context.runtime, ex.toString());
        }
    }

    private Group getGroup(boolean required) {
        if (group == null) {
            if (publicKey != null) {
                return group = new Group(getRuntime(), this);
            }
            if (required) throw new IllegalStateException("no group (without public key)");
        }
        return group;
    }

    /**
     * @return OpenSSL::PKey::EC::Group
     */
    @JRubyMethod
    public IRubyObject group() {
        final Group group = getGroup(false);
        return group == null ? getRuntime().getNil() : group;
    }

    @JRubyMethod(name = "group=")
    public IRubyObject set_group(IRubyObject group) {
        this.group = group.isNil() ? null : (Group) group;
        return group;
    }

    /**
     * @return OpenSSL::PKey::EC::Point
     */
    @JRubyMethod
    public IRubyObject public_key(final ThreadContext context) {
        if ( publicKey == null ) return context.nil;

        return new Point(context.runtime, publicKey, getGroup(true));
    }

    @JRubyMethod(name = "public_key=")
    public IRubyObject set_public_key(final ThreadContext context, final IRubyObject arg) {
        if ( ! ( arg instanceof Point )  ) {
            throw context.runtime.newTypeError(arg, _EC(context.runtime).getClass("Point"));
        }
        final Point point = (Point) arg;
        ECPublicKeySpec keySpec = new ECPublicKeySpec(point.asECPoint(), getParamSpec());
        try {
            this.publicKey = (ECPublicKey) SecurityHelper.getKeyFactory("ECDSA").generatePublic(keySpec);
            return arg;
        }
        catch (GeneralSecurityException ex) {
            throw newECError(context.runtime, ex.getMessage());
        }
    }

    private static ECParameterSpec getParamSpec(final String curveName) {
        ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(curveName);
        return new ECNamedCurveSpec(spec.getName(), spec.getCurve(), spec.getG(), spec.getN(), spec.getH(), spec.getSeed());
    }

    private ECParameterSpec getParamSpec() {
        return getParamSpec(getCurveName());
    }

    /**
     * @return OpenSSL::BN
     */
    @JRubyMethod
    public IRubyObject private_key(final ThreadContext context) {
        if ( privateKey == null ) return context.nil;

        return BN.newBN(context.runtime, ((ECPrivateKey) privateKey).getS());
    }

    @JRubyMethod(name = "private_key=")
    public IRubyObject set_private_key(final ThreadContext context, final IRubyObject arg) {
        final BigInteger s;
        if ( arg instanceof BN ) {
            s = ((BN) (arg)).getValue();
        }
        else {
            s = (BigInteger) arg;
        }
        ECPrivateKeySpec keySpec = new ECPrivateKeySpec(s, getParamSpec());
        try {
            this.privateKey = SecurityHelper.getKeyFactory("ECDSA").generatePrivate(keySpec);
            return arg;
        }
        catch (GeneralSecurityException ex) {
            throw newECError(context.runtime, ex.getMessage());
        }
    }

    @JRubyMethod(name = "public_key?")
    public RubyBoolean public_p() {
        return publicKey != null ? getRuntime().getTrue() : getRuntime().getFalse();
    }

    @JRubyMethod(name = "private_key?")
    public RubyBoolean private_p() {
        return privateKey != null ? getRuntime().getTrue() : getRuntime().getFalse();
    }

    @Override
    @JRubyMethod(name = "to_der")
    public RubyString to_der() {
        final byte[] bytes;
        try {
            bytes = toDER();
        }
        catch (IOException e) {
            throw newECError(getRuntime(), e.getMessage());
        }
        return StringHelper.newString(getRuntime(), bytes);
    }

    private byte[] toDER() throws IOException {
        if ( publicKey != null && privateKey == null ) {
            return publicKey.getEncoded();
        }
        if ( privateKey == null ) {
            throw new IllegalStateException("private key as well as public key are null");
        }
        return privateKey.getEncoded();
    }

    @Override
    @JRubyMethod(name = "to_pem", alias = "export", rest = true)
    public RubyString to_pem(final IRubyObject[] args) {
        Arity.checkArgumentCount(getRuntime(), args, 0, 2);

        CipherSpec spec = null; char[] passwd = null;
        if ( args.length > 0 ) {
            spec = cipherSpec( args[0] );
            if ( args.length > 1 ) passwd = password( args[1] );
        }

        try {
            final StringWriter writer = new StringWriter();
            if ( privateKey != null ) {
                PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd);
            }
            else {
                PEMInputOutput.writeECPublicKey(writer, publicKey);
            }
            return RubyString.newString(getRuntime(), writer.getBuffer());
        }
        catch (IOException ex) {
            throw newECError(getRuntime(), ex.getMessage());
        }
    }

    @JRubyClass(name = "OpenSSL::PKey::EC::Group")
    public static final class Group extends RubyObject {

        private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
            public Group allocate(Ruby runtime, RubyClass klass) { return new Group(runtime, klass); }
        };

        static void createGroup(final Ruby runtime, final RubyClass EC) {
            RubyClass Group = EC.defineClassUnder("Group", runtime.getObject(), ALLOCATOR);

            // OpenSSL::PKey::EC::Group::Error
            RubyClass OpenSSLError = OpenSSL._OpenSSLError(runtime);
            Group.defineClassUnder("Error", OpenSSLError, OpenSSLError.getAllocator());

            Group.defineAnnotatedMethods(Group.class);
        }

        private transient PKeyEC key;
        private ECParameterSpec paramSpec;
        private RubyString curve_name;

        public Group(Ruby runtime, RubyClass type) {
            super(runtime, type);
        }

        Group(Ruby runtime, PKeyEC key) {
            this(runtime, _EC(runtime).getClass("Group"));
            this.key = key;
            this.paramSpec = key.publicKey.getParams();
        }

        private String getCurveName() {
            if (key != null) return key.getCurveName();
            return curve_name.toString();
        }

        @JRubyMethod(rest = true, visibility = Visibility.PRIVATE)
        public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
            final Ruby runtime = context.runtime;

            if ( Arity.checkArgumentCount(runtime, args, 1, 4) == 1 ) {
                IRubyObject arg = args[0];

                if ( arg instanceof Group ) {
                    IRubyObject curve_name = ((Group) arg).curve_name(context);
                    this.curve_name = curve_name.isNil() ? null : (RubyString) curve_name;
                    return this;
                }

                this.curve_name = ((RubyString) arg);

                // TODO PEM/DER parsing not implemented
            }
            return this;
        }

        @Override
        @JRubyMethod(name = { "==", "eql?" })
        public IRubyObject op_equal(final ThreadContext context, final IRubyObject obj) {
            if ( paramSpec == null ) return context.nil;
            if ( obj instanceof Group ) {
                final Group that = (Group) obj;
                boolean equals = this.paramSpec.equals(that.paramSpec);
                return context.runtime.newBoolean(equals);
            }
            return context.runtime.getFalse();
        }

        @JRubyMethod
        public IRubyObject curve_name(final ThreadContext context) {
            if (curve_name == null) {
                String prefix, curveName = key.getCurveName();
                // BC 1.54: "brainpoolP512t1" 1.55: "brainpoolp512t1"
                if (curveName.startsWith(prefix = "brainpoolp")) {
                    curveName = "brainpoolP" + curveName.substring(prefix.length());
                }
                curve_name = RubyString.newString(context.runtime, curveName);
            }
            return curve_name.dup();
        }

        @JRubyMethod
        public IRubyObject order(final ThreadContext context) {
            if ( paramSpec == null ) return context.nil;
            return BN.newBN(context.runtime, paramSpec.getOrder());
        }

        @JRubyMethod
        public IRubyObject cofactor(final ThreadContext context) {
            if ( paramSpec == null ) return context.nil;
            return context.runtime.newFixnum(paramSpec.getCofactor());
        }

        @JRubyMethod
        public IRubyObject seed(final ThreadContext context) {
            if ( paramSpec == null ) return context.nil;
            final byte[] seed = paramSpec.getCurve().getSeed();
            return seed == null ? context.nil : StringHelper.newString(context.runtime, seed);
        }

        @JRubyMethod
        public IRubyObject degree(final ThreadContext context) {
            if ( paramSpec == null ) return context.nil;
            final int fieldSize = paramSpec.getCurve().getField().getFieldSize();
            return context.runtime.newFixnum(fieldSize);
        }

        @JRubyMethod
        public IRubyObject generator(final ThreadContext context) {
            if ( paramSpec == null ) return context.nil;
            final ECPoint generator = paramSpec.getGenerator();
            //final int bitLength = paramSpec.getOrder().bitLength();
            return new Point(context.runtime, generator, this);
        }

        @JRubyMethod(name = { "to_pem" }, alias = "export", rest = true)
        public RubyString to_pem(final ThreadContext context, final IRubyObject[] args) {
            Arity.checkArgumentCount(context.runtime, args, 0, 2);

            CipherSpec spec = null; char[] passwd = null;
            if ( args.length > 0 ) {
                spec = cipherSpec( args[0] );
                if ( args.length > 1 ) passwd = password(args[1]);
            }

            try {
                final StringWriter writer = new StringWriter();
                PEMInputOutput.writeECParameters(writer, getCurveOID(getCurveName()), spec, passwd);
                return RubyString.newString(context.runtime, writer.getBuffer());
            }
            catch (IOException ex) {
                throw newECError(context.runtime, ex.getMessage());
            }
        }

        final EllipticCurve getCurve() {
            if (paramSpec == null) {
                paramSpec = getParamSpec(getCurveName());
            }
            return paramSpec.getCurve();
        }

//        @Override
//        @JRubyMethod
//        @SuppressWarnings("unchecked")
//        public IRubyObject inspect() {
//            final EllipticCurve curve = getCurve();
//            final StringBuilder part = new StringBuilder();
//            String cname = getMetaClass().getRealClass().getName();
//            part.append("#<").append(cname).append(":0x");
//            part.append(Integer.toHexString(System.identityHashCode(this)));
//            // part.append(' ');
//            part.append(" a:").append(curve.getA()).append(" b:").append(curve.getA());
//            return RubyString.newString(getRuntime(), part.append('>'));
//        }

    }

    @JRubyClass(name = "OpenSSL::PKey::EC::Point")
    public static final class Point extends RubyObject {

        private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
            public Point allocate(Ruby runtime, RubyClass klass) { return new Point(runtime, klass); }
        };

        static void createPoint(final Ruby runtime, final RubyClass EC) {
            RubyClass Point = EC.defineClassUnder("Point", runtime.getObject(), ALLOCATOR);

            // OpenSSL::PKey::EC::Point::Error
            RubyClass OpenSSLError = OpenSSL._OpenSSLError(runtime);
            Point.defineClassUnder("Error", OpenSSLError, OpenSSLError.getAllocator());

            Point.defineAnnotatedMethods(Point.class);
        }

        public Point(Ruby runtime, RubyClass type) {
            super(runtime, type);
        }

        // private transient ECPublicKey publicKey;
        private ECPoint point;
        //private int bitLength;
        private Group group;

        Point(Ruby runtime, ECPublicKey publicKey, Group group) {
            this(runtime, _EC(runtime).getClass("Point"));
            //this.publicKey = publicKey;
            this.point = publicKey.getW();
            this.group = group;
        }

        Point(Ruby runtime, ECPoint point, Group group) {
            this(runtime, _EC(runtime).getClass("Point"));
            this.point = point;
            this.group = group;
        }

        private static RaiseException newError(final Ruby runtime, final String message) {
            final RubyClass Error = _EC(runtime).getClass("Point").getClass("Error");
            return Utils.newError(runtime, Error, message);
        }

        @JRubyMethod(rest = true, visibility = Visibility.PRIVATE)
        public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
            final Ruby runtime = context.runtime;

            final int argc = Arity.checkArgumentCount(runtime, args, 1, 2);
            final IRubyObject arg = args[0];

            if ( arg instanceof Point ) {
                this.group = ((Point) arg).group;
                this.point = ((Point) arg).point;
                return this;
            }

            if ( arg instanceof Group ) {
                this.group = (Group) arg;
            }
            if ( argc == 2 ) { // (group, bn)
                final byte[] encoded = ((BN) args[1]).getValue().abs().toByteArray();
                try {
                    this.point = ECPointUtil.decodePoint(group.getCurve(), encoded);
                }
                catch (IllegalArgumentException ex) {
                    // MRI: OpenSSL::PKey::EC::Point::Error: invalid encoding
                    throw newError(context.runtime, ex.getMessage());
                }
            }

            return this;
        }

        @Override
        @JRubyMethod(name = { "==", "eql?" })
        public IRubyObject op_equal(final ThreadContext context, final IRubyObject obj) {
            if (obj instanceof Point) {
                final Point that = (Point) obj;
                boolean equals = this.point.equals(that.point);
                return context.runtime.newBoolean(equals);
            }
            return context.runtime.getFalse();
        }

        /**
         * @return OpenSSL::PKey::EC::Group
         */
        @JRubyMethod
        public IRubyObject group() {
            return group == null ? getRuntime().getNil() : group;
        }

        private ECPoint asECPoint() {
            return point; // return publicKey.getW();
        }

        private int bitLength() {
            return group.paramSpec.getOrder().bitLength();
        }

        @JRubyMethod
        public BN to_bn(final ThreadContext context) {
            final byte[] encoded = encode(bitLength(), point);
            return BN.newBN(context.runtime, new BigInteger(1, encoded));
        }

        private boolean isInfinity() {
            return point == ECPoint.POINT_INFINITY;
        }

        @JRubyMethod(name = "infinity?")
        public RubyBoolean infinity_p() {
            return getRuntime().newBoolean( isInfinity() );
        }

        @JRubyMethod(name = "set_to_infinity!")
        public IRubyObject set_to_infinity_b() {
            this.point = ECPoint.POINT_INFINITY;
            return this;
        }

        @Override
        @JRubyMethod
        @SuppressWarnings("unchecked")
        public IRubyObject inspect() {
            VariableEntry entry = new VariableEntry( "group", group == null ? (Object) "nil" : group );
            return ObjectSupport.inspect(this, (List) Collections.singletonList(entry));
        }

    }

    static byte[] encode(final ECPublicKey pubKey) {
        return encode(pubKey.getParams().getOrder().bitLength(), pubKey.getW());
    }

    private static byte[] encode(final int bitLength, final ECPoint point) {
        if ( point == ECPoint.POINT_INFINITY )  return new byte[1];

        final int bytesLength = (bitLength + 7) / 8;
        byte[] encoded = new byte[1 + bytesLength + bytesLength];

        encoded[0] = 0x04;

        addIntBytes(point.getAffineX(), bytesLength, encoded, 1);
        addIntBytes(point.getAffineY(), bytesLength, encoded, 1 + bytesLength);

        return encoded;
    }

    private static void addIntBytes(BigInteger i, final int length, final byte[] dest, final int destOffset) {
       final byte[] bytes = i.toByteArray();

       if (length < bytes.length) {
           System.arraycopy(bytes, bytes.length - length, dest, destOffset, length);
        }
        else if (length > bytes.length) {
            System.arraycopy(bytes, 0, dest, destOffset + (length - bytes.length), bytes.length);
        }
        else {
            System.arraycopy(bytes, 0, dest, destOffset, length);
        }
    }

}