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

package com.actelion.research.chem.reaction;

import com.actelion.research.chem.*;
import com.actelion.research.util.ArrayUtils;

import java.util.ArrayList;
import java.util.Arrays;

public class ReactionEncoder
{
	public static final char MOLECULE_DELIMITER = ' ';
	public static final char PRODUCT_IDENTIFIER = '!';
	public static final char CATALYST_DELIMITER = '+';	// character must not collide with idcode or coordinate encodings
	public static final char OBJECT_DELIMITER = '#';

	public static final String MOLECULE_DELIMITER_STRING = " ";
	public static final String OBJECT_DELIMITER_STRING = "#";

	public static final int INCLUDE_MAPPING = 1;
	public static final int INCLUDE_COORDS = 2;
	public static final int INCLUDE_DRAWING_OBJECTS = 4;
	public static final int INCLUDE_CATALYSTS = 8;

	public static final int INCLUDE_ALL = 15;
	public static final int INCLUDE_RXN_CODE_ONLY = 0;
	public static final int INCLUDE_DEFAULT = INCLUDE_MAPPING | INCLUDE_COORDS;

	public static final int RETAIN_REACTANT_AND_PRODUCT_ORDER = 16;


    private ReactionEncoder()
    {}

	/**
	 * Creates a String containing a unique reaction code by
	 * creating idcodes of every reactant and product and
	 * concatenating them in lexical order.
	 * If mapping information is available this will be encoded
	 * in a 2nd string. Otherwise this will be null.
	 * Coordinates, if available, will be encoded in a 3rd string.
	 * If there are drawing objects assigned to this reaction
	 * then these are encoded in a 4th string.
	 *
	 * @return String[5] with reaction code, mapping, coordinates, drawing objects, catalysts
	 */
	public static String[] encode(Reaction reaction, boolean keepAbsoluteCoordinates) {
		return encode(reaction, keepAbsoluteCoordinates, true);
	}

	/**
	 * Creates a non-unique String containing a reaction code by
	 * creating idcodes of every reactant and product and
	 * concatenating them in original order.
	 * If mapping information is available this will be encoded
	 * in a 2nd string. Otherwise this will be null.
	 * Coordinates, if available, will be encoded in a 3rd string.
	 * If there are drawing objects assigned to this reaction
	 * then these are encoded in a 4th string.
	 * If the reaction contains catalysts, they are encoded as 5th string.
	 *
	 * @param reaction
	 * @param keepAbsoluteCoordinates
	 * @param sortByIDCode
	 * @return String[5] with reaction code, mapping, coordinates, drawing objects, catalysts
	 */
	private static String[] encode(Reaction reaction, boolean keepAbsoluteCoordinates, boolean sortByIDCode) {
		if (reaction == null
			|| reaction.getReactants() == 0
			|| reaction.getProducts() == 0) {
			return null;
		}

		String[] idcode = new String[reaction.getMolecules()];
		String[] mapping = new String[reaction.getMolecules()];
		String[] coords = new String[reaction.getMolecules()];

		for (int i = 0; i < reaction.getMolecules(); i++) {
			StereoMolecule mol = reaction.getMolecule(i);

			// reactants may not use cAtomQFRxnParityHint
			if (mol.isFragment() && i < reaction.getReactants())
				for (int atom=0; atom<mol.getAllAtoms(); atom++)
					mol.setAtomQueryFeature(atom, Molecule.cAtomQFRxnParityHint, false);

			Canonizer canonizer = new Canonizer(mol);
			idcode[i] = canonizer.getIDCode();
			if (idcode[i] == null) {
				return null;
			}

			mapping[i] = canonizer.getEncodedMapping();
			coords[i] = canonizer.getEncodedCoordinates(keepAbsoluteCoordinates);
		}

		StringBuilder idcodeSequence = new StringBuilder();
		StringBuilder coordsSequence = new StringBuilder();
		StringBuilder mappingSequence = new StringBuilder();

		for (int i = 0; i < reaction.getReactants(); i++) {
			int index = i;
			if (sortByIDCode) {
				String maxString = "";
				index = -1;
				for (int j = 0; j < reaction.getReactants(); j++) {
					if (maxString.compareTo(idcode[j]) < 0) {
						maxString = idcode[j];
						index = j;
					}
				}
			}
			if (i > 0) {
				idcodeSequence.append(MOLECULE_DELIMITER);
				mappingSequence.append(MOLECULE_DELIMITER);
				coordsSequence.append(MOLECULE_DELIMITER);
			}
			idcodeSequence.append(idcode[index]);
			mappingSequence.append(mapping[index]);
			coordsSequence.append(coords[index]);
			idcode[index] = "";
		}

		idcodeSequence.append(PRODUCT_IDENTIFIER);
		mappingSequence.append(MOLECULE_DELIMITER);
		coordsSequence.append(MOLECULE_DELIMITER);

		for (int i = reaction.getReactants(); i < reaction.getMolecules(); i++) {
			int index = i;
			if (sortByIDCode) {
				String maxString = "";
				index = -1;
				for (int j = reaction.getReactants(); j < reaction.getMolecules(); j++) {
					if (maxString.compareTo(idcode[j]) < 0) {
						maxString = idcode[j];
						index = j;
					}
				}
			}
			if (i > reaction.getReactants()) {
				idcodeSequence.append(MOLECULE_DELIMITER);
				mappingSequence.append(MOLECULE_DELIMITER);
				coordsSequence.append(MOLECULE_DELIMITER);
			}
			idcodeSequence.append(idcode[index]);
			mappingSequence.append(mapping[index]);
			coordsSequence.append(coords[index]);
			idcode[index] = "";
		}

		String[] result = new String[5];
		result[0] = idcodeSequence.toString();
		if (mappingSequence.length() > reaction.getMolecules() - 1)   // delimiters only
		{
			result[1] = mappingSequence.toString();
		}
		if (coordsSequence.length() > reaction.getMolecules() - 1)   // delimiters only
		{
			result[2] = coordsSequence.toString();
		}
		if (reaction.getDrawingObjects() != null) {
			result[3] = reaction.getDrawingObjects().toString();
		}
		if (reaction.getCatalysts() != 0) {
			result[4] = encodeCatalysts(reaction, keepAbsoluteCoordinates);
		}

		return result;
	}

