/* 
 * Copyright (C) 2005-2007  Christian Hoppe <chhoppe@users.sf.net>
 *                    2014  Mark B Vine (orcid:0000-0002-7794-0426)
 *
 *  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.
 *  All we ask is that proper credit is given for our work, which includes
 *  - but is not limited to - adding the above copyright notice to the beginning
 *  of your source code files, and to any copyright notice that you may distribute
 *  with programs based on this work.
 *
 *  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.modeling.builder3d;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import org.openscience.cdk.CDKConstants;
import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.exception.NoSuchAtomTypeException;
import org.openscience.cdk.geometry.GeometryUtil;
import org.openscience.cdk.graph.ConnectivityChecker;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IBond;
import org.openscience.cdk.interfaces.IChemObjectBuilder;
import org.openscience.cdk.interfaces.IRingSet;
import org.openscience.cdk.layout.AtomPlacer;
import org.openscience.cdk.ringsearch.RingPartitioner;
import org.openscience.cdk.tools.ILoggingTool;
import org.openscience.cdk.tools.LoggingToolFactory;
import org.openscience.cdk.tools.manipulator.RingSetManipulator;

/**
 *  The main class to generate the 3D coordinates of a molecule ModelBuilder3D.
 *  Its use looks like:
 *  <pre>
 *  ModelBuilder3D mb3d = ModelBuilder3D.getInstance();
 *  IAtomContainer molecule = mb3d.generate3DCoordinates(molecule, false);
 *  </pre>
 *
 *  <p>Standing problems:
 *  <ul>
 *    <li>condensed ring systems which are unknown for the template class
 *    <li>vdWaals clashes
 *    <li>stereochemistry
 *    <li>chains running through ring systems
 *  </ul>
 *
 * @author      cho
 * @author      steinbeck
 * @cdk.created 2004-09-07
 * @cdk.module  builder3d
 * @cdk.githash
 * @cdk.keyword 3D coordinates
 * @cdk.keyword coordinate generation, 3D
 */
public class ModelBuilder3D {

    private static Map<String, ModelBuilder3D> memyselfandi    = new HashMap<String, ModelBuilder3D>();

    private TemplateHandler3D                  templateHandler = null;

    private Map                                parameterSet    = null;

    private final ForceFieldConfigurator       ffc             = new ForceFieldConfigurator();

    String                                     forceFieldName  = "mm2";

    private static ILoggingTool                logger          = LoggingToolFactory
                                                                       .createLoggingTool(ModelBuilder3D.class);

    /**
     * Constructor for the ModelBuilder3D object.
     *
     * @param  templateHandler  templateHandler Object
     * @param  ffname           name of force field
     */
    private ModelBuilder3D(TemplateHandler3D templateHandler, String ffname, IChemObjectBuilder builder)
            throws CDKException {
        setTemplateHandler(templateHandler);
        setForceField(ffname, builder);
    }

    public static ModelBuilder3D getInstance(TemplateHandler3D templateHandler, String ffname,
            IChemObjectBuilder chemObjectBuilder) throws CDKException {
        if (ffname == null || ffname.length() == 0) throw new CDKException("The given ffname is null or empty!");
        if (templateHandler == null) throw new CDKException("The given template handler is null!");

        String builderCode = templateHandler.getClass().getName() + "#" + ffname;
        if (!memyselfandi.containsKey(builderCode)) {
            ModelBuilder3D builder = new ModelBuilder3D(templateHandler, ffname, chemObjectBuilder);
            memyselfandi.put(builderCode, builder);
            return builder;
        }
        return memyselfandi.get(builderCode);
    }

    public static ModelBuilder3D getInstance(IChemObjectBuilder builder) throws CDKException {
        return getInstance(TemplateHandler3D.getInstance(), "mm2", builder);
    }

    /**
     * Gives a list of possible force field types.
     *
     * @return                the list
     */
    public String[] getFfTypes() {
        return ffc.getFfTypes();
    }

