content: /*
   * 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.io.*;
  import java.text.DecimalFormat;
  import java.text.DecimalFormatSymbols;
  import java.util.Locale;
  
  public class MolfileCreator {
      private static final float TARGET_AVBL = 1.5f;
  
      private StringBuilder mBuilder;
      private DecimalFormat mDoubleFormat;
  
      /**
       * This creates a new molfile version 2 from the given molecule.
       * If the average bond length is smaller than 1.0 or larger than 3.0,
       * then all coordinates are scaled to achieve an average bond length of 1.5.
       * @param mol
       */
      public MolfileCreator(ExtendedMolecule mol) {
          this(mol, true);
          }
  
      /**
       * This creates a new molfile version 2 from the given molecule.
       * If allowScaling==true and the average bond length is smaller than 1.0 or larger than 3.0,
       * then all coordinates are scaled to achieve an average bond length of 1.5.
       * @param mol
       * @param allowScaling
       */
      public MolfileCreator(ExtendedMolecule mol, boolean allowScaling) {
          this(mol, allowScaling, new StringBuilder(32768));
          }
  
      /**
       * This creates a new molfile version 2 from the given molecule.
       * If allowScaling==true and the average bond length is smaller than 1.0 or larger than 3.0,
       * then all coordinates are scaled to achieve an average bond length of 1.5.
       * If a StringBuilder is given, then the molfile will be appended to that.
       * @param mol
       * @param allowScaling
       * @param builder null or StringBuilder to append to
       */
      public MolfileCreator(ExtendedMolecule mol, boolean allowScaling, StringBuilder builder) {
          this(mol, allowScaling, 0.0, builder);
      }
  
      /**
       * This creates a new molfile version 2 from the given molecule.
       * If allowScaling==true and the average bond length is smaller than 1.0 or larger than 3.0,
       * then all coordinates are scaled to achieve an average bond length of 1.5.
       * If scalingFactor is given, then the molecule is scaled accordingly independent of the average bond length.
       * If a StringBuilder is given, then the molfile will be appended to that.
       * @param mol
       * @param allowScaling if false, then no scaling is performed
       * @param scalingFactor if not 0.0 then the molecule is scaled by this factor
       * @param builder null or StringBuilder to append to
       */
      public MolfileCreator(ExtendedMolecule mol, boolean allowScaling, double scalingFactor, StringBuilder builder) {
  		mDoubleFormat = new DecimalFormat("0.0000", new DecimalFormatSymbols(Locale.ENGLISH)); //English local ('.' for the dot)
          final String nl = System.lineSeparator();
  
          mol.ensureHelperArrays(Molecule.cHelperParities);
  
          boolean isRacemic = true;
          for (int atom=0; atom<mol.getAtoms(); atom++) {
              if (mol.getAtomParity(atom) != Molecule.cAtomParityNone
               && mol.getAtomParity(atom) != Molecule.cAtomParityUnknown
               && mol.getAtomESRType(atom) != Molecule.cESRTypeAnd) {
                  isRacemic = false;
                  break;
                  }
              }
          int maxESRGroup = -1;
          if (isRacemic) {
              int[] esrGroupCount = new int[Molecule.cESRMaxGroups];
              int maxGroupCount = 0;
              for (int atom=0; atom<mol.getAtoms(); atom++) {
                  if (mol.getAtomParity(atom) != Molecule.cAtomParityNone
                   && mol.getAtomParity(atom) != Molecule.cAtomParityUnknown
                   && mol.getAtomESRType(atom) == Molecule.cESRTypeAnd) {
                      int group = mol.getAtomESRGroup(atom);
                      esrGroupCount[group]++;
                      if (maxGroupCount < esrGroupCount[group]) {
                          maxGroupCount = esrGroupCount[group];
                          maxESRGroup = group;
                          }
                      break;
                      }
                  }
              }
  
          mBuilder = (builder == null) ? new StringBuilder() : builder;
  
          String name = (mol.getName() != null) ? mol.getName() : "";
          mBuilder.append(name+nl);
          mBuilder.append("Actelion Java MolfileCreator 1.0"+nl+nl);
  
          appendThreeDigitInt(mol.getAllAtoms());
          appendThreeDigitInt(mol.getAllBonds());
          mBuilder.append("  0  0");
          appendThreeDigitInt((!isRacemic) ? 1 : 0);
          mBuilder.append("  0  0  0  0  0999 V2000"+nl);
  
          boolean hasCoordinates = (mol.getAllAtoms() == 1);
          for(int atom=1; atom<mol.getAllAtoms(); atom++) {
              if (mol.getAtomX(atom) != mol.getAtomX(0)
               || mol.getAtomY(atom) != mol.getAtomY(0)
               || mol.getAtomZ(atom) != mol.getAtomZ(0)) {
                  hasCoordinates = true;
                  break;
              }
          }
  
          double grafac = 1.0;
  
          if (hasCoordinates) {
          	if (scalingFactor != 0) {
                  grafac = scalingFactor;
                  }
              else if (allowScaling) {
                  double avbl = mol.getAverageBondLength();
                  if (avbl != 0.0f) {
                      if (avbl < 1.0f || avbl > 3.0f)
                          grafac = TARGET_AVBL / avbl;
                      }
                  else { // make the minimum distance between any two atoms twice as long as TARGET_AVBL
                      double minDistance = Double.MAX_VALUE;
                      for (int atom1=1; atom1<mol.getAllAtoms(); atom1++) {
                          for (int atom2=0; atom2<atom1; atom2++) {
                              double dx = mol.getAtomX(atom2) - mol.getAtomX(atom1);
                              double dy = mol.getAtomY(atom2) - mol.getAtomY(atom1);
                              double dz = mol.getAtomZ(atom2) - mol.getAtomZ(atom1);
                              double distance = dx*dx + dy*dy + dz*dz;
                              if (minDistance > distance)
                                  minDistance = distance;
                              }
                          }
                      grafac = 2.0f * TARGET_AVBL / minDistance;
                      }
                  }
              }
  
          for (int atom=0; atom<mol.getAllAtoms(); atom++) {
              if (hasCoordinates) {
                  appendTenDigitDouble(grafac * mol.getAtomX(atom));
                  appendTenDigitDouble(grafac * -mol.getAtomY(atom));
                  appendTenDigitDouble(grafac * -mol.getAtomZ(atom));
                  }
              else {
                  mBuilder.append("    0.0000    0.0000    0.0000");
                  }
              if (mol.getAtomList(atom) != null)
                  mBuilder.append(" L  ");
              else if ((mol.getAtomQueryFeatures(atom) & Molecule.cAtomQFAny) != 0)
                  mBuilder.append(" A  ");
              else if ((mol.getAtomicNo(atom) >= 129 && mol.getAtomicNo(atom) <= 144) || mol.getAtomicNo(atom) == 154)
                  mBuilder.append(" R# ");
              else {
                  String atomLabel = mol.getAtomLabel(atom);
                  mBuilder.append(" "+atomLabel);
                  if (atomLabel.length() == 1)
                      mBuilder.append("  ");
                  else if (atomLabel.length() == 2)
                      mBuilder.append(" ");
                  }
  
              mBuilder.append(" 0  0  0");	// massDif, charge, parity
  
              long hydrogenFlags = Molecule.cAtomQFHydrogen & mol.getAtomQueryFeatures(atom);
              if (hydrogenFlags == 0)
                  mBuilder.append("  0");
              else if (hydrogenFlags == (Molecule.cAtomQFNot0Hydrogen | Molecule.cAtomQFNot1Hydrogen))
                  mBuilder.append("  3");	// at least 2 hydrogens
              else if (hydrogenFlags == Molecule.cAtomQFNot0Hydrogen)
                  mBuilder.append("  2");	// at least 1 hydrogens
              else if (hydrogenFlags == (Molecule.cAtomQFNot1Hydrogen | Molecule.cAtomQFNot2Hydrogen | Molecule.cAtomQFNot3Hydrogen))
                  mBuilder.append("  1");	// no hydrogens
              else if (hydrogenFlags == (Molecule.cAtomQFNot0Hydrogen | Molecule.cAtomQFNot2Hydrogen | Molecule.cAtomQFNot3Hydrogen))
                  mBuilder.append("  2");	// use at least 1 hydrogens as closest match for exactly one
  
              mBuilder.append(((mol.getAtomQueryFeatures(atom) & Molecule.cAtomQFMatchStereo) != 0) ? "  1" : "  0");
  
              int valence = mol.getAtomAbnormalValence(atom);
              if (valence == -1)
                  mBuilder.append("  0");
              else if (valence == 0)
                  mBuilder.append(" 15");
              else
                  appendThreeDigitInt(valence);
  
              mBuilder.append("  0  0  0");
              appendThreeDigitInt(mol.getAtomMapNo(atom));
              mBuilder.append("  0  0"+nl);
              }
  
          for (int bond=0; bond<mol.getAllBonds(); bond++) {
              int order,stereo;
              switch (mol.getBondType(bond)) {
              case Molecule.cBondTypeSingle:		order = 1; stereo = 0; break;
              case Molecule.cBondTypeDouble:		order = 2; stereo = 0; break;
              case Molecule.cBondTypeTriple:		order = 3; stereo = 0; break;
              case Molecule.cBondTypeDown:		order = 1; stereo = 6; break;
              case Molecule.cBondTypeUp:			order = 1; stereo = 1; break;
              case Molecule.cBondTypeCross:		order = 2; stereo = 3; break;
              case Molecule.cBondTypeDelocalized:	order = 4; stereo = 0; break;
              case Molecule.cBondTypeMetalLigand:	order = 8; stereo = 0; break;
              default:							order = 1; stereo = 0; break; }
  
              if (isRacemic && (stereo == 1 || stereo == 6)) {
                  int atom = mol.getBondAtom(0, bond);
                  if (mol.getAtomESRType(atom) == Molecule.cESRTypeOr)
                      stereo = 0; // we interpret 'either' bonds as racemic and don't use it for OR atoms
                  else if (mol.getAtomESRType(atom) == Molecule.cESRTypeAnd
                        && mol.getAtomESRGroup(atom) != maxESRGroup)
                      stereo = 4; // we use 'either' bonds for all racemic atoms that are not in the largest AND group
                  }
  
              // if query features cannot be expressed exactly stay on the loosely defined side
              int bondType = mol.getBondQueryFeatures(bond) & Molecule.cBondQFBondTypes;
              if (bondType != 0) {
                  if (bondType == Molecule.cBondTypeDelocalized)
                      order = 4;	// aromatic
                  else if (bondType == (Molecule.cBondTypeSingle | Molecule.cBondTypeDouble))
                      order = 5;	// single or double
                  else if (bondType == (Molecule.cBondTypeSingle | Molecule.cBondTypeDelocalized))
                      order = 6;	// single or aromatic
                  else if (bondType == (Molecule.cBondTypeDouble | Molecule.cBondTypeDelocalized))
                      order = 7;	// single or double
                  else
                      order = 8;	// any
                  }
  
              int ringState = mol.getBondQueryFeatures(bond) & Molecule.cBondQFRingState;
              int topology = (ringState == 0) ? 0 : (ringState == Molecule.cBondQFRing) ? 1 : 2;
  
              appendThreeDigitInt(1 + mol.getBondAtom(0, bond));
              appendThreeDigitInt(1 + mol.getBondAtom(1, bond));
              appendThreeDigitInt(order);
              appendThreeDigitInt(stereo);
              mBuilder.append("  0");
              appendThreeDigitInt(topology);
              mBuilder.append("  0"+nl);
              }
  
          int no = 0;
          for (int atom=0; atom<mol.getAllAtoms(); atom++)
              if (mol.getAtomCharge(atom) != 0)
                  no++;
  
          if (no != 0) {
              int count = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                  if (mol.getAtomCharge(atom) != 0) {
                      if (count == 0) {
                          mBuilder.append("M  CHG");
                          appendThreeDigitInt(Math.min(8, no));
                          }
                      mBuilder.append(" ");
                      appendThreeDigitInt(atom + 1);
                      int charge = mol.getAtomCharge(atom);
                      if (charge < 0) {
                          mBuilder.append("  -");
                          charge = -charge;
                          }
                      else
                          mBuilder.append("   ");
                      mBuilder.append((char)('0' + charge));
                      no--;
                      if (++count == 8 || no == 0) {
                          count = 0;
                          mBuilder.append(nl);
                          }
                      }
                  }
              }
  
          no = 0;
          for (int atom=0; atom<mol.getAllAtoms(); atom++)
              if (!mol.isNaturalAbundance(atom))
                  no++;
  
          if (no != 0) {
              int count = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                  if (!mol.isNaturalAbundance(atom)) {
                      if (count == 0) {
                          mBuilder.append("M  ISO");
                          appendThreeDigitInt(Math.min(8, no));
                          }
                      mBuilder.append(" ");
                      appendThreeDigitInt(atom + 1);
                      mBuilder.append(" ");
                      appendThreeDigitInt(mol.getAtomMass(atom));
                      no--;
                      if (++count == 8 || no == 0) {
                          count = 0;
                          mBuilder.append(nl);
                          }
                      }
                  }
              }
  
          no = 0;
          for (int atom=0; atom<mol.getAllAtoms(); atom++)
              if (mol.getAtomRadical(atom) != 0)
                  no++;
  
          if (no != 0) {
              int count = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                  if (mol.getAtomRadical(atom) != 0) {
                      if (count == 0) {
                          mBuilder.append("M  RAD");
                          appendThreeDigitInt(Math.min(8, no));
                          }
                      mBuilder.append(" ");
                      appendThreeDigitInt(atom + 1);
                      switch (mol.getAtomRadical(atom)) {
                      case Molecule.cAtomRadicalStateS:
                          mBuilder.append("   1");
                          break;
                      case Molecule.cAtomRadicalStateD:
                          mBuilder.append("   2");
                          break;
                      case Molecule.cAtomRadicalStateT:
                          mBuilder.append("   3");
                          break;
                          }
                      no--;
                      if (++count == 8 || no == 0) {
                          count = 0;
                          mBuilder.append(nl);
                          }
                      }
                  }
              }
  
          no = 0;
          for (int atom=0; atom<mol.getAllAtoms(); atom++)
              if ((mol.getAtomicNo(atom) >= 129 && mol.getAtomicNo(atom) <= 144) || mol.getAtomicNo(atom) == 154)
                  no++;
  
          if (no != 0) {
              int count = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                  int atomicNo = mol.getAtomicNo(atom);
                  if ((atomicNo >= 129 && atomicNo <= 144) || atomicNo == 154) {
                      if (count == 0) {
                          mBuilder.append("M  RGP");
                          appendThreeDigitInt(Math.min(8, no));
                          }
                      mBuilder.append(" ");
                      appendThreeDigitInt(atom + 1);
                      mBuilder.append(" ");
                      appendThreeDigitInt(atomicNo == 154 ? 0 : atomicNo >= 142 ? atomicNo - 141 : atomicNo - 125);
                      no--;
                      if (++count == 8 || no == 0) {
                          count = 0;
                          mBuilder.append(nl);
                          }
                      }
                  }
              }
  
          if (mol.isFragment()) {
              no = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++)
                  if ((mol.getAtomQueryFeatures(atom) & Molecule.cAtomQFRingState) != 0)
                      no++;
  
              if (no != 0) {
                  int count = 0;
                  for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                      long ringFeatures = mol.getAtomQueryFeatures(atom) & Molecule.cAtomQFRingState;
                      if (ringFeatures != 0) {
                          if (count == 0) {
                              mBuilder.append("M  RBC");
                              appendThreeDigitInt(Math.min(8, no));
                              }
                          mBuilder.append(" ");
                          appendThreeDigitInt(atom + 1);
                          if (ringFeatures == (Molecule.cAtomQFNot2RingBonds | Molecule.cAtomQFNot3RingBonds | Molecule.cAtomQFNot4RingBonds))
                                  mBuilder.append("  -1");
                          else if (ringFeatures == Molecule.cAtomQFNotChain)
                                  mBuilder.append("   1");	// any ring atom; there is no MDL equivalent
                          else if (ringFeatures == (Molecule.cAtomQFNotChain | Molecule.cAtomQFNot3RingBonds | Molecule.cAtomQFNot4RingBonds))
                                  mBuilder.append("   2");
                          else if (ringFeatures == (Molecule.cAtomQFNotChain | Molecule.cAtomQFNot2RingBonds | Molecule.cAtomQFNot4RingBonds))
                                  mBuilder.append("   3");
                          else if (ringFeatures == (Molecule.cAtomQFNotChain | Molecule.cAtomQFNot2RingBonds | Molecule.cAtomQFNot3RingBonds))
                                  mBuilder.append("   4");
                          no--;
                          if (++count == 8 || no == 0) {
                              count = 0;
                              mBuilder.append(nl);
                              }
                          }
                      }
                  }
  
              for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                  int[] atomList = mol.getAtomList(atom);
                  if (atomList != null) {
                      mBuilder.append("M  ALS ");
                      appendThreeDigitInt(atom+1);
                      appendThreeDigitInt(atomList.length);
                      mBuilder.append(((mol.getAtomQueryFeatures(atom) & Molecule.cAtomQFAny) != 0) ? " T " : " F ");
                      for (int i=0; i<atomList.length; i++) {
                          String label = Molecule.cAtomLabel[atomList[i]];
                          switch (label.length()) {
                          case 1:
                              mBuilder.append(label+"   ");
                              break;
                          case 2:
                              mBuilder.append(label+"  ");
                              break;
                          case 3:
                              mBuilder.append(label+" ");
                              break;
                          default:
                              mBuilder.append("   ?");
                              break;
                              }
                          }
                      mBuilder.append(nl);
                      }
                  }
  
              no = 0;
              for (int atom=0; atom<mol.getAllAtoms(); atom++)
                  if ((mol.getAtomQueryFeatures(atom) & (Molecule.cAtomQFMoreNeighbours | Molecule.cAtomQFNoMoreNeighbours)) != 0)
                      no++;
  
              if (no != 0) {
                  int count = 0;
                  for (int atom=0; atom<mol.getAllAtoms(); atom++) {
                      long substitution = mol.getAtomQueryFeatures(atom) & (Molecule.cAtomQFMoreNeighbours | Molecule.cAtomQFNoMoreNeighbours);
                      if (substitution != 0) {
                          if (count == 0) {
                              mBuilder.append("M  SUB");
                              appendThreeDigitInt(Math.min(8, no));
                              }
                          mBuilder.append(" ");
                          appendThreeDigitInt(atom + 1);
                          if ((substitution & Molecule.cAtomQFMoreNeighbours) != 0)
                              mBuilder.append("   "+(mol.getAllConnAtoms(atom)+1));
                          else
                              mBuilder.append("  -2");
                          no--;
                          if (++count == 8 || no == 0) {
                              count = 0;
                              mBuilder.append(nl);
                              }
                          }
                      }
                  }
              }
  
          no = 0;
          for (int atom=0; atom<mol.getAllAtoms(); atom++)
              if (mol.getAtomCustomLabel(atom) != null)
                  no++;
  
          if (no != 0) {
              int sgroupNo = 0;
              for (int atom = 0; atom<mol.getAllAtoms(); atom++) {
                  String label = mol.getAtomCustomLabel(atom);
                  if (label != null) {
                      sgroupNo++;
  
                      mBuilder.append("M  STY  1 ");
                      appendThreeDigitInt(sgroupNo);
                      mBuilder.append(" DAT");
                      mBuilder.append(nl);
  
                      mBuilder.append("M  SLB  1 ");
                      appendThreeDigitInt(sgroupNo);    // sgroup-no
                      mBuilder.append(" ");
                      appendThreeDigitInt(sgroupNo);    // extindex
                      mBuilder.append(nl);
  
                      mBuilder.append("M  SAL ");
                      appendThreeDigitInt(sgroupNo);    // sgroup-no
                      mBuilder.append("  1 ");
                      appendThreeDigitInt(atom+1); // atom
                      mBuilder.append(nl);
  
                      mBuilder.append("M  SDT ");
                      appendThreeDigitInt(sgroupNo);    // sgroup-no
                      mBuilder.append(" ");
                      mBuilder.append(MolfileParser.FIELD_NAME_CUSTOM_LABEL);
                      mBuilder.append(nl);
  
                      mBuilder.append("M  SDD ");
                      appendThreeDigitInt(sgroupNo);    // sgroup-no
                      mBuilder.append(" ");
                      appendTenDigitDouble(mol.getAtomX(atom));   // absolute and detached coords (DA)
                      appendTenDigitDouble(mol.getAtomY(atom));
                      mBuilder.append("    DA    ALL  1       5");
                      mBuilder.append(nl);
  
                      mBuilder.append("M  SED ");
                      appendThreeDigitInt(sgroupNo);    // sgroup-no
                      mBuilder.append(" ");
                      mBuilder.append(label);
                      mBuilder.append(nl);
                  }
              }
          }
  
          mBuilder.append("M  END"+nl);
          }
  
  
      /**
       * If a pre-filled StringBuilder was passed to the constructor, then this returns
       * the original content with the appended molfile.
       * @return
       */
      public String getMolfile() {
          return mBuilder.toString();
          }
  
  
      public void writeMolfile(Writer theWriter) throws IOException {
          theWriter.write(mBuilder.toString());
          }
  
  
      private void appendThreeDigitInt(int data) {
          if (data < 0 || data > 999) {
              mBuilder.append("  ?");
              return;
              }
  
          boolean digitFound = false;
          for (int i=0; i<3; i++) {
              int theChar = data / 100;
              if (theChar==0) {
                  if (i==2 || digitFound)
                      mBuilder.append((char)'0');
                  else
                      mBuilder.append((char)' ');
                  }
              else {
                  mBuilder.append((char)('0' + theChar));
                  digitFound = true;
                  }
              data = 10 * (data % 100);
              }
          }
  
      private void appendTenDigitDouble(double theDouble) {
      	String val = mDoubleFormat.format(theDouble);
      	for(int i=val.length(); i<10; i++) mBuilder.append(' ');
      	mBuilder.append(val);
          }
  	}
  
  