package math.matrix;

import math.Maths;
import math.equation.Polynomial;
import stdlib.StdDraw;
import exception.ComplexException;
import exception.IllegalDimensionException;
import exception.NoSquareException;
import exception.SingularException;

/**
 * 2D matrix object m<sub>ij</sub>&isin;&#x211D;
 * with 2 rows and n columns
 * @author Daniel Weschke
 */
public class Matrix2D extends Matrix{
	
	/**
	 * UID
	 */
	private static final long serialVersionUID = 2228035464656481002L;

	/**
	 * Create an null matrix.
	 */
	public Matrix2D(){
		super();
	}

	/**
	 * Create an 2-by-n matrix with zeros.
	 * @param n number of columns 
	 */
	public Matrix2D(int n){
		super(2,n);
	}

	/**
	 * Create an 2-by-n matrix based on a 2d array.
	 * @param a 2d double array
	 */
	public Matrix2D(double[][] a){
		super(a);
	}

	/**
	 * Create an 2-by-2 matrix based on a 2d array.
	 * @param a11
	 * @param a12
	 * @param a21
	 * @param a22
	 */
	public Matrix2D(double a11, double a12, double a21, double a22){
		super(2,2);
		set(0,0,a11);
		set(0,1,a12);
		set(1,0,a21);
		set(1,1,a22);
	}
	
	public Matrix2D(Matrix B){
		super(B);
	}
	
	/**
	 * Set the matrix with given vector at given row
	 * @param i row
	 * @param x value
	 * @param y value
	 * @return filled matrix
	 */
	public Matrix set(int i, double x, double y){
		set(0, i, x);
		set(1, i, y);
		return this;
	}