	private static String encodeCatalysts(Reaction reaction, boolean keepAbsoluteCoordinates) {
		StringBuilder sb = new StringBuilder();
		for (int i=0; i<reaction.getCatalysts(); i++) {
			if (sb.length() != 0)
				sb.append(CATALYST_DELIMITER);
			Canonizer canonizer = new Canonizer(reaction.getCatalyst(i));
			sb.append(canonizer.getIDCode());
			if (keepAbsoluteCoordinates) {
				sb.append(" ");
				sb.append(canonizer.getEncodedCoordinates(true));
			}
		}
		return sb.toString();
	}

	/**
	 * Creates a String containing a reaction code by creating idcodes of every reactant and product and
	 * concatenating them in original (if mode includes RETAIN_REACTANT_AND_PRODUCT_ORDER) or in
	 * lexical order. In the latter case this string is a canonical reaction encoding.
	 * If mapping information is available this will be encoded in a 2nd string.
	 * Coordinates, if available, will be encoded in a 3rd string.
	 * If there are drawing objects assigned to this reaction then these are encoded in a 4th string.
	 *
	 * @return One String with reaction code, coordinates, mapping, drawing objects as defined by mode.
	 */
	public static String encode(Reaction reaction, boolean keepAbsoluteCoordinates, int mode) {
		String[] result = encode(reaction, keepAbsoluteCoordinates, (mode & RETAIN_REACTANT_AND_PRODUCT_ORDER) == 0);
		if (result == null) {
			return null;
		}

		StringBuffer buf = new StringBuffer(result[0]);

		if (mode != 0) {
			buf.append(OBJECT_DELIMITER);
			if ((mode & INCLUDE_MAPPING) != 0
				&& result.length > 1
				&& result[1] != null) {
				buf.append(result[1]);
			}
		}

		mode &= ~INCLUDE_MAPPING;
		if (mode != 0) {
			buf.append(OBJECT_DELIMITER);
			if ((mode & INCLUDE_COORDS) != 0
				&& result.length > 2
				&& result[2] != null) {
				buf.append(result[2]);
			}
		}

		mode &= ~INCLUDE_COORDS;
		if (mode != 0) {
			buf.append(OBJECT_DELIMITER);
			if ((mode & INCLUDE_DRAWING_OBJECTS) != 0
				&& result.length > 3
				&& result[3] != null) {
				buf.append(result[3]);
			}
		}

		mode &= ~INCLUDE_DRAWING_OBJECTS;
		if (mode != 0) {
			buf.append(OBJECT_DELIMITER);
			if ((mode & INCLUDE_CATALYSTS) != 0
				&& result.length > 4
				&& result[4] != null) {
				buf.append(result[4]);
			}
		}

		return buf.toString();
	}

