/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.cryptolib.elgamal.service;

import java.math.BigInteger;

import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.constants.Constants;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.configuration.ElGamalPolicyFromProperties;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalDecrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalKeyPairGenerator;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalReEncrypter;
import com.scytl.cryptolib.elgamal.encrytionparams.SubgroupParamsGenerator;
import com.scytl.cryptolib.elgamal.factory.ElGamalFactory;
import com.scytl.cryptolib.ffc.cryptoapi.FFCDomainParameters;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

/**
 * Class which provides high-level ElGamal cryptographic services.
 */
public final class ElGamalService implements ElGamalServiceAPI {

    private final ElGamalFactory _elGamalFactory;

    private final SubgroupParamsGenerator _elGamalEncryptionParametersGenerator;

    /**
     * Creates and instance of service and initializes all properties according
     * to the properties specified by
     * {@link Constants#CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH}.
     * 
     * @throws GeneralCryptoLibException
     *             if the default properties are invalid.
     */
    public ElGamalService() throws GeneralCryptoLibException {
        this(Constants.CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH);
    }

    /**
     * Constructor which initializes its state using the properties file located
     * at the specified path.
     * 
     * @param path
     *            the path of the properties file to be used to configure the
     *            service.
     * @throws GeneralCryptoLibException
     *             if the path is invalid or the properties are invalid.
     */
    public ElGamalService(final String path)
            throws CryptoLibException, GeneralCryptoLibException {

        Validate.notNullOrBlank(path, "Properties file path");

        ElGamalPolicyFromProperties policy =
            new ElGamalPolicyFromProperties(path);
        _elGamalFactory = new ElGamalFactory(policy);

        _elGamalEncryptionParametersGenerator =
            _elGamalFactory.createElGamalEncryptionParametersGenerator();
    }

    /**
     * Provided as a part of encryption parameter generation functionality.
     */
    @Override
    public ElGamalEncryptionParameters generateEncryptionParameters() {

        return _elGamalEncryptionParametersGenerator.generate();
    }

    @Override
    public ElGamalEncryptionParameters generateEncryptionParameters(
            FFCDomainParameters parameters)
            throws GeneralCryptoLibException {
        Validate.notNull(parameters, "FFC domain parameters");
        BigInteger p = parameters.primes().p().value();
        BigInteger q = parameters.primes().q().value();
        BigInteger g = parameters.g();
        return new ElGamalEncryptionParameters(p, q, g);
    }

    @Override
    public CryptoAPIElGamalKeyPairGenerator getElGamalKeyPairGenerator() {

        return _elGamalFactory.createCryptoElGamalKeyPairGenerator();
    }

    @Override
    public CryptoAPIElGamalEncrypter createEncrypter(
            final ElGamalPublicKey publicKey)
            throws GeneralCryptoLibException {

        Validate.notNull(publicKey, "ElGamal public key");
        validateGroupMembership(publicKey);

        return _elGamalFactory.createEncrypter(publicKey);
    }

    @Override
    public CryptoAPIElGamalDecrypter createDecrypter(
            final ElGamalPrivateKey privateKey)
            throws GeneralCryptoLibException {

        Validate.notNull(privateKey, "ElGamal private key");
        validateGroupMembership(privateKey);

        return _elGamalFactory.createDecrypter(privateKey);
    }

    @Override
    public CryptoAPIElGamalReEncrypter createReEncrypter(
            ElGamalPublicKey publicKey) throws GeneralCryptoLibException {

        Validate.notNull(publicKey, "ElGamal public key");
        validateGroupMembership(publicKey);

        return _elGamalFactory.createReEncrypter(publicKey);
    }

    private void validateGroupMembership(final ElGamalPublicKey publicKey)
            throws GeneralCryptoLibException {

        ZpSubgroup group = publicKey.getGroup();

        for (ZpGroupElement element : publicKey.getKeys()) {
            if (!group.isGroupMember(element)) {
                throw new GeneralCryptoLibException("Element "
                    + element.getValue()
                    + " is not a member of the public key's mathematical group.");
            }
        }
    }

    private void validateGroupMembership(
            final ElGamalPrivateKey privateKey)
            throws GeneralCryptoLibException {

        for (Exponent exponent : privateKey.getKeys()) {
            if (!privateKey.getGroup().getQ().equals(exponent.getQ())) {
                throw new GeneralCryptoLibException("Exponent "
                    + exponent.getValue()
                    + " is not an exponent of the private key's mathematical group.");
            }
        }
    }
}