    /**
     * Sets the forceField attribute of the ModelBuilder3D object.
     *
     * @param  ffname  forceField name
     */
    private void setForceField(String ffname, IChemObjectBuilder builder) throws CDKException {
        if (ffname == null) {
            ffname = "mm2";
        }
        try {
            forceFieldName = ffname;
            ffc.setForceFieldConfigurator(ffname, builder);
            parameterSet = ffc.getParameterSet();
        } catch (CDKException ex1) {
            logger.error("Problem with ForceField configuration due to>" + ex1.getMessage());
            logger.debug(ex1);
            throw new CDKException("Problem with ForceField configuration due to>" + ex1.getMessage(), ex1);
        }
    }

    /**
     * Generate 3D coordinates with force field information.
     */
    public IAtomContainer generate3DCoordinates(IAtomContainer molecule, boolean clone) throws CDKException,
            NoSuchAtomTypeException, CloneNotSupportedException, IOException {
        String[] originalAtomTypeNames = new String[molecule.getAtomCount()];
        for (int i = 0; i < originalAtomTypeNames.length; i++) {
            originalAtomTypeNames[i] = molecule.getAtom(i).getAtomTypeName();
        }

        logger.debug("******** GENERATE COORDINATES ********");
        for (int i = 0; i < molecule.getAtomCount(); i++) {
            molecule.getAtom(i).setFlag(CDKConstants.ISPLACED, false);
            molecule.getAtom(i).setFlag(CDKConstants.VISITED, false);
        }
        //CHECK FOR CONNECTIVITY!
        logger.debug("#atoms>" + molecule.getAtomCount());
        if (!ConnectivityChecker.isConnected(molecule)) {
            throw new CDKException("Molecule is NOT connected, could not layout.");
        }

        // setup helper classes
        AtomPlacer atomPlacer = new AtomPlacer();
        AtomPlacer3D ap3d = new AtomPlacer3D();
        AtomTetrahedralLigandPlacer3D atlp3d = new AtomTetrahedralLigandPlacer3D();
        ap3d.initilize(parameterSet);
        atlp3d.setParameterSet(parameterSet);

        if (clone) molecule = (IAtomContainer) molecule.clone();
        atomPlacer.setMolecule(molecule);

        if (ap3d.numberOfUnplacedHeavyAtoms(molecule) == 1) {
            logger.debug("Only one Heavy Atom");
            ap3d.getUnplacedHeavyAtom(molecule).setPoint3d(new Point3d(0.0, 0.0, 0.0));
            try {
                atlp3d.add3DCoordinatesForSinglyBondedLigands(molecule);
            } catch (CDKException ex3) {
                logger.error("PlaceSubstitutensERROR: Cannot place substitutents due to:" + ex3.getMessage());
                logger.debug(ex3);
                throw new CDKException("PlaceSubstitutensERROR: Cannot place substitutents due to:" + ex3.getMessage(),
                        ex3);
            }
            return molecule;
        }
        //Assing Atoms to Rings,Aliphatic and Atomtype
        IRingSet ringSetMolecule = ffc.assignAtomTyps(molecule);
        List ringSystems = null;
        IRingSet largestRingSet = null;
        int numberOfRingAtoms = 0;

        if (ringSetMolecule.getAtomContainerCount() > 0) {
            if (templateHandler == null) {
                throw new CDKException(
                        "You are trying to generate coordinates for a molecule with rings, but you have no template handler set. Please do setTemplateHandler() before generation!");
            }
            ringSystems = RingPartitioner.partitionRings(ringSetMolecule);
            largestRingSet = RingSetManipulator.getLargestRingSet(ringSystems);
            IAtomContainer largestRingSetContainer = RingSetManipulator.getAllInOneContainer(largestRingSet);
            numberOfRingAtoms = largestRingSetContainer.getAtomCount();
            templateHandler.mapTemplates(largestRingSetContainer, numberOfRingAtoms);
            if (!checkAllRingAtomsHasCoordinates(largestRingSetContainer)) {
                throw new CDKException("RingAtomLayoutError: Not every ring atom is placed! Molecule cannot be layout.");
            }

            setAtomsToPlace(largestRingSetContainer);
            searchAndPlaceBranches(molecule, largestRingSetContainer, ap3d, atlp3d, atomPlacer);
            largestRingSet = null;
        } else {
            //logger.debug("****** Start of handling aliphatic molecule ******");
            IAtomContainer ac = null;

            ac = atomPlacer.getInitialLongestChain(molecule);
            setAtomsToUnVisited(molecule);
            setAtomsToUnPlaced(molecule);
            ap3d.placeAliphaticHeavyChain(molecule, ac);
            //ZMatrixApproach
            ap3d.zmatrixChainToCartesian(molecule, false);
            searchAndPlaceBranches(molecule, ac, ap3d, atlp3d, atomPlacer);
        }
        layoutMolecule(ringSystems, molecule, ap3d, atlp3d, atomPlacer);
        //logger.debug("******* PLACE SUBSTITUENTS ******");
        try {
            atlp3d.add3DCoordinatesForSinglyBondedLigands(molecule);
        } catch (CDKException ex3) {
            logger.error("PlaceSubstitutensERROR: Cannot place substitutents due to:" + ex3.getMessage());
            logger.debug(ex3);
            throw new CDKException("PlaceSubstitutensERROR: Cannot place substitutents due to:" + ex3.getMessage(), ex3);
        }
        // restore the original atom type names
        for (int i = 0; i < originalAtomTypeNames.length; i++) {
            molecule.getAtom(i).setAtomTypeName(originalAtomTypeNames[i]);
        }

        return molecule;
    }

