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

/**
 * Objects of this class represent a single amino acid sequence
 *
 */
public class Sequence implements Cloneable {
	
	private String name = "Noname";
	private ArrayList<SequencePosition> positions = new ArrayList<SequencePosition>();
	
	// Constructors
	
	/**
	 * Constructs a new Sequence object
	 *
	 * 
	 */
	public Sequence(){
		
	}
	
	/**
	 * Constructs a new Sequence object of given name
	 *
	 * 
	 */
	public Sequence(String name){
		this.name = name;
	}
	
	// Getters and setters

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

	/**
	 * Sets the name of sequence
	 * @param name name of sequence
	 */
	public void setName(String name) {
		this.name = name;
	}
	
	/**
	 * Returns sequence as an ArrayList object
	 * @return sequence as an ArrayList
	 */
	public ArrayList<SequencePosition> getAsArrayList(){
		return positions;
	}

	/**
	 * Returns the length of sequence (gaps excluded)
	 * @return length of sequence
	 */
	public int getSequenceLength(){
		int ret = 0;
		for (int i = 0; i < positions.size(); i++)
		if (!isPositionAGap(i)) ret++;
		return ret;
	}
	
	
	/**
	 * Returns the length of sequence (gaps included)
	 * @return length of sequence
	 */
	public int getSequenceLengthWithGaps(){
		return positions.size();
	}
	
	/**
	 * Returns sequence position #i
	 * @return sequence position #i
	 */
	public SequencePosition getPosition(int i){
		return positions.get(i);
	}
		
	/**
	 * Returns sequence position of given index
	 * @param index index of position
	 * @return sequence position of given index
	 */
	public SequencePosition getPositionByIndex(int index){
		for (int i = 0; i < getSequenceLengthWithGaps(); i++)
			if (getIndexOfPosition(i)==index) return getPosition(i);		
		return null;
	}
	
	/**
	 * Returns the index of sequence position #i
	 * @return index of sequence position 
	 */
	public int getIndexOfPosition(int i){
		return positions.get(i).getIndex();
	}

	/**
	 * Returns the residue type of sequence position #i
	 * @return residue type of sequence position
	 */
	public ResidueType getResidueTypeOfPosition(int i){
		return positions.get(i).getResidueType();
	}
	
	/**
	 * Returns the annotation of sequence position #i
	 * @return annotation of sequence position
	 */
	public String getAnnotationOfPosition(int i){
		return positions.get(i).getAnnotation();
	}
	
	/**
	 * Returns the residue type of sequence position of given index
	 * @param index index of position
	 * @return residue type of sequence position
	 */
	public ResidueType getResidueTypeOfPositionOfIndex(int index){
		return getPositionByIndex(index).getResidueType();
	}

	/**
	 * Returns the annotation of sequence position of given index
	 * @param index index of position
	 * @return annotation of sequence position
	 */
	public String getAnnotationOfPositionOfIndex(int index){
		return getPositionByIndex(index).getAnnotation();
	}
	
	// Modifications
	
	/**
	 * Adds a new position to the sequence
	 * @param pos new sequence position
	 * 
	 */
	public void addPosition(SequencePosition pos){
		positions.add((SequencePosition)pos.clone());
	}
	
	/**
	 * Adds a new position of given residue type and annotation to the sequence
	 * @param type residue type of new position
	 * @param annotation annotation of new position
	 */
	public void addPosition(ResidueType type, String annotation){
		SequencePosition pos = new SequencePosition(type, annotation);
		pos.setIndex(calculateIndexUponAddition());
		positions.add(pos);
	}
	
	/**
	 * Adds a new position of given index and residue type to the sequence
	 * @param index index of new position
	 * @param type residue type of new position
	 */
	public void addPosition(int index, ResidueType type){
		SequencePosition pos = new SequencePosition(index,type);
		positions.add(pos);
	}
	
	/**
	 * Adds a new position of given index, residue type and annotation to the sequence
	 * @param index index of new position
	 * @param type residue type of new position
	 * @param annotation annotation of new position
	 */
	public void addPosition(int index, ResidueType type, String annotation){
		SequencePosition pos = new SequencePosition(index,type,annotation);
		positions.add(pos);
	}
	
	/**
	 * Adds a series of new positions to the sequence taken from a String object
	 * @param str String object encoding a series of sequence positions
	 */
	public void addPositionsFromString(String str){
		str = str.trim();
		for (int k = 0; k < str.length(); k++) {
			String code = str.substring(k,k+1);
			if (code.equals("-")) addGap();
			else addPosition(new ResidueType(code),"");	
		}
	}
	
