/* Copyright (c) 2012,
 * Mathias Kuhring, KuhringM@rki.de, Robert Koch-Institut, Germany, 
 * All rights reserved. For details, please note the license.txt.
 */

package de.rki.ng4.ipig;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Iterator;
import java.util.Properties;
import java.util.Map.Entry;

import de.rki.ng4.ipig.data.Gene;
import de.rki.ng4.ipig.data.GeneSet;
import de.rki.ng4.ipig.exceptions.ExitException;
import de.rki.ng4.ipig.tools.Configurator;
import de.rki.ng4.ipig.tools.Info;
import de.rki.ng4.ipig.tools.Logger;
import de.rki.ng4.ipig.tools.Translator;

/**
 * <p>The GeneControl sorts out genes with impractical or inconvenient properties.</p>
 * 
 * <p>In first place, the gene annotation has to be consistent with the corresponding aminoacid sequence (reference).</p>
 * 
 * <p>The criteria are:</p>
 * <p>1. Is the cds congruend mod 3?</p>
 * <p>2. Does the expession of the cds lead to an peptide similar to the provided references?</p> 
 * <p>3. What is the position of the stop codon?</p> 
 * 
 * <p>There will be different output files, one containing the useful gene annotations, one containing the useless once
 * and a log file with some hints/explanations regarding the rejected annotations. The first file is suitable for the mapping.</p>
 * 
 * @author Mathias Kuhring
 *
 */
public class GeneControl {

	private static Translator trans;
	private static double simThresh;

	public void run() throws ExitException {
		checkFiles();

		Info info = new Info("start gene control");
		System.out.println();

		// parameter parsing
		try{
			simThresh = Double.parseDouble(Configurator.getProperty("minSimilarity", "0.95"));
			if (simThresh > 1 || simThresh <= 0);
		}
		catch (NumberFormatException e){
			simThresh = 0.95;
			String message = "error:\tunsuitable similarity threshold (" + e.getMessage() + ")\n" 
					+ "\tuse default value instead (minSimilarity=" + simThresh + ")";
			System.out.println(message);
			Logger.write(Configurator.getSysProperty("msPepSetName", "genecontrol")+".log", message);
		}

		// get filenames

		String geneAnnoFile	= Configurator.getProperty("FgeneAnnoFile");
		File gaFile = new File(geneAnnoFile);
		String gaFileName = gaFile.getName().substring(0, gaFile.getName().lastIndexOf("."));
		String geneAaSeqFile = Configurator.getProperty("FgeneAaSeqFile");

		String outputPath = new File(Configurator.getProperty("FoutputPath","")).getAbsolutePath() + "/";

		// load annotations
		Info.setInfomode(true);
		Gene.setStrict(false);
		GeneSet genes = new GeneSet(geneAnnoFile);
		Info.setInfomode(false);
		
		try {
			// preparing output files
			BufferedWriter logBuffer = new BufferedWriter (new FileWriter(new File(outputPath + gaFileName + "_control.log")));

			BufferedWriter goodBuffer = new BufferedWriter (new FileWriter(new File(outputPath + gaFileName + "_good.txt")));
			goodBuffer.write(genes.getHeader());
			goodBuffer.newLine();

			BufferedWriter badBuffer = new BufferedWriter (new FileWriter(new File(outputPath + gaFileName + "_bad.txt")));
			badBuffer.write(genes.getHeader());
			badBuffer.newLine();

			System.out.println("check for non-coding genes and uncommon chromosomes");
			checkNonCoding(genes, badBuffer, logBuffer);
			checkUncommonChroms(genes, badBuffer, logBuffer);

			// calc. gene subset size to avoid heap overflow
			long Xmx = Runtime.getRuntime().freeMemory();
			int max = (int) Xmx / (1024*1024);
			int size = genes.size();
			int parts = (int) Math.ceil((double) size / (double) max);
			System.out.println("check integrity in " + parts + " parts with " + max + " genes each");

			GeneSet subset;

			if (trans == null)
				trans = new Translator();

			// loading and checking sequences for each subset
			for (int i=0; i<parts; i++){
				Configurator.checkBreak();

				System.out.println("part " + (i+1) + " of " + parts + "...");
				// getting the subset
				subset = new GeneSet(genes.getSubset(i*max, i*max+max));
				// loading the sequences
				subset.readAaSequences(geneAaSeqFile);
				subset.readNaSequences(Configurator.getProperty("refSeqPath"));

				String expr;
				String message;
				int stop;
				double similarity;
				for (Gene gene : subset.getAll()){
					// checking the gene

					expr = getExpression(gene);

					// case 0: expr == null if the cds is not congruent 0 mod 3, so couldn't translated correct (regarding the codons)
					if (expr != null){
						stop = checkStopPos(expr);
						similarity = checkSimilarity(expr, gene.getAaSequence());

						// case 1: stop at cds end and quite similar to reference
						if (stop == 0 && similarity >= simThresh){
							good(goodBuffer, gene);
						}
						/* case 2: cds excludes stop but quite similar to reference -> useful after cds extension by on codon:
						 * usually, the last codon in the cds is a stop codon, which is not included in the aa seq, so cds-length - 1 == aa-seq-length.
						 * but if the stop codon is not within the cds (e.g. it's unknown or cds is incomplete), cds-length == aa-seq-length.
						 * this would cause some trouble within the mapping calculations (coordinate transformations of reverse strand mappings in particular).
						 * So extending the cds by on codon gives us cds-length - 1 == aa-seq-length.
						 * Indeed, there is no new stop codon now, so you should't use these "adapted" annotations for something else than the mapper.
						 */
						else if (stop == 1 && similarity >= simThresh){
							extendCds(gene);
							message = "cds excludes stop -> extended by one codon";
							log(logBuffer, gene, expr, message);
							good(goodBuffer, gene);
						}
						// case 3: stop occurs before cds end -> ejected
						// might be correctable if reference matches expression up to the stop,
						// but probably hard to consider introns
						else if (stop == -1){
							message = "a stop before cds end";
							log(logBuffer, gene, expr, message);
							bad(badBuffer, gene);
						}
						// case 4: similarity of 0 means different length compared to reference -> ejected
						else if (similarity == 0){
							message = "deviation to reference in length";
							log(logBuffer, gene, expr, message);
							bad(badBuffer, gene);
						}
						// case 5: if similarity to low, gene might not be trustworthy, well explored or annotated -> ejected
						else if (stop == 0 && similarity < simThresh){
							message = "similarity to reference < " + simThresh + " (e.g. to many SNPs etc.)";
							log(logBuffer, gene, expr, message);
							bad(badBuffer, gene);
						}
						else{
							message = "uncategorized case (stop = " + stop + ", similarity = " + similarity + ")";
							log(logBuffer, gene, expr, message);
							bad(badBuffer, gene);
						}
					}
					else{
						message = "cds not concruent mod 3 (no expression)";
						log(logBuffer, gene, expr, message);
						bad(badBuffer, gene);
					}
				}

				subset.deleteAaSequences();
				subset.deleteNaSequences();
				subset = null;
				System.gc();
			}

			logBuffer.close();
			goodBuffer.close();
			badBuffer.close();
		} catch (IOException e) {
			throw new ExitException(e.getMessage());
		}

		Info.setInfomode(true);
		info.stop();
		Info.setInfomode(false);
	}

