/**
 * 
 * Written by Márton Münz and Philip C Biggin
 * Copyright (c) University of Oxford, United Kingdom
 * Visit http://sbcb.bioch.ox.ac.uk/jgromacs/
 * 
 * This source code file is part of JGromacs v1.0.
 * 
 * JGromacs v1.0 is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * JGromacs v1.0. 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with JGromacs v1.0. If not, see <http://www.gnu.org/licenses/>.
 * 
 */

package jgromacs.analysis;

import java.util.ArrayList;

import jama.Matrix;

import jgromacs.data.Angle;
import jgromacs.data.Point3D;
import jgromacs.data.Residue;
import jgromacs.data.Structure;
import jgromacs.data.Trajectory;

/**
 * Collection of methods for analysing angles
 *
 */
public class Angles {

	/**
	 * Calculates the angle between two vectors
	 * @param v1 first vector
	 * @param v2 second vector
	 * @return angle between vectors
	 */
	public static Angle getAngleBetweenVectors(Point3D v1, Point3D v2){
		double inner = v1.innerProduct(v2);
		double lengths = v1.length()*v2.length();
		double angleInRad = Math.acos(inner/lengths);
		Angle ret = new Angle();
		ret.setInRadians(angleInRad);
		return ret;
	}
	
	/**
	 * Calculates the angle between two planes defined by points A, B, C and points B, C, D
	 * @param A point A 
	 * @param B point B
	 * @param C point C 
	 * @param D point D
	 * @return angle between planes
	 */
	public static Angle getAngleBetweenPlanes(Point3D A, Point3D B, Point3D C, Point3D D){
		Point3D normal1 = getNormalVectorOfPlane(B, A, C);
		Point3D normal2 = getNormalVectorOfPlane(C, B, D);
		return getAngleBetweenVectors(normal1, normal2);
	}
	
	/**
	 * Calculates the normal vector of a plane defined by the C,A and C,B vectors 
	 * @param C point C 
	 * @param A point A 
	 * @param B point B
	 * @return normal vector
	 */
	private static Point3D getNormalVectorOfPlane(Point3D C, Point3D A, Point3D B){
		Point3D v1 = A.minus(C);
		Point3D v2 = B.minus(C);
		return v1.crossProduct(v2);
	}
		
