/* Copyright (C) 1997-2007  Egon Willighagen <egonw@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.
 * 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.io.cml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

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

import org.openscience.cdk.CDKConstants;
import org.openscience.cdk.dict.DictRef;
import org.openscience.cdk.geometry.CrystalGeometryTools;
import org.openscience.cdk.interfaces.IAtom;
import org.openscience.cdk.interfaces.IAtomContainer;
import org.openscience.cdk.interfaces.IAtomContainerSet;
import org.openscience.cdk.interfaces.IBond;
import org.openscience.cdk.interfaces.IBond.Order;
import org.openscience.cdk.interfaces.IChemFile;
import org.openscience.cdk.interfaces.IChemModel;
import org.openscience.cdk.interfaces.IChemSequence;
import org.openscience.cdk.interfaces.ICrystal;
import org.openscience.cdk.interfaces.IMonomer;
import org.openscience.cdk.interfaces.IPseudoAtom;
import org.openscience.cdk.interfaces.IReaction;
import org.openscience.cdk.interfaces.IReactionSet;
import org.openscience.cdk.interfaces.ISingleElectron;
import org.openscience.cdk.interfaces.IStrand;
import org.openscience.cdk.interfaces.ITetrahedralChirality.Stereo;
import org.openscience.cdk.stereo.TetrahedralChirality;
import org.openscience.cdk.tools.ILoggingTool;
import org.openscience.cdk.tools.LoggingToolFactory;
import org.openscience.cdk.tools.manipulator.AtomContainerManipulator;
import org.openscience.cdk.tools.manipulator.BondManipulator;
import org.openscience.cdk.tools.periodictable.PeriodicTable;
import org.xml.sax.Attributes;

/**
 * Core CML 1.x and 2.x elements are parsed by this class (see {@cdk.cite WIL01}).
 *
 * <p>Please file a bug report if this parser fails to parse
 * a certain element or attribute value in a valid CML document.
 *
 * @cdk.module io
 * @cdk.githash
 *
 * @author Egon Willighagen &lt;egonw@sci.kun.nl&gt;
 **/
public class CMLCoreModule implements ICMLModule {

    protected ILoggingTool                     logger;
    protected final String                     SYSTEMID         = "CML-1999-05-15";
    //    protected IChemicalDocumentObject cdo;

    // data model to store things into
    protected IChemFile                        currentChemFile;

    protected IAtomContainer                   currentMolecule;
    protected IAtomContainerSet                currentMoleculeSet;
    protected IChemModel                       currentChemModel;
    protected IChemSequence                    currentChemSequence;
    protected IReactionSet                     currentReactionSet;
    protected IReaction                        currentReaction;
    protected IAtom                            currentAtom;
    protected IBond                            currentBond;
    protected IStrand                          currentStrand;
    protected IMonomer                         currentMonomer;
    protected Map<String, IAtom>               atomEnumeration;
    protected List<String>                     moleculeCustomProperty;

    // helper fields
    protected int                              formulaCounter;
    protected int                              atomCounter;
    protected List<String>                     elsym;
    protected List<String>                     eltitles;
    protected List<String>                     elid;
    protected List<String>                     formula;
    protected List<String>                     formalCharges;
    protected List<String>                     partialCharges;
    protected List<String>                     isotope;
    protected List<String>                     atomicNumbers;
    protected List<String>                     exactMasses;
    protected List<String>                     x3;
    protected List<String>                     y3;
    protected List<String>                     z3;
    protected List<String>                     x2;
    protected List<String>                     y2;
    protected List<String>                     xfract;
    protected List<String>                     yfract;
    protected List<String>                     zfract;
    protected List<String>                     hCounts;
    protected List<String>                     atomParities;
    protected List<String>                     parityARef1;
    protected List<String>                     parityARef2;
    protected List<String>                     parityARef3;
    protected List<String>                     parityARef4;
    protected List<String>                     atomDictRefs;
    protected List<String>                     atomAromaticities;
    protected List<String>                     spinMultiplicities;
    protected List<String>                     occupancies;
    protected Map<Integer, List<String>>       atomCustomProperty;
    protected boolean                          parityAtomsGiven;
    protected boolean                          parityGiven;

    protected int                              bondCounter;
    protected List<String>                     bondid;
    protected List<String>                     bondARef1;
    protected List<String>                     bondARef2;
    protected List<String>                     order;
    protected List<String>                     bondStereo;
    protected List<String>                     bondDictRefs;
    protected List<String>                     bondElid;
    protected List<Boolean>                    bondAromaticity;
    protected Map<String, Map<String, String>> bondCustomProperty;
    protected boolean                          stereoGiven;
    protected String                           inchi;
    protected int                              curRef;
    protected int                              CurrentElement;
    protected String                           BUILTIN;
    protected String                           DICTREF;
    protected String                           elementTitle;
    protected String                           currentChars;

    protected double[]                         unitcellparams;
    protected int                              crystalScalar;

    //    private Vector3d aAxis;
    //    private Vector3d bAxis;
    //    private Vector3d cAxis;
    boolean                                    cartesianAxesSet = false;

    public CMLCoreModule(IChemFile chemFile) {
        logger = LoggingToolFactory.createLoggingTool(CMLCoreModule.class);
        this.currentChemFile = chemFile;
    }

    public CMLCoreModule(ICMLModule conv) {
        logger = LoggingToolFactory.createLoggingTool(CMLCoreModule.class);
        inherit(conv);
    }

    @Override
    public void inherit(ICMLModule convention) {
        if (convention instanceof CMLCoreModule) {
            CMLCoreModule conv = (CMLCoreModule) convention;

            // copy the data model
            this.currentChemFile = conv.currentChemFile;
            this.currentMolecule = conv.currentMolecule;
            this.currentMoleculeSet = conv.currentMoleculeSet;
            this.currentChemModel = conv.currentChemModel;
            this.currentChemSequence = conv.currentChemSequence;
            this.currentReactionSet = conv.currentReactionSet;
            this.currentReaction = conv.currentReaction;
            this.currentAtom = conv.currentAtom;
            this.currentStrand = conv.currentStrand;
            this.currentMonomer = conv.currentMonomer;
            this.atomEnumeration = conv.atomEnumeration;
            this.moleculeCustomProperty = conv.moleculeCustomProperty;

            // copy the intermediate fields
            this.logger = conv.logger;
            this.BUILTIN = conv.BUILTIN;
            this.atomCounter = conv.atomCounter;
            this.formulaCounter = conv.formulaCounter;
            this.elsym = conv.elsym;
            this.eltitles = conv.eltitles;
            this.elid = conv.elid;
            this.formalCharges = conv.formalCharges;
            this.partialCharges = conv.partialCharges;
            this.isotope = conv.isotope;
            this.atomicNumbers = conv.atomicNumbers;
            this.exactMasses = conv.exactMasses;
            this.x3 = conv.x3;
            this.y3 = conv.y3;
            this.z3 = conv.z3;
            this.x2 = conv.x2;
            this.y2 = conv.y2;
            this.xfract = conv.xfract;
            this.yfract = conv.yfract;
            this.zfract = conv.zfract;
            this.hCounts = conv.hCounts;
            this.atomParities = conv.atomParities;
            this.parityARef1 = conv.parityARef1;
            this.parityARef2 = conv.parityARef2;
            this.parityARef3 = conv.parityARef3;
            this.parityARef4 = conv.parityARef4;
            this.atomDictRefs = conv.atomDictRefs;
            this.atomAromaticities = conv.atomAromaticities;
            this.spinMultiplicities = conv.spinMultiplicities;
            this.occupancies = conv.occupancies;
            this.bondCounter = conv.bondCounter;
            this.bondid = conv.bondid;
            this.bondARef1 = conv.bondARef1;
            this.bondARef2 = conv.bondARef2;
            this.order = conv.order;
            this.bondStereo = conv.bondStereo;
            this.bondCustomProperty = conv.bondCustomProperty;
            this.atomCustomProperty = conv.atomCustomProperty;
            this.bondDictRefs = conv.bondDictRefs;
            this.bondAromaticity = conv.bondAromaticity;
            this.curRef = conv.curRef;
            this.unitcellparams = conv.unitcellparams;
            this.inchi = conv.inchi;
        } else {
            logger.warn("Cannot inherit information from module: ", convention.getClass().getName());
        }
    }

