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

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Vector;

import de.rki.ng4.ipig.exceptions.ExitException;
import de.rki.ng4.ipig.mapping.Mapper;
import de.rki.ng4.ipig.mapping.Position;
import de.rki.ng4.ipig.tools.Configurator;
import de.rki.ng4.ipig.tools.Info;
import de.rki.ng4.ipig.tools.Logger;

/**
 * <p>The Exporter is in charge of exporting peptides (as a @see PeptideSet) into BED of GFF3 files.</p>
 * 
 * <p>It will uses some user parameters regarding the coloring or categorization of peptides by scores
 * from the config file or gui resp.</p>
 * 
 * <p>For more informations on the BED or GFF3 format see http://genome.ucsc.edu/goldenPath/help/customTrack.html#BED
 * or http://www.sequenceontology.org/gff3.shtml resp.</p>
 * 
 * @author Mathias Kuhring
 */
public class Exporter {

	// user parameters as in a ipig config file or gui, will be loaded in the constructor
	private int minScore;
	private int maxScore;
	private double thresh1;
	private double thresh2;
	private String color1;
	private String color2;
	private String color3;

	/**
	 * The Constructor tries to read necessary user parameter from the Configurator.
	 * 
	 * @throws ExitException if it fails to parse the user parameters
	 */
	public Exporter() throws ExitException{
		try{
			minScore = Integer.parseInt(Configurator.getProperty("minScore"));
			maxScore = Integer.parseInt(Configurator.getProperty("maxScore"));
			if (minScore >= maxScore){
				throw new NumberFormatException("minScore >= maxScore");
			}
		}
		catch (NumberFormatException e){
			String message = "error:\tunsuitable score bounds (" + e.getMessage() + ")";
			Logger.write(Configurator.getSysProperty("msPepSetName", "ipig")+".log", message);
			throw new ExitException(message);
		}

		try{
			thresh1 = Double.parseDouble(Configurator.getProperty("threshold1"));
			thresh2 = Double.parseDouble(Configurator.getProperty("threshold2"));
			if (!((minScore < thresh1) && (thresh1 < thresh2) && (thresh2 < maxScore))){
				throw new NumberFormatException("relation needed: minScore < threshold1 < threshold2 < maxScore");
			}
		}
		catch (NumberFormatException e){
			String message = "error:\tunsuitable thresholds (" + e.getMessage() + ")";
			Logger.write(Configurator.getSysProperty("msPepSetName", "ipig")+".log", message);
			throw new ExitException(message);
		}

		try{
			color1 = Configurator.getProperty("color1");
			color2 = Configurator.getProperty("color2");
			color3 = Configurator.getProperty("color3");
			checkColor(color1);
			checkColor(color2);
			checkColor(color3);
		}
		catch (NumberFormatException e){
			String message = "error:\tunsuitable colors (" + e.getMessage() + ")";
			Logger.write(Configurator.getSysProperty("msPepSetName", "ipig")+".log", message);
			throw new ExitException(message);
		}	
	}
	
	/*
	 * Checks if color is in the correct format and range
	 * from 000,000,000 up to 255,255,255
	 */
	private void checkColor (String color){
		if (color == null)
			throw new NumberFormatException("incorrect rgb value: " + color);
		String[] splits = color.split(",");
		if (splits.length != 3){
			throw new NumberFormatException("incorrect rgb value: " + color);
		}
		int value;
		for (String split : splits){
			value = Integer.parseInt(split);
			if (value < 0 || value > 255){
				throw new NumberFormatException("incorrect rgb value: " + color);
			}
		}
	}

