/*
 * 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.scytl.keystore.factory;

import static java.text.MessageFormat.format;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.configuration.Provider;
import com.scytl.cryptolib.primitives.derivation.factory.CryptoKeyDeriverFactory;
import com.scytl.cryptolib.scytl.keystore.factory.CryptoScytlKeyStoreWithPBKDFHelper.ENTRY_TYPE;
import com.scytl.cryptolib.symmetric.cipher.factory.SymmetricCipherFactory;
import com.scytl.cryptolib.symmetric.key.configuration.ConfigSecretKeyAlgorithmAndSpec;

/**
 * Class that provides the functionality to create and load Derived KeyStores.
 * <p>
 * Instances of this class are immutable.
 */
public final class ScytlKeyStoreWithPBKDFGenerator {

    private final String _keyStoreType;

    private final Provider _keyStoreProvider;

    private final CryptoKeyDeriverFactory _cryptoPBKDFDeriverFactory;

    private final SymmetricCipherFactory _symmetricCipherFactory;

    private final ConfigSecretKeyAlgorithmAndSpec _configSecretKeyAlgorithmAndSpec;

    /**
     * Creates an instance of the class and initialized it by the provided
     * arguments.
     * 
     * @param cryptoPBKDFDeriverFactory
     *            the derivator of crypto key.
     * @param keyStoreType
     *            the type of KeyStore to be created.
     * @param keyStoreProvider
     *            the provider of the {@link java.security.KeyStore}.
     * @param symmetricCipherFactory
     *            the symmetric cipher factory.
     * @param configSecretKeyAlgorithmAndSpec
     *            the secret key type.
     */
    ScytlKeyStoreWithPBKDFGenerator(
            final CryptoKeyDeriverFactory cryptoPBKDFDeriverFactory,
            final String keyStoreType, final Provider keyStoreProvider,
            final SymmetricCipherFactory symmetricCipherFactory,
            final ConfigSecretKeyAlgorithmAndSpec configSecretKeyAlgorithmAndSpec) {
        _cryptoPBKDFDeriverFactory = cryptoPBKDFDeriverFactory;
        _keyStoreType = keyStoreType;
        _keyStoreProvider = keyStoreProvider;
        _symmetricCipherFactory = symmetricCipherFactory;
        _configSecretKeyAlgorithmAndSpec = configSecretKeyAlgorithmAndSpec;
    }

    /**
     * Creates a empty Key store, ready to add any entry.
     * 
     * @return an empty {@link CryptoScytlKeyStoreWithPBKDF} ready to be used.
     */
    public CryptoScytlKeyStoreWithPBKDF create() {
        return new CryptoScytlKeyStoreWithPBKDF(_keyStoreType,
            _keyStoreProvider,
            _cryptoPBKDFDeriverFactory.createPBKDFDeriver(),
            _symmetricCipherFactory, _configSecretKeyAlgorithmAndSpec);
    }

    /**
     * Loads a key store given the following parameters.
     * 
     * @param in
     *            the input stream from which the KeyStore is loaded.
     * @param password
     *            the password used to open the KeyStore.
     * @return the key store container as a {@link java.security.KeyStore}.
     * @throws IOException
     *             if there is an I/O or format problem with the key store data.
     * @throws GeneralCryptoLibException
     *             if there is problems while reading store.
     */
    public CryptoScytlKeyStoreWithPBKDF load(final InputStream in,
            final char[] password)
            throws IOException, GeneralCryptoLibException {
        return new CryptoScytlKeyStoreWithPBKDF(_keyStoreType,
            _keyStoreProvider,
            _cryptoPBKDFDeriverFactory.createPBKDFDeriver(), in, password,
            _symmetricCipherFactory, _configSecretKeyAlgorithmAndSpec);
    }

    /**
     * Loads a key store given the following parameters.
     * 
     * @param in
     *            the input stream from which the KeyStore is loaded. Excepted
     *            in JSON format
     * @param password
     *            the password used to open the KeyStore.
     * @return the key store container as a {@link java.security.KeyStore}.
     * @throws GeneralCryptoLibException
     *             if there is problems while reading store.
     */
    public CryptoScytlKeyStoreWithPBKDF loadFromJSON(final InputStream in,
            final char[] password) throws GeneralCryptoLibException {
        return new CryptoScytlKeyStoreWithPBKDF(true, _keyStoreType,
            _keyStoreProvider,
            _cryptoPBKDFDeriverFactory.createPBKDFDeriver(), in, password,
            _symmetricCipherFactory, _configSecretKeyAlgorithmAndSpec);
    }

    /**
     * Formats the given
     * {@link com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore}
     * to JSON format.
     * 
     * @param in
     *            the stream from the
     *            {@link com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore}
     *            will be read.
     * @return the given
     *         {@link com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore}
     *         in JSON format.
     * @throws IOException
     *             if there is an I/O or format problem with the key store data.
     * @throws GeneralCryptoLibException
     *             if there is problems while reading store.
     */
    public String formatKeyStoreToJSON(final InputStream in)
            throws IOException, GeneralCryptoLibException {

        boolean saltInitialized = false;

        byte[] salt =
            new byte[_cryptoPBKDFDeriverFactory.getSaltBitLength()
                / Byte.SIZE];

        Map<String, byte[]> secretKeys = new HashMap<>();
        Map<String, byte[]> elGamalPrivateKeys = new HashMap<>();
        byte[] keyStoreJS = {};
        try {
            ZipInputStream zin =
                new ZipInputStream(new BufferedInputStream(in));
            ZipEntry entry = zin.getNextEntry();
            while (entry != null) {
                String[] entryName = entry.getName().split("\\.");
                ENTRY_TYPE entryType =
                    ENTRY_TYPE.valueOf(entryName[0].toUpperCase());
                switch (entryType) {
                case SALT:
                    if (zin.read(salt) > 0) {
                        saltInitialized = true;
                    }
                    break;
                case SECRET:
                    secretKeys.put(entryName[1], readBytes(zin));
                    break;
                case ELGAMAL:
                    elGamalPrivateKeys.put(entryName[1], readBytes(zin));
                    break;
                case STORE:
                    keyStoreJS = readBytes(zin);
                    break;
                default:
                    throw new GeneralCryptoLibException(
                        format("Unknown entry type ''{0}''.", entryType));
                }
                entry = zin.getNextEntry();
            }

        } catch (ZipException e) {
            throw new GeneralCryptoLibException(
                "There was a problem reading the store." + e.getMessage(),
                e);

        }
        if (!saltInitialized) {
            throw new GeneralCryptoLibException(
                "There was a problem reading the store. salt was not initialized.");
        }
        return CryptoScytlKeyStoreWithPBKDFHelper.toJSON(salt, secretKeys,
            elGamalPrivateKeys, keyStoreJS);

    }
    
    private byte[] readBytes(final InputStream stream) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try {
            int length;
            byte[] buffer = new byte[1024 * 10];
            while ((length = stream.read(buffer)) > 0) {
                bytes.write(buffer, 0, length);
            }
        } finally {
            bytes.close();
        }
        return bytes.toByteArray();
    }
}