	/**
	 * Calculates dihedral angle Phi of residue #i of a structure
	 * @param s structure
	 * @return dihedral angle Phi
	 */
	public static Angle getDihedralPhi(Structure s, int i){
		if (i<=0) return new Angle();
		Residue res = s.getResidue(i);
		Residue resMinus = s.getResidue(i-1);
		Point3D A = resMinus.getCTerminalCarbon().getCoordinates();
		Point3D B = res.getNTerminalNitrogen().getCoordinates();
		Point3D C = res.getAlphaCarbon().getCoordinates();
		Point3D D = res.getCTerminalCarbon().getCoordinates();
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates dihedral angle Psi of residue #i of a structure
	 * @param s structure
	 * @return dihedral angle Psi
	 */
	public static Angle getDihedralPsi(Structure s, int i){
	    if (i>=s.getNumberOfResidues()) return new Angle();
		Residue res = s.getResidue(i);
		Residue resPlus = s.getResidue(i+1);
		Point3D A = res.getNTerminalNitrogen().getCoordinates();
		Point3D B = res.getAlphaCarbon().getCoordinates();
		Point3D C = res.getCTerminalCarbon().getCoordinates();
		Point3D D = resPlus.getNTerminalNitrogen().getCoordinates();
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates dihedral angle Omega of residue #i of a structure
	 * @param s structure
	 * @return dihedral angle Omega
	 */
	public static Angle getDihedralOmega(Structure s, int i){
	    if (i>=s.getNumberOfResidues()) return new Angle();
		Residue res = s.getResidue(i);
		Residue resPlus = s.getResidue(i+1);
		Point3D A = res.getAlphaCarbon().getCoordinates();
		Point3D B = res.getCTerminalCarbon().getCoordinates();
		Point3D C = resPlus.getNTerminalNitrogen().getCoordinates();
		Point3D D = resPlus.getAlphaCarbon().getCoordinates();
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates side chain dihedral angle Chi1 of residue #i of a structure
	 * The residue can be ARG,ASN,ASP,CYS,GLN,GLU,HIS,ILE,LEU,LYS,MET,PHE,PRO,SER,THR,TRP,TYR or VAL
	 * @param s structure
	 * @return dihedral angle Chi1
	 */
	public static Angle getDihedralChi1(Structure s, int i){
	  	Residue res = s.getResidue(i);
	  	String code = res.getResidueType().get3LetterCode().toUpperCase();
	  	if (code.equals("ALA")||code.equals("GLY")) return null;
	  	Point3D A = res.getNTerminalNitrogen().getCoordinates();
	  	Point3D B = res.getAlphaCarbon().getCoordinates();
		Point3D C = res.getBetaCarbon().getCoordinates();
		Point3D D = new Point3D();
		if (code.equals("ARG")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("ASN")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("ASP")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("CYS")) D = res.getAtomByName("SG").getCoordinates();
		else if (code.equals("GLN")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("GLU")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("HIS")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("ILE")) D = res.getAtomByName("CG1").getCoordinates();
		else if (code.equals("LEU")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("LYS")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("MET")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("PHE")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("PRO")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("SER")) D = res.getAtomByName("OG").getCoordinates();
		else if (code.equals("THR")) D = res.getAtomByName("OG1").getCoordinates();
		else if (code.equals("TRP")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("TYR")) D = res.getGammaCarbon().getCoordinates();
		else if (code.equals("VAL")) D = res.getAtomByName("CG1").getCoordinates();
		else return null;
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates side chain dihedral angle Chi2 of residue #i of a structure
	 * The residue can only be ARG,ASN,ASP,GLN,GLU,HIS,ILE,LEU,LYS,MET,PHE,PRO,TRP or TYR
	 * @param s structure
	 * @return dihedral angle Chi2
	 */
	public static Angle getDihedralChi2(Structure s, int i){
		Residue res = s.getResidue(i);
	  	String code = res.getResidueType().get3LetterCode().toUpperCase();
		if (code.equals("ALA")||code.equals("GLY")||code.equals("CYS")||code.equals("SER")||code.equals("THR")||code.equals("VAL")) return null;
		Point3D A = res.getAlphaCarbon().getCoordinates();
		Point3D B = res.getBetaCarbon().getCoordinates();
		Point3D C;
		if (code.equals("ILE")) C = res.getAtomByName("CG1").getCoordinates();
		else  C = res.getGammaCarbon().getCoordinates();
		Point3D D = new Point3D();
		if (code.equals("ARG")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("ASN")) D = res.getAtomByName("OD1").getCoordinates();
		if (code.equals("ASP")) D = res.getAtomByName("OD1").getCoordinates();
		if (code.equals("GLN")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("GLU")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("HIS")) D = res.getAtomByName("ND1").getCoordinates();
		if (code.equals("ILE")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("LEU")) D = res.getAtomByName("CD1").getCoordinates();
		if (code.equals("LYS")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("MET")) D = res.getAtomByName("SD").getCoordinates();
		if (code.equals("PHE")) D = res.getAtomByName("CD1").getCoordinates();
		if (code.equals("PRO")) D = res.getDeltaCarbon().getCoordinates();
		if (code.equals("TRP")) D = res.getAtomByName("CD1").getCoordinates();
		if (code.equals("TYR")) D = res.getAtomByName("CD1").getCoordinates();
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates side chain dihedral angle Chi3 of residue #i of a structure
	 * The residue can only be ARG,GLN,GLU,LYS or MET
	 * @param s structure
	 * @return dihedral angle Chi3
	 */
	public static Angle getDihedralChi3(Structure s, int i){
		Residue res = s.getResidue(i);
	  	String code = res.getResidueType().get3LetterCode().toUpperCase();
		if (!(code.equals("ARG")||code.equals("GLN")||code.equals("GLU")||code.equals("LYS")||code.equals("MET"))) return null;
		Point3D A = res.getBetaCarbon().getCoordinates();
		Point3D B = res.getGammaCarbon().getCoordinates();
		Point3D C = new Point3D();
		Point3D D = new Point3D();
		if (code.equals("ARG")) { 
			C = res.getDeltaCarbon().getCoordinates();
			D = res.getAtomByName("NE").getCoordinates();
		}
		if (code.equals("GLN")) {
			C = res.getDeltaCarbon().getCoordinates();
			D = res.getAtomByName("OE1").getCoordinates();
		}
		if (code.equals("GLU")) {
			C = res.getDeltaCarbon().getCoordinates();
			D = res.getAtomByName("OE1").getCoordinates();
		}
		if (code.equals("LYS")) {
			C = res.getDeltaCarbon().getCoordinates();
			D = res.getAtomByName("CE").getCoordinates();
		}
		if (code.equals("MET")) {
			C = res.getAtomByName("SD").getCoordinates();
			D = res.getAtomByName("CE").getCoordinates();
		}
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates side chain dihedral angle Chi4 of residue #i of a structure
	 * The residue can only be ARG or LYS
	 * @param s structure
	 * @return dihedral angle Chi4
	 */
	public static Angle getDihedralChi4(Structure s, int i){
		Residue res = s.getResidue(i);
	  	String code = res.getResidueType().get3LetterCode().toUpperCase();
		if (!(code.equals("ARG")||code.equals("LYS"))) return null;
		Point3D A = res.getGammaCarbon().getCoordinates();
		Point3D B = res.getDeltaCarbon().getCoordinates();
		Point3D C = new Point3D();
		Point3D D = new Point3D();
		if (code.equals("ARG")) { 
			C = res.getAtomByName("NE").getCoordinates();
			D = res.getAtomByName("CZ").getCoordinates();
		}
		if (code.equals("LYS")) {
			C = res.getAtomByName("CE").getCoordinates();
			D = res.getAtomByName("NZ").getCoordinates();
		}
		Point3D b1 = B.minus(A);
		Point3D b2 = C.minus(B);
		Point3D b3 = D.minus(C);
		double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
		double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
		double angle = Math.atan2(x, y);
		Angle ret = new Angle();
		ret.setInRadians(angle);
		return ret;
	}
	
	/**
	 * Calculates side chain dihedral angle Chi5 of residue #i of a structure
	 * The residue can only be ARG
	 * @param s structure
	 * @return dihedral angle Chi5
	 */
	public static Angle getDihedralChi5(Structure s, int i){
		Residue res = s.getResidue(i);
	  	String code = res.getResidueType().get3LetterCode().toUpperCase();
	  	if (code.equals("ARG")){
	  		Point3D A = res.getDeltaCarbon().getCoordinates();
			Point3D B = res.getAtomByName("NE").getCoordinates();
			Point3D C = res.getAtomByName("CZ").getCoordinates();
			Point3D D = res.getAtomByName("NH1").getCoordinates();
			Point3D b1 = B.minus(A);
			Point3D b2 = C.minus(B);
			Point3D b3 = D.minus(C);
			double x = (b1.multiplyByScalar(b2.length())).innerProduct(b2.crossProduct(b3));
			double y = (b1.crossProduct(b2)).innerProduct(b2.crossProduct(b3));
			double angle = Math.atan2(x, y);
			Angle ret = new Angle();
			ret.setInRadians(angle);
			return ret;
	  	}
	  	else return null;
	}
	
	/**
	 * Calculates the time series of dihedral angle Phi of residue #i over a trajectory
	 * @param t trajectory
	 * @return time series of Phi
	 */
	public static ArrayList<Angle> getDihedralPhiTimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralPhi(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Psi of residue #i over a trajectory
	 * @param t trajectory
	 * @return time series of Psi
	 */
	public static ArrayList<Angle> getDihedralPsiTimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralPsi(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Omega of residue #i over a trajectory
	 * @param t trajectory
	 * @return time series of Omega
	 */
	public static ArrayList<Angle> getDihedralOmegaTimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralOmega(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Chi1 of residue #i over a trajectory
	 * The residue can be ARG,ASN,ASP,CYS,GLN,GLU,HIS,ILE,LEU,LYS,MET,PHE,PRO,SER,THR,TRP,TYR or VAL
	 * @param t trajectory
	 * @return time series of Chi1
	 */
	public static ArrayList<Angle> getDihedralChi1TimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralChi1(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Chi2 of residue #i over a trajectory
	 * The residue can only be ARG,ASN,ASP,GLN,GLU,HIS,ILE,LEU,LYS,MET,PHE,PRO,TRP or TYR
	 * @param t trajectory
	 * @return time series of Chi2
	 */
	public static ArrayList<Angle> getDihedralChi2TimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralChi2(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Chi3 of residue #i over a trajectory
	 * The residue can only be ARG,GLN,GLU,LYS or MET
	 * @param t trajectory
	 * @return time series of Chi3
	 */
	public static ArrayList<Angle> getDihedralChi3TimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralChi3(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Chi4 of residue #i over a trajectory
	 * The residue can only be ARG or LYS
	 * @param t trajectory
	 * @return time series of Chi4
	 */
	public static ArrayList<Angle> getDihedralChi4TimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralChi4(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of dihedral angle Chi5 of residue #i over a trajectory
	 * The residue can only be ARG
	 * @param t trajectory
	 * @return time series of Chi5
	 */
	public static ArrayList<Angle> getDihedralChi5TimeSeries(Trajectory t, int i){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			ret.add(getDihedralChi5(frame, i));
		}
		return ret;
	}
	
	/**
	 * Calculates the time series of torsion angle over a trajectory defined by four atoms 
	 * @param t trajectory
	 * @param atom1 index of first atom
	 * @param atom2 index of second atom
	 * @param atom3 index of third atom
	 * @param atom4 index of fourth atom
	 * @return time series of torsion angle
	 */
	public static ArrayList<Angle> getTorsionAngleTimeSeries(Trajectory t, int atom1, int atom2, int atom3, int atom4){
		ArrayList<Angle> ret = new ArrayList<Angle>();
		for (int k = 0; k < t.getNumberOfFrames(); k++) {
			Structure frame = t.getFrameAsStructure(k);
			Point3D A = frame.getAtomByIndex(atom1).getCoordinates(); 
			Point3D B = frame.getAtomByIndex(atom2).getCoordinates(); 
			Point3D C = frame.getAtomByIndex(atom3).getCoordinates(); 
			Point3D D = frame.getAtomByIndex(atom4).getCoordinates(); 
			ret.add(getAngleBetweenPlanes(A, B, C, D));
		}
		return ret;
	}
	
	/**
	 * Calculates the Ramachandran Plot of a structure
	 * @param s structure (a polypeptide chain)
	 * @return (N-2)x2 matrix of (phi,psi) pairs
	 */
	public static Matrix getRamachandranPlot(Structure s){
		Matrix ret = new Matrix(s.getNumberOfResidues()-2,2);
		for (int i = 1; i < s.getNumberOfResidues()-1; i++) {
			Angle phi = getDihedralPhi(s, i);
			Angle psi = getDihedralPsi(s, i);
			ret.set(i-1, 0, phi.getInDegrees());
			ret.set(i-1, 1, psi.getInDegrees());
		}
		return ret;
	}
	
}
