package aalg.algebra;

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

/**
 * Generator that generates multimonoids over rational field.
 * @author Ing. Petr Ležák
 */
public class RationalMultiMonoidGenerator {
	
	private static final int MAX_ITERATIONS = 1000;

	private final int partCount;   // Number of multimonoid parts
	private final RationalField field;   // Underlying field
	private final RationalFieldElement[] elements;   // Array of elements that are used for selection of random element
	private final Random rnd;   // Random number generator
	
	// Multimonoid coefficients
	private final VectorImpl<RationalFieldElement> vectorE;
	private final VectorImpl<RationalFieldElement> vectorK;
	private final Matrix<RationalFieldElement> matrixL;
	private final Cube<RationalFieldElement> cubeM;
	
	// Solvers of equation systems
	private final EquationsCubeM<RationalFieldElement> cubeMEquations;
	private final EquationsVectorE<RationalFieldElement> solverVectorE;
	
	private final MultiMonoidValidator<RationalFieldElement> validator;   // Validator of the multimonoid
	private MultiMonoid<RationalFieldElement> rationalMonoid;   // Generated multimonoid
	
	// Statistics
	private long iterations;
	private long unsolvableEquationsMCount;
	private long invalidMonoidCount;
	private long unsolvableEquationsECount;
	private long singularMatrixLCount;
	
	/**
	 * Creates multimonoid generator.
	 * @param partCount number of parts
	 * @param minVal minimal random element (inclusive)
	 * @param maxVal maximal random element (inclusive)
	 * @param rnd random number generator
	 */
	public RationalMultiMonoidGenerator (final int partCount, final int minVal, final int maxVal, final Random rnd) {
		this.partCount = partCount;
		this.field = RationalField.INSTANCE;
		this.rnd = rnd;
		
		this.vectorE = new DenseVector<RationalFieldElement>(field, partCount);
		this.vectorK = new DenseVector<RationalFieldElement>(field, partCount);
		this.matrixL = new Matrix<RationalFieldElement>(field, partCount, partCount);
		this.cubeM = new Cube<RationalFieldElement>(field, partCount, partCount, partCount);
		
		final int elementCount = maxVal - minVal + 1;
		this.elements = new RationalFieldElement[elementCount];
		
		for (int i = 0; i < elementCount; i++)
			this.elements[i] = new RationalFieldElement(BigInteger.valueOf(minVal + i), BigInteger.ONE);
		
		this.validator = new MultiMonoidValidator<RationalFieldElement> (field, partCount, vectorE, vectorK, matrixL, cubeM);
		
		this.cubeMEquations = new EquationsCubeM<RationalFieldElement>(field, partCount);
		this.solverVectorE = new EquationsVectorE<RationalFieldElement>(field, partCount);
	}
	
	/**
	 * Calculates vectorK from vectorE and matrixL.
	 */
	private void calculateK() {
		for (int i = 0; i < partCount; i++) {
			RationalFieldElement sum = field.getAdditiveIdentity();
			
			for (int k = 0; k < partCount; k++) {
				sum = field.add(sum,  field.multiply(matrixL.get (i, k), vectorE.get(k)));
			}
			
			vectorK.set(i, field.additiveInverse(sum));
		}
	}
	
	/**
	 * Calculates vectorE from matrixL and cubeM.
	 */
	private void selectE() {
		solverVectorE.createEquations(matrixL, cubeM);
		
		if (solverVectorE.solveEquations()) {
			selectRandomVector(solverVectorE.getParameters());
			solverVectorE.solutionToVectorE(vectorE);
			
			calculateK();
			
			if (validator.validateMonoid()) {
				final MultiMonoidElement<RationalFieldElement> identity = new MultiMonoidElement<RationalFieldElement> (vectorE);
				
				rationalMonoid = new MultiMonoid<RationalFieldElement>(field, partCount, identity, vectorK, matrixL, cubeM);
			}
			else
				invalidMonoidCount++;
		}
		else
			unsolvableEquationsECount++;
	}
	
	/**
	 * Selects random element from array elements.
	 * @return random element
	 */
	private RationalFieldElement selectRandomElement() {
		final int num = rnd.nextInt(elements.length);
		
		return elements[num];
	}
	
	/**
	 * Fills given vector with random elements.
	 * @param vector filled vector
	 */
	private void selectRandomVector(final IVector<RationalFieldElement> vector) {
		for (int i = 0; i < vector.getSize(); i++)
			vector.set(i, selectRandomElement());
	}

	/**
	 * Fills given matrix with random elements.
	 * @param matrix filled matrix
	 */
	private void selectRandomMatrix(final Matrix<RationalFieldElement> matrix) {
		for (int i = 0; i < matrix.getRowCount(); i++)
			selectRandomVector(matrix.getRowVector(i));
	}
	
	/**
	 * Selects random regular matrixL and calls selectM.
	 */
	private void selectL() {
		for (int i = 0; i < MAX_ITERATIONS && rationalMonoid == null; i++) {
			selectRandomMatrix (matrixL);
			
			if (!matrixL.isSingular()) {
				selectM();
			}
			else
				singularMatrixLCount++;
			
			iterations++;
		}
	}

	/**
	 * Solves equations for matrixM and calls selectE.
	 */
	private void selectM() {
		cubeMEquations.createEquations(matrixL);
		
		if (cubeMEquations.solveEquations()) {
			selectRandomVector(cubeMEquations.getParameters());
			
			cubeMEquations.solutionToCubeM(cubeM);
			
			// Just a check - this equations must be fulfilled by the solver.
			if (!validator.validateSymmetryM())
				throw new RuntimeException("Wrong symmetry M");

			if (!validator.validateEquationML1())
				throw new RuntimeException("Wrong equation ML1");

			if (!validator.validateEquationML2())
				throw new RuntimeException("Wrong equation ML2");

			// Continue with selection
			selectE();
		}
		else
			unsolvableEquationsMCount++;
	}

	/**
	 * Clears statistics.
	 */
	public void clearStatistics() {
		iterations = 0;
		unsolvableEquationsMCount = 0;
		invalidMonoidCount = 0;
		unsolvableEquationsECount = 0;
		singularMatrixLCount = 0;
	}

	/**
	 * Prints statistics to System.out.
	 */
	public void printStatistics() {
		System.out.println ("Iterations: " + iterations);
		System.out.println ("SingularMatrixLCount: " + singularMatrixLCount);
		System.out.println ("UnsolvableEquationsMCount: " + unsolvableEquationsMCount);
		System.out.println ("UnsolvableEquationsECount: " + unsolvableEquationsECount);
		System.out.println ("InvalidMonoidCount: " + invalidMonoidCount);
	}
	
	/**
	 * Generates rational multimonoid and returns it.
	 * @return
	 */
	public MultiMonoid<RationalFieldElement> generateRationalMonoid() {
		rationalMonoid = null;
		selectL();
		
		return rationalMonoid;
	}

	/**
	 * Returns number of multimonoid parts.
	 * @return number of multimonoid parts
	 */
	public int getPartCount() {
		return partCount;
	}

}
