/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */
package org.biojava.nbio.aaproperties;

import org.biojava.nbio.aaproperties.xml.AminoAcidCompositionTable;
import org.biojava.nbio.aaproperties.xml.CaseFreeAminoAcidCompoundSet;
import org.biojava.nbio.core.exceptions.CompoundNotFoundException;
import org.biojava.nbio.core.sequence.ProteinSequence;
import org.biojava.nbio.core.sequence.compound.AminoAcidCompound;
import org.biojava.nbio.core.sequence.compound.AminoAcidCompoundSet;
import org.biojava.nbio.core.sequence.io.*;
import org.biojava.nbio.core.sequence.template.CompoundSet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


public class CommandPrompt {

	/**
	 * The main method
	 * @param args
	 * 	See showHelp for a list of available arguments
	 * @throws Exception
	 *  To handle exception thrown by reading of XML files
	 */
	public static void main(String[] args) throws Exception{
		run(args);
	}

	private static AminoAcidCompositionTable checkForValidityAndObtainAATable(String inputLocation, int propertyListSize, String aminoAcidCompositionLocation,
			String elementMassLocation) throws Exception{
		if(inputLocation == null) {
			showHelp();
			throw new Error("Please do provide location of input file.");
		}
		if(propertyListSize == 0){
			showHelp();
			throw new Error("Please at least specify a property to compute.");
		}
		AminoAcidCompositionTable aaTable = null;
		if(aminoAcidCompositionLocation != null && elementMassLocation == null){
			aaTable = PeptideProperties.obtainAminoAcidCompositionTable(new File(aminoAcidCompositionLocation));
		}else if(aminoAcidCompositionLocation != null && elementMassLocation != null){
			aaTable = PeptideProperties.obtainAminoAcidCompositionTable(new File(aminoAcidCompositionLocation, elementMassLocation));
		}else if(aminoAcidCompositionLocation == null && elementMassLocation != null){
			throw new Error("You have define the location of Element Mass XML file. Please also define the location of Amino Acid Composition XML file");
		}
		return aaTable;
	}

	private static void readInputAndGenerateOutput(String outputLocation, List<Character> propertyList, List<Character> specificList,
			String delimiter, String inputLocation, AminoAcidCompositionTable aaTable, int decimalPlace) throws Exception{
		PrintStream output;
		if(outputLocation != null)
			output = new PrintStream(new File(outputLocation));
		else
			output = System.out;
		printHeader(output, propertyList, specificList, delimiter);
		LinkedHashMap<String, ProteinSequence> a = readInputFile(inputLocation, aaTable);
		//Need for the last sequence
		for(Entry<String, ProteinSequence> entry:a.entrySet()){
			compute(output, entry.getValue().getOriginalHeader(), entry.getValue().getSequenceAsString().trim(), delimiter, aaTable, propertyList, specificList,
					decimalPlace);
		}
		output.close();
	}

