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

import org.biojava.nbio.structure.AminoAcidImpl;
import org.biojava.nbio.structure.Atom;
import org.biojava.nbio.structure.AtomImpl;
import org.biojava.nbio.structure.Element;
import org.biojava.nbio.structure.Structure;
import org.biojava.nbio.structure.StructureException;
import org.biojava.nbio.structure.StructureIO;
import org.biojava.nbio.structure.StructureTools;
import static org.junit.Assert.*;

import org.biojava.nbio.structure.chem.ChemCompGroupFactory;
import org.biojava.nbio.structure.chem.DownloadChemCompProvider;
import org.junit.Ignore;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Testing of Accessible Surface Area calculations
 *
 *
 * @author Jose Duarte
 *
 */
public class TestAsaCalc {


	@Test
	public void testAsa3PIU() throws StructureException, IOException {

		// important: without this the tests can fail when running in maven (but not in IDE)
		// that's because it depends on the order on how tests were run - JD 2018-03-10
		ChemCompGroupFactory.setChemCompProvider(new DownloadChemCompProvider());

		Structure structure = StructureIO.getStructure("3PIU");

		AsaCalculator asaCalc = new AsaCalculator(structure,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				1000, 1, false);

		double totResidues = 0;
		double totAtoms = 0;

		GroupAsa[] groupAsas = asaCalc.getGroupAsas();

		double[] asas = asaCalc.calculateAsas();

		for (double asa:asas) {
			totAtoms += asa;
		}

		for (GroupAsa groupAsa: groupAsas) {
			//System.out.println(groupAsa.getGroup().getPDBName() + " " + groupAsa.getGroup().getResidueNumber() + " " + groupAsa.getAsaU());
			totResidues+=groupAsa.getAsaU();

			assertTrue(groupAsa.getRelativeAsaU() <= 1.0);
		}

		assertEquals(totAtoms, totResidues, 0.000001);

		assertEquals(17462.0, totAtoms, 1.0);

	}

	@Ignore("This is a performance test to be run manually")
	@Test
	public void testMultithreadScaling() throws StructureException, IOException {

		// important: without this the tests can fail when running in maven (but not in IDE)
		// that's because it depends on the order on how tests were run - JD 2018-03-10
		ChemCompGroupFactory.setChemCompProvider(new DownloadChemCompProvider());

		Structure structure = StructureIO.getStructure("3hbx");
		int[] numThreads = {1, 2, 3, 4};
		long timeSingleThread = 0;
		for (int numThread : numThreads) {
			AsaCalculator asaCalc = new AsaCalculator(structure,
					AsaCalculator.DEFAULT_PROBE_SIZE,
					100, numThread, false);

			long start = System.currentTimeMillis();
			asaCalc.calculateAsas();
			long end = System.currentTimeMillis();
			long time = end - start;
			if (numThread == 1) {
				timeSingleThread = time;
			}
			System.out.printf("%6d threads : %6d ms (x%3.1f)\n", numThread, time, (double)timeSingleThread/time);
		}
		// nothing to assert
	}

	@Test
	public void testNeighborIndicesFinding() throws StructureException, IOException {
		// important: without this the tests can fail when running in maven (but not in IDE)
		// that's because it depends on the order on how tests were run - JD 2018-03-10
		ChemCompGroupFactory.setChemCompProvider(new DownloadChemCompProvider());

		Structure structure = StructureIO.getStructure("3PIU");

		AsaCalculator asaCalc = new AsaCalculator(structure,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				1000, 1, false);

		AsaCalculator.IndexAndDistance[][] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();

		AsaCalculator.IndexAndDistance[][] allNbs = asaCalc.findNeighborIndices();

		for (int indexToTest =0; indexToTest < asaCalc.getAtomCoords().length; indexToTest++) {
			//int indexToTest = 198;
			AsaCalculator.IndexAndDistance[] nbsSh = allNbsSh[indexToTest];
			AsaCalculator.IndexAndDistance[] nbs = allNbs[indexToTest];

			List<Integer> listOfMatchingIndices = new ArrayList<>();
			for (int i = 0; i < nbsSh.length; i++) {
				for (int j = 0; j < nbs.length; j++) {
					if (nbs[j].index == nbsSh[i].index) {
						listOfMatchingIndices.add(j);
						break;
					}
				}
			}

//		for (int i = 0; i<nbs.length; i++) {
//			double dist = asaCalc.getAtomCoords()[i].distance(asaCalc.getAtomCoords()[indexToTest]);
//			if (listOfMatchingIndices.contains(i)) {
//				System.out.printf("Matching     - indices %d-%d: %5.2f\n", indexToTest, i, dist);
//			} else {
//				System.out.printf("Not matching - indices %d-%d: %5.2f\n", indexToTest, i, dist);
//			}
//		}

			assertEquals(nbs.length, nbsSh.length);

			assertEquals(nbs.length, listOfMatchingIndices.size());
		}

	}

