/**
 * 
 * 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.data;

import java.util.ArrayList;

import jgromacs.data.Atom;
import jgromacs.db.ResidueType;

/**
 * Objects of this class represent a single residue
 *
 */
public class Residue implements Cloneable{

	private int index = 0;
	private String name = "Noname";
	private String chainID = "A";
	private ArrayList<Atom> atoms = new ArrayList<Atom>();
	private ResidueType residueType = new ResidueType();
	
	// Constructors
	
	/**
	 * Constructs a new Residue object
	 *
	 * 
	 */
	public Residue(){

	}
	
	/**
	 * Constructs a new Residue object of given name
	 *
	 * 
	 */
	public Residue(String name) {
		super();
		this.name = name;
	}

	/**
	 * Constructs a new Residue object of given index, name and residue type
	 *
	 * 
	 */
	public Residue(int index, String name, ResidueType residueType) {
		super();
		this.index = index;
		this.name = name;
		this.residueType = residueType;
	}

	/**
	 * Constructs a new Residue object of given index, name, chainID and residue type
	 *
	 * 
	 */
	public Residue(int index, String name, String chainID,
			ResidueType residueType) {
		super();
		this.index = index;
		this.name = name;
		this.chainID = chainID;
		this.residueType = residueType;
	}


	// Getters and setters
	
	/**
	 * Returns the index of residue
	 * @return index of the residue
	 */
	public int getIndex() {
		return index;
	}

	/**
	 * Sets the index of residue
	 * @param index index of the residue
	 */
	public void setIndex(int index) {
		this.index = index;
	}

	/**
	 * Returns the name of residue
	 * @return name of the residue
	 */
	public String getName() {
		return name;
	}

	/**
	 * Sets the name of residue
	 * @param name name of the residue
	 */
	public void setName(String name) {
		this.name = name;
	}
	
	/**
	 * Returns the type of residue
	 * @return residue type
	 */
	public ResidueType getResidueType() {
		return residueType;
	}

	/**
	 * Sets the type of residue
	 * @param residueType residue type
	 */
	public void setResidueType(ResidueType residueType) {
		this.residueType = residueType;
	}

	/**
	 * Returns the chain ID of residue
	 * @return chain ID of residue
	 */
	public String getChainID(){
		return chainID;	
	}
	
	/**
	 * Sets the chain ID of residue
	 * @param chainID chain ID
	 */
	public void setChainID(String chainID){
		this.chainID = chainID;
	}
	
	/**
	 * Returns the number of atoms in the residue
	 * @return number of atoms
	 * 
	 */
	public int getNumberOfAtoms(){
		return atoms.size();
	}
	
	/**
	 * Returns the list of atoms as an ArrayList object
	 * @return list of atoms as an ArrayList
	 * 
	 */
	public ArrayList<Atom> getAtomsAsArrayList(){
		return atoms;
	}
	
	/**
	 * Returns atom #i of the residue
	 * @return atom #i 
	 * 
	 */
	public Atom getAtom(int i){
		return atoms.get(i);
	}
	
	/**
	 * Returns the atom of given index
	 * @param index index of atom
	 * @return atom of given index
	 */
	public Atom getAtomByIndex(int index){
		for (int i = 0; i < atoms.size(); i++) {
			if (atoms.get(i).getIndex()==index)
				return atoms.get(i);
		}
		return null;	
	}
	
	/**
	 * Returns the atom of name
	 * @param name name of atom
	 * @return atom of given name
	 */
	public Atom getAtomByName(String name){
		for (int i = 0; i < atoms.size(); i++) {
			if (atoms.get(i).getName().equals(name))
				return atoms.get(i);
		}
		return null;	
	}

	/**
	 * Returns the 1 letter code of residue type
	 * @return 1 letter code
	 * 
	 */
	public String get1LetterCode(){
		return residueType.get1LetterCode();
	}
	
	/**
	 * Returns the 3 letter code of residue type
	 * @return 3 letter code
	 * 
	 */
	public String get3LetterCode(){
		return residueType.get3LetterCode();
	}
	
	/**
	 * Returns residue index and the 3 letter code of residue type
	 * @return combined code
	 * 
	 */
	public String getCombinedCode(){
		return getIndex()+residueType.get3LetterCode();
	}
	
	// Modifications
	
	/**
	 * Adds a new atom to the residue
	 * @param atom new atom
	 */
	public void addAtom(Atom atom){
		atoms.add(atom);
	}
	
	/**
	 * Replaces atom #i of the residue with the given atom
	 * @param atom new atom
	 * 
	 */
	public void setAtom(int i, Atom atom){
		atoms.set(i, atom);
	}
	
	/**
	 * Removes atom #i from the residue
	 * 
	 */
	public void removeAtom(int i){
		atoms.remove(i);
	}
	
	/**
	 * Removes the given atom from the residue
	 * @param atom the atom to be removed
	 */
	public void removeAtom(Atom atom){
		atoms.remove(atom);
	}
	