	private void checkNonCoding(GeneSet genes, BufferedWriter badBuffer, BufferedWriter logBuffer) throws ExitException {
		Gene gene;
		for (Iterator<Gene> it = genes.getAll().iterator(); it.hasNext();){
			gene = it.next();
			if (gene.getCdsStart() == gene.getCdsEnd()){
				String message = "non-coding gene";
				try {
					logBuffer.newLine();
					logBuffer.write(gene.getName() + ": " + message);
					logBuffer.newLine();
					bad(badBuffer, gene);
				} catch (IOException e) {
					throw new ExitException(e.getMessage());
				}
				it.remove();
			}
		}
		System.gc();
	}

	private void checkUncommonChroms(GeneSet genes, BufferedWriter badBuffer, BufferedWriter logBuffer) throws ExitException{
		Gene gene;
		for (Iterator<Gene> it = genes.getAll().iterator(); it.hasNext();){
			gene = it.next();
			if (!gene.getChrom().matches("chr(\\d+|X|Y|M|[IVX]+)")){
				String message = "uncommon chromosome (" + gene.getChrom() + ")";
				try {
					logBuffer.newLine();
					logBuffer.write(gene.getName() + ": " + message);
					logBuffer.newLine();
					bad(badBuffer, gene);
				} catch (IOException e) {
					throw new ExitException(e.getMessage());
				}
				it.remove();
			}
		}
		System.gc();
	}

	// write into the log file
	private void log(BufferedWriter logBuffer, Gene gene, String expr, String message) throws IOException{
		logBuffer.newLine();
		logBuffer.write(gene.getName() + ": " + message);
		logBuffer.newLine();
		logBuffer.write(">reference:  " + gene.getAaSequence());
		logBuffer.newLine();
		logBuffer.write(">expression: " + expr);
		logBuffer.newLine();
	}