	/**
	 * Rotation by ϑ degrees counterclockwise or -ϑ degrees clockwise.
	 * @param ϑ in degrees
	 * @return 2d rotated matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D rotate(double ϑ) throws IllegalDimensionException{
		if(!isM2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) TensorII2D.rotation(ϑ).times(this);
	}
	
	/**
	 * Rotation by 45 degrees counterclockwise
	 * @return 2d rotated matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D rotate45() throws IllegalDimensionException{
		if(!isM2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{1,-1},{1,1}}).times(Math.sqrt(2)/2,this);
	}
	
	/**
	 * Rotation by 90 degrees counterclockwise
	 * @return 2d rotated matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D rotate90() throws IllegalDimensionException{
		if(!isM2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{0,-1},{1,0}}).times(this);
	}
	
	/**
	 * Rotation by 180 degrees counterclockwise
	 * @return 2d rotated matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D rotate180() throws IllegalDimensionException{
		if(!isM2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{-1,0},{0,-1}}).times(this);
	}
	
	/**
	 * Rotation by 270 degrees counterclockwise or 90 degrees clockwise.
	 * @return 2d rotated matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D rotate270() throws IllegalDimensionException{
		if(!isM2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{-1,0},{0,-1}}).times(this);
	}
	
	/**
	 * Reflection against the x axis
	 * @return matrix reflected against the x axis
	 * @throws IllegalDimensionException
	 */
	public Matrix2D flipVertical() throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{1,0},{0,-1}}).times(this);
	}
	
	/**
	 * Reflection against the y axis.
	 * @return matrix reflected against the y axis
	 * @throws IllegalDimensionException
	 */
	public Matrix2D flipHoriontal() throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{-1,0},{0,1}}).times(this);
	}
	
	/**
	 * Reflection against the x and y axis.
	 * @return matrix reflected against the x and y axis
	 * @throws IllegalDimensionException
	 */
	public Matrix2D flipBoth() throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return rotate180();
	}
	
	/**
	 * Horizontal shear.
	 * @param s shear factor
	 * @return horizontal sheared matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D shearHorizontal(double s) throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{1,s},{0,1}}).times(this);
	}
	
	/**
	 * Vertical shear
	 * @param s shear factor
	 * @return vertical sheared matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D shearVertical(double s) throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{1,0},{s,1}}).times(this);
	}
	
	/**
	 * @param s scalar
	 * @return squeezed matrix
	 * @throws IllegalDimensionException
	 */
	public Matrix2D squeezeMapping(double s) throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{s,0},{0,1/s}}).times(this);
	}

	/**
	 * Projection onto the x axis
	 * @return matrix projected onto the x axis
	 * @throws IllegalDimensionException
	 */
	public Matrix2D projectionToX() throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{1,0},{0,0}}).times(this);
	}
	
	/**
	 * Projection onto the y axis
	 * @return matrix projected onto the y
	 * @throws IllegalDimensionException
	 */
	public Matrix2D projectionToY() throws IllegalDimensionException{
		if(!isR2()) throw new IllegalDimensionException("Illegal matrix dimensions.");
		return (Matrix2D) new Matrix2D(new double[][]{{0,0},{0,1}}).times(this);
	}
	
	/**
	 * @return Mirror matrix (orthogonal). Mirrored vector due to a plane defined through a vector v. 
	 * @throws IllegalDimensionException
	 */
	public static Matrix2D mirror(Vector v) throws IllegalDimensionException{
		// P = I - alpha v v'; alpha = 2 / (v'v)
		//System.out.println(Diagonal2D.identity(v.n()) instanceof Diagonal2D);
		return (Matrix2D) Diagonal2D.identity(v.n()).minus(v.tensorProduct(v).times(2/v.dot(v)));
	}
	
    /**
     * Real eigenvalue calculated with the root formula of second and third degree.
     * @return eigenvalues λ<sub>i</sub>
     * @throws NoSquareException 
     * @throws IllegalDimensionException 
     * @throws ComplexException 
     */
	public Vector2D eigenvaluesRe(int analytic) throws NoSquareException, IllegalDimensionException, ComplexException{
		// TODO: also Imaginary
		if(m!=n && m!=2) throw new IllegalDimensionException("Illegal matrix dimensions.");
		// A = [ a, b; c, d]
		// det| a-l, b; c, d-l| = l^2 - tr(A)*l + det (A)
		Polynomial p = new Polynomial(1,-trace(),det(analytic));
		return new Vector2D(p.rootsRe());
	}
	
	/**
	* @return eigenvectors x<sub>i</sub> in matrix Λ
	* @throws IllegalDimensionException
	* @throws NoSquareException
	* @throws ComplexException
	*/
	public Matrix2D eigenvectors(int analytic) throws NoSquareException, IllegalDimensionException, ComplexException{
		Vector lambda = eigenvaluesRe(analytic);
		Matrix2D S = new Matrix2D();
		if(isR2()){
			Matrix2D Al;
			Vector2D x = new Vector2D();
			int i;
			double a1, a2;
			for(i=0;i<2;i++){
				Al = (Matrix2D) this.minus(Matrix.identity(2).times(lambda.get(i)));
				a2 = Al.get(1, 0);
				a1 = -Al.get(1, 1); // to the other side: a1*x1 = -a2*x2
				if(a1 == 0 && a2 == 0){
					a2 = Al.get(0, 0);
					a1 = -Al.get(0, 1); // to the other side: a1*x1 = -a2*x2
					if(a2 > 0){
						x.set(0, a1);
						x.set(1, a2);
					} else {
						x.set(0, -a1);
						x.set(1, -a2);
					}
				} else
				if(a2 > 0){
					x.set(0, a1);
					x.set(1, a2);
				} else {
					x.set(0, -a1);
					x.set(1, -a2);
				}
				S.setN(i, x.normalize());
			}
		}
		return S;
	}

    public void plot() throws IllegalDimensionException{
    	if(!isR2()) throw new IllegalDimensionException();
    	StdDraw sd = new StdDraw();
    	double maxM0 = Maths.sumAbs(getM(0).getArray());
    	double maxM1 = Maths.sumAbs(getM(1).getArray());
    	double maxN0 = Maths.sumAbs(getN(0).getArray());
    	double maxN1 = Maths.sumAbs(getN(1).getArray());
    	double max = Maths.max(maxM0,maxM1,maxN0,maxN1);
    	sd.setScale(-max, max);
    	StdDraw.polygon(
    			new double[]{0,get(0,0),get(0,0)+get(0,1),get(0,1)},
    			new double[]{0,get(1,0),get(1,0)+get(1,1),get(1,1)});
    	StdDraw.filledPolygon(
    			new double[]{0,get(0,0),get(0,0)+get(0,1),get(0,1)},
    			new double[]{0,get(1,0),get(1,0)+get(1,1),get(1,1)});
    }

	public static void main(String[] args) {
		Matrix2D a = new Matrix2D(0,1,1,0);
		try {
			Matrix b = new Matrix2D(new double[][]{{0,-1},{1,0}}).times(a);
			System.out.println(b);
//			System.out.println((Matrix2D) b);

			Matrix2D P = new Matrix2D(new double[][]{ { 1 }, { 1 } });
	        System.out.println(P.setName("P"));
	        System.out.println("rot(P,45°) = \n" + P.rotate(45));
	        
	        Vector2D v = new Vector2D(1,1);
	        Vector2D w = new Vector2D(0,1);
			System.out.println(Diagonal2D.identity(v.n()));
	        P = (Matrix2D) Matrix2D.mirror(v);
	        System.out.println("Pw = ");
	        P.times(w).println();
	        
	        Matrix A = new Matrix2D(new double[][]{ { 2, 1 }, { 0, 1 } });
	        System.out.println(A.setName("A"));
	        Matrix B = A.transpose().times(A);
	        System.out.println("A'A = \n" + B);
	        Matrix X = B.eigenvectors();
	        System.out.println("X = \n" + X);
	        System.out.println("X'A'A = \n" + X.transpose().times(B));
	        Matrix L2 = X.transpose().times(B).times(X);
	        System.out.println("L^2 = X'A'AX = \n" + L2);
	        Matrix L = L2.times(new Matrix(new double[][]{ { 1/Math.sqrt(L2.get(0, 0)), 0 }, { 0, 1/Math.sqrt(L2.get(1, 1)) } }));
	        System.out.println("L = \n" + L);
	        Matrix S = X.times(L).times(X);
	        System.out.println("S = XLX' = \n" + S);
	        Matrix2D Q = (Matrix2D) A.times(S.inverse());
	        System.out.println("Q = AS^-1 = \n" + Q);
	        Q.plot();
		} catch (IllegalDimensionException | SingularException e) {
			e.printStackTrace();
		}
	}

}