    /**
     * Gets the ringSetOfAtom attribute of the ModelBuilder3D object.
     *
     *@return              The ringSetOfAtom value
     */
    private IRingSet getRingSetOfAtom(List ringSystems, IAtom atom) {
        IRingSet ringSetOfAtom = null;
        for (int i = 0; i < ringSystems.size(); i++) {
            if (((IRingSet) ringSystems.get(i)).contains(atom)) {
                return (IRingSet) ringSystems.get(i);
            }
        }
        return ringSetOfAtom;
    }

    /**
     * Layout the molecule, starts with ring systems and than aliphatic chains.
     *
     *@param  ringSetMolecule  ringSystems of the molecule
     */
    private void layoutMolecule(List ringSetMolecule, IAtomContainer molecule, AtomPlacer3D ap3d,
            AtomTetrahedralLigandPlacer3D atlp3d, AtomPlacer atomPlacer) throws CDKException, IOException,
            CloneNotSupportedException {
        //logger.debug("****** LAYOUT MOLECULE MAIN *******");
        IAtomContainer ac = null;
        int safetyCounter = 0;
        IAtom atom = null;
        //Place rest Chains/Atoms
        do {
            safetyCounter++;
            atom = ap3d.getNextPlacedHeavyAtomWithUnplacedRingNeighbour(molecule);
            if (atom != null) {
                //logger.debug("layout RingSystem...");
                IAtom unplacedAtom = ap3d.getUnplacedRingHeavyAtom(molecule, atom);
                IRingSet ringSetA = getRingSetOfAtom(ringSetMolecule, unplacedAtom);
                IAtomContainer ringSetAContainer = RingSetManipulator.getAllInOneContainer(ringSetA);
                templateHandler.mapTemplates(ringSetAContainer, ringSetAContainer.getAtomCount());

                if (checkAllRingAtomsHasCoordinates(ringSetAContainer)) {
                } else {
                    throw new IOException(
                            "RingAtomLayoutError: Not every ring atom is placed! Molecule cannot be layout.Sorry");
                }

                Point3d firstAtomOriginalCoord = unplacedAtom.getPoint3d();
                Point3d centerPlacedMolecule = ap3d.geometricCenterAllPlacedAtoms(molecule);

                setBranchAtom(molecule, unplacedAtom, atom, ap3d.getPlacedHeavyAtoms(molecule, atom), ap3d, atlp3d);
                layoutRingSystem(firstAtomOriginalCoord, unplacedAtom, ringSetA, centerPlacedMolecule, atom, ap3d);
                searchAndPlaceBranches(molecule, ringSetAContainer, ap3d, atlp3d, atomPlacer);
                //logger.debug("Ready layout Ring System");
                ringSetA = null;
                unplacedAtom = null;
                firstAtomOriginalCoord = null;
                centerPlacedMolecule = null;
            } else {
                //logger.debug("layout chains...");
                setAtomsToUnVisited(molecule);
                atom = ap3d.getNextPlacedHeavyAtomWithUnplacedAliphaticNeighbour(molecule);
                if (atom != null) {
                    ac = atom.getBuilder().newInstance(IAtomContainer.class);
                    ac.addAtom(atom);
                    searchAndPlaceBranches(molecule, ac, ap3d, atlp3d, atomPlacer);
                    ac = null;
                }
            }
        } while (!ap3d.allHeavyAtomsPlaced(molecule) || safetyCounter > molecule.getAtomCount());
    }