	/**
	 * <p>This function writes a set of peptides into a bed formated file.</p>
	 * 
	 * <p>A peptide corresponds to a line in bed format (http://genome.ucsc.edu/goldenPath/help/customTrack.html#BED).</p>
	 * 
	 * <p>The bed file will have different tracks for uniqueness (unique, non-unique, unmarked)
	 * and the mapping methods (annotation or alternative), so at most 3*2 = 6 tracks.
	 * Unmapped peptides won't be exported.</p>
	 * 
	 * <p>Modification to the format are as follows:</p>
	 * 
	 * <p>The score is the peptide's original score, so it's not scaled between 0 and 1000.
	 * For coloring purposes each track uses the parameter itemRgb="On" and each peptide line
	 * provide an rgb color which depends on the score and color settings in the configuration. </p>
	 * 
	 * @param pepset
	 * @param outputPath
	 * @param msPepSetName
	 * @throws ExitException
	 */
	public void bed(PeptideSet pepset, String outputPath, String msPepSetName) throws ExitException{
		Info info = new Info("write bed");
		int count = 0; 

		try {
			
			Peptide pep;
			
			// prepare unique peptides
			Vector<Integer> pepidx = pepset.getUnique("unique");
			StringBuffer uniqueAnno = new StringBuffer("");
			StringBuffer uniqueAlt = new StringBuffer("");
			for (int p=0; p<pepidx.size(); p++){
				pep = pepset.get(pepidx.get(p));
				if (pep.isGeneMapped()){
					if (pep.getMatchMethod().matches(Mapper.ANNOTATION)){
						if (uniqueAnno.toString().matches(""))
							uniqueAnno.append("track name=\"" + msPepSetName + " (u,anno)\" description=\"" + msPepSetName + " (unique, annotation mapped)\" visibility=full itemRgb=On\n");
						uniqueAnno.append(bedFeature(pep) + "\n");
					}
					if (pep.getMatchMethod().matches(Mapper.ALTERNATIVE)){
						if (uniqueAlt.toString().matches(""))
							uniqueAlt.append("track name=\"" + msPepSetName + " (u,alt)\" description=\"" + msPepSetName + " (unique, alternative mapped)\" visibility=full itemRgb=On\n");
						uniqueAlt.append(bedFeature(pep) + "\n");
					}
					count++;
				}
				Configurator.checkBreak();
			}

			// prepare nonunique peptides
			pepidx = pepset.getUnique("nonunique");
			StringBuffer nonuniqueAnno = new StringBuffer("");
			StringBuffer nonuniqueAlt = new StringBuffer("");
			for (int p=0; p<pepidx.size(); p++){
				pep = pepset.get(pepidx.get(p));
				if (pep.isGeneMapped()){
					if (pep.getMatchMethod().matches(Mapper.ANNOTATION)){
						if (nonuniqueAnno.toString().matches(""))
							nonuniqueAnno.append("track name=\"" + msPepSetName + " (n,anno)\" description=\"" + msPepSetName + " (nonunique, annotation mapped)\" visibility=full itemRgb=On\n");
						nonuniqueAnno.append(bedFeature(pep) + "\n");
					}
					if (pep.getMatchMethod().matches(Mapper.ALTERNATIVE)){
						if (nonuniqueAlt.toString().matches(""))
							nonuniqueAlt.append("track name=\"" + msPepSetName + " (n,alt)\" description=\"" + msPepSetName + " (nonunique, alternative mapped)\" visibility=full itemRgb=On\n");
						nonuniqueAlt.append(bedFeature(pep) + "\n");
					}
					count++;
				}
				Configurator.checkBreak();
			}

			// prepare unmarked peptides
			pepidx = pepset.getUnique("rest");
			StringBuffer unmarkedAnno = new StringBuffer("");
			StringBuffer unmarkedAlt = new StringBuffer("");
			for (int p=0; p<pepidx.size(); p++){
				pep = pepset.get(pepidx.get(p));
				if (pep.isGeneMapped()){
					if (pep.getMatchMethod().matches(Mapper.ANNOTATION)){
						if (unmarkedAnno.toString().matches(""))
							unmarkedAnno.append("track name=\"" + msPepSetName + " (anno)\" description=\"" + msPepSetName + " (unmarked, annotation mapped)\" visibility=full itemRgb=On\n");
						unmarkedAnno.append(bedFeature(pep) + "\n");
					}
					if (pep.getMatchMethod().matches(Mapper.ALTERNATIVE)){
						if (unmarkedAlt.toString().matches(""))
							unmarkedAlt.append("track name=\"" + msPepSetName + " (alt)\" description=\"" + msPepSetName + " (unmarked, alternative mapped)\" visibility=full itemRgb=On\n");
						unmarkedAlt.append(bedFeature(pep) + "\n");
					}
					count++;
				}
				Configurator.checkBreak();
			}

			BufferedWriter trackBuffer = new BufferedWriter (new FileWriter(new File(outputPath + msPepSetName + "_annomapped.bed")));
			trackBuffer.write(uniqueAnno.toString() + nonuniqueAnno.toString() + unmarkedAnno.toString());
			trackBuffer.close();
			
			BufferedWriter trackBuffer2 = new BufferedWriter (new FileWriter(new File(outputPath + msPepSetName + "_altmapped.bed")));
			trackBuffer2.write(uniqueAlt.toString() + nonuniqueAlt.toString() + unmarkedAlt.toString());
			trackBuffer2.close();
		} catch (IOException e) {
			System.out.println(e.getMessage());
		}

		info.stop(count, pepset.size());
	}

