package net.i2p.crypto.elgamal;

import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Arrays;

import net.i2p.crypto.SHA256Generator;
import net.i2p.crypto.SigUtil;
import net.i2p.util.NativeBigInteger;
import net.i2p.util.RandomSource;

/**
 * ElG signatures with SHA-256
 *
 * ref: https://en.wikipedia.org/wiki/ElGamal_signature_scheme
 *
 * @since 0.9.25
 */
public final class ElGamalSigEngine extends Signature {
    private final MessageDigest digest;
    private ElGamalKey key;

    /**
     * No specific hash requested, allows any ElGamal key.
     */
    public ElGamalSigEngine() {
        this(SHA256Generator.getDigestInstance());
    }

    /**
     * Specific hash requested, only matching keys will be allowed.
     * @param digest the hash algorithm that keys must have to sign or verify.
     */
    public ElGamalSigEngine(MessageDigest digest) {
        super("ElGamal");
        this.digest = digest;
    }

    @Override
    protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
        digest.reset();
        if (privateKey instanceof ElGamalPrivateKey) {
            ElGamalPrivateKey privKey = (ElGamalPrivateKey) privateKey;
            key = privKey;
        } else {
            throw new InvalidKeyException("cannot identify ElGamal private key: " + privateKey.getClass());
        }
    }

    @Override
    protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
        digest.reset();
        if (publicKey instanceof ElGamalPublicKey) {
            key = (ElGamalPublicKey) publicKey;
        } else {
            throw new InvalidKeyException("cannot identify ElGamal public key: " + publicKey.getClass());
        }
    }

    @Override
    protected void engineUpdate(byte b) throws SignatureException {
        digest.update(b);
    }

    @Override
    protected void engineUpdate(byte[] b, int off, int len)
            throws SignatureException {
        digest.update(b, off, len);
    }

    /**
     *  @return ASN.1 R,S
     */
    @Override
    protected byte[] engineSign() throws SignatureException {
        BigInteger elgp = key.getParams().getP();
        BigInteger pm1 = elgp.subtract(BigInteger.ONE);
        BigInteger elgg = key.getParams().getG();
        BigInteger x = ((ElGamalPrivateKey) key).getX();
        if (!(x instanceof NativeBigInteger))
            x = new NativeBigInteger(x);
        byte[] data = digest.digest();

        BigInteger k;
        boolean ok;
        do {
            k = new BigInteger(2048, RandomSource.getInstance());
            ok = k.compareTo(pm1) == -1;
            ok = ok && k.compareTo(BigInteger.ONE) == 1;
            ok = ok && k.gcd(pm1).equals(BigInteger.ONE);
        } while (!ok);

        BigInteger r = elgg.modPow(k, elgp);
        BigInteger kinv = k.modInverse(pm1);
        BigInteger h = new NativeBigInteger(1, data);
        BigInteger s = (kinv.multiply(h.subtract(x.multiply(r)))).mod(pm1);
        // todo if s == 0 go around again

        byte[] rv;
        try {
            rv = SigUtil.sigBytesToASN1(r, s);
        } catch (IllegalArgumentException iae) {
            throw new SignatureException("ASN1", iae);
        }
        return rv;
    }

    /**
     *  @param sigBytes ASN.1 R,S
     */
    @Override
    protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
        BigInteger elgp = key.getParams().getP();
        BigInteger pm1 = elgp.subtract(BigInteger.ONE);
        BigInteger elgg = key.getParams().getG();
        BigInteger y = ((ElGamalPublicKey) key).getY();
        if (!(y instanceof NativeBigInteger))
            y = new NativeBigInteger(y);
        byte[] data = digest.digest();

        try {
            BigInteger[] rs = SigUtil.aSN1ToBigInteger(sigBytes, 256);
            BigInteger r = rs[0];
            BigInteger s = rs[1];
            if (r.signum() != 1 || s.signum() != 1 ||
                r.compareTo(elgp) != -1 || s.compareTo(pm1) != -1)
                return false;
            NativeBigInteger h = new NativeBigInteger(1, data);
            BigInteger modvalr = r.modPow(s, elgp);
            BigInteger modvaly = y.modPow(r, elgp);
            BigInteger modmulval = modvalr.multiply(modvaly).mod(elgp);
            BigInteger v = elgg.modPow(h, elgp);

            boolean ok = v.compareTo(modmulval) == 0;
            return ok;
        } catch (RuntimeException e) {
            throw new SignatureException("verify", e);
        }
    }

    /**
     * @deprecated replaced with <a href="#engineSetParameter(java.security.spec.AlgorithmParameterSpec)">this</a>
     */
    @Override
    @Deprecated
    protected void engineSetParameter(String param, Object value) {
        throw new UnsupportedOperationException("engineSetParameter unsupported");
    }

    /**
     * @deprecated
     */
    @Override
    @Deprecated
    protected Object engineGetParameter(String param) {
        throw new UnsupportedOperationException("engineSetParameter unsupported");
    }
}