	/**
	 * Creates a Reaction object by interpreting a reaction code,
	 * mapping, coordinates and drawing objects that were earlier created
	 * by this class.
	 * If rxnCoords are relative or null, and if ensureCoordinates==true
	 * then all reactants and products are placed automatically along a
	 * horizontal line.
	 *
	 * @return Reaction
	 */
	public static Reaction decode(String rxnCode, String rxnMapping, String rxnCoords,
								  String rxnObjects, String rxnCatalysts, boolean ensureCoordinates, Reaction rxn) {
		if (rxnCode == null || rxnCode.length() == 0) {
			return null;
		}

		boolean isProduct = false;
		int idcodeIndex = 0;
		int mappingIndex = 0;
		int coordsIndex = 0;

		int productIndex = rxnCode.indexOf(PRODUCT_IDENTIFIER);
		if (productIndex == -1) {
			return null;
		}

		if (rxn == null)
			rxn = new Reaction();
		else
			rxn.clear();

		while (idcodeIndex != -1) {
			if (idcodeIndex > productIndex) {
				isProduct = true;
			}

			int delimiterIndex = rxnCode.indexOf(MOLECULE_DELIMITER, idcodeIndex);
			if (!isProduct
				&& (delimiterIndex > productIndex || delimiterIndex == -1)) {
				delimiterIndex = productIndex;
			}

			String idcode = null;
			if (delimiterIndex == -1) {
				idcode = rxnCode.substring(idcodeIndex);
				idcodeIndex = -1;
			} else {
				idcode = rxnCode.substring(idcodeIndex, delimiterIndex);
				idcodeIndex = delimiterIndex + 1;
			}

			String mapping = null;
			if (rxnMapping != null && rxnMapping.length() != 0) {
				delimiterIndex = rxnMapping.indexOf(MOLECULE_DELIMITER, mappingIndex);
				if (delimiterIndex == -1) {
					mapping = rxnMapping.substring(mappingIndex);
				} else {
					mapping = rxnMapping.substring(mappingIndex, delimiterIndex);
					mappingIndex = delimiterIndex + 1;
				}
			}

			String coords = null;
			if (rxnCoords != null && rxnCoords.length() != 0) {
				delimiterIndex = rxnCoords.indexOf(MOLECULE_DELIMITER, coordsIndex);
				if (delimiterIndex == -1) {
					coords = rxnCoords.substring(coordsIndex);
				} else {
					coords = rxnCoords.substring(coordsIndex, delimiterIndex);
					coordsIndex = delimiterIndex + 1;
				}
			}

			IDCodeParser parser = new IDCodeParser(ensureCoordinates);
			StereoMolecule mol = parser.getCompactMolecule(idcode, coords);

			if (mapping != null) {
				parser.parseMapping(mapping.getBytes());
			}

			if (isProduct) {
				rxn.addProduct(mol);
			} else {
				rxn.addReactant(mol);
			}
		}

		if (rxnObjects != null && rxnObjects.length() != 0) {
			rxn.setDrawingObjects(new DrawingObjectList(rxnObjects));
		}

		if (rxnCatalysts != null && rxnCatalysts.length() != 0) {
			IDCodeParser parser = new IDCodeParser(ensureCoordinates);
			int index1 = 0;
			int index2 = rxnCatalysts.indexOf(CATALYST_DELIMITER);
			while (index2 != -1) {
				rxn.addCatalyst(parser.getCompactMolecule(rxnCatalysts.substring(index1, index2)));
				index1 = index2+1;
				index2 = rxnCatalysts.indexOf(CATALYST_DELIMITER, index1);
			}
			rxn.addCatalyst(parser.getCompactMolecule(rxnCatalysts.substring(index1)));
		}

		return rxn;
	}