	// write good/useful genes
	private void good(BufferedWriter goodBuffer, Gene gene) throws IOException{
		goodBuffer.write(gene.toString());
		goodBuffer.newLine();
	}

	// write bad/non-useful genes
	private void bad(BufferedWriter badBuffer, Gene gene) throws IOException{
		badBuffer.write(gene.toString());
		badBuffer.newLine();
	}

	// extends a gene's cds by 3, so one codon
	private void extendCds(Gene gene){
		if (gene.getStrand() == '+')
			gene.shiftCdsEnd(3);
		else
			gene.shiftCdsStart(-3);
	}

	// checks where the first stop (*) occurs, either at the end of the cds/expr (-> 0), before (-> -1) or after (-> 1).
	private int checkStopPos(String expr){
		String[] splits = expr.split("\\*");

		// case1: cds includes stop, but not at the end
		if (splits[0].length() < expr.length()-1){
			return -1;
		}
		// case2: cds excludes stop 
		if (splits[0].length() > expr.length()-1){
			return 1;
		}
		// case3: cds includes stop at the end (splits[0].length() == expr.length()-1)
		return 0;
	}

	// returns the expression of a gene, so the resulting peptide
	// returns null if the cds is not congruent mod 3
	private String getExpression(Gene gene) {
		String cds;
		if (gene.getStrand() == '+'){
			cds = gene.getCds();
		}
		else{
			cds = Translator.complement(new StringBuffer(gene.getCds()).reverse().toString());
		}
		if (checkCongruence3(cds))
			return trans.dnaToPeptide(cds);
		else
			return null;
	}

	// checks if the cds is congruent mod 3
	private boolean checkCongruence3(String cds){
		if (cds.length() % 3 == 0)
			return true;
		else
			return false;
	}

	// computes the similarity between the expression (up to an stop codon, if included) and the aa sequence
	// similarity is given between 0 (lowest) and 1 (highest). If different in length than similarity is 0.
	private double checkSimilarity(String expr, String aaseq){
		expr = expr.split("\\*")[0];
		double distance = hammingDistance(expr, aaseq);
		double similarity;

		if (distance > -1)
			similarity = (expr.length()-distance)/expr.length();
		else
			similarity = 0;

		return similarity;
	}

	// an easy metric for similarity computation.
	// if the seqs have the same length, every different position increases the distance by one.
	// if the length is different it returns -1-
	private int hammingDistance(String expr, String aaseq){
		if (!checkLength(expr, aaseq))
			return -1;

		int distance = 0;

		for (int c=0; c<expr.length(); c++){
			if (expr.charAt(c) != aaseq.charAt(c))
				distance++;
		}

		return distance;
	}

	// compares the length between the expression (up to an stop codon, if included) and the aa sequence
	private boolean checkLength(String expr, String aaseq){
		if (expr.split("\\*")[0].length() == aaseq.length())
			return true;
		else
			return false;
	}

	// checks if all necessary files/paths are indicated and if they exist
	private static void checkFiles() throws ExitException{
		StringBuffer message = new StringBuffer();
		boolean failed = false;
		Properties props = Configurator.getProperties();

		if (!props.containsKey("FgeneAnnoFile")){
			if (failed) message.append("\n");
			failed = true;
			message.append("no gene annotation file specified");
		}
		if (!props.containsKey("FgeneAaSeqFile")){
			if (failed) message.append("\n");
			failed = true;
			message.append("no gene aa-sequence file specified");
		}
		if (!props.containsKey("refSeqPath")){
			if (failed) message.append("\n");
			failed = true;
			message.append("no chromosome path specified");
		}
		if (props.containsKey("FoutputPath") && props.getProperty("FoutputPath").matches("")){
			Configurator.removeProperty("FoutputPath");
		}
		if (failed) throw new ExitException(message.toString());

		for (Entry<Object, Object> entry : Configurator.getProperties().entrySet()){
			if (((String) entry.getKey()).contains("File")){
				File test = new File((String) entry.getValue());
				checkFile(test);
			}
			if (((String) entry.getKey()).contains("Path")){
				File test = new File((String) entry.getValue());
				if (!test.isDirectory()){
					throw new ExitException("error: can't find path (" + test.getAbsolutePath() + ")");
				}
			}
		}
	}

	// a simple file check
	private static void checkFile(File test) throws ExitException{
		if (!test.isFile()){
			throw new ExitException("error: can't find file (" + test.getAbsolutePath() + ")");
		}
	}
}