	/**
	 * Adds a new gap to the sequence
	 * 
	 */
	public void addGap(){
		SequencePosition pos = new SequencePosition(-999,new ResidueType());
		positions.add(pos);
	}
	
	/**
	 * Inserts a new gap to position #i 
	 * 
	 */
	public void insertGap(int i){
		SequencePosition pos = new SequencePosition(-999,new ResidueType());
		positions.add(i, pos);
	}

	/**
	 * Replaces position #i with the given sequence position
	 * @param pos new sequence position
	 * 
	 */
	public void setPosition(int i, SequencePosition pos){
		positions.set(i,pos);
	}
	
	/**
	 * Replaces position #i with the a sequence position of given residue type and annotation
	 * @param type residue type of new sequence position
	 * @param annotation annotation of new sequence position
	 * 
	 */
	public void setPosition(int i, ResidueType type, String annotation){
		int index = calculateIndexUponSetting(i);
		SequencePosition pos = new SequencePosition(index,type,annotation);
		positions.set(i,pos);
	}
	
	/**
	 * Replaces position #i with the a sequence position of given index and residue type 
	 * @param index index of new sequence position
	 * @param type residue type of new sequence position
	 *  
	 */
	public void setPosition(int i, int index, ResidueType type){
		SequencePosition pos = new SequencePosition(index,type);
		positions.set(i,pos);
	}
	
	/**
	 * Replaces position #i with the a sequence position of given index, residue type and annotation
	 * @param index index of new sequence position
	 * @param type residue type of new sequence position
	 * @param annotation annotation of new sequence position
	 *  
	 */
	public void setPosition(int i, int index, ResidueType type, String annotation){
		SequencePosition pos = new SequencePosition(index,type,annotation);
		positions.set(i,pos);
	}
	
	/**
	 * Replaces the position of given index with the a sequence position of given residue type and annotation
	 * @param index index of sequence position
	 * @param type residue type of new sequence position
	 * @param annotation annotation of new sequence position
	 *  
	 */
	public void setPositionByIndex(int index, ResidueType type, String annotation){
		for (int i = 0; i < getSequenceLengthWithGaps(); i++){
			if (getIndexOfPosition(i)==index)
			setPosition(i, new SequencePosition(index,type,annotation));
		}
	}
	
	/**
	 * Replaces the position of given index with the a sequence position of given residue type
	 * @param index index of sequence position
	 * @param type residue type of new sequence position
	 *  
	 */
	public void setPositionByIndex(int index, ResidueType type){
		for (int i = 0; i < getSequenceLengthWithGaps(); i++){
			if (getIndexOfPosition(i)==index)
			setPosition(i, new SequencePosition(index,type));
		}
	}

	/**
	 * Removes sequence position #i
	 * 
	 */
	public void removePosition(int i){
		positions.remove(i);
	}
	
	/**
	 * Removes the given sequence position
	 * @param pos sequence position to be removed
	 */
	public void removePosition(SequencePosition pos){
		positions.remove(pos);
	}
	
	/**
	 * Removes the sequence position of given index
	 * @param index index of sequence position to be removed
	 */
	public void removePositionByIndex(int index){
	  SequencePosition pos = getPositionByIndex(index);
	  removePosition(pos);
	}

	/**
	 * Removes all gaps from the sequence
	 * 
	 */
	public void removeGaps(){
		for (int i = 0; i < getSequenceLengthWithGaps(); i++) {
			if (isPositionAGap(i)) {
				removePosition(i);
				i--;
			}
		}
	}
	
	/**
	 * Reindexes all positions in the sequence starting from a given index
	 * @param startindex index of position #0
	 */
	public void reIndexPositions(int startindex){
		for (int i = 0; i < getSequenceLengthWithGaps(); i++) {
			if (!isPositionAGap(i)){
			getPosition(i).setIndex(startindex);
			startindex++;
			}
		}
	}
	
	/**
	 * Reindexes all positions in the sequence starting from 1
	 * 
	 */
	public void reIndexPositions(){
		reIndexPositions(1);
	}
	
	
	
	/**
	 * Inserts the given sequence position to position #i
	 * Note that you may have to re-index sequence positions after using this method 
	 * @param pos new sequence position
	 * 
	 */
	public void insertPosition(int i, SequencePosition pos){
		positions.add(i,(SequencePosition)pos.clone());
	}
	
