/*
 * 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.products.ov.channel.cc.commons.util;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.products.ov.channel.cc.commons.keymanagement.FingerprintGeneratorException;

/**
 * Allows the generation of a fingerprint (a hash) for various structures.
 * <P>
 * Uses the hash algorithm that is configured within the CryptoLib properties
 * file. The generated hash is encoded in Base64. Supports the following
 * structures as input:
 * <ul>
 * <li>{@link com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey}.</li>
 * <li>byte array.</li>
 * <li>{@link java.lang.String}.</li>
 * </ul>
 * <P>
 * It also supports any List of entities that can be mapped to a byte array
 */
public class FingerprintGenerator {

    private PrimitivesServiceAPI _primitiveService;

    public FingerprintGenerator(final PrimitivesServiceAPI primitiveService) {
        validateInput(primitiveService);
        _primitiveService = primitiveService;
    }

    /**
     * Generate a fingerprint of the received
     * {@link com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey}.
     *
     * @param publicKey
     *            the public key for which to generate a fingerprint.
     * @return The fingerprint of the received public key.
     * @throws FingerprintGeneratorException
     */
    public String generate(final ElGamalPublicKey publicKey) throws FingerprintGeneratorException {

        validateInput(publicKey);

        return generate(getElGamalPublicKeyAsElementsList(publicKey), BigInteger::toByteArray);

    }

    /**
     * Generate a fingerprint of the received list given a mapper to bytes.
     * 
     * @param elements
     * @throws FingerprintGeneratorException
     * @returnhe fingerprint of the received list.
     */
    public <T> String generate(final List<T> elements, Function<T, byte[]> mapper)
            throws FingerprintGeneratorException {

        validateInput(elements);

        MessageDigest messageDigest = _primitiveService.getRawMessageDigest();
        messageDigest.reset();
        for (T element : elements) {
            messageDigest.update(mapper.apply(element));
        }
        return Base64.getEncoder().encodeToString(messageDigest.digest());
    }

    /**
     * Generate a fingerprint of the received String.
     *
     * @param data
     *            the string for which to generate a fingerprint.
     * @return The fingerprint of the received byte array.
     * @throws FingerprintGeneratorException
     */
    public String generate(final String data) throws FingerprintGeneratorException {

        validateInput(data);

        return generate(data.getBytes(StandardCharsets.UTF_8));

    }

    /**
     * Generate a fingerprint of the received byte array.
     *
     * @param bytes
     *            the byte array for which to generate a fingerprint.
     * @return The fingerprint of the received byte array.
     * @throws FingerprintGeneratorException
     */
    public String generate(final byte[] bytes) throws FingerprintGeneratorException {

        validateInput(bytes);

        byte[] hashOfBytes;
        try {
            hashOfBytes = _primitiveService.getHash(bytes);
        } catch (GeneralCryptoLibException e) {
            throw new FingerprintGeneratorException("Error while trying to generate hash of the received bytes", e);
        }
        return Base64.getEncoder().encodeToString(hashOfBytes);
    }

    private List<BigInteger> getElGamalPublicKeyAsElementsList(final ElGamalPublicKey publicKey) {

        List<ZpGroupElement> publicKeys = publicKey.getKeys();

        return publicKeys.stream().map(keyElement -> getZpGroupElementAsBigIntegerList(keyElement))
            .flatMap(List::stream).collect(Collectors.toList());

    }

    private List<BigInteger> getZpGroupElementAsBigIntegerList(ZpGroupElement element) {
        List<BigInteger> resultList = new ArrayList<BigInteger>(3);
        resultList.add(element.getP());
        resultList.add(element.getQ());
        resultList.add(element.getValue());
        return resultList;
    }

    private void validateInput(final String data) {

        if (data == null) {
            throw new IllegalArgumentException("The received string is null");
        }
    }

    private <T> void validateInput(final List<T> data) {

        if (data == null) {
            throw new IllegalArgumentException("The received list is null");
        }
    }

    private void validateInput(final byte[] bytes) {

        if ((bytes == null) || (bytes.length < 1)) {
            throw new IllegalArgumentException("The received byte array must be an initialized non-empty array");
        }
    }

    private void validateInput(final ElGamalPublicKey publicKey) {

        if (publicKey == null) {
            throw new IllegalArgumentException("The received ElGamal publicKey is null");
        }
    }

    private void validateInput(final PrimitivesServiceAPI primitiveService) {

        if (primitiveService == null) {
            throw new IllegalArgumentException("The received primitives service is null");
        }
    }

}
