/* Copyright (C) 2000-2009  Christoph Steinbeck, Stefan Kuhn<shk3@users.sf.net>
 *
 * 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.
 *
 * 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.structgen.stochastic.operator;

import java.util.ArrayList;
import java.util.List;

import org.openscience.cdk.exception.CDKException;
import org.openscience.cdk.graph.ConnectivityChecker;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IAtomContainerSet;
import org.openscience.cdk.math.RandomNumbersTool;
import org.openscience.cdk.structgen.stochastic.PartialFilledStructureMerger;
import org.openscience.cdk.tools.SaturationChecker;

/**
 * Modified molecular structures by applying crossover operator on a pair of parent structures
 * and generate a pair of offspring structures. Each of the two offspring structures inherits
 * a certain fragments from both of its parents.
 *
 * @cdk.module structgen
 * @cdk.githash
 */
public class CrossoverMachine {

    PartialFilledStructureMerger pfsm;

    /** selects a partitioning mode*/
    int                          splitMode                = 2;
    /** selects a partitioning scale*/
    int                          numatoms                 = 5;
    /** Indicates that <code>crossover</code> is using SPLIT_MODE_RADNDOM mode. */
    public static final int      SPLIT_MODE_RADNDOM       = 0;
    /** Indicates that <code>crossover</code> is using SPLIT_MODE_DEPTH_FIRST mode. */
    public static final int      SPLIT_MODE_DEPTH_FIRST   = 1;
    /** Indicates that <code>crossover</code> is using SPLIT_MODE_BREADTH_FIRST mode. */
    public static final int      SPLIT_MODE_BREADTH_FIRST = 2;

    /**
     * Constructs a new CrossoverMachine operator.
     */
    public CrossoverMachine() {
        pfsm = new PartialFilledStructureMerger();
    }

