package aalg.algebra;

import java.math.BigInteger;
import java.util.Random;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Generator that generates multigroup over prime field.
 * @author Ing. Petr Ležák
 */
public class PrimeMultiGroupGenerator {

	private final RationalMultiMonoidGenerator rationalMonoidGenerator;   // Generator that generates rational multimonoid
	private final BigInteger minPrime;   // Minimal prime number
	private final Random rnd;   // Random number generator
	private final int partCount;   // Number of parts
	private MultiMonoid<RationalFieldElement> rationalMonoid;    // Generated rational multimonoid
	private BigInteger prime;   // Prime number - used as a modulus for underlying prime field
	private MultiGroup<PrimeFieldElement> primeGroup;   // Prime multigroup
	private MultiMonoidElement<PrimeFieldElement> groupGenerator;   // Full group generator
	private BigInteger groupOrder;   // Order of the group
	private PrimeField field;   // Underlying prime field
	
	// Factors of groupOrder without they multiplicity
	private final SortedSet<BigInteger> factors;
	
	private static final int PRIME_CERTANITY = 100;
	private static final int MAX_ITERATIONS = 1000;
	
	private static final BigInteger MIN_PRIME_DIVISOR = BigInteger.valueOf(4);
	
	private static final int[] PRIMES = {
		2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,
		43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97
	};
	
	/**
	 * Creates generator.
	 * @param rationalMonoidGenerator generator of rational multimonoid
	 * @param minPrime minimal prime
	 * @param rnd random number generator
	 */
	public PrimeMultiGroupGenerator(final RationalMultiMonoidGenerator rationalMonoidGenerator, final BigInteger minPrime, final Random rnd) {
		this.rationalMonoidGenerator = rationalMonoidGenerator;
		this.minPrime = minPrime;
		this.rnd = rnd;
		this.partCount = rationalMonoidGenerator.getPartCount();
		this.factors = new TreeSet<BigInteger>();
	}
	
	/**
	 * Generates random potential group generator.
	 */
	private void createRandomGenerator() {
		int size = rationalMonoid.getSize();
		final IVector<PrimeFieldElement> parts = new DenseVector<PrimeFieldElement>(field, size);
		
		for (int i = 0; i < size; i++) {
			final BigInteger num = BigInteger.valueOf(rnd.nextLong()).mod(field.getPrime());
			
			parts.set(i, new PrimeFieldElement(num));
		}
		
		groupGenerator = new MultiMonoidElement<PrimeFieldElement>(parts);
	}
	
	/**
	 * Factorizes groupOrder = prime^partCount - 1 into product of factors.
	 * Method tries to factor into prime numbers but may fail with this and
	 * calculate some compound factor.
	 * Multiplicity of factors is not calculated.
	 */
	private void factorizeGroupOrder() {
		factors.clear();
		
		switch (partCount) {
			case 1:
				// p - 1
				factorize (prime.subtract(BigInteger.ONE));
				break;
				
			case 2:
				// p^2 - 1 = (p-1)(p+1)
				factorize (prime.subtract(BigInteger.ONE));
				factorize (prime.add(BigInteger.ONE));
				break;
				
			case 3:
				// p^3 - 1 = (p-1)(p^2 + p + 1)
				factorize (prime.subtract(BigInteger.ONE));
				factorize (prime.multiply(prime).add(prime).add(BigInteger.ONE));
				break;
			
			case 4:
				// p^4 - 1 = (p-1)(p+1)(p^2 + 1)
				factorize (prime.subtract(BigInteger.ONE));
				factorize (prime.add(BigInteger.ONE));
				factorize (prime.multiply(prime).add(BigInteger.ONE));
				break;

			default:
				throw new RuntimeException("Unsupported part count: " + partCount);
		}
	}
	
	/**
	 * Factorizes given number by given factor - divides it by highest power of given factor. 
	 * @param number number to factor
	 * @param factor removed factor
	 * @return number / factor^n
	 */
	private BigInteger factorizeByFactor (final BigInteger number, final BigInteger factor) {
		BigInteger factorized = number;
		boolean finish = false;
		
		while (!finish) {
			final BigInteger[] divRem = factorized.divideAndRemainder(factor);
			
			if (divRem[1].equals(BigInteger.ZERO)) {
				factorized = divRem[0];
				factors.add(factor);
			}
			else
				finish = true;
		}
		
		return factorized;
	}
	
