package meerkat.crypto.concrete;

import com.google.protobuf.ByteString;
import com.google.protobuf.GeneratedMessage;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import meerkat.protobuf.ConcreteCrypto;
import meerkat.protobuf.ConcreteCrypto.GroupElement;
import meerkat.protobuf.Crypto;
import org.spongycastle.jce.spec.ECParameterSpec;
import org.spongycastle.jce.spec.ECPublicKeySpec;
import org.spongycastle.math.ec.ECPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.factcenter.qilin.primitives.concrete.ECElGamal;
import org.factcenter.qilin.primitives.concrete.ECGroup;
import org.factcenter.qilin.primitives.generic.ElGamal;
import org.factcenter.qilin.util.Pair;

import java.io.ByteArrayInputStream;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;

/**
 * utilities for ECElgamal
 */
public class ECElGamalUtils {

    private static GlobalCryptoSetup globalCryptoSetup = GlobalCryptoSetup.getInstance();

    final static Logger logger = LoggerFactory.getLogger(ECElGamalUtils.class);

    public final static String ENCRYPTION_KEY_ALGORITHM = "ECDH";

    /**
     * Serialize an El-Gamal public key into a form acceptable by {@link ECElGamalEncryption}
     * @param pk
     * @return
     */
    public static ConcreteCrypto.ElGamalPublicKey serializePk(ECGroup group, ElGamal.PK<ECPoint> pk) {
        ECPoint pkPoint = pk.getPK();
        ECParameterSpec params = group.getCurveParams();

        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(pkPoint, params);

        try {
            KeyFactory fact = KeyFactory.getInstance(ENCRYPTION_KEY_ALGORITHM,
                    globalCryptoSetup.getBouncyCastleProvider());
            PublicKey javaPk = fact.generatePublic(pubKeySpec);
            ConcreteCrypto.ElGamalPublicKey serializedPk = ConcreteCrypto.ElGamalPublicKey.newBuilder()
                .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build();

            return serializedPk;
        } catch (Exception e) {
            logger.error("Should never happen!", e);
            throw new RuntimeException("Error converting public key!", e);
        }
    }

    /**
     * Standard (non-threshold) decryption for testing purposes.
     * @param secretKey
     * @return
     */
    public static <T extends Message>  T decrypt(Class<T> plaintextMessageType, ECElGamal.SK secretKey, ECGroup group, Crypto.RerandomizableEncryptedMessage opaqueCipher)
            throws InvalidProtocolBufferException {
        ConcreteCrypto.ElGamalCiphertext cipherText = ConcreteCrypto.ElGamalCiphertext.parseFrom(opaqueCipher.getData());
        GroupElement c1encoded = cipherText.getC1();
        GroupElement c2encoded = cipherText.getC2();

        ECPoint c1 = ECElGamalEncryption.decodeElement(group, c1encoded);
        ECPoint c2 = ECElGamalEncryption.decodeElement(group, c2encoded);

        ECPoint plaintextEncoded = secretKey.decrypt(new Pair<>(c1, c2));

        byte[] plaintext = group.injectiveDecode(plaintextEncoded);

        ByteArrayInputStream in = new ByteArrayInputStream(plaintext);

        try {
            java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder");
            Message.Builder builder = (Message.Builder) newBuilder.invoke(plaintextMessageType);
            builder.mergeDelimitedFrom(in);
            return plaintextMessageType.cast(builder.build());
        } catch (Exception e) {
            logger.error("Error parsing incoming message", e);
            throw new InvalidProtocolBufferException("Plaintext protobuf error");
        }
    }

}