	/*
	 * Building a string representation of a peptide as in bed format (http://genome.ucsc.edu/goldenPath/help/customTrack.html#BED).
	 * Modification to the format are as written in the function bed().
	 */
	private String bedFeature(Peptide pep){
		StringBuffer line = new StringBuffer();

		for (Position pos : pep.getPositions()){

			line.append(pos.getChrom() + "\t");
			line.append(pos.getStartPos().firstElement() + "\t");
			line.append(pos.getEndPos().lastElement() + "\t");

			if (pos.getStrand() == '+')
				line.append(pep.getPep_query() + pos.getModifier() + "_" + pep.getSequence() 
						+ "_z=" + pep.getPep_exp_z() + "_sh=" + pep.getPositions().size() + "_>" + "\t");
			else
				line.append(pep.getPep_query() + pos.getModifier() + "_" + new StringBuffer(pep.getSequence()).reverse().toString() 
						+ "_z=" + pep.getPep_exp_z() + "_sh=" + pep.getPositions().size() + "_<" + "\t");

			line.append(pep.getPep_Score() + "\t");
			line.append(pos.getStrand() + "\t");
			line.append(pos.getStartPos().firstElement() + "\t");
			line.append(pos.getEndPos().lastElement() + "\t");

			line.append(bedColor(pep) + "\t");

			line.append(pos.getStartPos().size() + "\t");
			for (int i=0; i<pos.getStartPos().size(); i++){
				line.append((pos.getEndPos().get(i) - pos.getStartPos().get(i)) + ",");
			}
			line.append("\t");
			for (int i=0; i<pos.getStartPos().size(); i++){
				line.append((pos.getStartPos().get(i) - pos.getStartPos().firstElement()) + ",");
			}

			line.append("\n");
		}

		line.delete(line.lastIndexOf("\n"), line.length());
		return line.toString();
	}

	/*
	 * Determines the rgb string for a peptide's bed string, depending on the score and color settings in the configuration.
	 */
	private String bedColor(Peptide pep){
		double score = 0;
		try{
			score = Double.parseDouble(pep.getPep_Score());
		}
		catch (NumberFormatException e){
			String message = "score parsing error:\t" + pep.getSequence() + "\t" + pep.getPep_Score();
			System.out.println(message);
			Logger.write(Configurator.getSysProperty("msPepSetName", "ipig")+".log", message);
		}

		if (score > thresh2) return color3;
		else if (score > thresh1) return color2;
		else return color1;
	}

	/**
	 * <p>This function writes a set of peptides into gff3 formated files.</p>
	 * 
	 * <p>A peptide is represented as a feature in gff3 format (http://www.sequenceontology.org/gff3.shtml).</p>
	 * 
	 * <p>The peptides are separated into to different files ending with "_annomapped.gff3" and "_altmapped.gff3"
	 *  corresponding to the method the peptides are mapped to the genome (Mapper.ANNOTATION or Mapper.ALTERNATIVE).
	 *  Unmapped peptides won't be exported.</p>
	 * 
	 * <p>Modification to the format are as follows:</p>
	 * 
	 * <p>The SOURCE is "ipig"</p>
	 * 
	 * <p>The values in the TYPE column are a self creations, starting with "peptide", followed by a mark for the uniqueness (unique, non-unique, unmarked)
	 * and a classification for the score in three groups (depending on user parameters threshold1 and threshold2).</p>
	 * 
	 * <p>The SCORE is the same as in the input file.</p>
	 * 
	 * <p>ATTRIBUTES are:</p>
	 * <p>1.)	ID, which is the pep_query plus a modifier to make the id unique.
	 * 		The ID is in charge for grouping parts of the same feature from different lines,
	 * 		so the importing software should handle those as one element.</p>
	 * <p>2.)	Name, which is the peptide sequence.
	 * 		Best way to keep the sequence in this format and to compare it with translations (e.g. in Geneious).</p>
	 * <p>3.)	z, custom attribute representing the charge the peptide is measured with.</p>
	 * <p>4.)	shared, custom attribute representing the peptide's frequency of occurrence on different positions (including the current).</p>
	 
	 * @param pepset	A PeptideSet with the peptides to be exported
	 * @param outputPath Path were the files will be written
	 * @param msPepSetName	Name of peptide set, will be used as part of the filename
	 * @throws ExitException 
	 */
	public void gff3(PeptideSet pepset, String outputPath, String msPepSetName) throws ExitException{
		Info info = new Info("write gff3");
		int count = 0; 

		try {
			BufferedWriter gffBuffer = new BufferedWriter (new FileWriter(new File(outputPath + msPepSetName + "_annomapped.gff3")));
			BufferedWriter gffBuffer2 = new BufferedWriter (new FileWriter(new File(outputPath + msPepSetName + "_altmapped.gff3")));

			for (Peptide pep : pepset.getAll()){
				if (pep.isGeneMapped()){
					if (pep.getMatchMethod().matches(Mapper.ANNOTATION)){
						gffBuffer.write(gff3Feature(pep));
						gffBuffer.newLine();
					}
					if (pep.getMatchMethod().matches(Mapper.ALTERNATIVE)){
						gffBuffer2.write(gff3Feature(pep));
						gffBuffer2.newLine();
					}
					count++;
				}

				Configurator.checkBreak();
			}

			gffBuffer.close();
			gffBuffer2.close();
		} 
		catch (IOException e) {
			System.out.println(e.getMessage());
		}

		info.stop(count, pepset.size());
	}