	/**
	 * Factorizes given number into product of factors.
	 * Method tries to factor into prime numbers but may fail with this and
	 * calculate some compound factor.
	 * @param number number to factor
	 */
	private void factorize(final BigInteger number) {
		BigInteger factorized = number;
		
		for (int prime: PRIMES) {
			factorized = factorizeByFactor(factorized, BigInteger.valueOf(prime));
		}
		
		if (!factorized.equals(BigInteger.ONE))
			factors.add(factorized);
	}
	
	/**
	 * Checks if all factors are prime numbers.
	 * @return true if all factors are prime numbers, false if not
	 */
	private boolean areFactorsPrimes() {
		for (BigInteger factor: factors) {
			if (!factor.isProbablePrime(PRIME_CERTANITY))
				return false;
		}
			
		return true;
	}
	
	/**
	 * Checks if groupGenerator generates all group elements - has order groupOrder.
	 * @return true if groupGenerator has order groupOrder, false if not
	 */
	private boolean isGenerator() {
		for (BigInteger factor: factors) {
			final BigInteger tst = groupOrder.divide(factor);
			final MultiMonoidElement<PrimeFieldElement> val = primeGroup.repeatOperation(groupGenerator, tst);
			
			if (val.equals(primeGroup.getIdentity()))
				return false;
		}
			
		return true;
		
	}
	
	/**
	 * Generates multigroup.
	 * @return domain parameters
	 */
	public DlDomainParameters<MultiMonoidElement<PrimeFieldElement>> generate() {
		prime = minPrime;
		
		boolean found = false;
		primeGroup = null;
		
		BigInteger largestFactor = null;
		
		while (!found) {
			rationalMonoid = rationalMonoidGenerator.generateRationalMonoid();
			
			int iterations = 0;
			
			while (iterations < MAX_ITERATIONS && !found) {
				prime = prime.nextProbablePrime();
				
				final BigInteger monoidOrder = prime.pow(partCount);
				groupOrder = monoidOrder.subtract(BigInteger.ONE);
				
				factorizeGroupOrder();
				
				largestFactor = factors.last();

				if (largestFactor.compareTo(prime.divide(MIN_PRIME_DIVISOR)) >= 0 && areFactorsPrimes()) {
					field = new PrimeField(prime);
					
					final RationalToPrimeFieldTransformation transformation = new RationalToPrimeFieldTransformation(field);
					
					try {
						final MultiMonoid<PrimeFieldElement> primeMonoid = rationalMonoid.transform(transformation);
						
						createRandomGenerator();
						
						if (primeMonoid.getInverse(groupGenerator) != null) {
							final MultiMonoidElement<PrimeFieldElement> powSize = primeMonoid.repeatOperation(groupGenerator, groupOrder);
							
							if (powSize.equals(primeMonoid.getIdentity())) {
								primeGroup = new MultiGroup<PrimeFieldElement> (primeMonoid, groupOrder);
								
								if (isGenerator()) {
									found = true;
								}
							}
						}
					}
					catch (TransformationException ex) {
					}
				}
				
				iterations++;
			}
		}
		
		final SubGroupDefinition<MultiMonoidElement<PrimeFieldElement>> fullSubGroup = new SubGroupDefinition<MultiMonoidElement<PrimeFieldElement>> (
			primeGroup, groupGenerator, groupOrder
		);

		final SubGroupDefinition<MultiMonoidElement<PrimeFieldElement>> primeSubGroup = new SubGroupDefinition<MultiMonoidElement<PrimeFieldElement>> (
			primeGroup, primeGroup.repeatOperation(groupGenerator, groupOrder.divide(largestFactor)), largestFactor
		);

		return new DlDomainParameters<MultiMonoidElement<PrimeFieldElement>>(
			primeGroup, fullSubGroup, primeSubGroup
		);

	}

}
