/*
 * Copyright (c) 1997 - 2016
 * Actelion Pharmaceuticals Ltd.
 * Gewerbestrasse 16
 * CH-4123 Allschwil, Switzerland
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the the copyright holder nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Thomas Sander
 */

package com.actelion.research.chem.conf;

import com.actelion.research.chem.Coordinates;
import com.actelion.research.chem.Molecule;
import com.actelion.research.chem.StereoMolecule;

import java.util.Arrays;

public class Conformer implements Comparable<Conformer> {
	private Coordinates[] mCoordinates;
	private StereoMolecule mMol;
	private String mName;
	private short[] mBondTorsion;
	private double mEnergy;
	private double mLikelihood;
	private TorsionDescriptor mTorsionDescriptor;

	/**
	 * Creates a Conformer, i.e. basically a new set of atom coordinates for the given Molecule.
	 * The conformer is initialized with an exact copy of the molecules internal coordinates.
	 *
	 * @param mol
	 */
	public Conformer(StereoMolecule mol) {
		mMol = mol;
		mCoordinates = new Coordinates[mol.getAllAtoms()];
		for (int atom = 0; atom < mol.getAllAtoms(); atom++)
			mCoordinates[atom] = new Coordinates(mol.getAtomCoordinates(atom));

		mEnergy = Double.NaN;
	}

	/**
	 * Creates a new conformer as an exact copy of the given one.
	 *
	 * @param c
	 */
	public Conformer(Conformer c) {
		this(c, c.mMol);
	}

	/**
	 * Use at own risk!!
	 *
	 * Initializes the conformer as a copy of c but replaces the
	 * StereoMolecule by the supplied molecule.
	 *
	 * @param c
	 * @param mol
	 */
	public Conformer(Conformer c, StereoMolecule mol) {
		mMol = mol;

		mCoordinates = new Coordinates[c.mCoordinates.length];
		for (int i = 0; i < mCoordinates.length; i++)
			mCoordinates[i] = new Coordinates(c.mCoordinates[i]);

		if (c.mBondTorsion != null)
			mBondTorsion = Arrays.copyOf(c.mBondTorsion, c.mBondTorsion.length);

		mName = (c.mName == null || c.mName.endsWith(" (copy)")) ? c.mName : c.mName.concat(" (copy)");

		mEnergy = Double.NaN;
	}


	/**
	 * Translate this conformer's coordinates such that its center of gravity
	 * is moved to P(0,0,0) assuming all atoms have the same mass.
	 *
	 * @return this conformer with centered coordinates
	 */
	public Conformer center() {
		Coordinates cog = new Coordinates();
		for (int atom = 0; atom < mMol.getAllAtoms(); atom++)
			cog.add(mCoordinates[atom]);
		cog.scale(1.0 / mMol.getAllAtoms());

		for (int atom = 0; atom < mMol.getAllAtoms(); atom++)
			mCoordinates[atom].sub(cog);

		return this;
	}

	/**
	 * Translate this conformer's coordinates by adding the dx,dy,dz shifts
	 * to all atom coordinates.
	 *
	 * @return this conformer with translated coordinates
	 */
	public Conformer translate(double dx, double dy, double dz) {
		for (int atom = 0; atom < mMol.getAllAtoms(); atom++)
			mCoordinates[atom].add(dx, dy, dz);

		return this;
	}

	public int getSize() {
		return mCoordinates.length;
	}

	public double getX(int atom) {
		return mCoordinates[atom].x;
	}

	public double getY(int atom) {
		return mCoordinates[atom].y;
	}

	public double getZ(int atom) {
		return mCoordinates[atom].z;
	}

	public Coordinates[] getCoordinates() {
		return mCoordinates;
	}

	public Coordinates getCoordinates(int atom) {
		return mCoordinates[atom];
	}

	/**
	 * Copies x,y,z from coords into this atom's coordinates
	 *
	 * @param atom
	 * @param coords
	 */
	public void setCoordinates(int atom, Coordinates coords) {
		mCoordinates[atom].set(coords);
	}

	/**
	 * Replaces the atom's Coordinates object by the given one
	 *
	 * @param atom
	 * @param coords
	 */
	public void setCoordinatesReplace(int atom, Coordinates coords) {
		mCoordinates[atom] = coords;
	}

	public void setX(int atom, double x) {
		mCoordinates[atom].x = x;
	}

	public void setY(int atom, double y) {
		mCoordinates[atom].y = y;
	}

	public void setZ(int atom, double z) {
		mCoordinates[atom].z = z;
	}

	/**
	 * Removes atoms Coordinates objects from those atoms that are marked in the given array.
	 * Make sure to also remove those atoms from the underlying Molecule.
	 * When this method is used, bond torsion angles are lost.
	 * @param isToBeDeleted
	 * @return
	 */
	public int deleteAtoms(boolean[] isToBeDeleted) {
		int count = 0;
		for (int i = 0; i < mCoordinates.length; i++)
			if (isToBeDeleted[i])
				count++;

		if (count != 0) {
			Coordinates[] newCoords = new Coordinates[mCoordinates.length - count];
			int newIndex = 0;
			for (int i = 0; i < mCoordinates.length; i++)
				if (!isToBeDeleted[i])
					newCoords[newIndex++] = mCoordinates[i];

			mCoordinates = newCoords;
			mBondTorsion = null;
		}

		return count;
	}

