/*
 * 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.asymmetric.keypair.factory;

import java.security.GeneralSecurityException;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.asymmetric.keypair.configuration.KeyPairPolicy;
import com.scytl.cryptolib.asymmetric.keypair.openssl.OpenSSLKeyPairGenerator;
import com.scytl.cryptolib.commons.configuration.Provider;
import com.scytl.cryptolib.commons.utils.system.OperatingSystem;
import com.scytl.cryptolib.primitives.securerandom.configuration.ConfigSecureRandomAlgorithmAndProvider;

/**
 * A factory class for generating {@link CryptoKeyPairGenerator} objects.
 */
public final class KeyPairGeneratorFactory {

    private final KeyPairPolicy _keyPairPolicy;

    /**
     * Constructs a {@code KeyPairGeneratorFactory} using the provided
     * {@link KeyPairPolicy}.
     * 
     * @param keyPairPolicy
     *            the {@link KeyPairPolicy} to be used to configure this
     *            {@code KeyPairGeneratorFactory}.
     *            <P>
     *            NOTE: The received {@link KeyPairPolicy} should be an
     *            immutable object. If this is the case, then the entire
     *            {@code KeyPairGeneratorFactory} class is thread safe.
     */
    public KeyPairGeneratorFactory(final KeyPairPolicy keyPairPolicy) {

        _keyPairPolicy = keyPairPolicy;
    }

    /**
     * Generates a new {@link CryptoKeyPairGenerator}, which is configured to
     * generate signing key pairs.
     * 
     * @return a new {@link CryptoKeyPairGenerator}.
     */
    public CryptoKeyPairGenerator createSigning() {

        String algorithm =
            _keyPairPolicy.getSigningKeyPairAlgorithmAndSpec()
                .getAlgorithm();

        Provider provider =
            _keyPairPolicy.getSigningKeyPairAlgorithmAndSpec()
                .getProvider();

        AlgorithmParameterSpec spec =
            _keyPairPolicy.getSigningKeyPairAlgorithmAndSpec().getSpec();

        return new CryptoKeyPairGenerator(createKeyPairGenerator(
            algorithm, provider, spec));
    }

    /**
     * Generates a new {@link CryptoKeyPairGenerator}, which is configured to
     * generate encryption key pairs.
     * 
     * @return a new {@link CryptoKeyPairGenerator}.
     */
    public CryptoKeyPairGenerator createEncryption() {

        String algorithm =
            _keyPairPolicy.getEncryptingKeyPairAlgorithmAndSpec()
                .getAlgorithm();

        Provider provider =
            _keyPairPolicy.getEncryptingKeyPairAlgorithmAndSpec()
                .getProvider();

        AlgorithmParameterSpec spec =
            _keyPairPolicy.getEncryptingKeyPairAlgorithmAndSpec()
                .getSpec();

        return new CryptoKeyPairGenerator(createKeyPairGenerator(
            algorithm, provider, spec));
    }

    private KeyPairGenerator createKeyPairGenerator(
            final String algorithm, final Provider provider,
            final AlgorithmParameterSpec spec) {

        KeyPairGenerator keyPairGenerator;

        try {
        	switch (provider) {
			case OPENSSL:
				keyPairGenerator = OpenSSLKeyPairGenerator.getInstance(algorithm);
				break;
			case DEFAULT:
				keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
				break;
			default:
                keyPairGenerator =
                    KeyPairGenerator.getInstance(algorithm,
                        provider.getProviderName());
				break;
			}

        } catch (GeneralSecurityException e) {

            throw new CryptoLibException(
                "Exception while trying to get an instance of KeyPairGenerator. Algorithm was: "
                    + algorithm + ", provider was: " + provider, e);
        }

        initialize(keyPairGenerator, spec);

        return keyPairGenerator;
    }

    private void initialize(final KeyPairGenerator keyPairGenerator,
            final AlgorithmParameterSpec spec) {

        ConfigSecureRandomAlgorithmAndProvider secureRandomAlgorithmAndProvider =
            _keyPairPolicy.getSecureRandomAlgorithmAndProvider();

        try {
            SecureRandom sr;
            switch (secureRandomAlgorithmAndProvider.getProvider()) {
    			case OPENSSL:
    				sr = null;
    				break;
    			case DEFAULT:
    			    checkOS();
    				sr = SecureRandom
                            .getInstance(secureRandomAlgorithmAndProvider
                                .getAlgorithm());
    				break;
    			default:
    			    checkOS();
    				sr = SecureRandom.getInstance(
	                        secureRandomAlgorithmAndProvider.getAlgorithm(),
	                        secureRandomAlgorithmAndProvider.getProvider()
	                            .getProviderName());
    				break;
			}

            keyPairGenerator.initialize(spec, sr);

        } catch (GeneralSecurityException e) {
            throw new CryptoLibException(
                "Exception while initializing the KeyPairGenerator", e);
        }
    }

    private void checkOS() {
        if (!_keyPairPolicy.getSecureRandomAlgorithmAndProvider()
            .isOSCompliant(OperatingSystem.current())) {

            throw new CryptoLibException(
                "The given algorithm and provider are not compliant with the "
                    + OperatingSystem.current() + " OS.");
        }
    }
}
