package aalg.algebra;

import java.util.ArrayList;
import java.util.List;

/**
 * Matrix with elements from given field.
 * Matrix has constant size.
 * @author Ing. Petr Ležák
 *
 * @param <F> type of elements
 */
public class Matrix<F extends IElement> {
	
	private final IField<F> field;   // Underlying field
	private final int rowCount;   // Number of rows of the matrix
	private final int columnCount;   // Number of columns of the matrix
	private final Density density;   // Density of the row vectors
	private final List<IVector<F>> elements;   // Row vectors

	/**
	 * Creates zero matrix over given field.
	 * @param field underlying field
	 * @param rowCount number of rows
	 * @param columnCount number of columns
	 * @param density matrix density
	 */
	public Matrix(final IField<F> field, final int rowCount, final int columnCount, final Density density) {
		this.field = field;
		this.rowCount = rowCount;
		this.columnCount = columnCount;
		this.density = density;
		this.elements = new ArrayList<IVector<F>>(rowCount);
		
		for (int i = 0; i < rowCount; i++) {
			this.elements.add(createVector(field, columnCount));
		}
	}

	/**
	 * Creates zero dense matrix over given field.
	 * @param field underlying field
	 * @param rowCount number of rows
	 * @param columnCount number of columns
	 */
	public Matrix(final IField<F> field, final int rowCount, final int columnCount) {
		this (field, rowCount, columnCount, Density.DENSE);
	}

	/**
	 * Sets all elements of the matrix to given value
	 * @param element elements value
	 */
	public void fill (final F element) {
		for (IVector<F> row: elements)
			row.fill(element);
	}
	
	/**
	 * Returns number of rows.
	 * @return number of rows
	 */
	public int getRowCount() {
		return rowCount;
	}

	/**
	 * Returns number of columns.
	 * @return number of columns
	 */
	public int getColumnCount() {
		return columnCount;
	}
	
	/**
	 * Returns element on given coordinates.
	 * @param rowIndex row index
	 * @param columnIndex column index
	 * @return element
	 */
	public F get (final int rowIndex, final int columnIndex) {
		return elements.get(rowIndex).get(columnIndex);
	}
	
	/**
	 * Sets element on given coordinates.
	 * @param rowIndex row index
	 * @param columnIndex column index
	 * @param element new value of element
	 */
	public void set (final int rowIndex, final int columnIndex, final F element) {
		elements.get(rowIndex).set(columnIndex, element);
	}

	/**
	 * Multiplies this matrix by given vector.
	 * @param v vector
	 * @return this * vector
	 */
	public IVector<F> multiply (final IVector<F> v) {
		if (columnCount != v.getSize())
			throw new RuntimeException("Vector and matrix size does not match");

		final IVector<F> result = new DenseVector<F>(field, rowCount);
		
		for (int i = 0; i < rowCount; i++) {
			F sum = field.getAdditiveIdentity();
			
			for (int j = 0; j < columnCount; j++) {
				sum = field.add(sum, field.multiply(this.get (i, j), v.get(j)));
			}
			
			result.set(i, sum);
		}
		
		return result;
	}
	
	/**
	 * Swaps rows with given indices. 
	 * @param index1 first row index
	 * @param index2 second row index
	 */
	public void swapRows (final int index1, final int index2) {
		if (index1 != index2) {
			final IVector<F> row1 = elements.get(index1);
			final IVector<F> row2 = elements.get(index2);
			
			elements.set(index1, row2);
			elements.set(index2, row1);
		}
	}
	
	/**
	 * Finds row win nonzero element on given column
	 * @param column column with searched nonzero element
	 * @param minRowIndex first searched row index (inclusive)
	 * @param maxRowIndex last searched row index (exclusive)
	 * @return index of row with nonzero element on given column; -1 if it does not exists
	 */
	private int getNonzeroRow (final int column, final int minRowIndex, final int maxRowIndex) {
		final F zero = field.getAdditiveIdentity();
		
		for (int i = minRowIndex; i < maxRowIndex; i++) {
			if (!get (i, column).equals(zero))
				return i;
		}
		
		return -1;
	}
	