    @Override
    public IChemFile returnChemFile() {
        return currentChemFile;
    }

    /**
     * Clean all data about parsed data.
     */
    protected void newMolecule() {
        newMoleculeData();
        newAtomData();
        newBondData();
        newCrystalData();
        newFormulaData();
    }

    /**
     * Clean all data about the molecule itself.
     */
    protected void newMoleculeData() {
        this.inchi = null;
    }

    /**
     * Clean all data about read formulas.
     */
    protected void newFormulaData() {
        formulaCounter = 0;
        formula = new ArrayList<String>();
    }

    /**
     * Clean all data about read atoms.
     */
    protected void newAtomData() {
        atomCounter = 0;
        elsym = new ArrayList<String>();
        elid = new ArrayList<String>();
        eltitles = new ArrayList<String>();
        formalCharges = new ArrayList<String>();
        partialCharges = new ArrayList<String>();
        isotope = new ArrayList<String>();
        atomicNumbers = new ArrayList<String>();
        exactMasses = new ArrayList<String>();
        x3 = new ArrayList<String>();
        y3 = new ArrayList<String>();
        z3 = new ArrayList<String>();
        x2 = new ArrayList<String>();
        y2 = new ArrayList<String>();
        xfract = new ArrayList<String>();
        yfract = new ArrayList<String>();
        zfract = new ArrayList<String>();
        hCounts = new ArrayList<String>();
        atomParities = new ArrayList<String>();
        parityARef1 = new ArrayList<String>();
        parityARef2 = new ArrayList<String>();
        parityARef3 = new ArrayList<String>();
        parityARef4 = new ArrayList<String>();
        atomAromaticities = new ArrayList<String>();
        atomDictRefs = new ArrayList<String>();
        spinMultiplicities = new ArrayList<String>();
        occupancies = new ArrayList<String>();
        atomCustomProperty = new HashMap<Integer, List<String>>();
    }

    /**
     * Clean all data about read bonds.
     */
    protected void newBondData() {
        bondCounter = 0;
        bondid = new ArrayList<String>();
        bondARef1 = new ArrayList<String>();
        bondARef2 = new ArrayList<String>();
        order = new ArrayList<String>();
        bondStereo = new ArrayList<String>();
        bondCustomProperty = new Hashtable<String, Map<String, String>>();
        bondDictRefs = new ArrayList<String>();
        bondElid = new ArrayList<String>();
        bondAromaticity = new ArrayList<Boolean>();
    }

    /**
     * Clean all data about read bonds.
     */
    protected void newCrystalData() {
        unitcellparams = new double[6];
        cartesianAxesSet = false;
        crystalScalar = 0;
        //        aAxis = new Vector3d();
        //        bAxis = new Vector3d();
        //        cAxis = new Vector3d();
    }

    @Override
    public void startDocument() {
        logger.info("Start XML Doc");
        // cdo.startDocument();
        currentChemSequence = currentChemFile.getBuilder().newInstance(IChemSequence.class);
        currentChemModel = currentChemFile.getBuilder().newInstance(IChemModel.class);
        currentMoleculeSet = currentChemFile.getBuilder().newInstance(IAtomContainerSet.class);
        currentMolecule = currentChemFile.getBuilder().newInstance(IAtomContainer.class);
        atomEnumeration = new HashMap<String, IAtom>();
        moleculeCustomProperty = new ArrayList<String>();

        newMolecule();
        BUILTIN = "";
        curRef = 0;
    }

    @Override
    public void endDocument() {
        //        cdo.endDocument();
        if (currentReactionSet != null && currentReactionSet.getReactionCount() == 0 && currentReaction != null) {
            logger.debug("Adding reaction to ReactionSet");
            currentReactionSet.addReaction(currentReaction);
        }
        if (currentReactionSet != null && currentChemModel.getReactionSet() == null) {
            logger.debug("Adding SOR to ChemModel");
            currentChemModel.setReactionSet(currentReactionSet);
        }
        if (currentMoleculeSet != null && currentMoleculeSet.getAtomContainerCount() != 0) {
            logger.debug("Adding reaction to MoleculeSet");
            currentChemModel.setMoleculeSet(currentMoleculeSet);
        }
        if (currentChemSequence.getChemModelCount() == 0) {
            logger.debug("Adding ChemModel to ChemSequence");
            currentChemSequence.addChemModel(currentChemModel);
        }
        if (currentChemFile.getChemSequenceCount() == 0) {
            // assume there is one non-animation ChemSequence
            //    		addChemSequence(currentChemSequence);
            currentChemFile.addChemSequence(currentChemSequence);
        }

        logger.info("End XML Doc");
    }

