/*
 * 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 static java.text.MessageFormat.format;

import java.math.BigInteger;

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

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

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

    private final SmallPrimesProvider primesProvider;

    /**
     * Constructor.
     * 
     * @param primesProvider
     */
    public FFCPrimeFactoryImpl(SmallPrimesProvider primesProvider) {
        this.primesProvider = primesProvider;
    }

    private static long getSmallCandidate(int length,
            PseudoRandom random) {
        long hash1 = random.next().longValue();
        long hash2 = random.next().longValue();
        long high = 1L << (length - 1);
        long mask = high - 1;
        return high | ((hash1 ^ hash2) & mask) | 1;
    }

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

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

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

    private static void validateLength(int length)
            throws GeneralCryptoLibException {
        if (length < 2) {
            throw new GeneralCryptoLibException(
                format("Invalid length ''{0}''.", length));
        }
    }

    @Override
    public FFCPrime newFFCPrime(int length, PseudoRandom random)
            throws GeneralCryptoLibException {
        validateLength(length);
        if (length <= 32) {
            return newSmallFFCPrime(length, random);
        } else {
            return newBigFFCPrime(length, random);
        }
    }

    private boolean isPrime(long c) {
        // c is less than 2^32, thus sqrt(c) < 2^16.
        int limit = (int) Math.ceil(Math.sqrt(c));
        for (int p : primesProvider.getSmallPrimes()) {
            if (p > limit) {
                break;
            }
            if (c % p == 0) {
                return false;
            }
        }
        return true;
    }

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

    private FFCPrime newSmallFFCPrime(int length, PseudoRandom random)
            throws GeneralCryptoLibException {
        int genCounter = 0;
        long c;
        do {
            if (genCounter > length * 4) {
                throw new GeneralCryptoLibException(
                    "Failed to create prime.");
            }
            c = getSmallCandidate(length, random);
            genCounter++;
        } while (!isPrime(c));
        return new FFCPrime(BigInteger.valueOf(c), random.seed(),
            genCounter);
    }
}
