/*
 * 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.primitives.ffc;

import static java.text.MessageFormat.format;

import java.math.BigInteger;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.ffc.cryptoapi.FFCDomainParameters;
import com.scytl.cryptolib.ffc.cryptoapi.FFCEngine;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrimes;
import com.scytl.cryptolib.primitives.messagedigest.factory.MessageDigestFactory;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomBytes;

/**
 * Implementation of {@link FFCEngine}.
 */
public final class FFCEngineImpl implements FFCEngine {
    private final PseudoRandomFactory randomFactory;

    private final FFCPrimesFactory primesFactory;

    private final GFactory gFactory;

    /**
     * Constructor. For internal use only.
     * 
     * @param randomFactory
     * @param primesFactory
     * @param gFactory
     */
    FFCEngineImpl(PseudoRandomFactory randomFactory,
            FFCPrimesFactory primesFactory, GFactory gFactory) {
        this.randomFactory = randomFactory;
        this.primesFactory = primesFactory;
        this.gFactory = gFactory;
    }

    /**
     * Creates a new instance.
     * 
     * @param random
     *            the random bytes generator
     * @param digestFactory
     *            the digest factory
     * @return the instance.
     */
    public static FFCEngineImpl newInstance(CryptoAPIRandomBytes random,
            MessageDigestFactory digestFactory) {
		PseudoRandomFactory randomFactory = newPseudoRandomFactory(digestFactory, random);
		FFCPrimesFactory primesFactory = newFFCPrimesFactory();
		GFactory gFactory = newGFactory(digestFactory);
        return new FFCEngineImpl(randomFactory, primesFactory, gFactory);
    }

    private static SmallPrimesProvider getSmallPrimesProvider() {
        return SmallPrimesProviderImpl.getInstance();
    }

    private static FFCPrimeFactory newFFCPrimeFactory() {
        SmallPrimesProvider primesProvider = getSmallPrimesProvider();
        return new FFCPrimeFactoryImpl(primesProvider);
    }

    private static FFCPrimesFactory newFFCPrimesFactory() {
        FFCPrimeFactory primeFactory = newFFCPrimeFactory();
        return new FFCPrimesFactoryImpl(primeFactory);
    }

    private static GFactory newGFactory(
            MessageDigestFactory digestFactory) {
        return new GFactoryImpl(digestFactory);
    }

    private static PseudoRandomFactory newPseudoRandomFactory(
            MessageDigestFactory digestFactory,
            CryptoAPIRandomBytes random) {
        return new PseudoRandomFactoryImpl(digestFactory, random);
    }

    private static void validateIndex(int index)
            throws GeneralCryptoLibException {
        if (index != (index & 0xFF)) {
            throw new GeneralCryptoLibException(
                format("Invalid index ''{0}''", index));
        }
    }

    private static void validatePrimeLengths(int l, int n)
            throws GeneralCryptoLibException {
        switch (l) {
        case 1024:
            if (n != 160) {
                throw new GeneralCryptoLibException(format(
                    "Invalid prime lengths ''{0}'' and ''{1}''.", l, n));
            }
            break;
        case 2048:
            if (n != 224 && n != 256) {
                throw new GeneralCryptoLibException(format(
                    "Invalid prime lengths ''{0}'' and ''{1}''.", l, n));
            }
            break;
        case 3072:
            if (n != 256) {
                throw new GeneralCryptoLibException(format(
                    "Invalid prime lengths ''{0}'' and ''{1}''.", l, n));
            }
            break;
        default:
            throw new GeneralCryptoLibException(format(
                "Invalid prime lengths ''{0}'' and ''{1}''.", l, n));
        }
    }

    @Override
    public FFCDomainParameters generateDomainParameters(int l, int n)
            throws GeneralCryptoLibException {
        validatePrimeLengths(l, n);
        PseudoRandom random = randomFactory.newPseudoRandom(n);
        FFCPrimes primes = primesFactory.newFFCPrimes(l, n, random);
        BigInteger g = gFactory.newG(primes, 1);
        return new FFCDomainParameters(primes, g);
    }

    @Override
    public BigInteger generateG(FFCPrimes primes, int index)
            throws GeneralCryptoLibException {
        Validate.notNull(primes, "Primes");
        validateIndex(index);
        return gFactory.newG(primes, index);
    }

    @Override
    public FFCPrimes generatePrimes(int l, int n)
            throws GeneralCryptoLibException {
        validatePrimeLengths(l, n);
        PseudoRandom random = randomFactory.newPseudoRandom(n);
        return primesFactory.newFFCPrimes(l, n, random);
    }

    @Override
    public void validateDomainParameters(FFCDomainParameters parameters)
            throws GeneralCryptoLibException {
        Validate.notNull(parameters, "Domain parameters");
        validatePrimes(parameters.primes());
        validateG(parameters.g(), parameters.primes(), 1);
    }

    @Override
    public void validateG(BigInteger g, FFCPrimes primes, int index)
            throws GeneralCryptoLibException {
        Validate.notNull(g, "Generator");
        Validate.notNull(primes, "Primes");
        validateIndex(index);
        if (!g.equals(gFactory.newG(primes, index))) {
            throw new GeneralCryptoLibException(
                format("Invalid generator ''{0}''.", g));
        }
    }

    @Override
    public void validatePrimes(FFCPrimes primes)
            throws GeneralCryptoLibException {
        Validate.notNull(primes, "Primes");
        int l = primes.p().value().bitLength();
        int n = primes.q().value().bitLength();
        BigInteger firstSeed = primes.firstSeed();
        validatePrimeLengths(l, n);
        PseudoRandom random = randomFactory.newPseudoRandom(firstSeed);
        if (!primes.equals(primesFactory.newFFCPrimes(l, n, random))) {
            throw new GeneralCryptoLibException(
                format("Invalid primes ''{0}''", primes));
        }
    }
}