	/**
	 * Inserts sequence position of given residue type and annotation to position #i
	 * Note that you may have to re-index sequence positions after using this method 
	 * @param type residue type of new sequence position
	 * @param annotation annotation of new sequence position
	 * 
	 */
	public void insertPosition(int i, ResidueType type, String annotation){
		int index = calculateIndexUponInsertion(i);
		SequencePosition pos = new SequencePosition(index,type,annotation);
		positions.add(i,pos);
	}
	
	/**
	 * Inserts sequence position of given index and residue type to position #i
	 * Note that you may have to re-index sequence positions after using this method 
	 * @param index index of new sequence position
	 * @param type residue type of new sequence position
	 * 
	 */
	public void insertPosition(int i, int index, ResidueType type){
		SequencePosition pos = new SequencePosition(index,type);
		positions.add(i,pos);
	}
	
	/**
	 * Inserts sequence position of given index, residue type and annotation to position #i
	 * Note that you may have to re-index sequence positions after using this method 
	 * @param index index of new sequence position
	 * @param type residue type of new sequence position
	 * @param annotation annotation of new sequence position
	 *  
	 */
	public void insertPosition(int i, int index, ResidueType type, String annotation){
		SequencePosition pos = new SequencePosition(index,type,annotation);
		positions.add(i,pos);
	}	
	
	// isGap?	
	
	/**
	 * Returns true if sequence position #i is a gap
	 * 
	 */
	public boolean isPositionAGap(int i){
		if (i>getSequenceLengthWithGaps()) return true;
		if (getIndexOfPosition(i)==-999) return true;
		return false;
	}	
	
	// Miscellaneous

	/**
	 * Returns subsequence beginning at sequence position #begin and ending at sequence position #end
	 * @param begin start position of subsequence
	 * @param end end position of subsequence
	 * @return subsequence
	 */
	public Sequence getSubSequence(int begin, int end){
		Sequence ret = new Sequence("Subsequence["+begin+","+end+"](from:"+getName()+")");
		for (int i = begin; i < end+1; i++)
			ret.addPosition(getPosition(i));
		return ret;
	}
		
	/**
	 * Returns subsequence beginning at sequence position #begin
	 * @param begin start position of subsequence
	 * @return subsequence
	 */
	public Sequence getSubSequenceFrom(int begin){
		Sequence ret = getSubSequence(begin,getSequenceLengthWithGaps()-1);
		ret.setName("Subsequence["+begin+"-...](from:"+getName()+")");
		return ret;
	}
	
	/**
	 * Returns subsequence ending at sequence position #end
	 * @param end end position of subsequence
	 * @return subsequence
	 */
	public Sequence getSubSequenceTo(int end){
		Sequence ret = getSubSequence(0,end);
		ret.setName("Subsequence[...-"+end+"](from:"+getName()+")");
		return ret;
	}
	
	
	/**
	 * Returns the reverse sequence
	 * @return reverse sequence
	 */
	public Sequence getReverse(){
		Sequence ret = new Sequence("Reverse("+getName()+")");
		for (int i = getSequenceLengthWithGaps()-1; i >= 0; i--)
			ret.addPosition(getPosition(i));
		return ret;
	}
	
	/**
	 * Returns the given sequence concatenated to this sequence
	 * @param other second sequence
	 * @return concatenated sequence
	 */
	public Sequence concat(Sequence other){
		Sequence ret = (Sequence)clone();
		for (int i = 0; i < other.getSequenceLengthWithGaps(); i++)
			ret.addPosition(other.getPosition(i));
		ret.setName("Concat("+ret.getName()+","+other.getName()+")");
		return ret;
	}
	
	/**
	 * Returns true if the given subsequence is contained in this sequence
	 * @param subsequence subsequence to search for
	 * 
	 */
	public boolean containsSubSequence(Sequence subsequence){
		
		for (int cursor = 0; cursor < getSequenceLengthWithGaps()-subsequence.getSequenceLengthWithGaps()+1; cursor++) {
			boolean ok = true;
			for (int i = 0; i < subsequence.getSequenceLengthWithGaps(); i++) {
				if (!subsequence.getPosition(i).getResidueType().equals(getPosition(cursor+i).getResidueType())) { 
					ok = false;
					break;
				}
			}
			if (ok) return true;
		}
		return false;
	
	}
	
	// toString, equals, clone
	