	/*
	 * Building a string representation of a peptide as a feature in gff3 format (http://www.sequenceontology.org/gff3.shtml).
	 * Modification to the format are as written in the function gff().
	 */
	private String gff3Feature(Peptide pep) {
		/* Example
		 * chr10	ipig	peptide_non-unique_low	104111702	104111708	31.83	+	2	ID=8b; Name=LSELLR; z=2; 
		 * chr10	ipig	peptide_non-unique_low	104112215	104112225	31.83	+	0	ID=8b; Name=LSELLR; z=2; 
		 */

		StringBuffer lines = new StringBuffer();

		for (Position pos : pep.getPositions()){

			Vector<Integer> phases = phases(pos);

			for (int i=0; i<pos.getStartPos().size(); i++){
				lines.append(pos.getChrom() + "\t");
				lines.append("ipig" + "\t");

				if (pep.getPep_isunique().matches("1"))
					lines.append("peptide_unique_" + gff3ScoreRating(pep) + "\t");
				else if (pep.getPep_isunique().matches("0"))
					lines.append("peptide_non-unique_" + gff3ScoreRating(pep) + "\t");
				else
					lines.append("peptide_unmarked_" + gff3ScoreRating(pep) + "\t");

				lines.append((pos.getStartPos().get(i) + 1) + "\t");
				lines.append(pos.getEndPos().get(i) + "\t");

				lines.append(pep.getPep_Score() + "\t");
				lines.append(pos.getStrand() + "\t");
				lines.append(phases.get(i) + "\t");

				lines.append("ID=" + pep.getPep_query() + pos.getModifier() + "; ");

				if (pos.getStrand() == '+')
					lines.append("Name=" + pep.getSequence() + "; ");
				else
					lines.append("Name=" + new StringBuffer(pep.getSequence()).reverse().toString() + "; ");

				lines.append("z=" + pep.getPep_exp_z() + "; ");
				lines.append("shared=" + pep.getPositions().size() + "; ");
				lines.append("mods=\"" + pep.getPep_var_mod() + "\"; ");
				lines.append("modpos=" + pep.getPep_var_mod_pos1() + "; ");

				lines.append("\n");
			}
		}

		lines.delete(lines.lastIndexOf("\n"), lines.length());
		return lines.toString();
	}

	/*
	 * Classification of a peptides score into three categories: low, mid, high.
	 * The separation of the three categories is set with the user parameters threshold1 and threshold2
	 */
	private String gff3ScoreRating(Peptide pep){
		double score = 0;
		try{
			score = Double.parseDouble(pep.getPep_Score());
		}
		catch (NumberFormatException e){
			String message = "score parsing error:\t" + pep.getSequence() + "\t" + pep.getPep_Score();
			System.out.println(message);
			Logger.write(Configurator.getSysProperty("msPepSetName", "ipig")+".log", message);
		}

		if (score > thresh2) return "high";
		else if (score > thresh1) return "mid";
		else return "low";
	}

	/*
	 * Calculates the phase of each part the peptides's position (e.g. if separated by an intron).
	 * Depending on the direction (+ or -) the first resp. last part is assumed to have phase 0.
	 * The other parts might have phase 1 or 2 if a codon is split by an intron.
	 * 
	 * See gff3 format description (http://www.sequenceontology.org/gff3.shtml) for more details regarding directions. 
	 */
	private Vector<Integer> phases(Position pos){
		Vector<Integer> phases = new Vector<Integer>();

		int sum = 0;
		if (pos.getStrand() == '+'){
			for (int i=pos.getStartPos().size()-1; i>-1; i--){
				sum += pos.getEndPos().get(i) - pos.getStartPos().get(i);
				phases.add(0, sum%3);
			}
		}
		else{
			for (int i=0; i<pos.getStartPos().size(); i++){
				sum += pos.getEndPos().get(i) - pos.getStartPos().get(i);
				phases.add(sum%3);
			}
		}

		return phases;
	}
}