    /**
     * Performs the n point crossover of two {@link IAtomContainer}.
     * Precondition: The atoms in the molecules are ordered by properties to
     * preserve (e. g. atom symbol). Due to its randomized nature, this method
     * fails in around 3% of all cases. A CDKException with message "Could not
     * mate these properly" will then be thrown.
     *
     * @return The children.
     * @exception CDKException if it was not possible to form offsprings.
     */
    public List<IAtomContainer> doCrossover(IAtomContainer dad, IAtomContainer mom) throws CDKException {
        int tries = 0;
        while (true) {
            int dim = dad.getAtomCount();
            IAtomContainer[] redChild = new IAtomContainer[2];
            IAtomContainer[] blueChild = new IAtomContainer[2];

            List<Integer> redAtoms = new ArrayList<Integer>();
            List<Integer> blueAtoms = new ArrayList<Integer>();

            /* *randomly divide atoms into two parts: redAtoms and blueAtoms.** */
            if (splitMode == SPLIT_MODE_RADNDOM) {
                /*
                 * better way to randomly divide atoms into two parts: redAtoms
                 * and blueAtoms.
                 */
                for (int i = 0; i < dim; i++)
                    redAtoms.add(Integer.valueOf(i));
                for (int i = 0; i < (dim - numatoms); i++) {
                    int ranInt = RandomNumbersTool.randomInt(0, redAtoms.size() - 1);
                    redAtoms.remove(Integer.valueOf(ranInt));
                    blueAtoms.add(Integer.valueOf(ranInt));
                }

            } else {
                /* split graph using depth/breadth first traverse */
                ChemGraph graph = new ChemGraph(dad);
                graph.setNumAtoms(numatoms);
                if (splitMode == SPLIT_MODE_DEPTH_FIRST) {
                    redAtoms = graph.pickDFgraph();
                } else {
                    //this is SPLIT_MODE_BREADTH_FIRST
                    redAtoms = graph.pickBFgraph();
                }

                for (int i = 0; i < dim; i++) {
                    Integer element = Integer.valueOf(i);
                    if (!(redAtoms.contains(element))) {
                        blueAtoms.add(element);
                    }
                }
            }
            /* * dividing over ** */
            redChild[0] = dad.getBuilder().newInstance(IAtomContainer.class, dad);
            blueChild[0] = dad.getBuilder().newInstance(IAtomContainer.class, dad);
            redChild[1] = dad.getBuilder().newInstance(IAtomContainer.class, mom);
            blueChild[1] = dad.getBuilder().newInstance(IAtomContainer.class, mom);

            List<IAtom> blueAtomsInRedChild0 = new ArrayList<IAtom>();
            for (int j = 0; j < blueAtoms.size(); j++) {
                blueAtomsInRedChild0.add(redChild[0].getAtom((Integer) blueAtoms.get(j)));
            }
            for (int j = 0; j < blueAtomsInRedChild0.size(); j++) {
                redChild[0].removeAtom(blueAtomsInRedChild0.get(j));
            }
            List<IAtom> blueAtomsInRedChild1 = new ArrayList<IAtom>();
            for (int j = 0; j < blueAtoms.size(); j++) {
                blueAtomsInRedChild1.add(redChild[1].getAtom((Integer) blueAtoms.get(j)));
            }
            for (int j = 0; j < blueAtomsInRedChild1.size(); j++) {
                redChild[1].removeAtom(blueAtomsInRedChild1.get(j));
            }
            List<IAtom> redAtomsInBlueChild0 = new ArrayList<IAtom>();
            for (int j = 0; j < redAtoms.size(); j++) {
                redAtomsInBlueChild0.add(blueChild[0].getAtom((Integer) redAtoms.get(j)));
            }
            for (int j = 0; j < redAtomsInBlueChild0.size(); j++) {
                blueChild[0].removeAtom(redAtomsInBlueChild0.get(j));
            }
            List<IAtom> redAtomsInBlueChild1 = new ArrayList<IAtom>();
            for (int j = 0; j < redAtoms.size(); j++) {
                redAtomsInBlueChild1.add(blueChild[1].getAtom((Integer) redAtoms.get(j)));
            }
            for (int j = 0; j < redAtomsInBlueChild1.size(); j++) {
                blueChild[1].removeAtom(redAtomsInBlueChild1.get(j));
            }
            //if the two fragments of one and only one parent have an uneven number
            //of attachment points, we need to rearrange them
            SaturationChecker satCheck = new SaturationChecker();
            double red1attachpoints = 0;
            for (int i = 0; i < redChild[0].getAtomCount(); i++) {
                red1attachpoints += satCheck.getCurrentMaxBondOrder(redChild[0].getAtom(i), redChild[0]);
            }
            double red2attachpoints = 0;
            for (int i = 0; i < redChild[1].getAtomCount(); i++) {
                red2attachpoints += satCheck.getCurrentMaxBondOrder(redChild[1].getAtom(i), redChild[1]);
            }
            boolean isok = true;
            if (red1attachpoints % 2 == 1 ^ red2attachpoints % 2 == 1) {
                isok = false;
                IAtomContainer firstToBalance = redChild[1];
                IAtomContainer secondToBalance = blueChild[0];
                if (red1attachpoints % 2 == 1) {
                    firstToBalance = redChild[0];
                    secondToBalance = blueChild[1];
                }
                //we need an atom which has
                //- an uneven number of "attachment points" and
                //- an even number of outgoing bonds
                for (IAtom atom : firstToBalance.atoms()) {
                    if (satCheck.getCurrentMaxBondOrder(atom, firstToBalance) % 2 == 1
                            && firstToBalance.getBondOrderSum(atom) % 2 == 0) {
                        //we remove this from it's current container and add it to the other one
                        firstToBalance.removeAtom(atom);
                        secondToBalance.addAtom(atom);
                        isok = true;
                        break;
                    }
                }
            }
            //if we have combinable fragments
            if (isok) {
                //combine the fragments crosswise
                IAtomContainerSet[] newstrucs = new IAtomContainerSet[2];
                newstrucs[0] = dad.getBuilder().newInstance(IAtomContainerSet.class);
                newstrucs[0].add(ConnectivityChecker.partitionIntoMolecules(redChild[0]));
                newstrucs[0].add(ConnectivityChecker.partitionIntoMolecules(blueChild[1]));
                newstrucs[1] = dad.getBuilder().newInstance(IAtomContainerSet.class);
                newstrucs[1].add(ConnectivityChecker.partitionIntoMolecules(redChild[1]));
                newstrucs[1].add(ConnectivityChecker.partitionIntoMolecules(blueChild[0]));

                //and merge
                List<IAtomContainer> children = new ArrayList<IAtomContainer>(2);
                for (int f = 0; f < 2; f++) {
                    try {
                        children.add(f, pfsm.generate(newstrucs[f]));
                    } catch (Exception ex) {
                        //if children are not correct, the outer loop will repeat,
                        //so we ignore this
                    }
                }
                if (children.size() == 2 && ConnectivityChecker.isConnected(children.get(0))
                        && ConnectivityChecker.isConnected(children.get(1))) return children;
            }
            tries++;
            if (tries > 20) throw new CDKException("Could not mate these properly");
        }
    }
}