	public static void run(String[] args) throws Exception{
		/*
		 * Parse input arguments
		 */
		List<Character> propertyList = new ArrayList<Character>();
		List<Character> specificList = new ArrayList<Character>();
		String inputLocation = null;
		String outputLocation = null;
		String aminoAcidCompositionLocation = null;
		String elementMassLocation = null;
		String delimiter = ",";
		int decimalPlace = 4;

		for(int i = 0; i < args.length; i++){
			if(args[i].charAt(0) != '-' || args[i].length() != 2){
				showHelp();
				throw new Error("Unknown option: " + args[i]);
			}else{
				switch(args[i].charAt(1)){
				//Required
				case 'i': inputLocation = args[++i]; break;
				//Optional
				case 'o': outputLocation = args[++i]; break;
				case 'f':
					i++;
					if(args[i].equalsIgnoreCase("csv")) delimiter = ",";
					else if(args[i].equalsIgnoreCase("tsv")) delimiter = "\t";
					else throw new Error("Invalid value for -f: " + args[i] + ". Please choose either csv or tsv only.");
					break;
				case 'x': aminoAcidCompositionLocation = args[++i]; break;
				case 'y': elementMassLocation = args[++i]; break;
				case 'd': decimalPlace = Integer.parseInt(args[++i]); break;
				//Properties
				case 'a':
					propertyList.add('1');
					propertyList.add('2');
					propertyList.add('3');
					propertyList.add('4');
					propertyList.add('5');
					propertyList.add('6');
					propertyList.add('7');
					propertyList.add('8');
					propertyList.add('9');
					break;
				case '1': propertyList.add('1'); break;
				case '2': propertyList.add('2'); break;
				case '3': propertyList.add('3'); break;
				case '4': propertyList.add('4'); break;
				case '5': propertyList.add('5'); break;
				case '6': propertyList.add('6'); break;
				case '7': propertyList.add('7'); break;
				case '8': propertyList.add('8'); break;
				case '9': propertyList.add('9'); break;
				case '0':
					propertyList.add('0');
					i++;
					if(args[i].length() != 1) throw new Error("Invalid value: " + args[i] + ". Amino Acid Symbol should be of single character");
					specificList.add(args[i].toUpperCase().charAt(0));
					break;
				default:
					showHelp();
					throw new Error("Unknown option: " + args[i]);
				}
			}
		}

		/*
		 * Check for validity of input arguments
		 */
		AminoAcidCompositionTable aaTable = checkForValidityAndObtainAATable(inputLocation, propertyList.size(), aminoAcidCompositionLocation,
				elementMassLocation);

		/*
		 * Read input file and generate output
		 */
		readInputAndGenerateOutput(outputLocation, propertyList, specificList, delimiter, inputLocation, aaTable, decimalPlace);
	}

	private static LinkedHashMap<String, ProteinSequence> readInputFile(String inputLocation, AminoAcidCompositionTable aaTable) throws Exception{
		FileInputStream inStream = new FileInputStream(inputLocation);
		CompoundSet<AminoAcidCompound>	set;
		if(aaTable == null){
			set = CaseFreeAminoAcidCompoundSet.getAminoAcidCompoundSet();
		}else{
			set = aaTable.getAminoAcidCompoundSet();
		}
		LinkedHashMap<String, ProteinSequence> ret;
		if ( inputLocation.toLowerCase().contains(".gb")) {
			GenbankReader<ProteinSequence, AminoAcidCompound> genbankReader = new GenbankReader<ProteinSequence, AminoAcidCompound>(
					inStream, new GenericGenbankHeaderParser<ProteinSequence, AminoAcidCompound>(),
					new ProteinSequenceCreator(set));
			ret = genbankReader.process();


		} else {
			FastaReader<ProteinSequence, AminoAcidCompound> fastaReader = new FastaReader<ProteinSequence, AminoAcidCompound>(
					inStream, new GenericFastaHeaderParser<ProteinSequence, AminoAcidCompound>(),
					new ProteinSequenceCreator(set));
			ret = fastaReader.process();

		}
		return ret;
	}

	public enum PropertyName{MolecularWeight, Absorbance_True, Absorbance_False, ExtinctionCoefficient_True, ExtinctionCoefficient_False,
		InstabilityIndex, ApliphaticIndex, AverageHydropathyValue, IsoelectricPoint, NetCharge_pH_7, A, R,
		N, D, C, E, Q, G, H, I, L,
		K, M, F, P, S, T, W, Y, V};

