/*
 * 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 com.scytl.math.BigIntegers.modPow;
import static com.scytl.math.BigIntegers.multiply;

import java.math.BigInteger;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrime;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrimes;

/**
 * Implementation of {@link FFCPrimesFactory}.
 */
class FFCPrimesFactoryImpl implements FFCPrimesFactory {
    private static final BigInteger TWO = BigInteger.valueOf(2);

    private static final BigInteger THREE = BigInteger.valueOf(3);

    private final FFCPrimeFactory primeFactory;

    /**
     * Constructor.
     * 
     * @param primeFactory
     */
    public FFCPrimesFactoryImpl(FFCPrimeFactory primeFactory) {
        this.primeFactory = primeFactory;
    }

    private static boolean isPrime(BigInteger p, BigInteger p0,
            BigInteger z) {
        return BigInteger.ONE.equals(p.gcd(z.subtract(BigInteger.ONE)))
            && BigInteger.ONE.equals(modPow(z, p0, p));
    }

    private static BigInteger newT(int l, PseudoRandom random,
            BigInteger twoQP0) {
        BigInteger high = BigInteger.ONE.shiftLeft(l - 1);
        BigInteger mask = high.subtract(BigInteger.ONE);
        BigInteger x = high.or(random.next(l).and(mask));
        return x.add(twoQP0).subtract(BigInteger.ONE).divide(twoQP0);
    }

    private static BigInteger newZ(int l, PseudoRandom random,
            BigInteger q, BigInteger t, BigInteger p) {
        BigInteger a = random.next(l);
        a = TWO.add(a.mod(p.subtract(THREE)));
        return modPow(a, multiply(multiply(t, q), TWO), p);
    }

    @Override
    public FFCPrimes newFFCPrimes(int l, int n, PseudoRandom random)
            throws GeneralCryptoLibException {
        BigInteger firstSeed = random.seed();
        FFCPrime q = primeFactory.newFFCPrime(n, random);
        FFCPrime p = newP(l, random, q);
        return new FFCPrimes(p, q, firstSeed);
    }

    private FFCPrime newP(int l, PseudoRandom random, FFCPrime q)
            throws GeneralCryptoLibException {
        BigInteger p;
        int genCounter = 0;
        FFCPrime prime = primeFactory.newFFCPrime((l + 1) / 2 + 1, random);
        BigInteger p0 = prime.value();
        BigInteger twoQP0 = multiply(multiply(p0, q.value()), TWO);
        BigInteger t = newT(l, random, twoQP0);
        while (true) {
            if (genCounter > l * 4) {
                throw new GeneralCryptoLibException(
                    "Failed to create prime.");
            }
            p = multiply(t, twoQP0).add(BigInteger.ONE);
            if (p.bitLength() > l) {
                t = BigInteger.ONE.shiftLeft(l - 1).add(twoQP0)
                    .subtract(BigInteger.ONE).divide(twoQP0);
                p = multiply(t, twoQP0).add(BigInteger.ONE);
            }
            genCounter++;
            BigInteger z = newZ(l, random, q.value(), t, p);
            if (isPrime(p, p0, z)) {
                break;
            }
            t = t.add(BigInteger.ONE);
        }
        genCounter += prime.genCounter();
        return new FFCPrime(p, random.seed(), genCounter);
    }
}