	/**
	 * Creates a Reaction object by interpreting a reaction code,
	 * mapping, coordinates and drawing objects that were earlier created
	 * by this class.
	 * If rxnCoords are relative or null, and if ensureCoordinates==true
	 * then all reactants and products are placed automatically along a
	 * horizontal line.
	 *
	 * @return Reaction
	 */
	public static Reaction decode(byte[] rxnCode, byte[] rxnMapping, byte[] rxnCoords,
								  String rxnObjects, byte[] rxnCatalysts, boolean ensureCoordinates) {
		if (rxnCode == null || rxnCode.length == 0) {
			return null;
		}

		boolean isProduct = false;
		int idcodeIndex = 0;
		int mappingIndex = 0;
		int coordsIndex = 0;

		int productIndex = indexOf(rxnCode, PRODUCT_IDENTIFIER);
		if (productIndex == -1)
			return null;

		Reaction rxn = new Reaction();

		while (idcodeIndex != -1) {
			if (idcodeIndex > productIndex)
				isProduct = true;

			int delimiterIndex = indexOf(rxnCode, MOLECULE_DELIMITER, idcodeIndex);
			if (!isProduct && (delimiterIndex > productIndex || delimiterIndex == -1))
				delimiterIndex = productIndex;

			int idcodeStart = idcodeIndex;
			idcodeIndex = (delimiterIndex == -1) ? -1 : delimiterIndex + 1;

			int mappingStart = -1;
			if (rxnMapping != null && mappingIndex < rxnMapping.length) {
				mappingStart = (rxnMapping[mappingIndex] == MOLECULE_DELIMITER) ? -1 : mappingIndex;
				delimiterIndex = indexOf(rxnMapping, MOLECULE_DELIMITER, mappingIndex);
				if (delimiterIndex != -1)
					mappingIndex = delimiterIndex + 1;
			}

			int coordsStart = -1;
			if (rxnCoords != null && rxnCoords.length != 0) {
				coordsStart = coordsIndex;
				delimiterIndex = indexOf(rxnCoords, MOLECULE_DELIMITER, coordsIndex);
				if (delimiterIndex != -1)
					coordsIndex = delimiterIndex + 1;
			}

			IDCodeParser parser = new IDCodeParser(ensureCoordinates);
			parser.neglectSpaceDelimitedCoordinates();
			StereoMolecule mol = parser.getCompactMolecule(rxnCode, rxnCoords, idcodeStart, coordsStart);

			if (mappingStart != -1)
				parser.parseMapping(rxnMapping, mappingStart);

			if (isProduct)
				rxn.addProduct(mol);
			else
				rxn.addReactant(mol);
		}

		if (rxnObjects != null && rxnObjects.length() != 0) {
			rxn.setDrawingObjects(new DrawingObjectList(rxnObjects));
		}

		if (rxnCatalysts != null && rxnCatalysts.length != 0) {
			IDCodeParser parser = new IDCodeParser(ensureCoordinates);
			int index1 = 0;
			int index2 = indexOf(rxnCatalysts, CATALYST_DELIMITER);
			while (index2 != -1) {
				rxn.addCatalyst(parser.getCompactMolecule(rxnCatalysts, index1));
				index1 = index2+1;
				index2 = indexOf(rxnCatalysts, CATALYST_DELIMITER, index1);
			}
			rxn.addCatalyst(parser.getCompactMolecule(rxnCatalysts, index1));
		}

		return rxn;
	}

	private static int indexOf(byte[] bytes, char ch) {
		for (int i=0; i<bytes.length; i++)
			if (bytes[i] == ch)
				return i;

		return -1;
		}

	private static int indexOf(byte[] bytes, char ch, int start) {
		for (int i=start; i<bytes.length; i++)
			if (bytes[i] == ch)
				return i;

		return -1;
	}

	public static Reaction decode(String s, boolean ensureCoordinates) {
		return decode(s, ensureCoordinates, null);
		}