	@Test
	public void testPerformance() throws StructureException, IOException {
		// important: without this the tests can fail when running in maven (but not in IDE)
		// that's because it depends on the order on how tests were run - JD 2018-03-10
		ChemCompGroupFactory.setChemCompProvider(new DownloadChemCompProvider());

		Structure structure = StructureIO.getStructure("3HBX");
		Atom[] atoms = StructureTools.getAllAtomArray(structure);
		System.out.printf("Total of %d atoms. n(n-1)/2= %d \n", atoms.length, atoms.length*(atoms.length-1)/2);

		int nThreads = 1;
		int nSpherePoints = 100;

		// 1. WITH SPATIAL HASHING
		long start = System.currentTimeMillis();
		AsaCalculator asaCalc = new AsaCalculator(atoms,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				nSpherePoints, nThreads);
		asaCalc.setUseSpatialHashingForNeighbors(true);

		double[] asas = asaCalc.calculateAsas();
		long end = System.currentTimeMillis();
		System.out.printf("ASA calculation took %6.2f s with spatial hashing\n", (end-start)/1000.0);

		double totAtoms = 0;
		for (double asa:asas) {
			totAtoms += asa;
		}
		double withSH = totAtoms;
		System.out.printf("Total ASA is %6.2f \n", totAtoms);


		// 2. WITHOUT SPATIAL HASHING
		start = System.currentTimeMillis();
		asaCalc = new AsaCalculator(atoms,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				nSpherePoints, nThreads);
		asaCalc.setUseSpatialHashingForNeighbors(false);

		asas = asaCalc.calculateAsas();
		end = System.currentTimeMillis();
		System.out.printf("ASA calculation took %6.2f s without spatial hashing\n", (end-start)/1000.0);

		totAtoms = 0;
		for (double asa:asas) {
			totAtoms += asa;
		}
		double withoutSH = totAtoms;
		System.out.printf("Total ASA is %6.2f \n", totAtoms);


		assertEquals(withoutSH, withSH, 0.000001);

	}

	@Test
	public void testNoNeighborsIssue() {

		Atom[] atoms = {
				getAtom(1.0, 1.0, 1.0),
				getAtom(10.0, 1.0, 1.0),
				getAtom(13.0, 1.0, 1.0)
		};

		AsaCalculator asaCalc = new AsaCalculator(atoms,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				1000, 1);

		AsaCalculator.IndexAndDistance[][] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();

		AsaCalculator.IndexAndDistance[][] allNbs = asaCalc.findNeighborIndices();

		assertEquals(3, allNbs.length);
		assertEquals(3, allNbsSh.length);

		for (int indexToTest =0; indexToTest < asaCalc.getAtomCoords().length; indexToTest++) {
			AsaCalculator.IndexAndDistance[] nbsSh = allNbsSh[indexToTest];
			AsaCalculator.IndexAndDistance[] nbs = allNbs[indexToTest];

			List<Integer> listOfMatchingIndices = new ArrayList<>();
			for (int i = 0; i < nbsSh.length; i++) {
				for (int j = 0; j < nbs.length; j++) {
					if (nbs[j].index == nbsSh[i].index) {
						listOfMatchingIndices.add(j);
						break;
					}
				}
			}

			assertEquals(nbs.length, nbsSh.length);

			assertEquals(nbs.length, listOfMatchingIndices.size());
		}

		// first atom should have no neighbors
		assertEquals(0, allNbsSh[0].length);
	}

	private Atom getAtom(double x, double y, double z) {
		Atom atom = new AtomImpl();
		AminoAcidImpl g = new AminoAcidImpl();
		g.setAminoType('A');
		atom.setGroup(g);
		atom.setName("CA");
		atom.setElement(Element.C);
		atom.setX(x);
		atom.setY(y);
		atom.setZ(z);
		return atom;
	}

	@Test
	public void testNoAtomsAsaCalc() {

		// in case of no atoms at all, the calculation should not fail and return an empty array
		Atom[] atoms = new Atom[0];

		AsaCalculator asaCalc = new AsaCalculator(atoms,
				AsaCalculator.DEFAULT_PROBE_SIZE,
				1000, 1);
		double[] asas = asaCalc.calculateAsas();
		assertNotNull(asas);
		assertEquals(0, asas.length);

	}
}
