/* $Revision$ $Author$ $Date$
 *
 * Copyright (C) 2007  Miguel Rojasch <miguelrojasch@users.sf.net>
 * 
 * Contact: cdk-devel@lists.sourceforge.net
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 * 
 * This program 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 
 */
package org.openscience.cdk.formula;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

import org.openscience.cdk.annotations.TestClass;
import org.openscience.cdk.config.IsotopeFactory;
import org.openscience.cdk.interfaces.IChemObjectBuilder;
import org.openscience.cdk.interfaces.IIsotope;
import org.openscience.cdk.interfaces.IMolecularFormula;
import org.openscience.cdk.tools.LoggingTool;
import org.openscience.cdk.tools.manipulator.MolecularFormulaManipulator;

/**
 * Generates all Combinatorial chemical isotopes given a structure.
 * 
 * @cdk.module  formula
 * @author      Miguel Rojas Cherto
 * @cdk.created 2007-11-20
 * @cdk.githash
 *
 * @cdk.keyword isotope pattern
 * 
 */
@TestClass("org.openscience.cdk.formula.IsotopePatternGeneratorTest")
public class IsotopePatternGenerator{

	private IChemObjectBuilder builder = null;
	private IsotopeFactory isoFactory;
	private IsotopePattern abundance_Mass = null;
	
	private LoggingTool logger = new LoggingTool(IsotopePatternGenerator.class);
	
	/** Minimal abundance of the isotopes to be added in the combinatorial search.*/
	private double minAbundance = .1;

	/**
	 *  Constructor for the IsotopeGenerator.
	 */
	public IsotopePatternGenerator(){
		this(0.1);
	}
	