    /**
     * Layout the ring system, rotate and translate the template.
     *
     *@param  originalCoord         coordinates of the placedRingAtom from the template
     *@param  placedRingAtom        placedRingAtom
     *@param  ringSet               ring system which placedRingAtom is part of
     *@param  centerPlacedMolecule  the geometric center of the already placed molecule
     *@param  atomB                 placed neighbour atom of  placedRingAtom
     */
    private void layoutRingSystem(Point3d originalCoord, IAtom placedRingAtom, IRingSet ringSet,
            Point3d centerPlacedMolecule, IAtom atomB, AtomPlacer3D ap3d) {
        //logger.debug("****** Layout ring System ******");System.out.println(">around atom:"+molecule.indexOf(placedRingAtom));
        IAtomContainer ac = RingSetManipulator.getAllInOneContainer(ringSet);
        Point3d newCoord = placedRingAtom.getPoint3d();
        Vector3d axis = new Vector3d(atomB.getPoint3d().x - newCoord.x, atomB.getPoint3d().y - newCoord.y,
                atomB.getPoint3d().z - newCoord.z);
        translateStructure(originalCoord, newCoord, ac);
        //Rotate Ringsystem to farthest possible point
        Vector3d startAtomVector = new Vector3d(newCoord.x - atomB.getPoint3d().x, newCoord.y - atomB.getPoint3d().y,
                newCoord.z - atomB.getPoint3d().z);
        IAtom farthestAtom = ap3d.getFarthestAtom(placedRingAtom.getPoint3d(), ac);
        Vector3d farthestAtomVector = new Vector3d(farthestAtom.getPoint3d().x - newCoord.x,
                farthestAtom.getPoint3d().y - newCoord.y, farthestAtom.getPoint3d().z - newCoord.z);
        Vector3d n1 = new Vector3d();
        n1.cross(axis, farthestAtomVector);
        n1.normalize();
        double lengthFarthestAtomVector = farthestAtomVector.length();
        Vector3d farthestVector = new Vector3d(startAtomVector);
        farthestVector.normalize();
        farthestVector.scale((startAtomVector.length() + lengthFarthestAtomVector));
        double dotProduct = farthestAtomVector.dot(farthestVector);
        double angle = Math.acos(dotProduct / (farthestAtomVector.length() * farthestVector.length()));
        Vector3d ringCenter = new Vector3d();

        for (int i = 0; i < ac.getAtomCount(); i++) {
            if (!(ac.getAtom(i).getFlag(CDKConstants.ISPLACED))) {
                ringCenter.x = (ac.getAtom(i).getPoint3d()).x - newCoord.x;
                ringCenter.y = (ac.getAtom(i).getPoint3d()).y - newCoord.y;
                ringCenter.z = (ac.getAtom(i).getPoint3d()).z - newCoord.z;
                ringCenter = AtomTetrahedralLigandPlacer3D.rotate(ringCenter, n1, angle);
                ac.getAtom(i).setPoint3d(
                        new Point3d(ringCenter.x + newCoord.x, ringCenter.y + newCoord.y, ringCenter.z + newCoord.z));
                //ac.getAtomAt(i).setFlag(CDKConstants.ISPLACED, true);
            }
        }

        //Rotate Ring so that geometric center is max from placed center
        //logger.debug("Rotate RINGSYSTEM");
        Point3d pointRingCenter = GeometryUtil.get3DCenter(ac);
        double distance = 0;
        double rotAngleMax = 0;
        angle = 1 / 180 * Math.PI;
        ringCenter = new Vector3d(pointRingCenter.x, pointRingCenter.y, pointRingCenter.z);
        ringCenter.x = ringCenter.x - newCoord.x;
        ringCenter.y = ringCenter.y - newCoord.y;
        ringCenter.z = ringCenter.z - newCoord.z;
        for (int i = 1; i < 360; i++) {
            ringCenter = AtomTetrahedralLigandPlacer3D.rotate(ringCenter, axis, angle);
            if (centerPlacedMolecule.distance(new Point3d(ringCenter.x, ringCenter.y, ringCenter.z)) > distance) {
                rotAngleMax = i;
                distance = centerPlacedMolecule.distance(new Point3d(ringCenter.x, ringCenter.y, ringCenter.z));
            }
        }

        //rotate ring around axis with best angle
        rotAngleMax = (rotAngleMax / 180) * Math.PI;
        for (int i = 0; i < ac.getAtomCount(); i++) {
            if (!(ac.getAtom(i).getFlag(CDKConstants.ISPLACED))) {
                ringCenter.x = (ac.getAtom(i).getPoint3d()).x;
                ringCenter.y = (ac.getAtom(i).getPoint3d()).y;
                ringCenter.z = (ac.getAtom(i).getPoint3d()).z;
                ringCenter = AtomTetrahedralLigandPlacer3D.rotate(ringCenter, axis, rotAngleMax);
                ac.getAtom(i).setPoint3d(new Point3d(ringCenter.x, ringCenter.y, ringCenter.z));
                ac.getAtom(i).setFlag(CDKConstants.ISPLACED, true);
            }
        }
    }