    @Override
    public void startElement(CMLStack xpath, String uri, String local, String raw, Attributes atts) {
        String name = local;
        logger.debug("StartElement");
        currentChars = "";

        BUILTIN = "";
        DICTREF = "";

        for (int i = 0; i < atts.getLength(); i++) {
            String qname = atts.getQName(i);
            if (qname.equals("builtin")) {
                BUILTIN = atts.getValue(i);
                logger.debug(name, "->BUILTIN found: ", atts.getValue(i));
            } else if (qname.equals("dictRef")) {
                DICTREF = atts.getValue(i);
                logger.debug(name, "->DICTREF found: ", atts.getValue(i));
            } else if (qname.equals("title")) {
                elementTitle = atts.getValue(i);
                logger.debug(name, "->TITLE found: ", atts.getValue(i));
            } else {
                logger.debug("Qname: ", qname);
            }
        }

        if ("atom".equals(name)) {
            atomCounter++;
            for (int i = 0; i < atts.getLength(); i++) {

                String att = atts.getQName(i);
                String value = atts.getValue(i);

                if (att.equals("id")) { // this is supported in CML 1.x
                    elid.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("elementType")) {
                    elsym.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("title")) {
                    eltitles.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("x2")) {
                    x2.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("xy2")) {
                    StringTokenizer tokenizer = new StringTokenizer(value);
                    x2.add(tokenizer.nextToken());
                    y2.add(tokenizer.nextToken());
                } // this is supported in CML 2.0
                else if (att.equals("xyzFract")) {
                    StringTokenizer tokenizer = new StringTokenizer(value);
                    xfract.add(tokenizer.nextToken());
                    yfract.add(tokenizer.nextToken());
                    zfract.add(tokenizer.nextToken());
                } // this is supported in CML 2.0
                else if (att.equals("xyz3")) {
                    StringTokenizer tokenizer = new StringTokenizer(value);
                    x3.add(tokenizer.nextToken());
                    y3.add(tokenizer.nextToken());
                    z3.add(tokenizer.nextToken());
                } // this is supported in CML 2.0
                else if (att.equals("y2")) {
                    y2.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("x3")) {
                    x3.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("y3")) {
                    y3.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("z3")) {
                    z3.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("xFract")) {
                    xfract.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("yFract")) {
                    yfract.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("zFract")) {
                    zfract.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("formalCharge")) {
                    formalCharges.add(value);
                } // this is supported in CML 2.0
                else if (att.equals("hydrogenCount")) {
                    hCounts.add(value);
                } else if (att.equals("isotopeNumber")) {
                    isotope.add(value);
                } else if (att.equals("dictRef")) {
                    logger.debug("occupancy: " + value);
                    atomDictRefs.add(value);
                } else if (att.equals("spinMultiplicity")) {
                    spinMultiplicities.add(value);
                } else if (att.equals("occupancy")) {
                    occupancies.add(value);
                }

                else {
                    logger.warn("Unparsed attribute: " + att);
                }

                parityAtomsGiven = false;
                parityGiven = false;
            }
        } else if ("atomArray".equals(name) && !xpath.endsWith("formula", "atomArray")) {
            boolean atomsCounted = false;
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                int count = 0;
                if (att.equals("atomID")) {
                    count = addArrayElementsTo(elid, atts.getValue(i));
                } else if (att.equals("elementType")) {
                    count = addArrayElementsTo(elsym, atts.getValue(i));
                } else if (att.equals("x2")) {
                    count = addArrayElementsTo(x2, atts.getValue(i));
                } else if (att.equals("y2")) {
                    count = addArrayElementsTo(y2, atts.getValue(i));
                } else if (att.equals("x3")) {
                    count = addArrayElementsTo(x3, atts.getValue(i));
                } else if (att.equals("y3")) {
                    count = addArrayElementsTo(y3, atts.getValue(i));
                } else if (att.equals("z3")) {
                    count = addArrayElementsTo(z3, atts.getValue(i));
                } else if (att.equals("xFract")) {
                    count = addArrayElementsTo(xfract, atts.getValue(i));
                } else if (att.equals("yFract")) {
                    count = addArrayElementsTo(yfract, atts.getValue(i));
                } else if (att.equals("zFract")) {
                    count = addArrayElementsTo(zfract, atts.getValue(i));
                } else {
                    logger.warn("Unparsed attribute: " + att);
                }
                if (!atomsCounted) {
                    atomCounter += count;
                    atomsCounted = true;
                }
            }
        } else if ("atomParity".equals(name)) {
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                if (att.equals("atomRefs4") && !parityAtomsGiven) {
                    //Expect exactly four references
                    try {
                        StringTokenizer st = new StringTokenizer(atts.getValue(i));
                        parityARef1.add((String) st.nextElement());
                        parityARef2.add((String) st.nextElement());
                        parityARef3.add((String) st.nextElement());
                        parityARef4.add((String) st.nextElement());
                        parityAtomsGiven = true;
                    } catch (Exception e) {
                        logger.error("Error in CML file: ", e.getMessage());
                        logger.debug(e);
                    }
                }
            }
        } else if ("bond".equals(name)) {
            bondCounter++;
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                logger.debug("B2 ", att, "=", atts.getValue(i));

                if (att.equals("id")) {
                    bondid.add(atts.getValue(i));
                    logger.debug("B3 ", bondid);
                } else if (att.equals("atomRefs") || // this is CML 1.x support
                        att.equals("atomRefs2")) { // this is CML 2.0 support

                    // expect exactly two references
                    try {
                        StringTokenizer st = new StringTokenizer(atts.getValue(i));
                        bondARef1.add((String) st.nextElement());
                        bondARef2.add((String) st.nextElement());
                    } catch (Exception e) {
                        logger.error("Error in CML file: ", e.getMessage());
                        logger.debug(e);
                    }
                } else if (att.equals("order")) { // this is CML 2.0 support
                    order.add(atts.getValue(i).trim());
                } else if (att.equals("dictRef")) {
                    bondDictRefs.add(atts.getValue(i).trim());
                }
            }

            stereoGiven = false;
            curRef = 0;
        } else if ("bondArray".equals(name)) {
            boolean bondsCounted = false;
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                int count = 0;
                if (att.equals("bondID")) {
                    count = addArrayElementsTo(bondid, atts.getValue(i));
                } else if (att.equals("atomRefs1")) {
                    count = addArrayElementsTo(bondARef1, atts.getValue(i));
                } else if (att.equals("atomRefs2")) {
                    count = addArrayElementsTo(bondARef2, atts.getValue(i));
                } else if (att.equals("atomRef1")) {
                    count = addArrayElementsTo(bondARef1, atts.getValue(i));
                } else if (att.equals("atomRef2")) {
                    count = addArrayElementsTo(bondARef2, atts.getValue(i));
                } else if (att.equals("order")) {
                    count = addArrayElementsTo(order, atts.getValue(i));
                } else {
                    logger.warn("Unparsed attribute: " + att);
                }
                if (!bondsCounted) {
                    bondCounter += count;
                    bondsCounted = true;
                }
            }
            curRef = 0;
        } else if ("bondStereo".equals(name)) {
            for (int i = 0; i < atts.getLength(); i++) {
                if (atts.getQName(i).equals("dictRef")) {
                    String value = atts.getValue(i);
                    if (value.startsWith("cml:") && value.length() > 4) {
                        bondStereo.add(value.substring(4));
                        stereoGiven = true;
                    }
                }
            }
        } else if ("bondType".equals(name)) {
            for (int i = 0; i < atts.getLength(); i++) {
                if (atts.getQName(i).equals("dictRef")) {
                    if (atts.getValue(i).equals("cdk:aromaticBond")) bondAromaticity.add(Boolean.TRUE);
                }
            }
        } else if ("molecule".equals(name)) {
            newMolecule();
            BUILTIN = "";
            //            cdo.startObject("Molecule");
            if (currentChemModel == null)
                currentChemModel = currentChemFile.getBuilder().newInstance(IChemModel.class);
            if (currentMoleculeSet == null)
                currentMoleculeSet = currentChemFile.getBuilder().newInstance(IAtomContainerSet.class);
            currentMolecule = currentChemFile.getBuilder().newInstance(IAtomContainer.class);
            for (int i = 0; i < atts.getLength(); i++) {
                if (atts.getQName(i).equals("id")) {
                    //                    cdo.setObjectProperty("Molecule", "id", atts.getValue(i));
                    currentMolecule.setID(atts.getValue(i));
                } else if (atts.getQName(i).equals("dictRef")) {
                    //                	cdo.setObjectProperty("Molecule", "dictRef", atts.getValue(i));
                    currentMolecule.setProperty(new DictRef(DICTREF, atts.getValue(i)), atts.getValue(i));
                }
            }
        } else if ("crystal".equals(name)) {
            newCrystalData();
            //            cdo.startObject("Crystal");
            currentMolecule = currentChemFile.getBuilder().newInstance(ICrystal.class, currentMolecule);
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                if (att.equals("z")) {
                    //                    cdo.setObjectProperty("Crystal", "z", atts.getValue(i));
                    ((ICrystal) currentMolecule).setZ(Integer.parseInt(atts.getValue(i)));
                }
            }
        } else if ("symmetry".equals(name)) {
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                if (att.equals("spaceGroup")) {
                    //                    cdo.setObjectProperty("Crystal", "spacegroup", atts.getValue(i));
                    ((ICrystal) currentMolecule).setSpaceGroup(atts.getValue(i));
                }
            }
        } else if ("identifier".equals(name)) {
            if (atts.getValue("convention") != null && atts.getValue("convention").equals("iupac:inchi")
                    && atts.getValue("value") != null) {
                //                cdo.setObjectProperty("Molecule", "inchi", atts.getValue("value"));
                currentMolecule.setProperty(CDKConstants.INCHI, atts.getValue("value"));
            }
        } else if ("scalar".equals(name)) {
            if (xpath.endsWith("crystal", "scalar")) crystalScalar++;
        } else if ("label".equals(name)) {
            if (xpath.endsWith("atomType", "label")) {
                //            	cdo.setObjectProperty("Atom", "atomTypeLabel", atts.getValue("value"));
                currentAtom.setAtomTypeName(atts.getValue("value"));
            }
        } else if ("list".equals(name)) {
            //            cdo.startObject("MoleculeSet");
            if (DICTREF.equals("cdk:model")) {
                currentChemModel = currentChemFile.getBuilder().newInstance(IChemModel.class);
                // see if there is an ID attribute
                for (int i = 0; i < atts.getLength(); i++) {
                    String att = atts.getQName(i);
                    if (att.equals("id")) {
                        currentChemModel.setID(atts.getValue(i));
                    }
                }
            } else if (DICTREF.equals("cdk:moleculeSet")) {
                currentMoleculeSet = currentChemFile.getBuilder().newInstance(IAtomContainerSet.class);
                // see if there is an ID attribute
                for (int i = 0; i < atts.getLength(); i++) {
                    String att = atts.getQName(i);
                    if (att.equals("id")) {
                        currentMoleculeSet.setID(atts.getValue(i));
                    }
                }
                currentMolecule = currentChemFile.getBuilder().newInstance(IAtomContainer.class);
            } else {
                // the old default
                currentMoleculeSet = currentChemFile.getBuilder().newInstance(IAtomContainerSet.class);
                // see if there is an ID attribute
                for (int i = 0; i < atts.getLength(); i++) {
                    String att = atts.getQName(i);
                    if (att.equals("id")) {
                        currentMoleculeSet.setID(atts.getValue(i));
                    }
                }
                currentMolecule = currentChemFile.getBuilder().newInstance(IAtomContainer.class);
            }
        } else if ("formula".equals(name)) {
            formulaCounter++;
            for (int i = 0; i < atts.getLength(); i++) {
                String att = atts.getQName(i);
                String value = atts.getValue(i);
                if (att.equals("concise")) {
                    formula.add(value);
                }
            }
        }
    }

    @Override
    public void endElement(CMLStack xpath, String uri, String name, String raw) {
        logger.debug("EndElement: ", name);

        String cData = currentChars;
        if ("bond".equals(name)) {
            if (!stereoGiven) bondStereo.add("");
            if (bondCounter > bondDictRefs.size()) bondDictRefs.add(null);
            if (bondCounter > bondAromaticity.size()) bondAromaticity.add(null);
        } else if ("atom".equals(name)) {
            if (atomCounter > eltitles.size()) {
                eltitles.add(null);
            }
            if (atomCounter > hCounts.size()) {
                hCounts.add(null);
            }
            if (atomCounter > atomDictRefs.size()) {
                atomDictRefs.add(null);
            }
            if (atomCounter > atomAromaticities.size()) {
                atomAromaticities.add(null);
            }
            if (atomCounter > isotope.size()) {
                isotope.add(null);
            }
            if (atomCounter > atomicNumbers.size()) {
                atomicNumbers.add(null);
            }
            if (atomCounter > exactMasses.size()) {
                exactMasses.add(null);
            }
            if (atomCounter > spinMultiplicities.size()) {
                spinMultiplicities.add(null);
            }
            if (atomCounter > occupancies.size()) {
                occupancies.add(null);
            }
            if (atomCounter > formalCharges.size()) {
                /*
                 * while strictly undefined, assume zero formal charge when no
                 * number is given
                 */
                formalCharges.add("0");
            }
            if (!parityGiven) {
                atomParities.add("");
            }
            if (!parityAtomsGiven) {
                parityARef1.add("");
                parityARef2.add("");
                parityARef3.add("");
                parityARef4.add("");
            }
            /*
             * It may happen that not all atoms have associated 2D or 3D
             * coordinates. accept that
             */
            if (atomCounter > x2.size() && x2.size() != 0) {
                /*
                 * apparently, the previous atoms had atomic coordinates, add
                 * 'null' for this atom
                 */
                x2.add(null);
                y2.add(null);
            }
            if (atomCounter > x3.size() && x3.size() != 0) {
                /*
                 * apparently, the previous atoms had atomic coordinates, add
                 * 'null' for this atom
                 */
                x3.add(null);
                y3.add(null);
                z3.add(null);
            }

            if (atomCounter > xfract.size() && xfract.size() != 0) {
                /*
                 * apparently, the previous atoms had atomic coordinates, add
                 * 'null' for this atom
                 */
                xfract.add(null);
                yfract.add(null);
                zfract.add(null);
            }
        } else if ("molecule".equals(name)) {
            storeData();
            //            cdo.endObject("Molecule");
            if (currentMolecule instanceof ICrystal) {
                logger.debug("Adding crystal to chemModel");
                currentChemModel.setCrystal((ICrystal) currentMolecule);
                currentChemSequence.addChemModel(currentChemModel);
            } else if (currentMolecule instanceof IAtomContainer) {
                logger.debug("Adding molecule to set");
                currentMoleculeSet.addAtomContainer(currentMolecule);
                logger.debug("#mols in set: " + currentMoleculeSet.getAtomContainerCount());
            }
        } else if ("crystal".equals(name)) {
            if (crystalScalar > 0) {
                // convert unit cell parameters to cartesians
                Vector3d[] axes = CrystalGeometryTools.notionalToCartesian(unitcellparams[0], unitcellparams[1],
                        unitcellparams[2], unitcellparams[3], unitcellparams[4], unitcellparams[5]);
                cartesianAxesSet = true;
                //                cdo.startObject("a-axis");
                //                cdo.setObjectProperty("a-axis", "x", new Double(aAxis.x).toString());
                //                cdo.setObjectProperty("a-axis", "y", new Double(aAxis.y).toString());
                //                cdo.setObjectProperty("a-axis", "z", new Double(aAxis.z).toString());
                //                cdo.endObject("a-axis");
                //                cdo.startObject("b-axis");
                //                cdo.setObjectProperty("b-axis", "x", new Double(bAxis.x).toString());
                //                cdo.setObjectProperty("b-axis", "y", new Double(bAxis.y).toString());
                //                cdo.setObjectProperty("b-axis", "z", new Double(bAxis.z).toString());
                //                cdo.endObject("b-axis");
                //                cdo.startObject("c-axis");
                //                cdo.setObjectProperty("c-axis", "x", new Double(cAxis.x).toString());
                //                cdo.setObjectProperty("c-axis", "y", new Double(cAxis.y).toString());
                //                cdo.setObjectProperty("c-axis", "z", new Double(cAxis.z).toString());
                //                cdo.endObject("c-axis");
                ((ICrystal) currentMolecule).setA(axes[0]);
                ((ICrystal) currentMolecule).setB(axes[1]);
                ((ICrystal) currentMolecule).setC(axes[2]);
            } else {
                logger.error("Could not find crystal unit cell parameters");
            }
            //            cdo.endObject("Crystal");
        } else if ("list".equals(name)) {
            //            cdo.endObject("MoleculeSet");
            // FIXME: I really should check the DICTREF, but there is currently
            // no mechanism for storing these for use with endTag() :(
            // So, instead, for now, just see if it already has done the setting
            // to work around duplication
            if (currentChemModel.getMoleculeSet() != currentMoleculeSet) {
                currentChemModel.setMoleculeSet(currentMoleculeSet);
                currentChemSequence.addChemModel(currentChemModel);
            }
        } else if ("coordinate3".equals(name)) {
            if (BUILTIN.equals("xyz3")) {
                logger.debug("New coord3 xyz3 found: ", currentChars);

                try {

                    StringTokenizer st = new StringTokenizer(currentChars);
                    x3.add(st.nextToken());
                    y3.add(st.nextToken());
                    z3.add(st.nextToken());
                    logger.debug("coord3 x3.length: ", x3.size());
                    logger.debug("coord3 y3.length: ", y3.size());
                    logger.debug("coord3 z3.length: ", z3.size());
                } catch (Exception exception) {
                    logger.error("CMLParsing error while setting coordinate3!");
                    logger.debug(exception);
                }
            } else {
                logger.warn("Unknown coordinate3 BUILTIN: " + BUILTIN);
            }
        } else if ("string".equals(name)) {
            if (BUILTIN.equals("elementType")) {
                logger.debug("Element: ", cData.trim());
                elsym.add(cData);
            } else if (BUILTIN.equals("atomRef")) {
                curRef++;
                logger.debug("Bond: ref #", curRef);

                if (curRef == 1) {
                    bondARef1.add(cData.trim());
                } else if (curRef == 2) {
                    bondARef2.add(cData.trim());
                }
            } else if (BUILTIN.equals("order")) {
                logger.debug("Bond: order ", cData.trim());
                order.add(cData.trim());
            } else if (BUILTIN.equals("formalCharge")) {
                // NOTE: this combination is in violation of the CML DTD!!!
                logger.warn("formalCharge BUILTIN accepted but violating CML DTD");
                logger.debug("Charge: ", cData.trim());
                String charge = cData.trim();
                if (charge.startsWith("+") && charge.length() > 1) {
                    charge = charge.substring(1);
                }
                formalCharges.add(charge);
            }
        } else if ("bondStereo".equals(name)) {
            if (!currentChars.isEmpty() && !stereoGiven) {
                bondStereo.add(currentChars);
                stereoGiven = Boolean.TRUE;
            }
        } else if ("atomParity".equals(name)) {
            if (!currentChars.isEmpty() && !parityGiven && parityAtomsGiven) {
                atomParities.add(currentChars);
                parityGiven = Boolean.TRUE;
            }
        } else if ("float".equals(name)) {
            if (BUILTIN.equals("x3")) {
                x3.add(cData.trim());
            } else if (BUILTIN.equals("y3")) {
                y3.add(cData.trim());
            } else if (BUILTIN.equals("z3")) {
                z3.add(cData.trim());
            } else if (BUILTIN.equals("x2")) {
                x2.add(cData.trim());
            } else if (BUILTIN.equals("y2")) {
                y2.add(cData.trim());
            } else if (BUILTIN.equals("order")) {
                // NOTE: this combination is in violation of the CML DTD!!!
                order.add(cData.trim());
            } else if (BUILTIN.equals("charge") || BUILTIN.equals("partialCharge")) {
                partialCharges.add(cData.trim());
            }
        } else if ("integer".equals(name)) {
            if (BUILTIN.equals("formalCharge")) {
                formalCharges.add(cData.trim());
            }
        } else if ("coordinate2".equals(name)) {
            if (BUILTIN.equals("xy2")) {
                logger.debug("New coord2 xy2 found.", cData);

                try {

                    StringTokenizer st = new StringTokenizer(cData);
                    x2.add(st.nextToken());
                    y2.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 175, 1);
                }
            }
        } else if ("stringArray".equals(name)) {
            if (BUILTIN.equals("id") || BUILTIN.equals("atomId") || BUILTIN.equals("atomID")) { // invalid according to CML1 DTD but found in OpenBabel 1.x output

                try {
                    boolean countAtoms = (atomCounter == 0) ? true : false;
                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {
                        if (countAtoms) {
                            atomCounter++;
                        }
                        String token = st.nextToken();
                        logger.debug("StringArray (Token): ", token);
                        elid.add(token);
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 186, 1);
                }
            } else if (BUILTIN.equals("elementType")) {

                try {
                    boolean countAtoms = (atomCounter == 0) ? true : false;
                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {
                        if (countAtoms) {
                            atomCounter++;
                        }
                        elsym.add(st.nextToken());
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 194, 1);
                }
            } else if (BUILTIN.equals("atomRefs")) {
                curRef++;
                logger.debug("New atomRefs found: ", curRef);

                try {
                    boolean countBonds = (bondCounter == 0) ? true : false;
                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {
                        if (countBonds) {
                            bondCounter++;
                        }
                        String token = st.nextToken();
                        logger.debug("Token: ", token);

                        if (curRef == 1) {
                            bondARef1.add(token);
                        } else if (curRef == 2) {
                            bondARef2.add(token);
                        }
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 194, 1);
                }
            } else if (BUILTIN.equals("atomRef")) {
                curRef++;
                logger.debug("New atomRef found: ", curRef); // this is CML1 stuff, we get things like:
                /*
                 * <bondArray> <stringArray builtin="atomRef">a2 a2 a2 a2 a3 a3
                 * a4 a4 a5 a6 a7 a9</stringArray> <stringArray
                 * builtin="atomRef">a9 a11 a12 a13 a5 a4 a6 a9 a7 a8 a8
                 * a10</stringArray> <stringArray builtin="order">1 1 1 1 2 1 2
                 * 1 1 1 2 2</stringArray> </bondArray>
                 */

                try {
                    boolean countBonds = (bondCounter == 0) ? true : false;
                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {
                        if (countBonds) {
                            bondCounter++;
                        }
                        String token = st.nextToken();
                        logger.debug("Token: ", token);

                        if (curRef == 1) {
                            bondARef1.add(token);
                        } else if (curRef == 2) {
                            bondARef2.add(token);
                        }
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 194, 1);
                }
            } else if (BUILTIN.equals("order")) {
                logger.debug("New bond order found.");

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {

                        String token = st.nextToken();
                        logger.debug("Token: ", token);
                        order.add(token);
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 194, 1);
                }
            }
        } else if ("integerArray".equals(name)) {
            logger.debug("IntegerArray: builtin = ", BUILTIN);

            if (BUILTIN.equals("formalCharge")) {

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens()) {

                        String token = st.nextToken();
                        logger.debug("Charge added: ", token);
                        formalCharges.add(token);
                    }
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 205, 1);
                }
            }
        } else if ("scalar".equals(name)) {
            if (xpath.endsWith("crystal", "scalar")) {
                logger.debug("Going to set a crystal parameter: " + crystalScalar, " to ", cData);
                try {
                    unitcellparams[crystalScalar - 1] = Double.parseDouble(cData.trim());
                } catch (NumberFormatException exception) {
                    logger.error("Content must a float: " + cData);
                }
            } else if (xpath.endsWith("bond", "scalar")) {
                if (DICTREF.equals("mdl:stereo")) {
                    bondStereo.add(cData.trim());
                    stereoGiven = true;
                } else {
                    Map<String, String> bp = bondCustomProperty.get(bondid.get(bondid.size() - 1));
                    if (bp == null) {
                        bp = new Hashtable<String, String>();
                        bondCustomProperty.put(bondid.get(bondid.size() - 1), bp);
                    }
                    bp.put(elementTitle, cData.trim());
                }
            } else if (xpath.endsWith("atom", "scalar")) {
                if (DICTREF.equals("cdk:partialCharge")) {
                    partialCharges.add(cData.trim());
                } else if (DICTREF.equals("cdk:atomicNumber")) {
                    atomicNumbers.add(cData.trim());
                } else if (DICTREF.equals("cdk:aromaticAtom")) {
                    atomAromaticities.add(cData.trim());
                } else if (DICTREF.equals("cdk:isotopicMass")) {
                    exactMasses.add(cData.trim());
                } else {
                    if (atomCustomProperty.get(Integer.valueOf(atomCounter - 1)) == null)
                        atomCustomProperty.put(Integer.valueOf(atomCounter - 1), new ArrayList<String>());
                    atomCustomProperty.get(Integer.valueOf(atomCounter - 1)).add(elementTitle);
                    atomCustomProperty.get(Integer.valueOf(atomCounter - 1)).add(cData.trim());
                }
            } else if (xpath.endsWith("molecule", "scalar")) {
                if (DICTREF.equals("pdb:id")) {
                    //                	cdo.setObjectProperty("Molecule", DICTREF, cData);
                    currentMolecule.setProperty(new DictRef(DICTREF, cData), cData);
                } else if (DICTREF.equals("cdk:molecularProperty")) {
                    currentMolecule.setProperty(elementTitle, cData);
                } else {
                    moleculeCustomProperty.add(elementTitle);
                    moleculeCustomProperty.add(cData.trim());
                }
            } else if (xpath.endsWith("reaction", "scalar")) {
                if (DICTREF.equals("cdk:reactionProperty")) {
                    currentReaction.setProperty(elementTitle, cData);
                }
            } else {
                logger.warn("Ignoring scalar: " + xpath);
            }
        } else if ("floatArray".equals(name)) {
            if (BUILTIN.equals("x3")) {

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        x3.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 205, 1);
                }
            } else if (BUILTIN.equals("y3")) {

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        y3.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 213, 1);
                }
            } else if (BUILTIN.equals("z3")) {

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        z3.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 221, 1);
                }
            } else if (BUILTIN.equals("x2")) {
                logger.debug("New floatArray found.");

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        x2.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 205, 1);
                }
            } else if (BUILTIN.equals("y2")) {
                logger.debug("New floatArray found.");

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        y2.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 454, 1);
                }
            } else if (BUILTIN.equals("partialCharge")) {
                logger.debug("New floatArray with partial charges found.");

                try {

                    StringTokenizer st = new StringTokenizer(cData);

                    while (st.hasMoreTokens())
                        partialCharges.add(st.nextToken());
                } catch (Exception e) {
                    notify("CMLParsing error: " + e, SYSTEMID, 462, 1);
                }
            }
        } else if ("basic".equals(name)) {
            // assuming this is the child element of <identifier>
            this.inchi = cData;
        } else if ("name".equals(name)) {
            if (xpath.endsWith("molecule", "name")) {
                if (DICTREF.length() > 0) {
                    //            		cdo.setObjectProperty("Molecule", DICTREF, cData);

                    currentMolecule.setProperty(new DictRef(DICTREF, cData), cData);
                } else {
                    //            		cdo.setObjectProperty("Molecule", "Name", cData);
                    currentMolecule.setTitle(cData);
                }
            }
        } else if ("formula".equals(name)) {
            currentMolecule.setProperty(CDKConstants.FORMULA, cData);
        } else {
            logger.debug("Skipping end element: " + name);
        }

        currentChars = "";
        BUILTIN = "";
        elementTitle = "";
    }

    @Override
    public void characterData(CMLStack xpath, char[] ch, int start, int length) {
        currentChars = currentChars + new String(ch, start, length);
        logger.debug("CD: ", currentChars);
    }

    protected void notify(String message, String systemId, int line, int column) {
        logger.debug("Message: ", message);
        logger.debug("SystemId: ", systemId);
        logger.debug("Line: ", line);
        logger.debug("Column: ", column);
    }

    protected void storeData() {
        if (inchi != null) {
            //            cdo.setObjectProperty("Molecule", "inchi", inchi);
            currentMolecule.setProperty(CDKConstants.INCHI, inchi);
        }
        if (formula != null && formula.size() > 0) {
            currentMolecule.setProperty(CDKConstants.FORMULA, formula);
        }
        Iterator<String> customs = moleculeCustomProperty.iterator();
        while (customs.hasNext()) {
            String x = customs.next();
            String y = customs.next();
            currentMolecule.setProperty(x, y);
        }
        storeAtomData();
        newAtomData();
        storeBondData();
        newBondData();
        convertCMLToCDKHydrogenCounts();
    }

    private void convertCMLToCDKHydrogenCounts() {
        for (IAtom atom : currentMolecule.atoms()) {
            if (atom.getImplicitHydrogenCount() != null) {
                int explicitHCount = AtomContainerManipulator.countExplicitHydrogens(currentMolecule, atom);
                if (explicitHCount != 0) {
                    atom.setImplicitHydrogenCount(atom.getImplicitHydrogenCount() - explicitHCount);
                }
            }
        }
    }

    protected void storeAtomData() {
        logger.debug("No atoms: ", atomCounter);
        if (atomCounter == 0) {
            return;
        }

        boolean hasID = false;
        boolean has3D = false;
        boolean has3Dfract = false;
        boolean has2D = false;
        boolean hasFormalCharge = false;
        boolean hasAtomAromaticities = false;
        boolean hasPartialCharge = false;
        boolean hasHCounts = false;
        boolean hasSymbols = false;
        boolean hasTitles = false;
        boolean hasIsotopes = false;
        boolean hasAtomicNumbers = false;
        boolean hasExactMasses = false;
        boolean hasDictRefs = false;
        boolean hasSpinMultiplicities = false;
        boolean hasAtomParities = false;
        boolean hasOccupancies = false;

        if (elid.size() == atomCounter) {
            hasID = true;
        } else {
            logger.debug("No atom ids: " + elid.size(), " != " + atomCounter);
        }

        if (elsym.size() == atomCounter) {
            hasSymbols = true;
        } else {
            logger.debug("No atom symbols: " + elsym.size(), " != " + atomCounter);
        }

        if (eltitles.size() == atomCounter) {
            hasTitles = true;
        } else {
            logger.debug("No atom titles: " + eltitles.size(), " != " + atomCounter);
        }

        if ((x3.size() == atomCounter) && (y3.size() == atomCounter) && (z3.size() == atomCounter)) {
            has3D = true;
        } else {
            logger.debug("No 3D info: " + x3.size(), " " + y3.size(), " " + z3.size(), " != " + atomCounter);
        }

        if ((xfract.size() == atomCounter) && (yfract.size() == atomCounter) && (zfract.size() == atomCounter)) {
            has3Dfract = true;
        } else {
            logger.debug("No 3D fractional info: " + xfract.size(), " " + yfract.size(), " " + zfract.size(), " != "
                    + atomCounter);
        }

        if ((x2.size() == atomCounter) && (y2.size() == atomCounter)) {
            has2D = true;
        } else {
            logger.debug("No 2D info: " + x2.size(), " " + y2.size(), " != " + atomCounter);
        }

        if (formalCharges.size() == atomCounter) {
            hasFormalCharge = true;
        } else {
            logger.debug("No formal Charge info: " + formalCharges.size(), " != " + atomCounter);
        }

        if (atomAromaticities.size() == atomCounter) {
            hasAtomAromaticities = true;
        } else {
            logger.debug("No aromatic atom info: " + atomAromaticities.size(), " != " + atomCounter);
        }

        if (partialCharges.size() == atomCounter) {
            hasPartialCharge = true;
        } else {
            logger.debug("No partial Charge info: " + partialCharges.size(), " != " + atomCounter);
        }

        if (hCounts.size() == atomCounter) {
            hasHCounts = true;
        } else {
            logger.debug("No hydrogen Count info: " + hCounts.size(), " != " + atomCounter);
        }

        if (spinMultiplicities.size() == atomCounter) {
            hasSpinMultiplicities = true;
        } else {
            logger.debug("No spinMultiplicity info: " + spinMultiplicities.size(), " != " + atomCounter);
        }

        if (atomParities.size() == atomCounter) {
            hasAtomParities = true;
        } else {
            logger.debug("No atomParity info: " + spinMultiplicities.size(), " != " + atomCounter);
        }

        if (occupancies.size() == atomCounter) {
            hasOccupancies = true;
        } else {
            logger.debug("No occupancy info: " + occupancies.size(), " != " + atomCounter);
        }

        if (atomDictRefs.size() == atomCounter) {
            hasDictRefs = true;
        } else {
            logger.debug("No dictRef info: " + atomDictRefs.size(), " != " + atomCounter);
        }

        if (isotope.size() == atomCounter) {
            hasIsotopes = true;
        } else {
            logger.debug("No isotope info: " + isotope.size(), " != " + atomCounter);
        }
        if (atomicNumbers.size() == atomCounter) {
            hasAtomicNumbers = true;
        } else {
            logger.debug("No atomicNumbers info: " + atomicNumbers.size(), " != " + atomCounter);
        }
        if (exactMasses.size() == atomCounter) {
            hasExactMasses = true;
        } else {
            logger.debug("No atomicNumbers info: " + atomicNumbers.size(), " != " + atomCounter);
        }

        for (int i = 0; i < atomCounter; i++) {
            logger.info("Storing atom: ", i);
            //            cdo.startObject("Atom");
            currentAtom = currentChemFile.getBuilder().newInstance(IAtom.class, "H");
            logger.debug("Atom # " + atomCounter);
            if (hasID) {
                //                cdo.setObjectProperty("Atom", "id", (String)elid.get(i));
                logger.debug("id: ", (String) elid.get(i));
                currentAtom.setID((String) elid.get(i));
                atomEnumeration.put((String) elid.get(i), currentAtom);
            }
            if (hasTitles) {
                if (hasSymbols) {
                    String symbol = (String) elsym.get(i);
                    if (symbol.equals("Du") || symbol.equals("Dummy")) {
                        //                        cdo.setObjectProperty("PseudoAtom", "label", (String)eltitles.get(i));
                        if (!(currentAtom instanceof IPseudoAtom)) {
                            currentAtom = currentChemFile.getBuilder().newInstance(IPseudoAtom.class, currentAtom);
                            if (hasID) atomEnumeration.put((String) elid.get(i), currentAtom);
                        }
                        ((IPseudoAtom) currentAtom).setLabel((String) eltitles.get(i));
                    } else {
                        //                        cdo.setObjectProperty("Atom", "title", (String)eltitles.get(i));
                        // FIXME: huh?
                        if (eltitles.get(i) != null)
                            currentAtom.setProperty(CDKConstants.TITLE, (String) eltitles.get(i));
                    }
                } else {
                    //                    cdo.setObjectProperty("Atom", "title", (String)eltitles.get(i));
                    // FIXME: huh?
                    if (eltitles.get(i) != null) currentAtom.setProperty(CDKConstants.TITLE, (String) eltitles.get(i));
                }
            }

            // store optional atom properties
            if (hasSymbols) {
                String symbol = (String) elsym.get(i);
                if (symbol.equals("Du") || symbol.equals("Dummy")) {
                    symbol = "R";
                }
                //                cdo.setObjectProperty("Atom", "type", symbol);
                if (symbol.equals("R") && !(currentAtom instanceof IPseudoAtom)) {
                    currentAtom = currentChemFile.getBuilder().newInstance(IPseudoAtom.class, currentAtom);
                    ((IPseudoAtom) currentAtom).setLabel("R");
                    if (hasID) atomEnumeration.put((String) elid.get(i), currentAtom);
                }
                currentAtom.setSymbol(symbol);
                if (!hasAtomicNumbers || atomicNumbers.get(i) == null)
                    currentAtom.setAtomicNumber(PeriodicTable.getAtomicNumber(symbol));
            }

            if (has3D) {
                //                cdo.setObjectProperty("Atom", "x3", (String)x3.get(i));
                //                cdo.setObjectProperty("Atom", "y3", (String)y3.get(i));
                //                cdo.setObjectProperty("Atom", "z3", (String)z3.get(i));
                if (x3.get(i) != null && y3.get(i) != null && z3.get(i) != null) {
                    currentAtom.setPoint3d(new Point3d(Double.parseDouble((String) x3.get(i)), Double
                            .parseDouble((String) y3.get(i)), Double.parseDouble((String) z3.get(i))));
                }
            }

            if (has3Dfract) {
                // ok, need to convert fractional into eucledian coordinates
                //                cdo.setObjectProperty("Atom", "xFract", (String)xfract.get(i));
                //                cdo.setObjectProperty("Atom", "yFract", (String)yfract.get(i));
                //                cdo.setObjectProperty("Atom", "zFract", (String)zfract.get(i));
                currentAtom.setFractionalPoint3d(new Point3d(Double.parseDouble((String) xfract.get(i)), Double
                        .parseDouble((String) yfract.get(i)), Double.parseDouble((String) zfract.get(i))));
            }

            if (hasFormalCharge) {
                //                cdo.setObjectProperty("Atom", "formalCharge",
                //                                      (String)formalCharges.get(i));
                currentAtom.setFormalCharge(Integer.parseInt((String) formalCharges.get(i)));
            }

            if (hasAtomAromaticities) {
                if (atomAromaticities.get(i) != null) currentAtom.setFlag(CDKConstants.ISAROMATIC, true);
            }

            if (hasPartialCharge) {
                logger.debug("Storing partial atomic charge...");
                //                cdo.setObjectProperty("Atom", "partialCharge",
                //                                      (String)partialCharges.get(i));
                currentAtom.setCharge(Double.parseDouble((String) partialCharges.get(i)));
            }

            if (hasHCounts) {
                //                cdo.setObjectProperty("Atom", "hydrogenCount", (String)hCounts.get(i));
                // convertCMLToCDKHydrogenCounts() is called to update hydrogen counts when molecule is stored
                String hCount = hCounts.get(i);
                if (hCount != null) {
                    currentAtom.setImplicitHydrogenCount(Integer.parseInt(hCount));
                } else {
                    currentAtom.setImplicitHydrogenCount((Integer) CDKConstants.UNSET);
                }
            }

            if (has2D) {
                if (x2.get(i) != null && y2.get(i) != null) {
                    //                    cdo.setObjectProperty("Atom", "x2", (String)x2.get(i));
                    //                    cdo.setObjectProperty("Atom", "y2", (String)y2.get(i));
                    currentAtom.setPoint2d(new Point2d(Double.parseDouble((String) x2.get(i)), Double
                            .parseDouble((String) y2.get(i))));
                }
            }

            if (hasDictRefs) {
                //                cdo.setObjectProperty("Atom", "dictRef", (String)atomDictRefs.get(i));
                if (atomDictRefs.get(i) != null)
                    currentAtom.setProperty("org.openscience.cdk.dict", (String) atomDictRefs.get(i));
            }

            if (hasSpinMultiplicities && spinMultiplicities.get(i) != null) {
                //                cdo.setObjectProperty("Atom", "spinMultiplicity", (String)spinMultiplicities.get(i));
                int unpairedElectrons = Integer.parseInt((String) spinMultiplicities.get(i)) - 1;
                for (int sm = 0; sm < unpairedElectrons; sm++) {
                    currentMolecule.addSingleElectron(currentChemFile.getBuilder().newInstance(ISingleElectron.class,
                            currentAtom));
                }
            }

            if (hasOccupancies && occupancies.get(i) != null) {
                //                cdo.setObjectProperty("Atom", "occupanciy", (String)occupancies.get(i));
                // FIXME: this has no ChemFileCDO equivalent, not even if spelled correctly
            }

            if (hasIsotopes) {
                //                cdo.setObjectProperty("Atom", "massNumber", (String)isotope.get(i));
                if (isotope.get(i) != null)
                    currentAtom.setMassNumber((int) Double.parseDouble((String) isotope.get(i)));
            }

            if (hasAtomicNumbers) {
                if (atomicNumbers.get(i) != null) currentAtom.setAtomicNumber(Integer.parseInt(atomicNumbers.get(i)));
            }

            if (hasExactMasses) {
                if (exactMasses.get(i) != null) currentAtom.setExactMass(Double.parseDouble(exactMasses.get(i)));
            }

            if (atomCustomProperty.get(Integer.valueOf(i)) != null) {
                Iterator<String> it = atomCustomProperty.get(Integer.valueOf(i)).iterator();
                while (it.hasNext()) {
                    currentAtom.setProperty(it.next(), it.next());
                }
            }

            //            cdo.endObject("Atom");

            currentMolecule.addAtom(currentAtom);
        }

        for (int i = 0; i < atomCounter; i++) {
            if (hasAtomParities &&
                atomParities.get(i) != null &&
                !atomParities.get(i).isEmpty()) {
                IAtom ligandAtom1 = atomEnumeration.get(parityARef1.get(i));
                IAtom ligandAtom2 = atomEnumeration.get(parityARef2.get(i));
                IAtom ligandAtom3 = atomEnumeration.get(parityARef3.get(i));
                IAtom ligandAtom4 = atomEnumeration.get(parityARef4.get(i));
                IAtom[] ligandAtoms = new IAtom[]{ligandAtom1, ligandAtom2, ligandAtom3, ligandAtom4};
                Stereo config;
                int parity = 0;
                try {
                    parity = (int) Math.signum(Double.parseDouble(atomParities.get(i)));
                } catch (NumberFormatException ex) {
                    // ignored
                }
                if (parity > 0)
                    config = Stereo.CLOCKWISE;
                else if (parity < 0)
                    config = Stereo.ANTI_CLOCKWISE;
                else {
                    config = null;
                    logger.warn("Cannot interpret stereo information, invalid parity: '" + atomParities.get(i) + "'");
                }
                if (config != null) {
                    TetrahedralChirality chirality = new TetrahedralChirality(currentMolecule.getAtom(i), ligandAtoms, config);
                    currentMolecule.addStereoElement(chirality);
                }
            }
        }

        if (elid.size() > 0) {
            // assume this is the current working list
            bondElid = elid;
        }
    }

    protected void storeBondData() {
        logger.debug("Testing a1,a2,stereo,order = count: " + bondARef1.size(), "," + bondARef2.size(), ","
                + bondStereo.size(), "," + order.size(), "=" + bondCounter);

        if ((bondARef1.size() == bondCounter) && (bondARef2.size() == bondCounter)) {
            logger.debug("About to add bond info...");

            Iterator<String> orders = order.iterator();
            Iterator<String> ids = bondid.iterator();
            Iterator<String> bar1s = bondARef1.iterator();
            Iterator<String> bar2s = bondARef2.iterator();
            Iterator<String> stereos = bondStereo.iterator();
            Iterator<Boolean> aroms = bondAromaticity.iterator();

            while (bar1s.hasNext()) {
                //                cdo.startObject("Bond");
                //                if (ids.hasNext()) {
                //                    cdo.setObjectProperty("Bond", "id", (String)ids.next());
                //                }
                //                cdo.setObjectProperty("Bond", "atom1",
                //                                      Integer.valueOf(bondElid.indexOf(
                //                                                          (String)bar1s.next())).toString());
                //                cdo.setObjectProperty("Bond", "atom2",
                //                                      Integer.valueOf(bondElid.indexOf(
                //                                                          (String)bar2s.next())).toString());
                IAtom a1 = (IAtom) atomEnumeration.get((String) bar1s.next());
                IAtom a2 = (IAtom) atomEnumeration.get((String) bar2s.next());
                currentBond = currentChemFile.getBuilder().newInstance(IBond.class, a1, a2);
                if (ids.hasNext()) {
                    currentBond.setID((String) ids.next());
                }

                if (orders.hasNext()) {
                    String bondOrder = (String) orders.next();

                    if ("S".equals(bondOrder)) {
                        //                        cdo.setObjectProperty("Bond", "order", "1");
                        currentBond.setOrder(Order.SINGLE);
                    } else if ("D".equals(bondOrder)) {
                        //                        cdo.setObjectProperty("Bond", "order", "2");
                        currentBond.setOrder(Order.DOUBLE);
                    } else if ("T".equals(bondOrder)) {
                        //                        cdo.setObjectProperty("Bond", "order", "3");
                        currentBond.setOrder(Order.TRIPLE);
                    } else if ("A".equals(bondOrder)) {
                        //                        cdo.setObjectProperty("Bond", "order", "1.5");
                        currentBond.setOrder(Order.SINGLE);
                        currentBond.setFlag(CDKConstants.ISAROMATIC, true);
                    } else {
                        //                        cdo.setObjectProperty("Bond", "order", bondOrder);
                        currentBond.setOrder(BondManipulator.createBondOrder(Double.parseDouble(bondOrder)));
                    }
                }

                if (stereos.hasNext()) {
                    //                    cdo.setObjectProperty("Bond", "stereo",
                    //                                          (String)stereos.next());
                    String nextStereo = (String) stereos.next();
                    if ("H".equals(nextStereo)) {
                        currentBond.setStereo(IBond.Stereo.DOWN);
                    } else if ("W".equals(nextStereo)) {
                        currentBond.setStereo(IBond.Stereo.UP);
                    } else if (nextStereo != null && !nextStereo.isEmpty()) {
                        logger.warn("Cannot interpret bond display information: '" + nextStereo + "'");
                    }
                }

                if (aroms.hasNext()) {
                    Object nextArom = aroms.next();
                    if (nextArom != null && ((boolean) nextArom)) {
                        currentBond.setFlag(CDKConstants.ISAROMATIC, true);
                    }
                }

                if (currentBond.getID() != null) {
                    Map<String, String> currentBondProperties = bondCustomProperty.get(currentBond.getID());
                    if (currentBondProperties != null) {
                        Iterator<String> keys = currentBondProperties.keySet().iterator();
                        while (keys.hasNext()) {
                            String key = keys.next();
                            currentBond.setProperty(key, currentBondProperties.get(key));
                        }
                        bondCustomProperty.remove(currentBond.getID());
                    }
                }

                //                cdo.endObject("Bond");
                currentMolecule.addBond(currentBond);
            }
        }
    }

    protected int addArrayElementsTo(List<String> toAddto, String array) {
        StringTokenizer tokenizer = new StringTokenizer(array);
        int i = 0;
        while (tokenizer.hasMoreElements()) {
            toAddto.add(tokenizer.nextToken());
            i++;
        }
        return i;
    }
}
