/*
 * 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;

import java.util.Arrays;

public class AromaticityResolver {
	ExtendedMolecule	mMol;
	private boolean		mAllHydrogensAreExplicit;
	private boolean[]	mIsDelocalizedRing,mIsDelocalizedAtom,mIsDelocalizedBond,mIsDelocalizedBridgeHead,
						mIsDelocalizedFiveRingMember;
    private int mDelocalizedAtoms, mDelocalizedBonds,mPiElectronsAdded;
	private final double[] mBondLength;

    /**
     * Creates a new AromaticityResolver for molecule mol.
     * @param mol
     */
    public AromaticityResolver(ExtendedMolecule mol) {
        this(mol, null);
        }

	/**
	 * Creates a new AromaticityResolver for molecule mol.
	 * @param mol
	 * @param bondLength null or bondLengths to consider for choices
	 */
	public AromaticityResolver(ExtendedMolecule mol, double[] bondLength) {
		mMol = mol;
		mBondLength = bondLength;
	}

	/**
     * This method promotes all necessary bonds of the defined delocalized part of the molecule
     * from single to double bonds in order to create a valid delocalized system
     * of conjugated single and double bonds.
	 * The delocalized part of the molecule may be defined by passing an array
	 * to isAromaticBond that has all bonds flagged, which are part of a delocalized area.
	 * In this case these bonds are assumed to have bond type cBondTypeSingle.
	 * Alternatively, one may pass null and indicate affected bonds with bond type cBondTypeDelocalized.
     * Non-cyclic atom chains defined to be delocalized are treated depending
     * on whether we have a molecule or a query fragment. For fragments the respective bond
     * types will be set to cBondTypeDelocalized; for molecules the chain will
     * have alternating single and double bonds starting with double at a non-ring end.
     * @return true if all bonds of the delocalized area could be consistently converted. 
     */
	public boolean locateDelocalizedDoubleBonds(boolean[] isAromaticBond) {
		return locateDelocalizedDoubleBonds(isAromaticBond, false, false);
		}

	/**
	 * This method promotes all necessary bonds of the defined delocalized part of the molecule
	 * from single to double bonds in order to create a valid delocalized system
	 * of conjugated single and double bonds.
	 * The delocalized part of the molecule may be defined by passing an array
	 * to isAromaticBond that has all bonds flagged, which are part of a delocalized area.
	 * In this case these bonds are assumed to have bond type cBondTypeSingle.
	 * Alternatively, one may pass null and indicate affected bonds with bond type cBondTypeDelocalized.
	 * Non-cyclic atom chains defined to be delocalized are treated depending
	 * on whether we have a molecule or a query fragment. For fragments the respective bond
	 * types will be set to cBondTypeDelocalized; for molecules the chain will
	 * have alternating single and double bonds starting with double at a non-ring end.
	 * @param isDelocalizedBond if null, then bond type cBondTypeDelocalized is used to indicate delocalized bonds
	 * @param mayChangeAtomCharges true if input molecule doesn't carry atom charges and these may be added to achieve aromaticity
	 * @param allHydrogensAreExplicit true this method can rely on all hydrogens being explicitly present
	 * @return true if all bonds of the delocalized area could be consistently converted.
	 */
	public boolean locateDelocalizedDoubleBonds(boolean[] isDelocalizedBond, boolean mayChangeAtomCharges, boolean allHydrogensAreExplicit) {
		// Helper arrays are calculated once only and stay untouched until entire kekulization is done!
		mMol.ensureHelperArrays(Molecule.cHelperNeighbours);

		RingCollection ringSet = new RingCollection(mMol, RingCollection.MODE_SMALL_RINGS_ONLY);
		initialize(isDelocalizedBond, ringSet);
		if (mDelocalizedBonds == 0)
            return true;

		if (mayChangeAtomCharges)
			for (int atom=0; atom<mMol.getAtoms(); atom++)
				if (mIsDelocalizedAtom[atom] && mMol.getAtomicNo(atom) == 7
				 && (mIsDelocalizedBridgeHead[atom]
				  || (mMol.getConnAtoms(atom) == 3 && !mIsDelocalizedFiveRingMember[atom])))
					mMol.setAtomCharge(atom, 1);

		mAllHydrogensAreExplicit = allHydrogensAreExplicit;

		protectFullValenceAtoms(mayChangeAtomCharges);

        if (mMol.isFragment())
        	promoteDelocalizedChains();

		protectObviousNonRingLeaks(ringSet);

		protectAmideBonds(ringSet);	// using rules for detecting aromatic (thio-)amide bonds
		protectDoubleBondAtoms();

		if (mayChangeAtomCharges)	// Tripos mol2 files contain ar-bonds and no charges in carboxylates etc.
			promoteAcidicAnions();

		promoteObviousBonds();

		while (promoteOuterShellDelocalizedRingSystems(ringSet, mayChangeAtomCharges))
			promoteObviousBonds();

		// try to find and promote entirely aromatic 6-rings
		while (promoteSixMemberedAromaticRings(ringSet, mayChangeAtomCharges))
			promoteObviousBonds();

		// try to find and promote other fully aromatic rings
		while (promoteOneDelocalizationLeak(ringSet, mayChangeAtomCharges))
			promoteObviousBonds();

		// Find and promote remaining aromatic bonds
        while (mDelocalizedBonds != 0) {
			for (int bond=0; bond<mMol.getBonds(); bond++) {
				if (mIsDelocalizedBond[bond]) {
					promoteBond(bond);
					promoteObviousBonds();
					}
				}
            }

		if (mayChangeAtomCharges)
			unchargeImmoniumNHChains(mayChangeAtomCharges);

		if (mDelocalizedAtoms - mPiElectronsAdded >= 2)
			connectSeparatedSingletons();

		// If finally one or more single delocalized atoms remain, and if these carry an implicit hydrogen,
		// and if it is allowed and possible, then add/remove a charge to make the atom's lone pair contribute
		// to the neighbour atom's delocalization. If modifying charges is not allowed, then remove an implicit
		// hydrogen including one of its binding electrons. An unpaired electron is closer to the intended aromatic
		// state than a wrong hybridisation and one implicit hydrogen too much.
		// In addition, during id-coordinate parsing of implicit hydrogen atoms, the number of implicit hydrogens
		// per non-H atom must exactly match the one given when the idcode with coordinates was encoded.
		for (int atom=0; atom<mMol.getAtoms(); atom++) {
			if (mIsDelocalizedAtom[atom] && mMol.getImplicitHydrogens(atom) != 0) {
				if (mayChangeAtomCharges
				 &&	((mMol.getAtomCharge(atom) == 1 && mMol.isElectronegative(atom))
				  || (mMol.getAtomCharge(atom) == -1 && mMol.getAtomicNo(atom) == 5)))
					mMol.setAtomCharge(atom, 0);
				else
					mMol.setAtomRadical(atom, Molecule.cAtomRadicalStateD);
				mPiElectronsAdded++;
				}
			}

		return (mDelocalizedAtoms == mPiElectronsAdded);
		}

	private void initialize(boolean[] isDelocalizedBond, RingCollection ringSet) {
		if (isDelocalizedBond != null) {
			mIsDelocalizedBond = isDelocalizedBond;
		}
		else {
			mIsDelocalizedBond = new boolean[mMol.getBonds()];
			for (int bond=0; bond<mMol.getBonds(); bond++) {
				if (mMol.getBondType(bond) == Molecule.cBondTypeDelocalized) {
					mIsDelocalizedBond[bond] = true;
					mMol.setBondType(bond, Molecule.cBondTypeSingle);
				}
			}
		}

		for (int bond=0; bond<mMol.getBonds(); bond++)
			if (mIsDelocalizedBond[bond])
				mDelocalizedBonds++;

		mIsDelocalizedAtom = new boolean[mMol.getAtoms()];
		mIsDelocalizedBridgeHead = new boolean[mMol.getAtoms()];
		for (int atom=0; atom<mMol.getAtoms(); atom++) {
			int delocalizedConnBonds = 0;
			for (int i=0; i<mMol.getConnAtoms(atom); i++)
				if (mIsDelocalizedBond[mMol.getConnBond(atom, i)])
					delocalizedConnBonds++;
			if (delocalizedConnBonds > 0) {
				mIsDelocalizedAtom[atom] = true;
				mDelocalizedAtoms++;
				if (delocalizedConnBonds == 3)
					mIsDelocalizedBridgeHead[atom] = true;
			}
		}

		mIsDelocalizedRing = new boolean[ringSet.getSize()];
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			mIsDelocalizedRing[ring] = true;
			for (int bond : ringSet.getRingBonds(ring)) {
				if (!mIsDelocalizedBond[bond]) {
					mIsDelocalizedRing[ring] = false;
					break;
				}
			}
		}