	/**
	 * Removes atom of given index from the residue
	 * @param index index of atom to be removed
	 * 
	 */
	public void removeAtomByIndex(int index){
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.getIndex()==index) removeAtom(atom);
		}
	}
	
	// Getting important atoms
	
	/**
	 * 	Returns the N-terminal nitrogen atom of amino acid
	 *  @return N-terminal nitrogen atom
	 *  
	 */
	public Atom getNTerminalNitrogen(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isNTerminalNitrogen()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the alpha carbon atom if any (otherwise returns null)
	 *  @return alpha carbon atom 
	 *  
	 */
	public Atom getAlphaCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isAlphaCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the beta carbon atom if any (otherwise returns null)
	 *  @return beta carbon atom 
	 *  
	 */
	public Atom getBetaCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isBetaCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the gamma carbon atom if any (otherwise returns null)
	 *  @return gamma carbon atom 
	 *  
	 */
	public Atom getGammaCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isGammaCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the delta carbon atom if any (otherwise returns null)
	 *  @return delta carbon atom 
	 *  
	 */
	public Atom getDeltaCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isDeltaCarbon()) return ret;
		}
		return null;
	}
	
	
	/**
	 * 	Returns the epsilon carbon atom if any (otherwise returns null)
	 *  @return epsilon carbon atom 
	 *  
	 */
	public Atom getEpsilonCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isEpsilonCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the zeta carbon atom if any (otherwise returns null)
	 *  @return zeta carbon atom 
	 *  
	 */
	public Atom getZetaCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isZetaCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the C-terminal carbon atom of amino acid
	 *  @return C-terminal carbon atom
	 *  
	 */
	public Atom getCTerminalCarbon(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isCTerminalCarbon()) return ret;
		}
		return null;
	}
	
	/**
	 * 	Returns the carbonyl oxygen atom of amino acid
	 *  @return carbonyl oxygen atom
	 *  
	 */
	public Atom getCarbonylOxygen(){
		Atom ret;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			ret = getAtom(i);
			if (ret.isCarbonylOxygen()) return ret;
		}
		return null;
	}
		
	// Getting important index sets

	/**
	 * 	Returns the index set of all atoms
	 *  @return index set of all atoms
	 * 
	 */
	public IndexSet getAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("AllAtoms");
		for (int i = 0; i < atoms.size(); i++)
		ret.addIndex(atoms.get(i).getIndex());
		return ret;
	}	
	
	/**
	 * 	Returns the index set of hydrogen atoms
	 *  @return index set of hydrogen atoms
	 * 
	 */
	public IndexSet getHydrogenAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("HydrogenAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isHydrogenAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}	
	
	/**
	 * 	Returns the index set of heavy atoms
	 *  @return index set of heavy atoms
	 * 
	 */
	public IndexSet getHeavyAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("HeavyAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isHeavyAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of backbone atoms 
	 *  @return index set of backbone atoms
	 * 
	 */
	public IndexSet getBackBoneAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("BackboneAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isBackboneAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of main chain atoms
	 *  @return index set of main chain atoms
	 * 
	 */
	public IndexSet getMainChainAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("MainChainAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isMainChainAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of main chain atoms and beta carbon atom 
	 *  @return index set of main chain atoms and beta carbon atom
	 * 
	 */
	public IndexSet getMainChainPlusCbAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("MainChainPlusCbAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isMainChainPlusCbAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of main chain atoms and hydrogen atoms
	 *  @return index set of main chain atoms and hydrogen atoms
	 * 
	 */
	public IndexSet getMainChainPlusHAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("MainChainPlusHAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isMainChainPlusHAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of side chain atoms
	 *  @return index set of side chain atoms
	 * 
	 */
	public IndexSet getSideChainAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("SideChainAtoms");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isSideChainAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
	
	/**
	 * 	Returns the index set of side chain atoms except of hydrogen atoms
	 *  @return index set of side chain atoms except of hydrogens
	 * 
	 */
	public IndexSet getSideChainMinusHAtomIndices(){
		IndexSet ret = new IndexSet();
		ret.setName("SideChainMinusH");
		for (int i = 0; i < atoms.size(); i++) {
			Atom atom = atoms.get(i);
			if (atom.isSideChainMinusHAtom()) ret.addIndex(atom.getIndex());
		}
		return ret;
	}
		
	// Coorddinates
	
	/**
	 * 	Sets the coordinates of atom #i
	 *  @param coordinates new atomic coordinates
	 */
	public void setAtomCoordinates(int i, Point3D coordinates){
		getAtom(i).setCoordinates(coordinates);
	}
	
	/**
	 * 	Sets the coordinates of atom of given index
	 *  @param coordinates new atomic coordinates
	 */
	public void setAtomOfIndexCoordinates(int index, Point3D coordinates){
		getAtomByIndex(index).setCoordinates(coordinates);
	}
	
	/**
	 * 	Sets the coordinates of all atoms
	 *  @param pointlist list of new atomic coordinates
	 */
	public void setAllAtomCoordinates(PointList pointlist){
		int numofatoms = getNumberOfAtoms();
		for (int i = 0; i < numofatoms; i++) {
			setAtomCoordinates(i, pointlist.getPoint(i));
		}
	}
	
	/**
	 * 	Returns the coordinates of all atoms
	 *  @return list of atomic coordinates
	 */
	public PointList getAllAtomCoordinates(){
		PointList ret = new PointList();
		for (int k = 0; k < getNumberOfAtoms(); k++)
		ret.addPoint(getAtom(k).getCoordinates());
		return ret;
	}
	
	/**
	 * 	Returns the position of alpha-carbon atom
	 *  @return position of alpha-carbon atom
	 */
	public Point3D getAlphaCarbonCoordinates(){
		return getAlphaCarbon().getCoordinates();
	}
	
	// Distances
		
	/**
	 * 	Returns the Euclidean distance of alpha-carbon atoms of this amino acid an another
	 *  @param other other amino acid
	 *  @return Euclidean distance of alpha-carbon atoms
	 */
	public double distanceAlphaCarbons(Residue other){
		Atom atom1 = getAlphaCarbon();
		Atom atom2 = other.getAlphaCarbon();
		return atom1.distance(atom2);
	}
	
	/**
	 * 	Returns the Euclidean distance of the closest atoms of this amino acid and another
	 *  @param other other amino acid
	 *  @return Euclidean distance of closest atoms
	 */
	public double distanceClosest(Residue other){
		double ret = 999999;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			Atom atom1 = getAtom(i);
			for (int j = 0; j < other.getNumberOfAtoms(); j++) {
				Atom atom2 = other.getAtom(j);
				double dist = atom1.distance(atom2);
				if (dist<ret) ret = dist;
			}
		}
		return ret;
	}
	
	/**
	 * 	Returns the Euclidean distance of the closest heavy atoms of this amino acid and another
	 *  @param other other amino acid
	 *  @return Euclidean distance of closest heavy atoms
	 */
	public double distanceClosestHeavy(Residue other){
		double ret = 999999;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			Atom atom1 = getAtom(i);
			if (atom1.isHeavyAtom()) {
			for (int j = 0; j < other.getNumberOfAtoms(); j++) {
				Atom atom2 = other.getAtom(j);
				if (atom2.isHeavyAtom()) {
				double dist = atom1.distance(atom2);
				if (dist<ret) ret = dist;
				}
			}
		 }
		}
		return ret;
	}
	
	// is?

	/**
	 * 	Returns true if it is an amino acid
	 * 
	 */
	public boolean isAminoAcid(){
	  return residueType.isAminoAcid();	
	}
	
	/**
	 * 	Returns true if it is a water molecule
	 * 
	 */
	public boolean isWater(){
		return residueType.isWater();
	}
	
	/**
	 * 	Returns true if it is not an amino acid neither a water molecule
	 * 
	 */
	public boolean isOther(){
		return residueType.isOther();
	}
	
	/**
	 * Returns true if the given atom is in the residue
	 * @param atom the atom to search for
	 */
	public boolean isAtomIn(Atom atom){
		return atoms.contains(atom);
	}
	
	// clone, equals, toString
	
	/**
	 * Returns an identical Residue object 
	 *
	 * @return clone of the residue
	 */
	public Object clone(){
		try {
			Residue ret = (Residue)super.clone();
			ret.index = index;
			ret.name = name;
			ret.chainID = chainID;
			ret.residueType = (ResidueType)residueType.clone();
			ret.atoms = new ArrayList<Atom>();
			for (int i = 0; i < atoms.size(); i++)
			ret.atoms.add((Atom)atoms.get(i).clone());
			return ret;
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Returns the String representation of residue
	 *
	 * @return String representation
	 */
	public String toString(){
		StringBuffer buf = new StringBuffer(name+"\t"+getCombinedCode()+"\t"+chainID+"\n");
		for (int i = 0; i < getNumberOfAtoms(); i++) buf.append(getAtom(i).toString()+"\n");
		return buf.toString();
	}
	
	/**
	 * Returns summary information about the residue
	 *
	 * @return summary information
	 */
	public String toStringInfo(){
		String ret = "Name: "+name+" | Code: "+getCombinedCode()+" | Chain ID: "+chainID+" | ("+getNumberOfAtoms()+" atoms)\n";;
		return ret;
	}
	
	/**
	 * Returns true if the two residues are identical
	 * @param other the other residue
	 * 
	 */
	public boolean equals(Object other){
		if (other == null) return false;
		if (other == this) return true;
		if (this.getClass() != other.getClass()) return false;
		Residue res = (Residue)other;
		if (!res.chainID.equals(chainID)) return false;
		if (!res.name.equals(name)) return false;
		if (res.index!=index) return false;
		if (!res.residueType.equals(residueType)) return false;
		if (res.getNumberOfAtoms()!=getNumberOfAtoms()) return false;
		for (int i = 0; i < getNumberOfAtoms(); i++) {
			if (!res.getAtom(i).equals(getAtom(i))) return false;
		}
		return true;
	}
	
	/**
	 * Returns hash code
	 * 
	 */
	public int hashCode() {
		return index+name.length()+atoms.size();
	}
	
	
}