	/**
	 * Returns the String representation of sequence
	 *
	 * @return String representation
	 */
	public String toString(){
		return toString1Letter();
	}
	
	/**
	 * Returns a String representation in which each residue is represented by its 1 letter code
	 *
	 * @return String representation
	 */
	public String toString1Letter(){
		return toString1Letter(60);
	}
	
	/**
	 * Returns a String representation in which each residue is represented by its 3 letter code
	 *
	 * @return String representation
	 */
	public String toString3Letter(){
		return toString3Letter(15);
	}
	
	/**
	 * Returns a String representation in which each residue is represented by its 1 letter code
	 * and the string is split to lines of the given length
	 * @param lengthOfLine max length of a line in the string
	 * @return String representation
	 */
	public String toString1Letter(int lengthOfLine){
		StringBuffer buf = new StringBuffer(">"+getName()+"\n");
		for (int i = 0; i < positions.size(); i++) {
			if (i>0) { 
			if (i%lengthOfLine==0) buf.append("\n");
			}
			if (isPositionAGap(i)) buf.append("-");
			else buf.append(getResidueTypeOfPosition(i).get1LetterCode());
		}
		return buf.toString();
	}
	
	/**
	 * Returns a String representation in which each residue is represented by its 3 letter code
	 * and the string is split to lines of the given length
	 * @param lengthOfLine max length of a line (number of residues) in the string
	 * @return String representation
	 */
	public String toString3Letter(int lengthOfLine){
		StringBuffer buf = new StringBuffer(">"+getName()+"\n");
		for (int i = 0; i < positions.size(); i++) {
			if (i>0) { 
			if (i%lengthOfLine==0) buf.append("\n");
			else buf.append(" ");
			}
			if (isPositionAGap(i)) buf.append("---");
			else buf.append(getResidueTypeOfPosition(i).get3LetterCode());
		}
		return buf.toString();
	}
	
	/**
	 * Returns summary information about the sequence
	 *
	 * @return summary information
	 */
	public String toStringInfo(){
		return "Name: "+name+" | ("+getSequenceLengthWithGaps()+" sequence poisitions)";
	}
	
	/**
	 * Returns an identical Sequence object 
	 *
	 * @return clone of the sequence
	 */
	public Object clone(){
		Sequence ret;
		try {
			ret = (Sequence)super.clone();
			ret.name  = name;
			ret.positions = new ArrayList<SequencePosition>();
			for (int i = 0; i < positions.size(); i++){
			SequencePosition sp = (SequencePosition)positions.get(i).clone();
			ret.positions.add(sp);
			}
			return ret;
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			return null;
		}	
	}
	
	/**
	 * Returns true if the two sequences are identical
	 * @param other the other sequence
	 * 
	 */
	public boolean equals(Object other){
		if (other == null) return false;
		if (other == this) return true;
		if (this.getClass() != other.getClass()) return false;
		Sequence seq = (Sequence)other;
		if (!name.equals(seq.name)) return false;
		if (getSequenceLengthWithGaps()!=seq.getSequenceLengthWithGaps()) return false;
		for (int i = 0; i < getSequenceLengthWithGaps(); i++) {
			if (!getPosition(i).equals(seq.getPosition(i))) return false;
		}
		return true;
	}
	
	/**
	 * Returns hash code
	 * 
	 */
	public int hashCode() {
		return positions.size()+name.length();
	}
	
	private int calculateIndexUponAddition(){
		for (int i = positions.size()-1; i >= 0; i--) 
		if (!isPositionAGap(i)) return positions.get(i).getIndex()+1;
		return 1;
	}
	
	private int calculateIndexUponSetting(int k){
		for (int i = k-1; i >= 0; i--) 
		if (!isPositionAGap(i)) return positions.get(i).getIndex()+1;
	
		for (int i = k+1; i < positions.size(); i++) 
		if (!isPositionAGap(i)) {
			int I = positions.get(i).getIndex();
			if (I>1) return I-1;
			else return 0;
		}
		
		return 1;
	}
	
	
	private int calculateIndexUponInsertion(int k){
		for (int i = k-1; i >= 0; i--) 
		if (!isPositionAGap(i)) return positions.get(i).getIndex()+1;
	
		for (int i = k; i < positions.size(); i++) 
		if (!isPositionAGap(i)) {
			int I = positions.get(i).getIndex();
			if (I>1) return I-1;
			else return 0;
		}
		
		return 1;
	}
}