    /**
     * Sets a branch atom to a ring or aliphatic chain.
     *
     *@param  unplacedAtom    The new branchAtom
     *@param  atomA           placed atom to which the unplaced atom is connected
     *@param  atomNeighbours  placed atomNeighbours of atomA
     */
    private void setBranchAtom(IAtomContainer molecule, IAtom unplacedAtom, IAtom atomA, IAtomContainer atomNeighbours,
            AtomPlacer3D ap3d, AtomTetrahedralLigandPlacer3D atlp3d) throws CDKException {
        //logger.debug("****** SET Branch Atom ****** >"+molecule.indexOf(unplacedAtom));
        IAtomContainer noCoords = molecule.getBuilder().newInstance(IAtomContainer.class);
        noCoords.addAtom(unplacedAtom);
        Point3d centerPlacedMolecule = ap3d.geometricCenterAllPlacedAtoms(molecule);
        IAtom atomB = atomNeighbours.getAtom(0);

        String atypeNameA = atomA.getAtomTypeName();
        String atypeNameB = atomB.getAtomTypeName();
        String atypeNameUnplaced = unplacedAtom.getAtomTypeName();

        double length = ap3d.getBondLengthValue(atypeNameA, atypeNameUnplaced);
        double angle = (ap3d.getAngleValue(atypeNameB, atypeNameA, atypeNameUnplaced)) * Math.PI / 180;
        /*
         * System.out.println("A:"+atomA.getSymbol()+" "+atomA.getAtomTypeName()+
         * " B:"+atomB.getSymbol()+" "+atomB.getAtomTypeName()
         * +" unplaced Atom:"
         * +unplacedAtom.getAtomTypeName()+" BL:"+length+" Angle:"+angle
         * +" FormalNeighbour:"
         * +atomA.getFormalNeighbourCount()+" HYB:"+atomA.getFlag
         * (CDKConstants.HYBRIDIZATION_SP2)
         * +" #Neigbhours:"+atomNeighbours.getAtomCount());
         */
        IAtom atomC = ap3d.getPlacedHeavyAtom(molecule, atomB, atomA);

        Point3d[] branchPoints = atlp3d.get3DCoordinatesForLigands(atomA, noCoords, atomNeighbours, atomC,
                (atomA.getFormalNeighbourCount() - atomNeighbours.getAtomCount()), length, angle);
        double distance = 0;
        int farthestPoint = 0;
        for (int i = 0; i < branchPoints.length; i++) {
            if (Math.abs(branchPoints[i].distance(centerPlacedMolecule)) > Math.abs(distance)) {
                distance = branchPoints[i].distance(centerPlacedMolecule);
                farthestPoint = i;
            }
        }

        int stereo = -1;
        IBond unplacedBond = molecule.getBond(atomA, unplacedAtom);
        if (atomA.getStereoParity() != CDKConstants.UNSET && atomA.getStereoParity() != 0
                || (unplacedBond.getStereo() == IBond.Stereo.UP || unplacedBond.getStereo() == IBond.Stereo.DOWN)
                && molecule.getMaximumBondOrder(atomA) == IBond.Order.SINGLE) {
            if (atomNeighbours.getAtomCount() > 1) {
                stereo = atlp3d.makeStereocenter(atomA.getPoint3d(), molecule.getBond(atomA, unplacedAtom),
                        (atomNeighbours.getAtom(0)).getPoint3d(), (atomNeighbours.getAtom(1)).getPoint3d(),
                        branchPoints);
            }
        }
        if (stereo != -1) {
            farthestPoint = stereo;
        }
        unplacedAtom.setPoint3d(branchPoints[farthestPoint]);
        unplacedAtom.setFlag(CDKConstants.ISPLACED, true);
    }

