package aalg.algebra;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;

/**
 * Multimonoid (or multi component monoid) is monoid which consist of several parts
 * (field elements).
 * @author Ing. Petr Ležák
 *
 * @param <F> type of underlying field
 */
public class MultiMonoid<F extends IElement> extends MonoidImpl<MultiMonoidElement<F>> {

	private final IField<F> field;
	private final int size;
	private final MultiMonoidElement<F> identity;
	private final IVector<F> vectorK;
	private final Matrix<F> matrixL;
	private final Cube<F> cubeM;
	
	/**
	 * Creates multimonoid above given field.
	 * @param field base field
	 * @param size number of components
	 * @param identity identity element
	 * @param vectorK vector with constant coefficients
	 * @param matrixL matrix with linear coefficients
	 * @param cubeM cube with cross coefficients
	 */
	public MultiMonoid(final IField<F> field, final int size, final MultiMonoidElement<F> identity, final IVector<F> vectorK, final Matrix<F> matrixL, final Cube<F> cubeM) {
		this.field = field;
		this.size = size;
		this.identity = identity;
		this.vectorK = vectorK.copy();
		this.matrixL = matrixL.copy();
		this.cubeM = cubeM.copy();
				
		final MultiMonoidValidator<F> validator = new MultiMonoidValidator<F> (field, size, identity.getParts(), vectorK, matrixL, cubeM);
		
		if (!validator.validateMonoid())
			throw new RuntimeException("Given parameters does not make monoid");
	}
	
	/**
	 * Returns base field.
	 * @return underlying field
	 */
	public IField<F> getBaseField() {
		return field;
	}
	
	/**
	 * Returns number of components.
	 * @return number of components
	 */
	public int getSize() {
		return size;
	}
	
	@Override
	public MultiMonoidElement<F> operation(final MultiMonoidElement<F> a, final MultiMonoidElement<F> b) {
		final VectorImpl<F> result = new DenseVector<F>(field, size);
		
		for (int i = 0; i < size; i++) {
			F sum = vectorK.get(i);
			
			for (int j = 0; j < size; j++) {
				final F elemA = a.getPartAt(j);
				final F elemB = b.getPartAt(j);
				final F coeff = matrixL.get(i, j);
				
				sum = field.add(sum, field.multiply(coeff, elemA));
				sum = field.add(sum, field.multiply(coeff, elemB));
			}

			for (int j = 0; j < size; j++) {
				final F elemA = a.getPartAt(j);
				
				for (int k = 0; k < size; k++) {
					final F elemB = b.getPartAt(k);
					final F coeff = cubeM.get(i, j, k);
				
					sum = field.add(sum, field.multiply(coeff, field.multiply (elemA, elemB)));
				}
			}

			result.set(i, sum);
		}
		
		return new MultiMonoidElement<F>(result);
	}

	@Override
	public MultiMonoidElement<F> getIdentity() {
		return identity;
	}

	@Override
	public MultiMonoidElement<F> getInverse(final MultiMonoidElement<F> x) {
		final Matrix<F> matrixQ = new Matrix<F>(field, size, size);
		
		for (int i = 0; i < size; i++) {
			for (int k = 0; k < size; k++) {
				F sum = matrixL.get(i, k);
			
				for (int j = 0; j < size; j++) {
					sum = field.add(sum, field.multiply(cubeM.get(i, j, k), x.getPartAt(j)));
				}
				
				matrixQ.set(i, k, sum);
			}
		}
		
		final IVector<F> identityVector = identity.getParts();
		final IVector<F> xParts = x.getParts();
		final IVector<F> rightSide = identityVector.subtract (vectorK).subtract(matrixL.multiply (xParts));
		final EquationSystemSolution<F> solution = matrixQ.solveEquationSystem(rightSide);
		
		if (solution != null && solution.getParameterCount() == 0) {
			final IVector<F> result = solution.getSolution(new DenseVector<F>(field, 0));

			return new MultiMonoidElement<F>(result);
		}
		else
			return null;
	}

	@Override
	public MultiMonoidElement<F> readElement(final InputStream stream) throws IOException {
		final IVector<F> parts = new DenseVector<F>(field, size);
		
		for (int i = 0; i < size; i++)
			parts.set(i, field.readElement(stream));
		
		return new MultiMonoidElement<F>(parts);
	}

	@Override
	public void writeElement(final MultiMonoidElement<F> element, final OutputStream stream) throws IOException {
		for (int i = 0; i < size; i++)
			field.writeElement(element.getPartAt(i), stream);
	}

	@Override
	public boolean containsElement (final MultiMonoidElement<F> element) {
		if (element == null || element.getPartCount() != size)
			return false;
		
		for (int i = 0; i < size; i++) {
			final F component = element.getPartAt(i);
			
			if (!field.containsElement(component))
				return false;
		}
		
		return getInverse(element) != null;
	}

	@Override
	public Iterable<MultiMonoidElement<F>> getElements() {
		return new MultiMonoidIterable<F>(this);
	}
	
	@Override
	public BigInteger getOrder() {
		return field.getOrder().pow(size);
	}

	@Override
	public MultiMonoidElement<F> elementFromString(final String string) {
		final String trimmed = string.trim();
		
		if (!trimmed.startsWith("[") || trimmed.endsWith("]"))
			throw new RuntimeException("Missing brackets");
		
		final String tokens[] = trimmed.substring(1, string.length() - 1).split(",");
		
		if (tokens.length != size)
			throw new RuntimeException("Wrong number of elements");
		
		final IVector<F> parts = new DenseVector<F>(field, size);
		
		for (int i = 0; i < size; i++)
			parts.set(i, field.elementFromString(tokens[i]));
			
		return new MultiMonoidElement<F>(parts);
	}
	
	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		
		builder.append("M = ");
		builder.append(cubeM.toString());

		builder.append(", L = ");
		builder.append(matrixL.toString());

		builder.append(", K = ");
		builder.append(vectorK.toString());

		builder.append(", E = ");
		builder.append(identity.toString());

		return builder.toString();
	}
	
	public <T extends IElement> MultiMonoid<T> transform (final IFieldTransformation<F, T> transformation) throws TransformationException {
		final MultiMonoid<T> result = new MultiMonoid<T> (
			transformation.getOutputField(),
			size,
			identity.transform (transformation),
			vectorK.transform (transformation),
			matrixL.transform (transformation),
			cubeM.transform (transformation)
		);
		
		return result;
	}

	@Override
	public BigInteger getElementIndex(final MultiMonoidElement<F> element) {
		final BigInteger partOrder = field.getOrder();
		BigInteger index = BigInteger.ZERO;
		
		for (int i = 0; i < element.getPartCount(); i++) {
			final F part = element.getPartAt(i);
			index = index.multiply(partOrder).add (field.getElementIndex(part));
		}
		
		return index;
	}
}