	/**
	 * Converts matrix to the extended triangular form and updates right side.
	 * Extended triangular means that first nonzero column c_r on row r is:
	 *   c_0 = 0
	 *   c_(r+1) - c_r - 1 = number of parameters on columns c_r, c_r + 1, ..., c_(r+1) - 1
	 *
	 * 
	 * @param rightSide right side of the equation system
	 * @return sorted list of parametrized column (variable) indices
	 */
	private List<Integer> extendedGaussianElimination(final IVector<F> rightSide) {
		final F zero = field.getAdditiveIdentity();
		final List<Integer> parameters = new ArrayList<Integer>();
		int firstRowIndex = 0;
		
		for (int i = 0; i < columnCount && firstRowIndex < rowCount; i++) {
			final int pivotRowIndex = getNonzeroRow(i, firstRowIndex, rowCount);
			
			if (pivotRowIndex >= 0) {
				swapRows(firstRowIndex, pivotRowIndex);
				rightSide.swapElements(firstRowIndex, pivotRowIndex);
				
				final IVector<F> pivotRow = elements.get(firstRowIndex);
				final F pivotElement = pivotRow.get(i);
				final F pivotRightSideElement = rightSide.get(firstRowIndex);
				
				for (int j = firstRowIndex + 1; j < rowCount; j++) {
					final IVector<F> eliminatedRow = elements.get(j);
					final F eliminatedElement = eliminatedRow.get(i);
					
					if (!eliminatedElement.equals(zero)) {
						final F coeff = field.divide(eliminatedElement, pivotElement);
						final IVector<F> newRow = pivotRow.multiply(coeff).subtract(eliminatedRow);
						elements.set (j, newRow);
						
						final F eliminatedRightSideElement = rightSide.get(j);
						final F newResultElement = field.subtract(field.multiply(pivotRightSideElement, coeff), eliminatedRightSideElement);
						rightSide.set(j, newResultElement);
					}
				}
				
				firstRowIndex++;
			}
			else {
				parameters.add(i);
			}
		}
		
		for (int i = firstRowIndex; i < rowCount; i++) {
			if (!rightSide.get(i).equals(zero))
				return null;
		}
		
		for (int i = rowCount + parameters.size(); i < columnCount; i++)
			parameters.add(i);
		
		return parameters;
	}
	
	/**
	 * Does back-trace on extended triangular form of matrix.
	 * @param rightSide right side of equation system
	 * @param parameters sorted list of parametrized column (variable) indices
	 * @return general (parametric) solution of linear equation system
	 */
	private EquationSystemSolution<F> gaussianBackTrace (final IVector<F> rightSide, final List<Integer> parameters) {
		final int paramCount = parameters.size();
		final DenseVector<F> constantVector = new DenseVector<F> (field, columnCount);
		final Matrix<F> parameterCoeffs = new Matrix<F> (field, columnCount, paramCount);
		
		final F zero = field.getAdditiveIdentity();
		final F one = field.getMultiplicativeIdentity();
		
		int rowIndex = columnCount - 1 - parameters.size();
		int parameterIndex = parameters.size() - 1;
		
		for (int i = columnCount - 1; i >= 0; i--) {
			if (parameterIndex >= 0 && (int) parameters.get(parameterIndex) == i) {
				parameterCoeffs.set(i, parameterIndex, one);
				parameterIndex--;
			}
			else {
				final F pivot = this.get (rowIndex, i);
				final F negPivot = field.additiveInverse(pivot);
				
				if (pivot.equals(zero))
					throw new RuntimeException("Zero pivot element");
				
				F sum = field.divide(rightSide.get(rowIndex), pivot);
				
				for (int j = i + 1; j < columnCount; j++) {
					final F coeff = field.divide(this.get (rowIndex, j), negPivot);
					final F constantAddition = field.multiply(constantVector.get(j), coeff);
					sum = field.add(sum, constantAddition);
					
					for (int k = 0; k < paramCount; k++) {
						final F element = field.add(parameterCoeffs.get(i, k), field.multiply(parameterCoeffs.get(j, k), coeff));
						
						parameterCoeffs.set(i, k, element);
					}
				}
				
				constantVector.set(i, sum);
				rowIndex--;
			}
		}

		return new EquationSystemSolution<F>(constantVector, parameterCoeffs);
	}
	