    /**
     * Search and place branches of a chain or ring.
     *
     *@param  chain          AtomContainer if atoms in an aliphatic chain or ring system
     */
    private void searchAndPlaceBranches(IAtomContainer molecule, IAtomContainer chain, AtomPlacer3D ap3d,
            AtomTetrahedralLigandPlacer3D atlp3d, AtomPlacer atomPlacer) throws CDKException {
        //logger.debug("****** SEARCH AND PLACE ****** Chain length: "+chain.getAtomCount());
        List atoms = null;
        IAtomContainer branchAtoms = molecule.getBuilder().newInstance(IAtomContainer.class);
        IAtomContainer connectedAtoms = molecule.getBuilder().newInstance(IAtomContainer.class);
        for (int i = 0; i < chain.getAtomCount(); i++) {
            atoms = molecule.getConnectedAtomsList(chain.getAtom(i));
            for (int j = 0; j < atoms.size(); j++) {
                IAtom atom = (IAtom) atoms.get(j);
                if (!(atom.getSymbol()).equals("H") & !(atom.getFlag(CDKConstants.ISPLACED))
                        & !(atom.getFlag(CDKConstants.ISINRING))) {
                    //logger.debug("SEARCH PLACE AND FOUND Branch Atom "+molecule.indexOf(chain.getAtomAt(i))+
                    //		" New Atom:"+molecule.indexOf(atoms[j])+" -> STORE");
                    connectedAtoms.add(ap3d.getPlacedHeavyAtoms(molecule, chain.getAtom(i)));
                    //logger.debug("Connected atom1:"+molecule.indexOf(connectedAtoms.getAtomAt(0))+" atom2:"+
                    //molecule.indexOf(connectedAtoms.getAtomAt(1))+ " Length:"+connectedAtoms.getAtomCount());
                    try {
                        setBranchAtom(molecule, atom, chain.getAtom(i), connectedAtoms, ap3d, atlp3d);
                    } catch (CDKException ex2) {
                        logger.error("SearchAndPlaceBranchERROR: Cannot find enough neighbour atoms due to"
                                + ex2.toString());
                        throw new CDKException("SearchAndPlaceBranchERROR: Cannot find enough neighbour atoms: "
                                + ex2.getMessage(), ex2);
                    }
                    branchAtoms.addAtom(atom);
                    connectedAtoms.removeAllElements();
                }
            }

        }//for ac.getAtomCount
        placeLinearChains3D(molecule, branchAtoms, ap3d, atlp3d, atomPlacer);
    }

