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


import org.biojava.nbio.structure.Atom;
import org.biojava.nbio.structure.Structure;
import org.biojava.nbio.structure.StructureTools;
import org.biojava.nbio.structure.align.ce.ConfigStrucAligParams;
import org.biojava.nbio.structure.align.client.PdbPair;
import org.biojava.nbio.structure.align.model.AFPChain;
import org.biojava.nbio.structure.align.util.AtomCache;
import org.biojava.nbio.structure.align.util.SynchronizedOutFile;
import org.biojava.nbio.structure.align.xml.AFPChainXMLConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.util.concurrent.Callable;
import java.util.zip.GZIPOutputStream;

/**
 * Simple Callable Class that calculates a pairwise alignment in a different
 * thread, so that multiple pairwise alignments can be run in parallel
 * (examples: all-to-all alignments, DB search alignments).
 * Adapted to a more general implementation since 4.1.0, because before it
 * was thought for DB search only.
 *
 * @author Aleix Lafita
 */
public class CallableStructureAlignment implements  Callable<AFPChain> {

	private final static Logger logger = LoggerFactory.getLogger(
			CallableStructureAlignment.class);

	//Structure information
	private PdbPair pair;
	private AtomCache cache;
	private Atom[] ca1;
	private Atom[] ca2;

	//File output information - for DB searches
	private SynchronizedOutFile outFile;
	private File outFileDir;

	//Algorithm information
	private String algorithmName;
	private ConfigStrucAligParams params;

	/**
	 * Default constructor. Used in DB search.
	 * Instantiates an empty object, everything has to be set independently.
	 */
	public CallableStructureAlignment() {}

	/**
	 * Constructor for all-to-all alignment calculation.
	 * Used for MultipleMC seed alignment calculation, for example.
	 *
	 * @param ca1 Atoms to align of the first structure
	 * @param ca2 Atoms to align of the second structure
	 * @param algorithmName the pairwise aligner algorithm to use, a new
	 * 			instance will be created for each thread.
	 * @param params parameter bean for the alignment.
	 */
	public CallableStructureAlignment(Atom[] ca1, Atom[] ca2, String algorithmName, ConfigStrucAligParams params){
		this.ca1 = ca1;
		this.ca2 = ca2;
		this.algorithmName = algorithmName;
		this.params = params;
	}

	@Override
	public AFPChain call() throws Exception {

		//Prepare the alignment algorithm
		StructureAlignment algorithm = StructureAlignmentFactory.getAlgorithm(algorithmName);
		if (params!=null) algorithm.setParameters(params);

		AFPChain afpChain = null;
		try {
			//Download the Atoms if they are not provided from the outisde (DB searches usually)
			if (ca1 == null) {
				Structure structure1 = cache.getStructure(pair.getName1());
				ca1 =  StructureTools.getRepresentativeAtomArray(structure1);
			} else ca1 = StructureTools.cloneAtomArray(ca1);

			Structure structure2 = null;
			if (ca2 == null) {
				structure2 = cache.getStructure(pair.getName2());
				ca2 = StructureTools.getRepresentativeAtomArray(structure2);
			} else ca2 = StructureTools.cloneAtomArray(ca2);

			afpChain = algorithm.align(ca1, ca2);

			if (pair!=null){
				afpChain.setName1(pair.getName1());
				afpChain.setName2(pair.getName2());
			}

			//Do not output anything if there is no File information
			if (outFile != null && outFileDir != null){
				String desc2 = structure2.getPDBHeader().getDescription();
				if ( desc2 == null)
					desc2="";
				afpChain.setDescription2(desc2);
				String result = afpChain.toDBSearchResult();
				logger.info("{}", result);

				outFile.write(result);

				String xml = AFPChainXMLConverter.toXML(afpChain, ca1, ca2);
				writeXML(outFileDir,pair.getName1(), pair.getName2(), xml);
			}

		} catch ( Exception e){
			logger.error("Exception: ", e);
		}
		return afpChain;
	}

	public PdbPair getPair() {
		return pair;
	}

	public void setPair(PdbPair pair) {
		this.pair = pair;
	}

	public AtomCache getCache() {
		return cache;
	}

	public void setCache(AtomCache cache) {
		this.cache = cache;
	}

	public SynchronizedOutFile getOutFile() {
		return outFile;
	}

	public void setOutFile(SynchronizedOutFile outFile) {
		this.outFile = outFile;
	}

	public Atom[] getCa1() {
		return ca1;
	}

	public void setCa1(Atom[] ca1) {
		this.ca1 = ca1;
	}

	private void writeXML(File outFileF, String name1, String name2, String xml)
	{
		try{
			// Create file
			File newF = new File(outFileF, "dbsearch_" +name1+"_" + name2+".xml.gz");

			try (OutputStreamWriter writer = new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(newF)))) {
				writer.write(xml);
			}
		} catch (Exception e){//Catch exception if any
			logger.error("Exception: ", e);
		}
	}

	public void setOutputDir(File outFileF) {
		this.outFileDir = outFileF;
	}

	public void setAlgorithmName(String algorithmName) {
		this.algorithmName = algorithmName;
	}

	public void setParameters(ConfigStrucAligParams parameters) {
		this.params = parameters;
	}
}