	/**
	 * Creates a Reaction object by interpreting a reaction code,
	 * mapping, coordinates and drawing objects that were earlier created
	 * by this class and are passed OBJECT_DELIMITER-delimited within
	 * one string.
	 * If rxnCoords are relative or null, and if ensureCoordinates==true
	 * then all reactants and products are placed automatically along a
	 * horizontal line.
	 *
	 * @return Reaction
	 */
	public static Reaction decode(String s, boolean ensureCoordinates, Reaction rxn) {
		if (s == null)
			return null;

		String rxnCode = s;
		String rxnMapping = null;
		String rxnCoords = null;
		String rxnObjects = null;
		String rxnCatalysts = null;
		int index1 = s.indexOf(OBJECT_DELIMITER);
		if (index1 == -1) {
			rxnCode = s;
		} else {
			rxnCode = s.substring(0, index1);
			int index2 = s.indexOf(OBJECT_DELIMITER, index1 + 1);
			if (index2 == -1) {
				rxnMapping = s.substring(index1 + 1);
			} else {
				rxnMapping = s.substring(index1 + 1, index2);
				int index3 = s.indexOf(OBJECT_DELIMITER, index2 + 1);
				if (index3 == -1) {
					rxnCoords = s.substring(index2 + 1);
				} else {
					rxnCoords = s.substring(index2 + 1, index3);
					int index4 = s.indexOf(OBJECT_DELIMITER, index3 + 1);
					if (index4 == -1) {
						rxnObjects = s.substring(index3 + 1);
					} else {
						rxnObjects = s.substring(index3 + 1, index4);
						rxnCatalysts = s.substring(index4 + 1);
					}
				}
			}
		}

		return decode(rxnCode, rxnMapping, rxnCoords, rxnObjects, rxnCatalysts, ensureCoordinates, rxn);
	}


	/**
	 * Creates a Reaction object by interpreting a reaction string encoded by this class.
	 * Include options define whether mapping, coordinates, catalysts, and drawing objects
	 # are included in the reaction object.
	 * @param s
	 * @param includeOptions
	 * @return Reaction
	 */
	public static Reaction decode(String s, int includeOptions, Reaction rxn) {
		if (s == null)
			return null;

		String rxnCode = s;
		String rxnMapping = null;
		String rxnCoords = null;
		String rxnObjects = null;
		String rxnCatalysts = null;
		int index1 = s.indexOf(OBJECT_DELIMITER);
		if (index1 == -1) {
			rxnCode = s;
		} else {
			rxnCode = s.substring(0, index1);
			int index2 = s.indexOf(OBJECT_DELIMITER, index1 + 1);
			if (index2 == -1) {
				rxnMapping = s.substring(index1 + 1);
			} else {
				rxnMapping = s.substring(index1 + 1, index2);
				int index3 = s.indexOf(OBJECT_DELIMITER, index2 + 1);
				if (index3 == -1) {
					rxnCoords = s.substring(index2 + 1);
				} else {
					rxnCoords = s.substring(index2 + 1, index3);
					int index4 = s.indexOf(OBJECT_DELIMITER, index3 + 1);
					if (index4 == -1) {
						rxnObjects = s.substring(index3 + 1);
					} else {
						rxnObjects = s.substring(index3 + 1, index4);
						rxnCatalysts = s.substring(index4 + 1);
					}
				}
			}
		}

		return decode(rxnCode,
			(includeOptions & INCLUDE_MAPPING) != 0 ? rxnMapping : null,
			(includeOptions & INCLUDE_COORDS) != 0 ? rxnCoords : null,
			(includeOptions & INCLUDE_DRAWING_OBJECTS) != 0 ? rxnObjects : null,
			(includeOptions & INCLUDE_CATALYSTS) != 0 ? rxnCatalysts : null,
			false, rxn);
	}

	/**
	 * Generates an array of all reactants and/or products of the encoded reaction string as bytes.
	 * If the string includes atom coordinates or if they are explicitly, these are used.
	 * At least one of includeReactants and includeProducts must be true.
	 * @param s encoded reaction
	 * @param includeReactants
	 * @param includeProducts
	 * @param includeMapping
	 * @return null (if reactants or products are missing) or StereoMolecule array with at least one molecule
	 */
	public static StereoMolecule[] decodeMolecules(String s, boolean includeCoords, boolean includeMapping, boolean includeReactants, boolean includeProducts) {
		if (s == null)
			return null;

		byte[] rxnCode = null;
		byte[] rxnMapping = null;
		byte[] rxnCoords = null;
		int index1 = s.indexOf(OBJECT_DELIMITER);
		if (index1 == -1) {
			rxnCode = s.getBytes();
		} else {
			rxnCode = s.substring(0, index1).getBytes();
			if (includeMapping || includeCoords) {
				int index2 = s.indexOf(OBJECT_DELIMITER, index1 + 1);
				if (index2 == -1) {
					if (includeMapping)
						rxnMapping = s.substring(index1 + 1).getBytes();
				} else {
					if (includeMapping)
						rxnMapping = s.substring(index1 + 1, index2).getBytes();
					if (includeCoords) {
						int index3 = s.indexOf(OBJECT_DELIMITER, index2 + 1);
						if (index3 == -1) {
							rxnCoords = s.substring(index2 + 1).getBytes();
						} else {
							rxnCoords = s.substring(index2 + 1, index3).getBytes();
							}
						}
					}
				}
			}

		return decodeMolecules(rxnCode, rxnCoords, rxnMapping, includeReactants, includeProducts);
		}

