package aalg.algebra;

/**
 * Solver of equation system that calculates cube M.
 * @author Ing. Petr Ležák
 *
 * @param <F> type of field item
 */
public class EquationsCubeM<F extends IElement> {

	private final IField<F> field;   // Underlying field
	private final int size;   // Number of parts of multimonoid
	private final Matrix<F> equationMatrix;   // Left side matrix of equation system
	private final IVector<F> equationRightSide;   // Right side of equation system
	private IVector<F> parameterValues;   // Selected parameter values
	private EquationSystemSolution<F> generalSolution;   // General (parametric) solution of equation system
	private IVector<F> particularSolution;   // Particular (with substituted parameters) solution of equation system
	private final int[][][] equationVariableIndices;   // Mapping Cube indices -> variable index; symmetry of cube M is taken into account in the mapping
	
	/**
	 * Creates equation solver.
	 * @param field underlying field
	 * @param size number of parts of multimonoid
	 */
	public EquationsCubeM(final IField<F> field, final int size) {
		this.field = field;
		this.size = size;
		
		final int equationCount = 2 * size * size * size;
		final int variableCount = size * size * (size + 1) / 2;
		
		equationMatrix = new Matrix<F>(field, equationCount, variableCount, Density.SPARSE);
		equationRightSide = new DenseVector<F>(field, equationCount);
		
		this.equationVariableIndices = new int[size][size][size];
		fillEquationVariableIndices();
	}

	/**
	 * Fills array equationVariableIndices.
	 */
	private void fillEquationVariableIndices() {
		int index = 0;
		
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size; j++) {
				for (int k = j; k < size; k++) {
					equationVariableIndices[i][j][k] = index;
					equationVariableIndices[i][k][j] = index;
					index++;
				}
			}
		}
		
		if (index != equationMatrix.getColumnCount())
			throw new RuntimeException("Wrong number of variables");
	}
	
	/**
	 * Adds given coefficient to one variable in one equation.
	 * Variable is calculated from cube element indices.
	 * @param equationIndex index of equation
	 * @param layerIndex index of cube layer
	 * @param rowIndex index of cube row
	 * @param columnIndex index of cube column
	 * @param coefficient added coefficient
	 */
	private void addCoefficientToEquation (final int equationIndex, final int layerIndex, final int rowIndex, final int columnIndex, final F coefficient) {
		final int variableIndex = equationVariableIndices[layerIndex][rowIndex][columnIndex];
		final F oldCoefficient = equationMatrix.get(equationIndex, variableIndex);
		final F newCoefficient = field.add(oldCoefficient, coefficient);
		
		equationMatrix.set(equationIndex, variableIndex, newCoefficient);
	}

	/**
	 * Creates equation system for given matrix L.
	 * @param matrixL matrix L
	 */
	public void createEquations(final Matrix<F> matrixL) {
		final F zero = field.getAdditiveIdentity();
		
		equationMatrix.fill(zero);
		equationRightSide.fill(zero);
		
		int equationIndex = 0;
		
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size; j++) {
				for (int l = 0; l < size; l++) {
					for (int m = 0; m < size; m++) {
						addCoefficientToEquation(equationIndex, i, l, m, matrixL.get(m, j));
						addCoefficientToEquation(equationIndex, i, j, m, field.additiveInverse(matrixL.get(m, l)));
					}
					
					equationIndex++;
				}
			}
		}
		
		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size; j++) {
				for (int k = 0; k < size; k++) {
					for (int m = 0; m < size; m++) {
						addCoefficientToEquation(equationIndex, m, j, k, matrixL.get(i, m));
						addCoefficientToEquation(equationIndex, i, j, m, field.additiveInverse(matrixL.get(m, k)));
					}
					
					equationIndex++;
				}
			}
		}
		
		if (equationIndex != equationMatrix.getRowCount())
			throw new RuntimeException("Wrong number of equations");
	}
	
	/**
	 * Calculates general solution of equation system.
	 * @return true if equations have solution, false if not
	 */
	public boolean solveEquations() {
		generalSolution = equationMatrix.solveEquationSystem(equationRightSide);
		
		if (generalSolution != null) {
			parameterValues = new DenseVector<F> (field, generalSolution.getParameterCount());
			return true;
		}
		else
			return false;
	}

	/**
	 * Returns number of equation solution parameters.
	 * @return number of equation solution parameters
	 */
	public int getParameterCount() {
		return generalSolution.getParameterCount();
	}

	/**
	 * Sets value of given parameter.
	 * @param index index of parameter
	 * @param value value of parameter
	 */
	public void setParameterValue(final int index, final F value) {
		parameterValues.set(index, value);
	}

	/**
	 * Transforms solution of equation system to cube M.
	 * @param cubeM solved cube M
	 */
	public void solutionToCubeM(final Cube<F> cubeM) {
		particularSolution = generalSolution.getSolution(parameterValues);

		for (int i = 0; i < size; i++) {
			for (int j = 0; j < size; j++) {
				for (int k = 0; k < size; k++) {
					final int index = equationVariableIndices[i][j][k];
					final F element = particularSolution.get(index);
					
					cubeM.set(i, j, k, element);
				}
			}
		}
	}

	/**
	 * Returns vector of parameters. This vector may be read to get parameter values
	 * or filled to set parameter values.
	 * @return vector of parameters
	 */
	public IVector<F> getParameters() {
		return parameterValues;
	}

}
