/*
 *                    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/
 *
 * Created on Aug 2, 2010
 * Author: Jianjiong Gao
 *
 */

package org.biojava.nbio.protmod.structure;

import org.biojava.nbio.structure.*;
import org.biojava.nbio.structure.chem.MetalBondDistance;
import org.biojava.nbio.structure.io.cif.MetalBondConverter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public final class StructureUtil {
	private StructureUtil() {
		throw new AssertionError();
	}

	/**
	 *
	 * @param group a {@link Group} in structure.
	 * @param isAminoAcid true if it is an amino acid.
	 * @return the {@link StructureGroup} of the group.
	 */
	public static StructureGroup getStructureGroup(Group group, boolean isAminoAcid) {
		ResidueNumber resNum = group.getResidueNumber();
		return new StructureGroup(resNum, group.getPDBName(), isAminoAcid);
	}

	/**
	 *
	 * @param atom a {@link Atom} in structure.
	 * @param isParentAminoAcid true if the containing group is an amino acid.
	 * @return the {@link StructureAtom} of the atom.
	 */
	public static StructureAtom getStructureAtom(Atom atom, boolean isParentAminoAcid) {

		Group g = atom.getGroup();
		String chainId = g.getChainId();
		StructureGroup strucGroup = getStructureGroup(g, isParentAminoAcid);
		strucGroup.setChainId(chainId);
		return new StructureAtom(strucGroup, atom.getName());
	}

	/**
	 *
	 * @param atom1 the first {@link Atom} in structure.
	 * @param isParentAminoAcid1 true if the first containing group is an amino acid..
	 * @param atom2 the second {@link Atom} in structure.
	 * @param isParentAminoAcid2 true if the second containing group is an amino acid..
	 * @return the {@link StructureAtomLinkage} of the two atoms.
	 */
	public static StructureAtomLinkage getStructureAtomLinkage(Atom atom1,
			boolean isParentAminoAcid1, Atom atom2, boolean isParentAminoAcid2) {
		StructureAtom strucAtom1 = getStructureAtom(atom1, isParentAminoAcid1);
		StructureAtom strucAtom2 = getStructureAtom(atom2, isParentAminoAcid2);
		double distance = getAtomDistance(atom1, atom2);
		return new StructureAtomLinkage(strucAtom1, strucAtom2, distance);
	}

	/**
	 *
	 * @param atom1 the first {@link Atom} in structure.
	 * @param atom2 the second {@link Atom} in structure.
	 * @return the distance between the two atoms in Angstrom.
	 */
	public static double getAtomDistance(Atom atom1, Atom atom2) {
		double distance;

		distance = Calc.getDistance(atom1, atom2);

		return distance;
	}

	/**
	 * Find a linkage between two groups within tolerance of bond length,
	 * from potential atoms.
	 * @param group1 the first {@link Group}.
	 * @param group2 the second {@link Group}.
	 * @param potentialNamesOfAtomOnGroup1 potential names of the atom on the first group.
	 * 		  If null, search all atoms on the first group.
	 * @param potentialNamesOfAtomOnGroup2 potential names of the atom on the second group.
	 * 		  If null, search all atoms on the second group.
	 * @param ignoreNCLinkage true to ignore all N-C linkages
	 * @param bondLengthTolerance bond length error tolerance.
	 * @return an array of two Atoms that form bond between each other
	 *  if found; null, otherwise.
	 */
	public static Atom[] findNearestAtomLinkage(final Group group1, final Group group2,
			List<String> potentialNamesOfAtomOnGroup1, List<String> potentialNamesOfAtomOnGroup2,
			final boolean ignoreNCLinkage, double bondLengthTolerance) {


		List<Atom[]> linkages = findAtomLinkages(group1, group2,
				potentialNamesOfAtomOnGroup1, potentialNamesOfAtomOnGroup2,
				ignoreNCLinkage, bondLengthTolerance);

		Atom[] ret = null;
		double minDistance = Double.POSITIVE_INFINITY;

		for (Atom[] linkage : linkages) {
			double distance;

			distance = Calc.getDistance(linkage[0], linkage[1]);


			if (distance < minDistance) {
				minDistance = distance;
				ret = linkage;
			}
		}

		return ret;
	}

	/**
	 * Find linkages between two groups within tolerance of bond length,
	 * from potential atoms.
	 * @param group1 the first {@link Group}.
	 * @param group2 the second {@link Group}.
	 * @param ignoreNCLinkage true to ignore all N-C linkages
	 * @param bondLengthTolerance bond length error tolerance.
	 * @return a list, each element of which is an array of two Atoms that form bond
	 * between each other.
	 */
	public static List<Atom[]> findAtomLinkages(final Group group1,
			final Group group2, final boolean ignoreNCLinkage,
			final double bondLengthTolerance) {
		return findAtomLinkages(group1, group2,
				null, null, ignoreNCLinkage, bondLengthTolerance);
	}

	/**
	 * Find linkages between two groups within tolerance of bond length,
	 * from potential atoms.
	 * @param group1 the first {@link Group}.
	 * @param group2 the second {@link Group}.
	 * @param potentialNamesOfAtomOnGroup1 potential names of the atom on the first group.
	 * 		  If null, search all atoms on the first group.
	 * @param potentialNamesOfAtomOnGroup2 potential names of the atom on the second group.
	 * 		  If null, search all atoms on the second group.
	 * @param ignoreNCLinkage true to ignore all N-C linkages
	 * @param bondLengthTolerance bond length error tolerance.
	 * @return a list, each element of which is an array of two Atoms that form bond
	 * between each other.
	 */
	public static List<Atom[]> findAtomLinkages(final Group group1,
			final Group group2,
			List<String> potentialNamesOfAtomOnGroup1,
			List<String> potentialNamesOfAtomOnGroup2,
			final boolean ignoreNCLinkage,
			final double bondLengthTolerance) {
		if (group1==null || group2==null) {
			throw new IllegalArgumentException("Null group(s).");
		}

		if (bondLengthTolerance<0) {
			throw new IllegalArgumentException("bondLengthTolerance cannot be negative.");
		}

		List<Atom[]> ret = new ArrayList<>();

		if (potentialNamesOfAtomOnGroup1 == null) {
			// if empty name, search for all atoms
			potentialNamesOfAtomOnGroup1 = getAtomNames(group1);
		}

		if (potentialNamesOfAtomOnGroup2 == null) {
			// if empty name, search for all atoms
			potentialNamesOfAtomOnGroup2 = getAtomNames(group2);
		}

		for (String namesOfAtomOnGroup1 : potentialNamesOfAtomOnGroup1) {
			for (String namesOfAtomOnGroup2 : potentialNamesOfAtomOnGroup2) {
				Atom[] atoms = findLinkage(group1, group2, namesOfAtomOnGroup1,
						namesOfAtomOnGroup2, bondLengthTolerance);
				if (atoms != null) {
					if (ignoreNCLinkage &&
							(("N".equals(atoms[0].getName()) && "C".equals(atoms[1].getName()))
									|| ("C".equals(atoms[0].getName()) && "N".equals(atoms[1].getName())))
							) {
						continue;
					}

					ret.add(atoms);
				}
			}
		}

		return ret;
	}

	/**
	 * Find a linkage between two groups within tolerance of bond length.
	 * @param group1 the first {@link Group}.
	 * @param group2 the second {@link Group}.
	 * @param nameOfAtomOnGroup1 atom name of the first group.
	 * @param nameOfAtomOnGroup2 atom name of the second group.
	 * @param bondLengthTolerance bond length error tolerance.
	 * @return an array of two Atoms that form bond between each other
	 *  if found; null, otherwise.
	 */
	public static Atom[] findLinkage(final Group group1, final Group group2,
			String nameOfAtomOnGroup1, String nameOfAtomOnGroup2,
			double bondLengthTolerance) {

		Atom[] ret = new Atom[2];

		ret[0] = group1.getAtom(nameOfAtomOnGroup1);
		ret[1] = group2.getAtom(nameOfAtomOnGroup2);

		if (ret[0]==null || ret[1]==null) {
			return null;
		}


		Atom a1 = ret[0];
		Atom a2 = ret[1];

		boolean hasBond =  a1.hasBond(a2);

		if ( hasBond ) {

			return ret;
		}

		// is it a metal ?

		if ( a1.getElement().isMetal() || a2.getElement().isMetal()){

			MetalBondDistance defined = getMetalDistanceCutoff(a1.getElement().name(),a2.getElement().name());

			if ( defined != null) {

				if (hasMetalBond(a1, a2, defined))
					return ret;
				else
					return null;
			}

		}

			// not a metal

			double distance = Calc.getDistance(a1, a2);

			float radiusOfAtom1 = ret[0].getElement().getCovalentRadius();
			float radiusOfAtom2 = ret[1].getElement().getCovalentRadius();

			if (Math.abs(distance - radiusOfAtom1 - radiusOfAtom2)
					> bondLengthTolerance) {
				return null;
			}


		return ret;
	}

	private static boolean hasMetalBond(Atom a1, Atom a2, MetalBondDistance definition) {
		double distance = Calc.getDistance(a1, a2);
		float min = definition.getLowerLimit();
		float max = definition.getUpperLimit();
		return (min < distance && max > distance);
	}

	private static MetalBondDistance getMetalDistanceCutoff(String name1, String name2) {
		Map<String,List<MetalBondDistance>> defs = MetalBondConverter.getMetalBondDefinitions();

		List<MetalBondDistance> distances = defs.get(name1);

		if ( distances == null){

			distances = defs.get(name2);
			String tmp = name1;
			 name2 = name1;
			 name1 = tmp;
		}
		if ( distances == null){
			return null;
		}

		for  ( MetalBondDistance d : distances){
			if ( name1.equalsIgnoreCase(d.getAtomType1()) && name2.equalsIgnoreCase(d.getAtomType2()) )
				return d;
		}

		// no matching atom definitions found
		return null;

	}

	/**
	 *
	 * @param group a {@link Group}.
	 * @return all atom names of the group.
	 */
	public static List<String> getAtomNames(Group group) {
		List<Atom> atoms = group.getAtoms();
		if (atoms == null) {
			return Collections.emptyList();
		}

		int n = atoms.size();
		List<String> ret = new ArrayList<>(n);
		for (int i=0; i<n; i++) {
			ret.add(atoms.get(i).getName());
		}

		return ret;
	}

	/**
	 * Get all amino acids in a chain.
	 * @param chain
	 * @return
	 */
	public static List<Group> getAminoAcids(Chain chain) {

		List<Group> gs = new ArrayList<>();

		for ( Group g : chain.getAtomGroups()){

			if ( g.isAminoAcid())
				gs.add(g);

		}

		return gs;

	}

}
