/* Copyright 2009-2013 David Hadka
 *
 * This file is part of the MOEA Framework.
 *
 * The MOEA Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * The MOEA Framework is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the MOEA Framework.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.moeaframework.util;

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

import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
import org.moeaframework.core.PRNG;

/**
 * Builds rotation matrices of any dimension constructively from one or more
 * 2D planar rotations.  Three types of construction mechanisms are provided:
 * 
 * <ol>
 *   <li>Specific rotation planes and thetas
 *     <pre>  new RotationMatrixBuilder(3).rotatePlane(0,1).withTheta(Math.PI/4)
 *       .rotatePlane(1,2).withTheta(Math.PI/8).create()</pre>
 *   <li>Specific rotation planes with random thetas
 *     <pre>  new RotationMatrixBuilder(3).rotatePlane(0,1).rotatePlane(1,2)
 *       .withRandomThetas().create()</pre>
 *   <li>Random subplanes with random thetas
 *     <pre>  new RotationMatrixBuilder(3).rotateK(2).create()</pre>
 * </ol>
 * 
 * <p>
 * References:
 * <ol>
 *   <li>Aguilera, A. and P&eacute;rez-Aguila, R.  "General n-Dimensional 
 *       Rotations."  WSCG 2004, pp. 1-8, 2004.
 * </ol>
 */
public class RotationMatrixBuilder {
	
	/**
	 * The 2D rotation planes.
	 */
	private static class Plane {
		
		/**
		 * The first axis defining the rotation plane.
		 */
		private final int firstAxis;
		
		/**
		 * The second axis defining the rotation plane.
		 */
		private final int secondAxis;
		
		/**
		 * The rotation angle in radians; or {@code NaN} if no rotation is 
		 * assigned.
		 */
		private double theta;
		
		/**
		 * Constructs a 2D rotation plane around the specified axes.
		 * 
		 * @param firstAxis the first axis
		 * @param secondAxis the second axis
		 */
		public Plane(int firstAxis, int secondAxis) {
			super();
			this.firstAxis = firstAxis;
			this.secondAxis = secondAxis;
			
			theta = Double.NaN;
		}

		/**
		 * Returns the first axis defining the rotation plane.
		 *
		 * @return the first axis defining the rotation plane
		 */
		public int getFirstAxis() {
			return firstAxis;
		}

		/**
		 * Returns the second axis defining the rotation plane.
		 *
		 * @return the second axis defining the rotation plane
		 */
		public int getSecondAxis() {
			return secondAxis;
		}

		/**
		 * Returns the rotation angle in radians; or {@code NaN} if no rotation
		 * is assigned.
		 * 
		 * @return the rotation angle in radians; or {@code NaN} if no rotation
		 *         is assigned
		 */
		public double getTheta() {
			return theta;
		}

		/**
		 * Sets the rotation angle in radians.
		 * 
		 * @param theta the rotation angle in radians
		 */
		public void setTheta(double theta) {
			this.theta = theta;
		}
		
	}
	
	/**
	 * The dimension of rotation matrices produced by this builder.
	 */
	private final int dimension;
	
	/**
	 * The 2D rotation planes currently used by this builder.
	 */
	private final List<Plane> planes;
	
	/**
	 * Constructs a rotation matrix builder for the given dimension.
	 * 
	 * @param dimension the dimension of rotation matrices produced by
	 *        this builder
	 */
	public RotationMatrixBuilder(int dimension) {
		super();
		this.dimension = dimension;
		
		planes = new ArrayList<Plane>();
	}
	