	private static void printHeader(PrintStream output, List<Character> propertyList, List<Character> specificList, String delimiter) throws IOException{
		int specificCount = 0;
		/*
		 * 1 Molecular weight
		 * 2 Absorbance (assumed Cys reduced and assume Cys to form cystines)
		 * 3 Extinction coefficient (assumed Cys reduced and assume Cys to form cystines)
		 * 4 Instability index
		 * 5 Apliphatic index
		 * 6 Average hydropathy value
		 * 7 Isoelectric point
		 * 8 Net charge at pH 7
		 * 9 Composition of the 20 standard amino acid
		 * 0 Composition of the specific amino acid
		 */
		List<String> sList = new ArrayList<String>();
		sList.add("SequenceName");
		for(Character c:propertyList){
			switch(c){
			case '1': sList.add(PropertyName.MolecularWeight.toString()); break;
			case '2': sList.add(PropertyName.Absorbance_True.toString()); sList.add(PropertyName.Absorbance_False.toString()); break;
			case '3': sList.add(PropertyName.ExtinctionCoefficient_True.toString()); sList.add(PropertyName.ExtinctionCoefficient_False.toString()); break;
			case '4': sList.add(PropertyName.InstabilityIndex.toString()); break;
			case '5': sList.add(PropertyName.ApliphaticIndex.toString()); break;
			case '6': sList.add(PropertyName.AverageHydropathyValue.toString()); break;
			case '7': sList.add(PropertyName.IsoelectricPoint.toString()); break;
			case '8': sList.add(PropertyName.NetCharge_pH_7.toString()); break;
			case '9':
				sList.add(PropertyName.A.toString()); sList.add(PropertyName.R.toString());
				sList.add(PropertyName.N.toString()); sList.add(PropertyName.D.toString());
				sList.add(PropertyName.C.toString()); sList.add(PropertyName.E.toString());
				sList.add(PropertyName.Q.toString()); sList.add(PropertyName.G.toString());
				sList.add(PropertyName.H.toString()); sList.add(PropertyName.I.toString());
				sList.add(PropertyName.L.toString()); sList.add(PropertyName.K.toString());
				sList.add(PropertyName.M.toString()); sList.add(PropertyName.F.toString());
				sList.add(PropertyName.P.toString()); sList.add(PropertyName.S.toString());
				sList.add(PropertyName.T.toString()); sList.add(PropertyName.W.toString());
				sList.add(PropertyName.Y.toString()); sList.add(PropertyName.V.toString());
				break;
			case '0': sList.add("" + specificList.get(specificCount++)); break;
			}
		}
		for(int i = 0; i < sList.size(); i++){
			if(i != 0) output.print(delimiter);
			output.print(sList.get(i));
		}
		output.println();
		output.flush();
	}

	private static void compute(PrintStream output, String header, String sequence, String delimiter,
			AminoAcidCompositionTable aaTable, List<Character> propertyList, List<Character> specificList, int decimalPlace) throws CompoundNotFoundException{
		/*
		 * 1 Molecular weight
		 * 2 Absorbance (assumed Cys reduced and assume Cys to form cystines)
		 * 3 Extinction coefficient
		 * 4 Instability index
		 * 5 Apliphatic index
		 * 6 Average hydropathy value
		 * 7 Isoelectric point
		 * 8 Net charge at pH 7
		 * 9 Composition of the 20 standard amino acid
		 * 0 Composition of the specific amino acid
		 */
		ProteinSequence pSequence;
		CompoundSet<AminoAcidCompound> aaSet;
		if(aaTable != null){
			sequence = Utils.checkSequence(sequence, aaTable.getSymbolSet());
			pSequence = new ProteinSequence(sequence, aaTable.getAminoAcidCompoundSet());
			aaSet = aaTable.getAminoAcidCompoundSet();
		}else{
			sequence = Utils.checkSequence(sequence);
			pSequence = new ProteinSequence(sequence);
			aaSet = AminoAcidCompoundSet.getAminoAcidCompoundSet();
		}
		IPeptideProperties pp = new PeptidePropertiesImpl();

		int specificCount = 0;
		List<Double> dList = new ArrayList<Double>();
		for(Character c:propertyList){
			switch(c){
			case '1':
				if(aaTable == null)
					dList.add(pp.getMolecularWeight(pSequence));
				else
					dList.add(pp.getMolecularWeight(pSequence));
				break;
			case '2':
				dList.add(pp.getAbsorbance(pSequence, true));
				dList.add(pp.getAbsorbance(pSequence, false));
				break;
			case '3':
				dList.add(pp.getExtinctionCoefficient(pSequence, true));
				dList.add(pp.getExtinctionCoefficient(pSequence, false));
				break;
			case '4': dList.add(pp.getInstabilityIndex(pSequence)); break;
			case '5': dList.add(pp.getApliphaticIndex(pSequence)); break;
			case '6': dList.add(pp.getAvgHydropathy(pSequence)); break;
			case '7': dList.add(pp.getIsoelectricPoint(pSequence)); break;
			case '8': dList.add(pp.getNetCharge(pSequence)); break;
			case '9':
				Map<AminoAcidCompound, Double> aaCompound2Double = pp.getAAComposition(pSequence);
				//(A, R, N, D, C, E, Q, G, H, I, L, K, M, F, P, S, T, W, Y, V)
				dList.add(aaCompound2Double.get(Constraints.A));
				dList.add(aaCompound2Double.get(Constraints.R));
				dList.add(aaCompound2Double.get(Constraints.N));
				dList.add(aaCompound2Double.get(Constraints.D));
				dList.add(aaCompound2Double.get(Constraints.C));
				dList.add(aaCompound2Double.get(Constraints.E));
				dList.add(aaCompound2Double.get(Constraints.Q));
				dList.add(aaCompound2Double.get(Constraints.G));
				dList.add(aaCompound2Double.get(Constraints.H));
				dList.add(aaCompound2Double.get(Constraints.I));
				dList.add(aaCompound2Double.get(Constraints.L));
				dList.add(aaCompound2Double.get(Constraints.K));
				dList.add(aaCompound2Double.get(Constraints.M));
				dList.add(aaCompound2Double.get(Constraints.F));
				dList.add(aaCompound2Double.get(Constraints.P));
				dList.add(aaCompound2Double.get(Constraints.S));
				dList.add(aaCompound2Double.get(Constraints.T));
				dList.add(aaCompound2Double.get(Constraints.W));
				dList.add(aaCompound2Double.get(Constraints.Y));
				dList.add(aaCompound2Double.get(Constraints.V));
				break;
			case '0': dList.add(pp.getEnrichment(pSequence, aaSet.getCompoundForString("" + specificList.get(specificCount++)))); break;
			}
		}
		output.print(header.replace(delimiter, "_"));
		dList.stream().forEach(item -> output.print(delimiter + Utils.roundToDecimals(item, decimalPlace)));
		output.println();
		output.flush();
	}