	/**
	 * Performs simple gaussian elimination of given matrix and right side.
	 * This method cannot handle singular matrices and cannot produce parametric solution. 
	 * Solves equation matrox * result = rightSide.
	 * Matrix and right side is modified. If elimination succeeds (matrix is nonsingular)
	 * method stores result into given vector and returns true.
	 * @param rightSide right side of equation system
	 * @return true if system is solved, false if not
	 */
	public EquationSystemSolution<F> solveEquationSystem (final IVector<F> rightSide) {
		// Elimination
		final List<Integer> parameters = extendedGaussianElimination(rightSide);

		if (parameters == null)
			return null;
		
		EquationSystemSolution<F> solution = gaussianBackTrace(rightSide, parameters);

		return solution;
	}
	
	/**
	 * Creates copy of the matrix.
	 * @return matrix copy
	 */
	public Matrix<F> copy() {
		final Matrix<F> cp = new Matrix<F>(field, rowCount, columnCount);
		cp.assign(this);
		
		return cp;
	}
	
	/**
	 * Assigns matrix to this. 
	 * @param orig assigned matrix
	 */
	public void assign(final Matrix<F> orig) {
		if (this.rowCount != orig.rowCount || this.columnCount != orig.columnCount)
			throw new RuntimeException("Different matrix size");
		
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < columnCount; j++)
				this.set(i, j, orig.get(i, j));
		}
	}
	
	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append ("[");
		
		for (int i = 0; i < rowCount; i++) {
			if (i > 0)
				builder.append("; ");

			for (int j = 0; j < columnCount; j++) {
				if (j > 0)
					builder.append(", ");
				
				builder.append(get(i, j));
			}
		}
		
		builder.append ("]");
		
		return builder.toString();
	}
	
	@Override
	public boolean equals (final Object obj) {
		if (!(obj instanceof Matrix<?>))
			return false;
		
		final Matrix<?> cmp = (Matrix<?>) obj;
		
		return elements.equals(cmp.elements);
	}
	
	@Override
	public int hashCode() {
		return elements.hashCode();
	}
	
	/**
	 * Creates vector by matrix density.
	 * Dense matrix creates dense vector, sparse matrix sparse vector.
	 * @param field underlying field
	 * @param size vector size
	 * @return vector 
	 */
	protected <T extends IElement> IVector<T> createVector(final IField<T> field, final int size) {
		switch (density) {
			case DENSE:
				return new DenseVector<T>(field, size);

			case SPARSE:
				return new SparseVector<T>(field, size);
				
			default:
				throw new RuntimeException("Unknown density: " + density);
		}
	}
	
	/**
	 * Returns vector of elements on given row.
	 * This vector may be read and modified and modifications are written to the matrix.
	 * @param rowIndex index of the row
	 * @return row vector
	 */
	public IVector<F> getRowVector (final int rowIndex) {
		return elements.get(rowIndex);
	}

	/**
	 * Verifies if matrix is singular.
	 * @return true if matrix is singular, false if not
	 */
	public boolean isSingular() {
		if (rowCount != columnCount)
			throw new RuntimeException("Matrix is not square");
		
		final Matrix<F> tmp = this.copy();
		final List<Integer> parameterList = tmp.extendedGaussianElimination(new DenseVector<F>(field, rowCount));
		
		return parameterList == null || !parameterList.isEmpty();
	}
	
	/**
	 * Transforms all elements of the matrix by given field transformation.
	 * @param transformation field transformation
	 * @return transformed matrix
	 * @throws TransformationException thrown if transformation is not possible
	 */
	public <T extends IElement> Matrix<T> transform (final IFieldTransformation<F, T> transformation) throws TransformationException {
		final Matrix<T> result = new Matrix<T> (transformation.getOutputField(), rowCount, columnCount);
		
		for (int i = 0; i < rowCount; i++) {
			for (int j = 0; j < columnCount; j++) {
				result.set (i, j, transformation.transformElement(this.get(i, j)));
			}
		}
		
		return result;
	}

	/**
	 * Returns underlying field.
	 * @return field
	 */
	public IField<F> getField() {
		return field;
	}
}