	/**
	 * Adds a rotation around the 2D rotation plane defined by the two axes.
	 * The plane is initially unrotated, but can be assigned a specific rotation
	 * angle if followed by {@link #withTheta(double)}.
	 * 
	 * @param i the first axis
	 * @param j the second axis
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder rotatePlane(int i, int j) {
		if ((i < 0) || (i >= dimension) || (j < 0) || 
				(j >= dimension) || (i == j)) {
			throw new IllegalArgumentException("invalid plane");
		}
		
		planes.add(new Plane(i, j));
		
		return this;
	}
	
	/**
	 * Sets the rotation angle, in radians, of the last 2D rotation plane
	 * added to this builder.  This method should always only follow 
	 * invocations of {@link #rotatePlane(int, int)}.
	 * 
	 * @param theta the rotation angle in radians
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder withTheta(double theta) {
		planes.get(planes.size()-1).setTheta(theta);
		
		return this;
	}
	
	/**
	 * Sets the rotation angle, in radians, of all 2D rotation planes added to
	 * this builder.
	 * 
	 * @param theta the rotation angle in radians
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder withThetas(double theta) {
		for (Plane plane : planes) {
			plane.setTheta(theta);
		}
		
		return this;
	}
	
	/**
	 * Assigns random rotation angles to all 2D rotation planes added to this 
	 * builder.
	 * 
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder withRandomThetas() {
		for (Plane plane : planes) {
			plane.setTheta(PRNG.nextDouble(0.0, 2.0*Math.PI));
		}
		
		return this;
	}
	
	/**
	 * Rotates all 2D rotation planes and assigns each a random rotation angle.
	 * 
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder rotateAll() {
		for (int i = 0; i < dimension - 1; i++) {
			for (int j = i+1; j < dimension; j++) {
				Plane plane = new Plane(i, j);
				plane.setTheta(PRNG.nextDouble(0.0, 2.0*Math.PI));
				planes.add(plane);
			}
		}
		
		return this;
	}
	
	/**
	 * Rotates {@code k} randomly-selected but unique 2D rotation planes and 
	 * assigns each a random rotation angle.  For an {@code N}-dimension 
	 * rotation matrix, there exist {@code (N choose 2)} 2D rotation planes.
	 * 
	 * @param k the number of randomly-selected 2D rotation planes
	 * @return a reference to this rotation matrix builder
	 */
	public RotationMatrixBuilder rotateK(int k) {
		List<Plane> tempPlanes = new ArrayList<Plane>();
		
		//generate the list of all available rotation planes
		for (int i = 0; i < dimension - 1; i++) {
			for (int j = i+1; j < dimension; j++) {
				tempPlanes.add(new Plane(i, j));
			}
		}
		
		if ((k < 0) || (k > tempPlanes.size())) {
			throw new IllegalArgumentException("invalid number of planes");
		}
		
		//shuffle to randomize which planes are rotated
		PRNG.shuffle(tempPlanes);
		
		//clear any existing planes
		planes.clear();
		
		//assign the first k planes
		for (int i = 0; i < k; i++) {
			Plane plane = tempPlanes.get(i);
			plane.setTheta(PRNG.nextDouble(0.0, 2.0*Math.PI));
			planes.add(plane);
		}
		
		return this;
	}
	
	/**
	 * Returns the rotation matrix resulting from applying all 2D rotation 
	 * planes and angles added to this builder.
	 * 
	 * @return the rotation matrix resulting from applying all 2D rotation 
	 *         planes and angles added to this builder
	 */
	public RealMatrix create() {
		RealMatrix rotation = newIdentityMatrix();
		
		for (Plane plane : planes) {
			double theta = plane.getTheta();
			
			if (Double.isNaN(theta)) {
				continue;
			}

			rotation = rotation.multiply(newRotationMatrix(
					plane.getFirstAxis(), plane.getSecondAxis(), theta));
		}
		
		return rotation;
	}
	
	/**
	 * Returns an identity matrix.
	 * 
	 * @return an identity matrix
	 */
	private RealMatrix newIdentityMatrix() {
		RealMatrix identity = MatrixUtils.createRealMatrix(dimension, 
				dimension); 
		
		for (int i = 0; i < dimension; i++) {
			identity.setEntry(i, i, 1.0);
		}
		
		return identity;
	}
	
	/**
	 * Returns a rotation matrix for rotating about the 2D plane defined by
	 * the specified axes.
	 * 
	 * @param i the first axis
	 * @param j the second axis
	 * @param theta the rotation angle in radians
	 * @return a rotation matrix for rotating about the 2D plane defined by
	 *         the specified axes
	 */
	private RealMatrix newRotationMatrix(int i, int j, double theta) {
		RealMatrix rotation = newIdentityMatrix();
		
		rotation.setEntry(i, i, Math.cos(theta));
		rotation.setEntry(i, j, -Math.sin(theta));
		rotation.setEntry(j, i, Math.sin(theta));
		rotation.setEntry(j, j, Math.cos(theta));
		
		return rotation;
	}

}