	/**
	 * Constructor for the IsotopeGenerator.
	 * 
	 * @param minAb Minimal abundance of the isotopes to be added 
	 * 				in the combinatorial search
	 * 
	 */
	public IsotopePatternGenerator(double minAb){
		minAbundance = minAb;
		logger.info("Generating all Isotope structures with IsotopeGenerator");
	}
	/**
	 * Get all combinatorial chemical isotopes given a structure. 
	 * 
	 * @param molFor  The IMolecularFormula to start
	 * @return        A IsotopePattern object containing the different combinations
	 */
	public IsotopePattern getIsotopes(IMolecularFormula molFor){
    	
		if(builder==null){
			try {
				isoFactory = IsotopeFactory.getInstance(molFor.getBuilder());
				builder = molFor.getBuilder();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
    	String mf = MolecularFormulaManipulator.getString(molFor,true);
    	
		// Divide the chemical formula into tokens (element and coefficients)
		HashMap<String, Integer> tokens = new HashMap<String, Integer>();
		IMolecularFormula molecularFormula = MolecularFormulaManipulator.getMajorIsotopeMolecularFormula(mf, builder);
		for(IIsotope isos :molecularFormula.isotopes())
			tokens.put(isos.getSymbol(), molecularFormula.getIsotopeCount(isos));

		int atomCount;
		for(IIsotope isos : molecularFormula.isotopes()){
			String elementSymbol = isos.getSymbol();
			atomCount = tokens.get(elementSymbol);
			
			for (int i = 0; i < atomCount; i++) {
				if (!calculateAbundanceAndMass(elementSymbol)) {
				}
			}
		}
		
		IsotopePattern isoP = IsotopePatternManipulator.sortAndNormalizedByIntensity(abundance_Mass);
		isoP = cleanAbundance(isoP, minAbundance);
		IsotopePattern isoPattern = IsotopePatternManipulator.sortByMass(isoP);
	
		return isoPattern;

	}
	/**
	 * Calculates the mass and abundance of all isotopes generated by adding one
	 * atom. Receives the periodic table element and calculate the isotopes, if
	 * there exist a previous calculation, add these new isotopes. In the
	 * process of adding the new isotopes, remove those that has an abundance
	 * less than setup parameter minAbundance, and remove duplicated masses.
	 * 
	 * @param elementSymbol  The chemical element symbol
	 * @return               
	 */
	private boolean calculateAbundanceAndMass(String elementSymbol) {

		IIsotope[] isotopes = isoFactory.getIsotopes(elementSymbol);

		if (isotopes == null)
			return false;

		if (isotopes.length == 0)
			return false;

		double mass, previousMass, abundance, totalAbundance, newAbundance;

		HashMap<Double, Double> isotopeMassAndAbundance = new HashMap<Double, Double>();
		IsotopePattern currentISOPattern = new IsotopePattern();

		// Generate isotopes for the current atom (element)
		for (int i = 0; i < isotopes.length; i++) {
			mass = isotopes[i].getExactMass();
			abundance = isotopes[i].getNaturalAbundance();
			currentISOPattern.addIsotope(new IsotopeContainer(mass, abundance));
		}

		
		// Verify if there is a previous calculation. If it exists, add the new
		// isotopes
		if (abundance_Mass == null) {

			abundance_Mass = currentISOPattern;
			return true;

		} else {
			for (int i = 0; i < abundance_Mass.getNumberOfIsotopes(); i++) {
				totalAbundance = abundance_Mass.getIsotopes().get(i).getIntensity();

				if (totalAbundance == 0)
					continue;

				for (int j = 0; j < currentISOPattern.getNumberOfIsotopes(); j++) {

					abundance = currentISOPattern.getIsotopes().get(j).getIntensity();
					mass = abundance_Mass.getIsotopes().get(i).getMass();

					if (abundance == 0)
						continue;

					newAbundance = totalAbundance * abundance * 0.01f;
					mass += currentISOPattern.getIsotopes().get(j).getMass();

					// Filter duplicated masses
					previousMass = searchMass(isotopeMassAndAbundance.keySet(),
							mass);
					if (isotopeMassAndAbundance.containsKey(previousMass)) {
						newAbundance += isotopeMassAndAbundance
								.get(previousMass);
						mass = previousMass;
					}

					// Filter isotopes too small
					if (isNotZero(newAbundance)) {
						isotopeMassAndAbundance.put(mass, newAbundance);
					}
					previousMass = 0;
				}
			}

			Iterator<Double> itr = isotopeMassAndAbundance.keySet().iterator();
			int i = 0;
			abundance_Mass = new IsotopePattern();
			while (itr.hasNext()) {
				mass = itr.next();
				abundance_Mass.addIsotope(new IsotopeContainer(mass, isotopeMassAndAbundance.get(mass)));
				i++;
			}
		}

		return true;

	}

	/**
	 * Search the key mass in this Set.
	 * 
	 * @param keySet  The Set object
	 * @param mass    The mass to look for
	 * @return        The key value
	 */
	private double searchMass(Set<Double> keySet, double mass) {
		double TOLERANCE = 0.00005f;
		double diff;
		for (double key : keySet) {
			diff = Math.abs(key - mass);
			if (diff < TOLERANCE)
				return key;
		}

		return 0.0d;
	}

	/**
	 * Detection if the value is zero.
	 * 
	 * @param number The number to analyze
	 * @return       TRUE, if it zero
	 */
	private boolean isNotZero(double number) {
		double pow = (double) Math.pow(10, 6);
		int fraction = (int) (number * pow);

		if (fraction <= 0)
			return false;

		return true;
	}

	/**
	 * Normalize the intensity (relative abundance) of all isotopes in relation
	 * of the most abundant isotope.
	 * 
	 * @param isopattern   The IsotopePattern object
	 * @param minAbundance The minimum abundance
	 * @return             The IsotopePattern cleaned
	 */
	private IsotopePattern cleanAbundance(IsotopePattern isopattern,
			double minAbundance) {

		double intensity, biggestIntensity = 0.0f;

		for (IsotopeContainer sc : isopattern.getIsotopes()) {

			intensity = sc.getIntensity();
			if (intensity > biggestIntensity)
				biggestIntensity = intensity;

		}

		for (IsotopeContainer sc : isopattern.getIsotopes()) {

			intensity = sc.getIntensity();
			intensity /= biggestIntensity;
			if (intensity < 0)
				intensity = 0;

			sc.setIntensity(intensity);

		}

		IsotopePattern sortedIsoPattern = new IsotopePattern();
		sortedIsoPattern.setMonoIsotope(new IsotopeContainer(isopattern.getIsotopes().get(0).getMass(),isopattern.getIsotopes().get(0).getIntensity()));
		for (int i = 1; i < isopattern.getNumberOfIsotopes();i++) {
			if (isopattern.getIsotopes().get(i).getIntensity() >= (minAbundance ))
				sortedIsoPattern.addIsotope(new IsotopeContainer(isopattern.getIsotopes().get(i).getMass(),isopattern.getIsotopes().get(i).getIntensity()));
		}
		return sortedIsoPattern;

	}
}