	private static void showHelp(){
		System.err.println("NAME");
		System.err.println("\tAn executable to generate physico-chemical properties of protein sequences.");
		System.err.println();

		System.err.println("EXAMPLES");
		System.err.println("\tjava -jar AAProperties.jar -i test.fasta -a");
		System.err.println("\t\tGenerates all possible properties.");
		System.err.println();
		System.err.println("\tjava -jar AAProperties.jar -i test.fasta -1 -3 -7");
		System.err.println("\t\tGenerates only molecular weight, extinction coefficient and isoelectric point.");
		System.err.println();
		System.err.println("\tjava -jar AAProperties.jar -i test.fasta -0 A -0 N -1");
		System.err.println("\t\tGenerates composition of two specific amino acid symbol and molecular weight.");
		System.err.println();

		System.err.println("OPTIONS");
		System.err.println("\tRequired");
		System.err.println("\t\t-i location of input FASTA file");
		System.err.println();

		System.err.println("\tOptional");
		System.err.println("\t\t-o location of output file [standard output (default)]");
		System.err.println("\t\t-f output format [csv (default) or tsv]");
		System.err.println("\t\t-x location of Amino Acid Composition XML file for defining amino acid composition");
		System.err.println("\t\t-y location of Element Mass XML file for defining mass of elements");
		System.err.println("\t\t-d number of decimals (int) [4 (default)]");
		System.err.println();

		System.err.println("\tProvide at least one of them");
		System.err.println("\t\t-a compute properties of option 1-9");
		System.err.println("\t\t-1 compute molecular weight");
		System.err.println("\t\t-2 compute absorbance");
		System.err.println("\t\t-3 compute extinction coefficient");
		System.err.println("\t\t-4 compute instability index");
		System.err.println("\t\t-5 compute apliphatic index");
		System.err.println("\t\t-6 compute average hydropathy value");
		System.err.println("\t\t-7 compute isoelectric point");
		System.err.println("\t\t-8 compute net charge at pH 7");
		System.err.println("\t\t-9 compute composition of 20 standard amino acid (A, R, N, D, C, E, Q, G, H, I, L, K, M, F, P, S, T, W, Y, V)");
		System.err.println("\t\t-0 compute composition of specific amino acid symbol");
		System.err.println();
	}
}