    /**
     * Layout all aliphatic chains with ZMatrix.
     *
     *@param  startAtoms     AtomContainer of possible start atoms for a chain
     */
    private void placeLinearChains3D(IAtomContainer molecule, IAtomContainer startAtoms, AtomPlacer3D ap3d,
            AtomTetrahedralLigandPlacer3D atlp3d, AtomPlacer atomPlacer) throws CDKException {
        //logger.debug("****** PLACE LINEAR CHAINS ******");
        IAtom dihPlacedAtom = null;
        IAtom thirdPlacedAtom = null;
        IAtomContainer longestUnplacedChain = molecule.getBuilder().newInstance(IAtomContainer.class);
        if (startAtoms.getAtomCount() == 0) {
            //no branch points ->linear chain
            //logger.debug("------ LINEAR CHAIN - FINISH ------");
        } else {
            for (int i = 0; i < startAtoms.getAtomCount(); i++) {
                //logger.debug("FOUND BRANCHED ALKAN");
                //logger.debug("Atom NOT NULL:" + molecule.indexOf(startAtoms.getAtomAt(i)));
                thirdPlacedAtom = ap3d.getPlacedHeavyAtom(molecule, startAtoms.getAtom(i));
                dihPlacedAtom = ap3d.getPlacedHeavyAtom(molecule, thirdPlacedAtom, startAtoms.getAtom(i));
                longestUnplacedChain.addAtom(dihPlacedAtom);
                longestUnplacedChain.addAtom(thirdPlacedAtom);
                longestUnplacedChain.addAtom(startAtoms.getAtom(i));

                longestUnplacedChain.add(atomPlacer.getLongestUnplacedChain(molecule, startAtoms.getAtom(i)));
                setAtomsToUnVisited(molecule);

                if (longestUnplacedChain.getAtomCount() < 4) {
                    //di,third,sec
                    //logger.debug("------ SINGLE BRANCH METHYLTYP ------");
                    //break;
                } else {
                    //logger.debug("LongestUnchainLength:"+longestUnplacedChain.getAtomCount());
                    ap3d.placeAliphaticHeavyChain(molecule, longestUnplacedChain);
                    ap3d.zmatrixChainToCartesian(molecule, true);
                    searchAndPlaceBranches(molecule, longestUnplacedChain, ap3d, atlp3d, atomPlacer);
                }
                longestUnplacedChain.removeAllElements();
            }//for

        }
        //logger.debug("****** HANDLE ALIPHATICS END ******");
    }

    /**
     * Translates the template ring system to new coordinates.
     *
     *@param  originalCoord  original coordinates of the placed ring atom from template
     *@param  newCoord       new coordinates from branch placement
     *@param  ac             AtomContainer contains atoms of ring system
     */
    private void translateStructure(Point3d originalCoord, Point3d newCoord, IAtomContainer ac) {
        Point3d transVector = new Point3d(originalCoord);
        transVector.sub(newCoord);
        for (int i = 0; i < ac.getAtomCount(); i++) {
            if (!(ac.getAtom(i).getFlag(CDKConstants.ISPLACED))) {
                ac.getAtom(i).getPoint3d().sub(transVector);
                //ac.getAtomAt(i).setFlag(CDKConstants.ISPLACED, true);
            }
        }
    }

    /**
     * Returns the largest (number of atoms) ring set in a molecule.
     *
     *@param  ac  AtomContainer
     *@return     boolean
     */
    private boolean checkAllRingAtomsHasCoordinates(IAtomContainer ac) {
        for (int i = 0; i < ac.getAtomCount(); i++) {
            if (ac.getAtom(i).getPoint3d() != null && ac.getAtom(i).getFlag(CDKConstants.ISINRING)) {
            } else if (!ac.getAtom(i).getFlag(CDKConstants.ISINRING)) {
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Sets the atomsToPlace attribute of the ModelBuilder3D object.
     *
     *@param  ac  The new atomsToPlace value
     */
    private void setAtomsToPlace(IAtomContainer ac) {
        for (int i = 0; i < ac.getAtomCount(); i++) {
            ac.getAtom(i).setFlag(CDKConstants.ISPLACED, true);
        }
    }

    /**
     * Sets the atomsToUnPlaced attribute of the ModelBuilder3D object.
     */
    private void setAtomsToUnPlaced(IAtomContainer molecule) {
        for (int i = 0; i < molecule.getAtomCount(); i++) {
            molecule.getAtom(i).setFlag(CDKConstants.ISPLACED, false);
        }
    }

    /**
     * Sets the atomsToUnVisited attribute of the ModelBuilder3D object.
     */
    private void setAtomsToUnVisited(IAtomContainer molecule) {
        for (int i = 0; i < molecule.getAtomCount(); i++) {
            molecule.getAtom(i).setFlag(CDKConstants.VISITED, false);
        }
    }

    /**
     * Sets the templateHandler attribute of the ModelBuilder3D object.
     *
     * @param  templateHandler  The new templateHandler value
     */
    private void setTemplateHandler(TemplateHandler3D templateHandler) throws CDKException {
        if (templateHandler == null) throw new NullPointerException("The given template handler is null!");

        this.templateHandler = templateHandler;
    }

    /**
     * Returns the number of loaded templates. Note that it may return 0 because
     * templates are lazy loaded, that is upon the first ring being laid out.
     *
     * @return 0, if not templates are loaded
     */
    public int getTemplateCount() {
        return this.templateHandler.getTemplateCount();
    }

}