//		mIsDelocalizedThreeOrSevenRingMember = new boolean[mMol.getAtoms()];
		mIsDelocalizedFiveRingMember = new boolean[mMol.getAtoms()];
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			if (mIsDelocalizedRing[ring] && ringSet.getRingSize(ring) != 6) {
				for (int atom : ringSet.getRingAtoms(ring)) {
					if (ringSet.getRingSize(ring) == 5)
						mIsDelocalizedFiveRingMember[atom] = true;
//					else
//						mIsDelocalizedThreeOrSevenRingMember[atom] = true;
				}
			}
		}

		mPiElectronsAdded = 0;
	}

	private boolean promoteSixMemberedAromaticRings(RingCollection ringSet, boolean mayChangeAtomCharges) {
		boolean found = false;
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			if (ringSet.getRingSize(ring) == 6) {
				boolean isQualifyingRing = true;
				int[] ringAtom = ringSet.getRingAtoms(ring);
				int[] ringBond = ringSet.getRingBonds(ring);
				for (int i=0; i<6; i++) {
					if (!checkAtomTypePi1(ringAtom[i], false)
					 || !mIsDelocalizedBond[ringBond[i]]) {
						isQualifyingRing = false;
						break;
					}
				}

				if (isQualifyingRing) {
					if (mayChangeAtomCharges)
						for (int i=0; i<6; i++)
							checkAtomTypePi1(ringAtom[i], true);

					for (int i=0; i<6; i+=2)
						promoteBond(ringBond[i]);

					found = true;
					break;
				}
			}
		}
		return found;
	}

	private boolean promoteOneDelocalizationLeak(RingCollection ringSet, boolean mayChangeAtomCharges) {
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			if (ringSet.getRingSize(ring) != 6 && mIsDelocalizedRing[ring]) {
				boolean isFullyDelocalizedRing = true;
				int[] ringBond = ringSet.getRingBonds(ring);
				for (int i=0; i<ringBond.length; i++) {
					if (!mIsDelocalizedBond[ringBond[i]]) {
						isFullyDelocalizedRing = false;
						break;
					}
				}

				if (isFullyDelocalizedRing) {
					int bestDelocalizationLeakIndex = -1;
					int bestDelocalizationLeakPriority = 0;
					double bestBondLengthSum = 0.0;
					int[] ringAtom = ringSet.getRingAtoms(ring);
					for (int i=0; i<ringAtom.length; i++) {
						int atom = ringAtom[i];
						double bondLengthSum = (mBondLength == null) ? 0.0 : mBondLength[ringBond[i]] + mBondLength[ringBond[(i==0?ringAtom.length:i)-1]];
						int priority = mIsDelocalizedFiveRingMember[atom] ?
								checkAtomTypeLeak5(atom, false)
							  : checkAtomTypeLeak7(atom, false);
						if (bestDelocalizationLeakPriority < priority) {
							bestDelocalizationLeakPriority = priority;
							bestDelocalizationLeakIndex = i;
							if (mBondLength != null)
								bestBondLengthSum = bondLengthSum;
						}
						else if (bestDelocalizationLeakPriority == priority && mBondLength != null) {
							if (bestBondLengthSum < bondLengthSum) {
								bestDelocalizationLeakIndex = i;
								bestBondLengthSum = bondLengthSum;
							}
						}
					}
					if (bestDelocalizationLeakIndex != -1) {
						int leakAtom = ringAtom[bestDelocalizationLeakIndex];
						if (mayChangeAtomCharges) {
							for (int atom : ringAtom)
								if (atom != leakAtom)
									checkAtomTypePi1(atom, true);
							if (mIsDelocalizedFiveRingMember[leakAtom])
								checkAtomTypeLeak5(leakAtom, true);
							else
								checkAtomTypeLeak7(leakAtom, true);
						}
						protectAtom(leakAtom);
						if (mIsDelocalizedFiveRingMember[leakAtom])
							protectIfIsHeme(ringAtom, ringBond, bestDelocalizationLeakIndex);
						return true;
					}
				}
			}
		}
		return false;
	}

	private boolean promoteOuterShellDelocalizedRingSystems(RingCollection ringSet, boolean mayChangeAtomCharges) {
		int[] sharedDelocalizedRingCount = new int[mMol.getBonds()];
		for (int r=0; r<ringSet.getSize(); r++) {
			int[] ringBond = ringSet.getRingBonds(r);
			boolean isDelocalized = true;
			for (int i=0; i<ringBond.length; i++) {
				if (!mIsDelocalizedBond[ringBond[i]]) {
					isDelocalized = false;
					break;
				}
			}
			if (isDelocalized)
				for (int i=0; i<ringBond.length; i++)
					sharedDelocalizedRingCount[ringBond[i]]++;
		}

		int delocalizedBonds = mDelocalizedBonds;

		for (int r=0; r<ringSet.getSize(); r++) {
			boolean found = false;
			int[] ringAtom = ringSet.getRingAtoms(r);
			int[] ringBond = ringSet.getRingBonds(r);
			for (int i=0; i<ringBond.length && !found; i++) {
				if (sharedDelocalizedRingCount[ringBond[i]] > 1) {
					int first = nextIndex(i, ringBond.length);
					if (sharedDelocalizedRingCount[ringBond[first]] == 1) {	// first unshared aromatic bond
						boolean incompatibleAtomFound = false;
						int next = nextIndex(first, ringBond.length);
						while (sharedDelocalizedRingCount[ringBond[next]] == 1) {
							if (!checkAtomTypePi1(ringAtom[next], false)
							 ||	(ringBond.length != 6 && mMol.getAtomicNo(ringAtom[next]) != 6))
								incompatibleAtomFound = true;
							next = nextIndex(next, ringBond.length);
						}
						if (!incompatibleAtomFound) {
							int bridgeBonds = (next > first) ? next - first : next + ringBond.length -first;
							if (bridgeBonds > 2 && (bridgeBonds & 1) == 1) {
								for (int j=1; j<bridgeBonds; j+=2) {
									int index = (first+j < ringBond.length) ? first+j : first+j-ringBond.length;
									if (mayChangeAtomCharges) {
										checkAtomTypePi1(ringAtom[index], true);
										checkAtomTypePi1(ringAtom[index == ringAtom.length-1 ? 0 : index+1], true);
									}
									promoteBond(ringBond[index]);
								}
								found = true;
							}
						}
					}
				}
			}
		}

		return delocalizedBonds != mDelocalizedBonds;
	}

	private int nextIndex(int i, int size) {
		return i == size-1 ? 0 : i+1;
	}

	private void protectIfIsHeme(int[] ringAtom, int[] ringBond, int leakIndex) {
		int leftAtom = ringAtom[leakIndex == 0 ? 4 : leakIndex-1];
		int rightAtom = ringAtom[leakIndex == 4 ? 0 : leakIndex+1];
		if (mMol.getConnAtoms(leftAtom) == 3 && mMol.getConnAtoms(rightAtom) == 3) {
			int[] pathAtom = new int[mMol.getAtoms()];
			boolean[] neglectBond = new boolean[mMol.getBonds()];
			for (int bond : ringBond)
				neglectBond[bond] = true;
			for (int bond=0; bond<mMol.getBonds(); bond++)
				if (!mIsDelocalizedBond[bond])
					neglectBond[bond] = true;

			int pathLen = mMol.getPath(pathAtom, leftAtom, rightAtom, 14, neglectBond);
			if (pathLen == 14) {
				boolean qualifies = true;
				for (int i=2; i<=12; i++) {
					if ((i & 3) != 1 ^ mMol.getAtomRingSize(pathAtom[i]) == 5) {
						qualifies = false;
						break;
					}
				}
				if (qualifies) {
					protectAtom(pathAtom[7]);
					for (int i=1; i<=5; i+=2) {
						promoteBond(mMol.getBond(pathAtom[i], pathAtom[i+1]));
						promoteBond(mMol.getBond(pathAtom[i+7], pathAtom[i+8]));
					}
				}
			}
		}
	}

	private void protectFullValenceAtoms(boolean mayChangeAtomCharges) {
		for (int atom=0; atom<mMol.getAtoms(); atom++)
			if (mIsDelocalizedAtom[atom]
			 && mMol.getLowestFreeValence(atom) == 0
			 && (!mayChangeAtomCharges
			  || (mMol.getAtomicNo(atom) == 5 && mMol.getAtomCharge(atom) < 0)
			  || (mMol.getAtomicNo(atom) == 6 || mMol.getAtomicNo(atom) == 14)
			  || (mMol.isElectronegative(atom) && mMol.getAtomCharge(atom) > 0)))
				protectAtom(atom);
		}

	private void protectAtom(int atom) {
		if (mIsDelocalizedAtom[atom]) {
			mIsDelocalizedAtom[atom] = false;
			mDelocalizedAtoms--;
			}
		for (int i=0; i<mMol.getConnAtoms(atom); i++) {
			int connBond = mMol.getConnBond(atom, i);
            if (mIsDelocalizedBond[connBond]) {
                mIsDelocalizedBond[connBond] = false;
                mDelocalizedBonds--;
                }
            }
		}

	private void promoteBond(int bond) {
		if (mMol.getBondType(bond) == Molecule.cBondTypeSingle) {
			mMol.setBondType(bond, Molecule.cBondTypeDouble);
			mPiElectronsAdded += 2;
			}

		for (int i=0; i<2; i++) {
			int bondAtom = mMol.getBondAtom(i, bond);
			mIsDelocalizedAtom[bondAtom] = false;
			for (int j=0; j<mMol.getConnAtoms(bondAtom); j++) {
				int connBond = mMol.getConnBond(bondAtom, j);
                if (mIsDelocalizedBond[connBond]) {
                    mIsDelocalizedBond[connBond] = false;
                    mDelocalizedBonds--;
                    }
                }
			}
		}

	private void promoteAcidicAnions() {
		for (int atom=0; atom<mMol.getAtoms(); atom++) {
			int delocalizedConnBondCount = 0;
			boolean delocalizedNextBondsFound = false;
			boolean electronegativeConnAtomFound = false;
			for (int i=0; i<mMol.getConnAtoms(atom) && !delocalizedNextBondsFound; i++) {
				int connBond = mMol.getConnBond(atom, i);
				if (mIsDelocalizedBond[connBond]) {
					delocalizedConnBondCount++;
					int connAtom = mMol.getConnAtom(atom, i);
					if (mMol.isElectronegative(connAtom))
						electronegativeConnAtomFound = true;
					for (int j=0; j<mMol.getConnAtoms(connAtom); j++) {
						int nextAtom = mMol.getConnAtom(connAtom, j);
						int nextBond = mMol.getConnBond(connAtom, j);
						if (nextAtom != atom && mIsDelocalizedBond[nextBond]) {
							delocalizedNextBondsFound = true;
							break;
						}
					}
				}
			}
			if (delocalizedConnBondCount >= 2 && !delocalizedNextBondsFound) {
				int[] connBondWithRank = new int[delocalizedConnBondCount];

				// We should have at least a free valence of one because otherwise protectFullValenceAtoms()
				// would have taken the delocalized bond flag away.
				int freeValence = mMol.getLowestFreeValence(atom);
				if (freeValence == 0)	// for P,S,As,Se we have valence steps of 2
					freeValence = 2;

				int index = 0;
				for (int i=0; i<mMol.getConnAtoms(atom); i++) {
					int connBond = mMol.getConnBond(atom, i);
					if (mIsDelocalizedBond[connBond]) {
						int connAtom = mMol.getConnAtom(atom, i);
						connBondWithRank[index++] = (getAcidAnionRank(connAtom) << 16) + i;
					}
				}

				Arrays.sort(connBondWithRank);

				// Acidic anions do not tale part in the added-electron to delocalized-atom balance.
				// Thus, remove delocalized-atoms from original count and don't add to the electron count.
				mDelocalizedAtoms -= 1 + delocalizedConnBondCount;

				mIsDelocalizedAtom[atom] = false;
				int doubleBondCount = Math.min(delocalizedConnBondCount-1, freeValence);
				for (int i=0; i<connBondWithRank.length; i++) {
					int connAtom = mMol.getConnAtom(atom, connBondWithRank[i] & 0x0000FFFF);
					int connBond = mMol.getConnBond(atom, connBondWithRank[i] & 0x0000FFFF);
					mIsDelocalizedAtom[connAtom] = false;
					mIsDelocalizedBond[connBond] = false;
					mDelocalizedBonds--;
					if (i < connBondWithRank.length - doubleBondCount) {
						mMol.setAtomCharge(connAtom, electronegativeConnAtomFound ? -1 : 1);
					}
					else {
						mMol.setBondType(connBond, Molecule.cBondTypeDouble);
					}
				}
			}
		}
	}

	private int getAcidAnionRank(int atom) {
		int atomicNo = mMol.getAtomicNo(atom);
		return atomicNo == 8 ?  14 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 7 ?  12 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 16 ? 10 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 34 ?  8 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 15 ?  6 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 33 ?  4 + (mMol.getConnAtoms(atom) == 1 ? 1 : 0)
			 : atomicNo == 6 ?   2 + (mMol.getConnAtoms(atom) == 1 ? 0 : 1)
			 :						 (mMol.getConnAtoms(atom) == 1 ? 0 : 1);
	}

	private void promoteObviousBonds() {
			// handle bond orders of aromatic bonds along the chains attached to 5- or 7-membered ring
		boolean terminalAromaticBondFound;
		do {
			terminalAromaticBondFound = false;
			for (int bond=0; bond<mMol.getBonds(); bond++) {
				if (mIsDelocalizedBond[bond]) {
					boolean isTerminalAromaticBond = false;
					for (int i=0; i<2; i++) {
                        int bondAtom = mMol.getBondAtom(i, bond);
					    boolean aromaticNeighbourFound = false;
						for (int j=0; j<mMol.getConnAtoms(bondAtom); j++) {
							if (bond != mMol.getConnBond(bondAtom, j)
							 && mIsDelocalizedBond[mMol.getConnBond(bondAtom, j)]) {
								aromaticNeighbourFound = true;
								break;
								}
							}
						if (!aromaticNeighbourFound) {
							isTerminalAromaticBond = true;
							break;
							}
						}

					if (isTerminalAromaticBond) {
						terminalAromaticBondFound = true;
						promoteBond(bond);
						}
					}
				}
			} while (terminalAromaticBondFound);
		}

	/**
	 * Protect query features cBondQFDelocalized in open aromatic chains of fragments
	 * with incomplete aromatic rings
	 */
	private void promoteDelocalizedChains() {
        for (int bond=0; bond<mMol.getBonds(); bond++) {
            if (mIsDelocalizedBond[bond]) {
                for (int i=0; i<2; i++) {
                    int terminalAtom = mMol.getBondAtom(i, bond);
                    boolean aromaticNeighbourFound = false;
                    for (int j=0; j<mMol.getConnAtoms(terminalAtom); j++) {
                        if (bond != mMol.getConnBond(terminalAtom, j)
                         && mIsDelocalizedBond[mMol.getConnBond(terminalAtom, j)]) {
                            aromaticNeighbourFound = true;
                            break;
                            }
                        }
                    if (!aromaticNeighbourFound) {
                        int terminalBond = bond;
                        int bridgeAtom = mMol.getBondAtom(1-i, bond);
                        while (terminalBond != -1) {
							mIsDelocalizedAtom[terminalAtom] = false;
                            mIsDelocalizedBond[terminalBond] = false;
                            mDelocalizedBonds--;
                            mMol.setBondType(terminalBond, Molecule.cBondTypeDelocalized);
                            terminalBond = -1;
                            terminalAtom = bridgeAtom;
                            for (int j=0; j<mMol.getConnAtoms(terminalAtom); j++) {
                                if (mIsDelocalizedBond[mMol.getConnBond(terminalAtom, j)]) {
                                    if (terminalBond == -1) {
                                        terminalBond = mMol.getConnBond(terminalAtom, j);
                                        bridgeAtom = mMol.getConnAtom(terminalAtom, j);
                                        }
                                    else {
                                        terminalAtom = -1;	// Stop here! We have hit an aromatic branch.
                                        terminalBond = -1;
                                        break;
                                        }
                                    }
                                }
                            }
						if (terminalAtom != -1)		// Regular end of aromatic chain (no branch).
							mIsDelocalizedAtom[bridgeAtom] = false;
                        break;
                        }
                    }
                }
            }
        }

	private void protectAmideBonds(RingCollection ringSet) {
		for (int bond=0; bond<mMol.getBonds(); bond++) {
			if (mIsDelocalizedBond[bond] && ringSet.qualifiesAsAmideTypeBond(bond)) {
				protectAtom(mMol.getBondAtom(0, bond));
				protectAtom(mMol.getBondAtom(1, bond));
				}
			}
		}

	private void protectDoubleBondAtoms() {
		for (int bond=0; bond<mMol.getBonds(); bond++) {
			if (mMol.getBondOrder(bond) == 2) {
				for (int i=0; i<2; i++) {
					int atom = mMol.getBondAtom(i, bond);
					if (mMol.getAtomicNo(atom) <= 8) {
						for (int j=0; j<mMol.getConnAtoms(atom); j++) {
							int connBond = mMol.getConnBond(atom, j);
							if (mIsDelocalizedBond[connBond]) {
								protectAtom(atom);
								break;
								}
							}
						}
					}
				}
			}
		}

	private void protectObviousNonRingLeaks(RingCollection ringSet) {
		// for all ring atoms add charges and protect preferred delocalization leak atoms
/*		boolean[] isAromaticRingAtom = new boolean[mMol.getAtoms()];
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			int ringSize = ringSet.getRingSize(ring);
			if (ringSize == 3 || ringSize == 5 || ringSize == 6 || ringSize == 7) {
				if (mIsDelocalizedRing[ring]) {
					for (int atom:ringSet.getRingAtoms(ring))
						isAromaticRingAtom[atom] = true;

					boolean possible = true;
					int leakAtom = -1;
					int leakPriority = 0;
					int bridgeHeadCount = 0;

					for (int atom:ringSet.getRingAtoms(ring)) {
						if (mIsDelocalizedBridgeHead[atom])
							bridgeHeadCount++;

						if (ringSize == 6 || mIsDelocalizedBridgeHead[atom]) {	// 6-membered or bridgehead atom
							if (!checkAtomTypePi1(atom, false)) {
								possible = false;
								break;
								}
							}
						else {	// non-bridgehead in 5- or 7-membered ring
							int priority = (ringSize == 5) ?
									checkAtomTypeLeak5(atom, false) : checkAtomTypeLeak7(atom, false);
							if (!checkAtomTypePi1(atom, false)) {
								if (leakPriority == 10) {
									possible = false;
									break;
									}
								leakAtom = atom;
								leakPriority = 20;	// MAX
								}
							else if (leakPriority < priority) {
								leakPriority = priority;
								leakAtom = atom;
								}
							}
						}

					if (possible && (ringSize == 6 || bridgeHeadCount == 0)) {
						for (int atom : ringSet.getRingAtoms(ring)) {
							if (atom == leakAtom) {
								if (ringSize == 5)
									checkAtomTypeLeak5(atom, true);	// 5-membered
								else
									checkAtomTypeLeak7(atom, true);	// 3- or 7-membered

								protectAtom(atom);
								}
							else {
								checkAtomTypePi1(atom, true);
								}
							}
						}
					}
				}
			}*/

		boolean[] isAromaticRingAtom = new boolean[mMol.getAtoms()];
		for (int ring=0; ring<ringSet.getSize(); ring++) {
			int ringSize = ringSet.getRingSize(ring);
			if (ringSize == 3 || ringSize == 5 || ringSize == 6 || ringSize == 7) {
				if (mIsDelocalizedRing[ring]) {
					for (int atom : ringSet.getRingAtoms(ring))
						isAromaticRingAtom[atom] = true;
				}
			}
		}

		// From here locate delocalized strings of atoms, which are not member
		// of an aromatic ring. Protect preferred atoms and add obvious atom charges.

		// count for every atom the number of delocalized bonds attached
		int[] delocalizedNeighbourCount = new int[mMol.getAtoms()];
		boolean[] hasMetalLigandBond = new boolean[mMol.getAtoms()];
		for (int bond=0; bond<mMol.getBonds(); bond++) {
			int atom1 = mMol.getBondAtom(0, bond);
			int atom2 = mMol.getBondAtom(1, bond);
			if (!isAromaticRingAtom[atom1] && !isAromaticRingAtom[atom2]) {
				if (mIsDelocalizedBond[bond]) {
					delocalizedNeighbourCount[atom1]++;
					delocalizedNeighbourCount[atom2]++;
					}
				if (mMol.getBondType(bond) == Molecule.cBondTypeMetalLigand) {
					hasMetalLigandBond[atom1] = true;
					hasMetalLigandBond[atom2] = true;
					}
				}
			}

		// From any delocalized atom with one delocalized neighbor (chain end)
		// locate the path to a branch atom (including) or chain end, whatever comes first.
		// Then mark every second atom from the startAtom as not being capable to assume
		// the role of a delocalization leak (priority:-1).
		int[] priority = new int[mMol.getAtoms()];
		int[] graphAtom = new int[mMol.getAtoms()];
		for (int seedAtom=0; seedAtom<mMol.getAtoms(); seedAtom++) {
			if (delocalizedNeighbourCount[seedAtom] == 1) {
				graphAtom[0] = seedAtom;
				int current = 0;
				int highest = 0;
				while (current <= highest) {
					for (int i=0; i<mMol.getConnAtoms(graphAtom[current]); i++) {
						if (mIsDelocalizedBond[mMol.getConnBond(graphAtom[current], i)]) {
							int candidate = mMol.getConnAtom(graphAtom[current], i);
							if ((current == 0 || candidate != graphAtom[current-1])
							 && delocalizedNeighbourCount[candidate] != 0) {
								graphAtom[++highest] = candidate;
								if ((delocalizedNeighbourCount[candidate] & 1) != 0) {	// 1 or 3
									for (int j=1; j<highest; j+=2)
										priority[graphAtom[j]] = -1;
									highest = 0;	// to break outer loop
									}
								break;
								}
							}
						}
					current++;
					}
				}
			}

		// For every connected delocalized area not being part of an aromatic ring
		// calculate delocalization leak priorities for all atoms not marked above.
		// Then protect the atom with the highest priority.
		boolean[] atomHandled = new boolean[mMol.getAtoms()];
		for (int seedAtom=0; seedAtom<mMol.getAtoms(); seedAtom++) {
			if (!atomHandled[seedAtom] && delocalizedNeighbourCount[seedAtom] != 0) {
				graphAtom[0] = seedAtom;
				atomHandled[seedAtom] = true;
				int current = 0;
				int highest = 0;
				while (current <= highest) {
					for (int i = 0; i < mMol.getConnAtoms(graphAtom[current]); i++) {
						if (mIsDelocalizedBond[mMol.getConnBond(graphAtom[current], i)]) {
							int candidate = mMol.getConnAtom(graphAtom[current], i);
							if (!atomHandled[candidate]) {
								graphAtom[++highest] = candidate;
								atomHandled[candidate] = true;
								}
							}
						}
					current++;
					}

				// if we have an odd number of delocalized atoms in a region, we need to assign a leak
				if ((highest & 1) == 0) {	// highest is atom count-1

					// check for all potential delocalization leak atoms, whether they are compatible
					for (int i = 0; i <= highest; i++)
						if (priority[graphAtom[i]] == 0)
							priority[graphAtom[i]] = checkAtomTypeLeakNonRing(graphAtom[i], false);

					// check for all atoms, which cannot be the leak, whether they can carry a pi-bond
					boolean isPossible = true;
					for (int i = 0; i <= highest; i++) {
						if (priority[graphAtom[i]] <= 0) {
							if (!checkAtomTypePi1(graphAtom[i], false)) {
								isPossible = false;
								break;
								}
							}
						}

					// find the preferred atom for the leak
					if (isPossible) {
						int maxPriority = 0;
						int maxAtom = -1;
						for (int i = 0; i <= highest; i++) {
							if (maxPriority < priority[graphAtom[i]]) {
								maxPriority = priority[graphAtom[i]];
								maxAtom = graphAtom[i];
								}
							}

						if (maxPriority > 0) {
							checkAtomTypeLeakNonRing(maxAtom, true);
							protectAtom(maxAtom);
							}
						}
					}
				}
			}
		}

	/**
	 * Flip pi-electrons in delocalized (N+)=C(-C=C)n-NH chains to the uncharged state N-C(=C-C)n=N
	 */
	private void unchargeImmoniumNHChains(boolean mayChangeAtomCharges) {
		for (int atom=0; atom<mMol.getAtoms(); atom++) {
			mMol.ensureHelperArrays(Molecule.cHelperNeighbours);	// make sure, connBondOrders are
			if (mMol.getAtomicNo(atom) == 7 && mMol.getAtomCharge(atom) == 1 && mMol.getAtomPi(atom) == 1&& !isAmineOxid(atom, mayChangeAtomCharges)) {
				boolean found = false;

				int[] graphAtom = new int[mMol.getAtoms()];
				int[] parentAtom = new int[mMol.getAtoms()];
				int[] graphLevel = new int[mMol.getAtoms()];

				graphAtom[0] = atom;
				parentAtom[atom] = -1;
				graphLevel[atom] = 1;

				int current = 0;
				int highest = 0;

				while (current <= highest && !found) {
					int currentAtom = graphAtom[current];
					for (int i=0; i<mMol.getConnAtoms(currentAtom) && !found; i++) {
						// We need alternating bond orders!
						boolean isCompatibleBond = ((graphLevel[currentAtom] & 1) == 0) ^ (mMol.getBondOrder(mMol.getConnBond(currentAtom, i)) > 1);
						int candidate = mMol.getConnAtom(currentAtom, i);
						if (graphLevel[candidate] == 0 && isCompatibleBond) {
							if (mMol.getAtomicNo(candidate) == 7 && mMol.getAtomPi(candidate) == 0 && mMol.getConnAtoms(candidate) == 2) {
								if ((graphLevel[currentAtom] & 1) == 0) {	// even number of bonds
									mMol.setAtomCharge(atom, 0);
									int parent = currentAtom;
									for (int j=0; j<graphLevel[currentAtom]; j++) {
										// trace candidate back to root atom and invert bond orders!
										int bond = mMol.getBond(candidate, parent);
										mMol.setBondOrder(bond, mMol.getBondOrder(bond) == 1 ? 2 : mMol.getBondOrder(bond)-1);
										candidate = parent;
										parent = parentAtom[candidate];
									}
									found = true;
								}
							}
							else {
								graphAtom[++highest] = candidate;
								parentAtom[candidate] = currentAtom;
								graphLevel[candidate] = graphLevel[currentAtom]+1;
							}
						}
					}
					current++;
				}
			}
		}
	}

	private boolean isAmineOxid(int atom, boolean mayChangeAtomCharges) {
		for (int i=0; i<mMol.getConnAtoms(atom); i++) {
			int connAtom = mMol.getConnAtom(atom, i);
			if (mMol.getAtomicNo(connAtom) == 8 && mMol.getConnAtoms(connAtom) == 1) {
				if (mayChangeAtomCharges && mMol.getAtomCharge(connAtom) == 0)
					mMol.setAtomCharge(connAtom, -1);
				return true;
			}
		}
		return false;
	}

	private void connectSeparatedSingletons() {
		for (int atom = 0; atom<mMol.getAtoms(); atom++) {
//			mMol.ensureHelperArrays(Molecule.cHelperNeighbours);	// evidently not needed; TLS 19Jul2025
			if (mIsDelocalizedAtom[atom]) {
				boolean found = false;

				int[] graphAtom = new int[mMol.getAtoms()];
				int[] parentAtom = new int[mMol.getAtoms()];
				int[] graphLevel = new int[mMol.getAtoms()];

				graphAtom[0] = atom;
				parentAtom[atom] = -1;
				graphLevel[atom] = 1;

				int current = 0;
				int highest = 0;

				while (current <= highest && !found) {
					int currentAtom = graphAtom[current];
					for (int i=0; i<mMol.getConnAtoms(currentAtom) && !found; i++) {
						// We need alternating bond orders!
						boolean isCompatibleBond = ((graphLevel[currentAtom] & 1) == 1) ^ (mMol.getBondOrder(mMol.getConnBond(currentAtom, i)) > 1);
						int candidate = mMol.getConnAtom(currentAtom, i);
						if (graphLevel[candidate] == 0 && isCompatibleBond) {
							if (mIsDelocalizedAtom[candidate]) {
								if ((graphLevel[currentAtom] & 1) == 1) {	// odd number of bonds
									mIsDelocalizedAtom[atom] = false;
									mIsDelocalizedAtom[candidate] = false;
									mPiElectronsAdded += 2;
									int parent = currentAtom;
									for (int j=0; j<graphLevel[currentAtom]; j++) {
										// trace candidate back to root atom and invert bond orders!
										int bond = mMol.getBond(candidate, parent);
										if (mIsDelocalizedBond[bond]) {
											mIsDelocalizedBond[bond] = false;
											mDelocalizedBonds--;
										}
										mMol.setBondOrder(bond, mMol.getBondOrder(bond) == 1 ? 2 : mMol.getBondOrder(bond)-1);
										candidate = parent;
										parent = parentAtom[candidate];
									}
									found = true;
								}
							}
							else {
								graphAtom[++highest] = candidate;
								parentAtom[candidate] = currentAtom;
								graphLevel[candidate] = graphLevel[currentAtom]+1;
							}
						}
					}
					current++;
				}
			}
		}
	}

	/**
	 * Checks, whether the atom is compatible with an aromatic atom of the type
	 * that carries one half of a delocalized double bond.
	 * @param atom
	 * @param correctCharge if true then may add a charge to make the atom compatible
	 * @return
	 */
	private boolean checkAtomTypePi1(int atom, boolean correctCharge) {
		int atomicNo = mMol.getAtomicNo(atom);
		if ((atomicNo >= 5 && atomicNo <= 8)
		 || atomicNo == 15 || atomicNo == 16 || atomicNo == 33 || atomicNo == 34 || atomicNo == 52) {	// P,S,As,Se,Te

			int freeValence = mMol.getLowestFreeValence(atom);
			if (freeValence != 0)
				return true;

			int charge = mMol.getAtomCharge(atom);
			if (atomicNo == 5 && charge >= 0) {
				if (correctCharge)
					mMol.setAtomCharge(atom, charge-1);
				return true;
				}
			if (atomicNo != 5 && charge <= 0) {
				if (correctCharge)
					mMol.setAtomCharge(atom, charge+1);
				return true;
				}
			}

		return false;
		}

	/**
	 * Checks, whether the atom is compatible with that aromatic atom of
	 * a 5-membered ring that supplies the additional electron pair.
	 * @param atom
	 * @param correctCharge if true then may add a charge to make the atom compatible
	 * @return 0 (not compatible) or priority to be used (higher numbers have higher priority)
	 */
	private int checkAtomTypeLeak5(int atom, boolean correctCharge) {
		if (mIsDelocalizedBridgeHead[atom])
			return 0;

		if (mMol.getAtomicNo(atom) == 7) {
			if (mMol.getAllConnAtoms(atom) == 3)
				return 6;	// -N(-*)-
			else if (mMol.getConnAtoms(atom) == 2)
				return mAllHydrogensAreExplicit ? 0 : 4;	// -N-
			}
		else if (mMol.getAtomicNo(atom) == 8) {
			return 10;	// furan
			}
		else if (mMol.getAtomicNo(atom) == 15 || mMol.getAtomicNo(atom) == 33) {
			if (mMol.getConnAtoms(atom) == 3)
				return 8;
			}
		else if (mMol.getAtomicNo(atom) == 16 || mMol.getAtomicNo(atom) == 34 || mMol.getAtomicNo(atom) == 52) {
			if (mMol.getConnAtoms(atom) == 2)
				return 11;	// thiophene
			if (mMol.getConnAtoms(atom) == 3) {
				if (mMol.getAtomCharge(atom) == 1)
					return 12;	// -S(+)(-*)-
				if (correctCharge)
					mMol.setAtomCharge(atom, 1);
				return 5;	// -S(-*)-
				}
			}
		else if (mMol.getAtomicNo(atom) == 6) {
			if (mMol.getAtomCharge(atom) == -1)
				return mMol.getAllConnAtoms(atom) == 3 ? 16		// -C(-)(-*)-
					 : mMol.getAllConnAtomsPlusMetalBonds(atom) == 3 ? 15 : 14;			// -C(-)...Met  	-C(-)-
			if (correctCharge)
				mMol.setAtomCharge(atom, -1);
			return (mMol.getAllConnAtoms(atom) != mMol.getAllConnAtomsPlusMetalBonds(atom)) ? 2 : 3;
			}

		return 0;
		}


	/**
	 * Checks, whether the atom is compatible with that aromatic atom of
	 * a 3- or 7-membered ring that supplies the empty orbital.
	 * @param atom
	 * @param correctCharge if true then may add a charge to make the atom compatible
	 * @return 0 (not compatible) or priority to be used (higher numbers have higher priority)
	 */
	private int checkAtomTypeLeak7(int atom, boolean correctCharge) {
		if (mIsDelocalizedBridgeHead[atom])
			return 0;

		if (mAllHydrogensAreExplicit) {
			if (mMol.getAllConnAtoms(atom) != 3)
				return 0;
			}
		else {
			if (mMol.getAllConnAtoms(atom) > 3)
				return 0;
			}

		if (mMol.getAtomicNo(atom) == 6) {
			if (correctCharge)
				mMol.setAtomCharge(atom, 1);
			return 2;
			}
		if (mMol.getAtomicNo(atom) == 5) {
			return 4;
			}

		return 0;
		}

	/**
	 * Checks, whether the atom is compatible with the (typically charged) atom
	 * in a delocalized chain of an odd number of atoms that does not carry a pi bond.
	 * @param atom
	 * @param correctCharge if true then may add a charge to make the atom compatible
	 * @return 0 (not compatible) or priority to be used (higher numbers have higher priority)
	 */
	private int checkAtomTypeLeakNonRing(int atom, boolean correctCharge) {
		if (mMol.getAtomCharge(atom) != 0)
			return 0;

		if (mAllHydrogensAreExplicit) {
			if (mMol.getAtomicNo(atom) == 5) {
				if (mMol.getOccupiedValence(atom) != 2)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, 1);
				return 1;
				}

			if (mMol.getAtomicNo(atom) == 7) {
				if (mMol.getOccupiedValence(atom) != 2)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 6 : 3;
				}

			if (mMol.getAtomicNo(atom) == 8) {
				if (mMol.getOccupiedValence(atom) != 1)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 7 : 4;
				}

			if (mMol.getAtomicNo(atom) == 16) {
				if (mMol.getOccupiedValence(atom) != 1)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 5 : 2;
				}

			if (mMol.getAtomicNo(atom) == 34) {
				if (mMol.getOccupiedValence(atom) != 1)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 4 : 1;
				}
			}
		else {
			if (mMol.getAtomicNo(atom) == 5) {
				if (mMol.getOccupiedValence(atom) > 2)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, 1);
				return 1;
				}

			if (mMol.getAtomicNo(atom) == 7) {
				if (mMol.getOccupiedValence(atom) > 2)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 5 : 3;
				}

			if (mMol.getAtomicNo(atom) == 8) {
				if (mMol.getOccupiedValence(atom) > 1)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 7 : 4;
				}

			if (mMol.getAtomicNo(atom) == 16) {
				if (mMol.getOccupiedValence(atom) > 1)
					return 0;
				if (correctCharge)
					mMol.setAtomCharge(atom, -1);
				return hasMetalNeighbour(atom) ? 5 : 2;
				}
			}

		return 0;
		}

	private boolean hasMetalNeighbour(int atom) {
		for (int i=0; i<mMol.getConnAtoms(atom); i++)
			if (mMol.isMetalAtom(mMol.getConnAtom(atom, i)))
				return true;

		return false;
		}
	}