	/**
	 * Calculates a signed torsion as an exterior spherical angle
	 * from a valid 4-atom strand.
	 * Looking along the central bond, the torsion angle is 0.0, if the
	 * projection of front and rear bonds point in the same direction.
	 * If the front bond is rotated in the clockwise direction, the angle
	 * increases, i.e. has a positive value.
	 * http://en.wikipedia.org/wiki/Dihedral_angle
	 *
	 * @param atom 4 valid atom indices defining a connected atom sequence
	 * @return torsion in the range: -pi <= torsion <= pi
	 */
	public double calculateTorsion(int[] atom) {
		Coordinates c1 = getCoordinates(atom[0]);
		Coordinates c2 = getCoordinates(atom[1]);
		Coordinates c3 = getCoordinates(atom[2]);
		Coordinates c4 = getCoordinates(atom[3]);

		Coordinates v1 = c2.subC(c1);
		Coordinates v2 = c3.subC(c2);
		Coordinates v3 = c4.subC(c3);

		Coordinates n1 = v1.cross(v2);
		Coordinates n2 = v2.cross(v3);

		return -Math.atan2(v2.getLength() * v1.dot(n2), n1.dot(n2));
	}

	/**
	 * Returns the current bond torsion angle in degrees, as it was set before.
	 * @param bond
	 * @return -1 or previously set torsion angle in the range 0 ... 359
	 */
	public short getBondTorsion(int bond) {
		return mBondTorsion == null ? -1 : mBondTorsion[bond];
	}

	/**
	 * Sets the current bond torsion to be retrieved later.
	 * @param bond
	 * @param torsion in degrees
	 */
	public void setBondTorsion(int bond, short torsion) {
		if (mBondTorsion == null) {
			mBondTorsion = new short[mMol.getAllBonds()];
			Arrays.fill(mBondTorsion, (short)-1);
			}

		while (torsion < 0)
			torsion += 360;
		while (torsion >= 360)
			torsion -= 360;

		mBondTorsion[bond] = torsion;
	}

	/**
	 * @return reference to the original molecule
	 */
	public StereoMolecule getMolecule() {
		return mMol;
	}

	/**
	 * Copies the molecule's atom coordinates to this Conformer.
	 *
	 * @param mol molecule identical to the original molecule passed in the Constructor
	 */
	public void copyFrom(StereoMolecule mol) {
		for (int atom = 0; atom < mol.getAllAtoms(); atom++)
			mCoordinates[atom].set(mol.getAtomCoordinates(atom));
	}

	/**
	 * Copies this conformer's atom coordinates to mol.
	 *
	 * @param mol molecule identical to the original molecule passed in the Constructor
	 */
	public void copyTo(StereoMolecule mol) {
		mol.invalidateHelperArrays(Molecule.cHelperBitsStereo);
		for (int atom = 0; atom < mol.getAllAtoms(); atom++)
			mol.getAtomCoordinates(atom).set(mCoordinates[atom]);
	}

	/**
	 * Copies the conformer's atom coordinates and torsion values to this Conformer.
	 *
	 * @param conformer other conformer of the molecule passed in the Constructor
	 */
	public void copyFrom(Conformer conformer) {
		for (int atom = 0; atom < conformer.getSize(); atom++)
			mCoordinates[atom].set(conformer.mCoordinates[atom]);

		if (conformer.mBondTorsion == null)
			mBondTorsion = null;
		else
			mBondTorsion = Arrays.copyOf(conformer.mBondTorsion, conformer.mBondTorsion.length);
	}

	/**
	 * Copies this Conformer's atom coordinates to the associated molecule.
	 *
	 * @return this conformer's associated StereoMolecule with updated coordinates
	 */
	public StereoMolecule toMolecule() {
		return toMolecule(mMol);
	}

	/**
	 * Copies this Conformer's atom coordinates to the given molecule.
	 * If null is passed as molecule, then a compact copy of this conformer's
	 * molecule is created and returned with this conformer's coordinates.
	 *
	 * @param mol null or original molecule passed in the Constructor or identical copy
	 * @return this conformer as StereoMolecule
	 */
	public StereoMolecule toMolecule(StereoMolecule mol) {
		if (mol == null)
			mol = mMol.getCompactCopy();
		mol.invalidateHelperArrays(Molecule.cHelperBitsStereo);
		for (int atom = 0; atom < mol.getAllAtoms(); atom++)
			mol.getAtomCoordinates(atom).set(mCoordinates[atom]);
		if (mName != null)
			mol.setName(mName);
		return mol;
	}

	public double getEnergy() {
		return mEnergy;
	}

	public void setEnergy(double energy) {
		mEnergy = energy;
	}