		/**
		 * Generates an array of all reactants and/or products of the encoded reaction string as bytes.
		 * If the string includes atom coordinates or if they are explicitly, these are used.
		 * At least one of includeReactants and includeProducts must be true.
		 * @param rxnBytes may contain atom coordinates
		 * @param coords may be null
		 * @param mapping may be null
		 * @param includeReactants
		 * @param includeProducts
		 * @return null (if reactants or products are missing) or StereoMolecule array with at least one molecule
		 */
	public static StereoMolecule[] decodeMolecules(byte[] rxnBytes, byte[] coords, byte[] mapping, boolean includeReactants, boolean includeProducts) {
		if (rxnBytes == null || rxnBytes.length == 0)
			return null;

		int reactantEnd = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.PRODUCT_IDENTIFIER);
		if (reactantEnd <= 0)
			return null;

		int productIndex = reactantEnd + 1;
		int productEnd = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.OBJECT_DELIMITER, productIndex);
		if (productEnd == -1)
			productEnd = rxnBytes.length;

		if (productIndex == productEnd)
			return null;

		int coordsIndex = 0;
		if (coords == null) {
			coordsIndex = 1+ArrayUtils.indexOf(rxnBytes, (byte) ReactionEncoder.OBJECT_DELIMITER, productEnd + 1);
			if (coordsIndex != 0)
				coords = rxnBytes;
		}
		if (coords != null) {
			if (!includeReactants) {
				int reactantCount = 1;
				for (int i=0; i<reactantEnd; i++)
					if (rxnBytes[i] == ReactionEncoder.MOLECULE_DELIMITER)
						reactantCount++;
				for (int i=0; reactantCount != 0 && i<coords.length; i++) {
					if (coords[i] == ReactionEncoder.MOLECULE_DELIMITER) {
						coordsIndex = i + 1;
						reactantCount--;
					}
				}
			}
		}

		int mappingIndex = 0;
		if (mapping != null) {
			if (!includeReactants) {
				int reactantCount = 1;
				for (int i=0; i<reactantEnd; i++)
					if (rxnBytes[i] == ReactionEncoder.MOLECULE_DELIMITER)
						reactantCount++;
				for (int i=0; reactantCount != 0 && i<mapping.length; i++) {
					if (mapping[i] == ReactionEncoder.MOLECULE_DELIMITER) {
						mappingIndex = i + 1;
						reactantCount--;
					}
				}
			}
		}

		ArrayList<StereoMolecule> moleculeList = new ArrayList<>();
		if (includeReactants) {
			int reactantIndex = 0;
			do {
				IDCodeParser parser = new IDCodeParser();
				parser.neglectSpaceDelimitedCoordinates();
				StereoMolecule reactant = parser.getCompactMolecule(rxnBytes, coords, reactantIndex, coordsIndex);
				if (reactant.getAllAtoms() != 0)
					moleculeList.add(reactant);

				reactantIndex = 1+ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.MOLECULE_DELIMITER, reactantIndex);
				if (coords != null)
					coordsIndex = 1+ArrayUtils.indexOf(coords, (byte)ReactionEncoder.MOLECULE_DELIMITER, coordsIndex);
				if (mapping != null) {
					parser.parseMapping(mapping, mappingIndex);
					mappingIndex = 1+ArrayUtils.indexOf(mapping, (byte)ReactionEncoder.MOLECULE_DELIMITER, mappingIndex);
				}
			} while (reactantIndex != 0 && reactantIndex < reactantEnd);
		}
		if (includeProducts) {
			do {
				IDCodeParser parser = new IDCodeParser();
				parser.neglectSpaceDelimitedCoordinates();
				StereoMolecule product = parser.getCompactMolecule(rxnBytes, coords, productIndex, coordsIndex);
				if (product.getAllAtoms() != 0)
					moleculeList.add(product);

				productIndex = 1+ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.MOLECULE_DELIMITER, productIndex);
				if (coords != null)
					coordsIndex = 1+ArrayUtils.indexOf(coords, (byte)ReactionEncoder.MOLECULE_DELIMITER, coordsIndex);
				if (mapping != null) {
					parser.parseMapping(mapping, mappingIndex);
					mappingIndex = 1+ArrayUtils.indexOf(mapping, (byte)ReactionEncoder.MOLECULE_DELIMITER, mappingIndex);
				}
			} while (productIndex != 0 && productIndex < productEnd);
		}

		return moleculeList.size() == 0 ? null : moleculeList.toArray(new StereoMolecule[0]);
	}

	/**
	 * Generates an array of all catalysts of the encoded reaction string as bytes.
	 * If the string includes atom coordinates, these are used.
	 * @param rxnBytes
	 * @return null or StereoMolecule array with at least one molecule
	 */
	public static StereoMolecule[] decodeCatalysts(byte[] rxnBytes) {
		if (rxnBytes == null || rxnBytes.length == 0)
			return null;

		int index = 0;
		for (int i=0; i<4; i++) {
			index = 1 + ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.OBJECT_DELIMITER, index);
			if (index == 0)
				return null;
		}

		if (index == rxnBytes.length)
			return null;

		ArrayList<StereoMolecule> catalystList = new ArrayList<StereoMolecule>();
		while (index != 0 && index < rxnBytes.length) {
			int nextIndex = 1+ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.CATALYST_DELIMITER, index);
			int coordsIndex = 1+ArrayUtils.indexOf(rxnBytes, (byte)' ', index);

			StereoMolecule catalyst = (coordsIndex != 0 && (nextIndex == 0 || nextIndex > coordsIndex)) ?
					  new IDCodeParser().getCompactMolecule(rxnBytes, rxnBytes, index, coordsIndex)
					: new IDCodeParser().getCompactMolecule(rxnBytes, null, index, -1);
			if (catalyst.getAllAtoms() != 0)
				catalystList.add(catalyst);

			index = nextIndex;
		}

		return catalystList.size() == 0 ? null : catalystList.toArray(new StereoMolecule[0]);
	}

	/**
	 * Generates an array of all reactants and/or products of the encoded reaction string as bytes.
	 * If the string includes atom coordinates or if they are explicitly, these are used.
	 * At least one of includeReactants and includeProducts must be true.
	 * @param rxnBytes may contain atom coordinates
	 * @return null (if reactants or products are missing) or StereoMolecule array with at least one molecule
	 */
	public static byte[][] getMoleculeIDCodes(byte[] rxnBytes, boolean includeReactants, boolean includeProducts) {
		if (rxnBytes == null || rxnBytes.length == 0)
			return null;

		int reactantEnd = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.PRODUCT_IDENTIFIER);
		if (reactantEnd <= 0)
			return null;

		int productIndex = reactantEnd + 1;
		int productEnd = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.OBJECT_DELIMITER, productIndex);
		if (productEnd == -1)
			productEnd = rxnBytes.length;

		if (productIndex == productEnd)
			return null;

		ArrayList<byte[]> moleculeList = new ArrayList<>();
		if (includeReactants) {
			int reactantIndex = 0;
			while (reactantIndex < reactantEnd) {
				int index2 = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.MOLECULE_DELIMITER, reactantIndex);
				if (index2 == -1)
					index2 = reactantEnd;
				moleculeList.add(Arrays.copyOfRange(rxnBytes, reactantIndex, index2));
				reactantIndex = 1 + index2;
			}
		}
		if (includeProducts) {
			while (productIndex < productEnd) {
				int index2 = ArrayUtils.indexOf(rxnBytes, (byte)ReactionEncoder.MOLECULE_DELIMITER, productIndex);
				if (index2 == -1)
					index2 = productEnd;
				moleculeList.add(Arrays.copyOfRange(rxnBytes, productIndex, index2));
				productIndex = 1 + index2;
			}
		}

		return moleculeList.size() == 0 ? null : moleculeList.toArray(new byte[0][]);
	}
}