	public double getLikelihood() {
		return mLikelihood;
	}

	public void setLikelihood(double likelihood) {
		mLikelihood = likelihood;
	}


	public String getName() {
		return mName == null ? mMol.getName() : mName;
	}

	public void setName(String name) {
		mName = name;
	}

	/**
	 * Returns true, if none of the torsion angles between both conformers
	 * are more different than TorsionDescriptor.TORSION_EQUIVALENCE_TOLERANCE;
	 * Calling this method requires that calculateDescriptor() has been called earlier.
	 * @param conformer
	 * @return true if all torsions are similar
	 */
	public boolean equals(Conformer conformer) {
		ensureDescriptors(conformer);
		return mTorsionDescriptor.equals(conformer.mTorsionDescriptor);
	}

	@Override
	public int compareTo(Conformer conformer) {
		ensureDescriptors(conformer);
		return mTorsionDescriptor.compareTo(conformer.mTorsionDescriptor);
	}

	/**
	 * Calculates the torsion descriptor for the current coordinates using the passed set of rotatable bonds.
	 * You may use TorsionDescriptorHelper.findRotatableBonds() once and pass it for every new conformer to this method.
	 * @param rotatableBond set of rotatable bonds to be considered
	 */
	public void calculateDescriptor(int[] rotatableBond) {
		mTorsionDescriptor = new TorsionDescriptorHelper(getMolecule(), rotatableBond).getTorsionDescriptor(this);
	}

	private void ensureDescriptors(Conformer c) {
		if (mTorsionDescriptor == null || c.mTorsionDescriptor == null) {
			TorsionDescriptorHelper tdh = new TorsionDescriptorHelper(mMol);
			if (mTorsionDescriptor == null)
				mTorsionDescriptor = tdh.getTorsionDescriptor(this);
			if (c.mTorsionDescriptor == null)
				c.mTorsionDescriptor = tdh.getTorsionDescriptor(c);
		}
	}


	/**
	 * Serializes the given conformer to a string.
	 * <p>
	 * Use the constructor Conformer(String serialized) to deserialize
	 * the conformer.
	 *
	 * @return
	 *
	public String serialize() {
		StringBuilder sb = new StringBuilder();

		Canonizer c = new Canonizer(mMol);
		String s_idcode = c.getIDCode();

		List<String> s_coordinates = new ArrayList<>();
		for (Coordinates coords:mCoordinates)
			s_coordinates.add(encodeCoordinates(coords));

		String s_bondTorsion = null;
		if (mBondTorsion == null) {
			s_bondTorsion = "N";
		} else {
			s_bondTorsion = "X" + SimpleEncoder.encodeShortArray(mBondTorsion);
		}

		sb.append(s_idcode);
		sb.append(" ");
		sb.append(String.join(";", s_coordinates));
		sb.append(" ");
		sb.append(String.join(";", s_bondTorsion));
		sb.append(" ");
		sb.append("" + mEnergy);
		sb.append(" ");
		sb.append("" + mName);

		return sb.toString();
	}*/

	/**
	 * Constructor for deserialization.
	 *
	 * @param serialized
	 *
	public Conformer(String serialized) {
		mTorsionDescriptor = null;

		String[] splits = serialized.split(" ");

		String idcode   = splits[0];
		String coords   = splits[1];
		String torsions = splits[2];
		String energy   = splits[3];
		String name     = splits[4];

		IDCodeParser p = new IDCodeParser();
		mMol = new StereoMolecule();
		p.parse(mMol,idcode);
		String[] c_split = coords.split(";");
		mCoordinates = new Coordinates[c_split.length];
		for(int zi=0;zi<c_split.length;zi++) { mCoordinates[zi] = decodeCoordinates(c_split[zi]); }

		if(torsions.startsWith("N")) {
			mBondTorsion = null;
		}
		else {
			mBondTorsion = SimpleEncoder.decodeShortArray(torsions.substring(1));
		}

		mEnergy = Double.parseDouble(energy);
		mName = name;
	}

	public static String encodeCoordinates(Coordinates c) {
		StringBuilder sb = new StringBuilder();

		byte[] data = ByteBuffer.allocate(4*3).putFloat((float) c.x)
				.putFloat((float) c.y)
				.putFloat((float) c.z).array();
		return SimpleEncoder.convertByteArrayToS3IDCode(data);
	}

	public static Coordinates decodeCoordinates(String s) {
		byte[] data = SimpleEncoder.convertS3IDCodeToByteArray(s);
		byte[] fx = new byte[]{data[0],data[1],data[2],data[3]};
		byte[] fy = new byte[]{data[4],data[5],data[6],data[7]};
		byte[] fz = new byte[]{data[8],data[9],data[10],data[11]};
		float cx = ByteBuffer.wrap(fx).getFloat();
		float cy = ByteBuffer.wrap(fy).getFloat();
		float cz = ByteBuffer.wrap(fz).getFloat();
		return new Coordinates(cx,cy,cz);
	}
*/
}
