/* 
A* -------------------------------------------------------------------
B* This file contains source code for the PyMOL computer program
C* copyright 1998-2000 by Warren Lyford Delano of DeLano Scientific. 
D* -------------------------------------------------------------------
E* It is unlawful to modify or remove this copyright notice.
F* -------------------------------------------------------------------
G* Please see the accompanying LICENSE file for further information. 
H* -------------------------------------------------------------------
I* Additional authors of this source file include:
-* 
-* 
-*
Z* -------------------------------------------------------------------
*/
#include"os_python.h"

#include"os_predef.h"
#include"os_std.h"
#include"os_gl.h"

#include"Base.h"
#include"Parse.h"
#include"Vector.h"
#include"MemoryDebug.h"
#include"Err.h"
#include"Map.h"
#include"Selector.h"
#include"ObjectMolecule.h"
#include"Ortho.h"
#include"Util.h"
#include"Matrix.h"
#include"Scene.h"
#include"P.h"
#include"PConv.h"
#include"Executive.h"
#include"Setting.h"
#include"Sphere.h"
#include"main.h"
#include"CGO.h"
#include"Editor.h"
#include"Sculpt.h"
#include"OVLexicon.h"
#include"ListMacros.h"
#include"File.h"
#include "Lex.h"
#include "MolV3000.h"
#include "HydrogenAdder.h"
#include "Feedback.h"

#ifdef _WEBGL
#endif

#define cMaxNegResi 100

#define ntrim ParseNTrim
#define nextline ParseNextLine
#define ncopy ParseNCopy
#define nskip ParseNSkip

#include <algorithm>
#include <array>
#include <cassert>
#include <set>
#include <unordered_map>
#include <vector>

#ifndef NO_MMLIBS
#include "mmpymolx.h"
#endif

static
CoordSet *ObjectMoleculeMMDStr2CoordSet(PyMOLGlobals * G, const char *buffer,
                                        AtomInfoType ** atInfoPtr, const char **restart);

static
void ObjectMoleculeTransformTTTf(ObjectMolecule * I, float *ttt, int state);

static
int ObjectMoleculeGetAtomGeometry(const ObjectMolecule * I, int state, int at);

static
void ObjectMoleculeInferHBondFromChem(ObjectMolecule * I);

/**
 * Order function for sorting bonds by atom indices (ignores other properties
 * like bond order).
 *
 * Pre-condition: index pairs are in order (index[0] < index[1])
 */
static
int BondTypeInOrder(PyMOLGlobals * G, const BondType * bonds, int i1, int i2) {
  auto lhs = bonds[i1].index;
  auto rhs = bonds[i2].index;
  return lhs[0] < rhs[0] || (lhs[0] == rhs[0] && lhs[1] < rhs[1]);
}

/**
 * Remove duplicated bonds. Disregards if two atoms would be bonded by two
 * bonds of different bond order (only one will be kept).
 */
static
void ObjectMoleculeRemoveDuplicateBonds(PyMOLGlobals * G, ObjectMolecule * I) {
  // make sure index pairs are in order
  for (int i = 0; i < I->NBond; ++i) {
    auto& bond = I->Bond[i];
    if (bond.index[0] > bond.index[1] && !bond.symop_2) {
      std::swap(bond.index[0], bond.index[1]);
    }
  }

  // get sorted indices
  int * sorted = pymol::malloc<int>(I->NBond);
  UtilSortIndexGlobals(G, I->NBond, I->Bond.data(), sorted,
      (UtilOrderFnGlobals *) BondTypeInOrder);

  // purge duplicated bonds and mark for removal
  for (int i = 0, j = -1; i < I->NBond; ++i) {
    int b = sorted[i];
    auto ptr = I->Bond + sorted[i];

    bool equal = false;
    if (j != -1) {
      const auto& other = I->Bond[j];
      if (ptr->index[0] == other.index[0] &&
          ptr->index[1] == other.index[1])
        equal = true;
    }

    if (!equal) {
      j = b;
    } else {
      AtomInfoPurgeBond(G, ptr);

      // mark for removal
      ptr->index[0] = ptr->index[1] = 0;
    }
  }

  FreeP(sorted);

  // remove purged bonds from array
  int j = 0;
  for (int i = 0; i < I->NBond; ++i) {
    auto& bond = I->Bond[i];
    if (bond.index[0] != bond.index[1]) {
      if (j != i)
        std::swap(I->Bond[j], bond);
      ++j;
    }
  }
  I->NBond = j;
  VLASize(I->Bond, BondType, I->NBond);
}

/**
 * Convert a discrete object to non-discrete. Will merge atoms from states
 * by identifiers. Atom level properties like color, b, q, etc. are lost
 * on the merged atoms.
 */
static
bool ObjectMoleculeSetNotDiscrete(PyMOLGlobals * G, ObjectMolecule * I) {
  if (!I->DiscreteFlag)
    return true;

  VLAFreeP(I->DiscreteAtmToIdx);
  VLAFreeP(I->DiscreteCSet);
  I->DiscreteFlag = false;

  for (int atm = 0; atm < I->NAtom; ++atm) {
    I->AtomInfo[atm].discrete_state = 0;
  }

  int * outdex;
  int * aindex = AtomInfoGetSortedIndex(G, I, I->AtomInfo, I->NAtom, &outdex);

  // merge duplicate atoms
  for (int i = 0, j = -1; i < I->NAtom; ++i) {
    int atm = aindex[i];
    auto ai = I->AtomInfo + atm;

    if (j == -1 || !AtomInfoMatch(G, ai, I->AtomInfo + j, false, false)) {
      j = atm;
    } else {
      ai->deleteFlag = true;
    }

    // remapping of merged indices
    outdex[atm] = j;
  }

  // point coordsets to merged atoms
  for (int state = 0; state < I->NCSet; ++state) {
    auto cs = I->CSet[state];
    if (!cs)
      continue;

    for (int idx = 0; idx < cs->NIndex; ++idx) {
      int atm = cs->IdxToAtm[idx];
      cs->IdxToAtm[idx] = outdex[atm];
    }
  }

  // point bonds to merged atoms
  for (int i = 0; i < I->NBond; ++i) {
    auto& bond = I->Bond[i];
    bond.index[0] = outdex[bond.index[0]];
    bond.index[1] = outdex[bond.index[1]];
  }

  AtomInfoFreeSortedIndexes(G, &aindex, &outdex);

  ObjectMoleculeRemoveDuplicateBonds(G, I);

  I->updateAtmToIdx();
  ObjectMoleculePurge(I);

  return true;
}

/**
 * Make a non-discrete object discrete, or vice versa.
 */
int ObjectMoleculeSetDiscrete(PyMOLGlobals * G, ObjectMolecule * I, int discrete)
{
  int state, idx, ao, an, ao1, ao2, an1, an2;
  int maxnatom, natom = I->NAtom, nbond = I->NBond;
  int *aostate2an = nullptr;
  char *bondseen = nullptr;
  BondType *bond;
  CoordSet *cs;

  if (!discrete) {
    if (!I->DiscreteFlag)
      return true;

    return ObjectMoleculeSetNotDiscrete(G, I);
  }

  if (I->DiscreteFlag)
    return true;

  // upper bound for number of discrete atoms
  maxnatom = I->NAtom * I->NCSet;

  // mapping (for bonds): atom_old -> atom_new
  ok_assert(1, aostate2an = pymol::malloc<int>(I->NAtom));
  ok_assert(1, bondseen = pymol::calloc<char >(I->NBond));

  // discrete setup
  I->DiscreteFlag = discrete;
  ok_assert(1, I->DiscreteAtmToIdx = pymol::vla<int>(maxnatom));
  ok_assert(1, I->DiscreteCSet = pymol::vla<CoordSet*>(maxnatom));

  // for all coordinate sets
  for (state = 0; state < I->NCSet; state++) {
    cs = I->CSet[state];

    if (!cs)
      continue;

    // init the atom_old -> atom_new array
    for (ao = 0; ao < I->NAtom; ao++)
      aostate2an[ao] = -1;

    // for all atoms in coordinate set
    for (idx = 0; idx < cs->NIndex; idx++) {
      ao = an = cs->IdxToAtm[idx];

      if (I->DiscreteCSet[ao]) {
        // seen before, have to copy
        an = natom++;

        VLACheck(I->AtomInfo, AtomInfoType, an);
        ok_assert(1, I->AtomInfo);

        AtomInfoCopy(G,
            I->AtomInfo + ao,
            I->AtomInfo + an);
        cs->IdxToAtm[idx] = an;
      }

      I->AtomInfo[an].discrete_state = state + 1; // 1-based :-(
      I->DiscreteCSet[an] = cs;
      I->DiscreteAtmToIdx[an] = idx;

      // mapping (for bonds): (atom_old, state) -> atom_new
      aostate2an[ao] = an;
    }

    // unused in discrete coordsets
    cs->AtmToIdx.clear();

    // for all old bonds
    for (idx = 0; idx < I->NBond; idx++) {
      bond = I->Bond + idx;

      // old atom indices
      ao1 = bond->index[0];
      ao2 = bond->index[1];

      // new atom indices
      an1 = aostate2an[ao1];
      an2 = aostate2an[ao2];

      // do both atoms exist in this coord set?
      if (an1 == -1 || an2 == -1)
        continue;

      if (bondseen[idx]) {
        // seen before, have to copy
        VLACheck(I->Bond, BondType, nbond);
        ok_assert(1, I->Bond);

        bond = I->Bond + (nbond++);
        AtomInfoBondCopy(G, I->Bond + idx, bond);
      } else {
        bondseen[idx] = true;
      }

      bond->index[0] = an1;
      bond->index[1] = an2;
    }
  }

  // not needed anymore
  mfree(aostate2an);
  mfree(bondseen);

  // update N* count fields
  I->NAtom = natom;
  I->NBond = nbond;

  // trim VLAs memory to actual size
  if (I->NBond)
    VLASize(I->Bond, BondType, I->NBond);
  if (I->NAtom)
    VLASize(I->AtomInfo, AtomInfoType, I->NAtom);

  I->setNDiscrete(I->NAtom);

  I->invalidate(cRepAll, cRepInvAll, -1);

  return true;

ok_except1:
  PRINTFB(G, FB_ObjectMolecule, FB_Errors)
    " %s: memory allocation failed\n", __func__ ENDFB(G);
  return false;
}

int ObjectMoleculeCheckFullStateSelection(ObjectMolecule * I, int sele, int state)
{
  PyMOLGlobals *G = I->G;
  int result = false;
  if((state >= 0) && (state < I->NCSet)) {
    const AtomInfoType *ai = I->AtomInfo.data();
    CoordSet *cs = I->CSet[state];
    if(cs) {
      int a;
      int at;
      result = true;
      for(a = 0; a < cs->NIndex; a++) {
        at = cs->IdxToAtm[a];
        if(!SelectorIsMember(G, ai[at].selEntry, sele)) {
          result = false;
          break;
        }
      }
    }
  }
  return result;
}

/* PARAMS
 *   ch -- ptr to empty str of length 'len'
 *  len -- str len 
 * RETURNS
 *   ch, with the caption in place
 * NOTES
 *   User owns the buffer so must clean up after it
 */
char *ObjectMolecule::getCaption(char * ch, int len) const
{
  auto I = this;
  int objState;
  int n = 0;
  int show_state = 0;
  int show_as_fraction = 0;
  const char *frozen_str = "";

  int state = ObjectGetCurrentState(I, false);
  int counter_mode = SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_state_counter_mode);
  int frozen = SettingGetIfDefined_i(I->G, I->Setting.get(), cSetting_state, &objState);

  /* if frozen print (blue) STATE / NSTATES
   * if not frozen, print STATE/NSTATES
   * if beyond NSTATES, print * /NSTATES.
   */

  if(frozen) { /* frozen color */
    frozen_str = "\\789";
  } else {
    if (I->DiscreteFlag) { /* discrete states */
      frozen_str = "\\993";
    } else { /* normal case */
      frozen_str = "";
    }
  }

  switch(counter_mode) {
  case 0: /* off */
    show_state = show_as_fraction = 0;
    break;
  case 2: /* just state */
    show_state = 1;
    show_as_fraction = 0;
    break;
  case -1: /* fraction, full-on */
  case 1:
  default:
    show_state = show_as_fraction = 1;
    break;
  }

  /* bail on null string or no room */
  if (!ch || len==0)
    return nullptr;

  ch[0] = 0;

  /* if the state is valid, setup the label */
  if(state >= 0) {
    if (state < I->NCSet) {
      const CoordSet *cs = I->CSet[state];
      if(cs) {
	if(show_state) {
	  if (show_as_fraction) {
	    if (strlen(cs->Name)) { 	  /* NAME */
	      n = snprintf(ch, len, "%s %s%d/%d", cs->Name, frozen_str, state+1, I->NCSet);
	    } 
	    else { /* no name */
	      n = snprintf(ch, len, "%s%d/%d", frozen_str, state+1, I->NCSet);
	    }
	  } else { /* not fraction */
	    if (strlen(cs->Name)) {
	      n = snprintf(ch, len, "%s %s%d", cs->Name, frozen_str, state+1);
	    } else { /* no name */
	      n = snprintf(ch, len, "%s%d", frozen_str, state+1);
	    }
	  }
	} else { /* no state */
	  n = snprintf(ch, len, "%s", cs->Name);
	}
      } else { /* no coord set, can be an N-state object missing a CSet */
      } 
    } else { /* state > NCSet, out of range due to other object or global setting */
      if(show_state) {
	if(show_as_fraction) {
	  n = snprintf(ch, len, "%s--/%d", frozen_str, I->NCSet);
	} else { /* no fraction */
	  n = snprintf(ch, len, "%s--", frozen_str);
	}
      }
    }
  } else if (state == -1) {
    // all states
    n = snprintf(ch, len, "%s*/%d", frozen_str, I->NCSet);
  } else {
    /* blank out the title if outside the valid # of states */
  }

  if (n > len)
    return nullptr;

  return ch;
}


#define MAX_BOND_DIST 50

/* find sets of atoms with identical skeletons */

struct match_info {
  AtomInfoType *ai_a;
  AtomInfoType *ai_b;
  BondType *bi_a;
  BondType *bi_b;
  const int *nbr_a, *nbr_b;
  int *matched;

  // values: -1 .. 2
  using mark_type = signed char;

  std::vector<mark_type> atom_mark_a;
  std::vector<mark_type> atom_mark_b;
  std::vector<mark_type> bond_mark_a;
  std::vector<mark_type> bond_mark_b;
};

#define recmat3(x,y,z) \
  (recursive_match(u_a[0],u_b[x],x_a[0],x_b[x],mi) &&   \
   recursive_match(u_a[1],u_b[y],x_a[1],x_b[y],mi) &&   \
   recursive_match(u_a[2],u_b[z],x_a[2],x_b[z],mi))

#define recmat4(w,x,y,z)  \
  (recursive_match(u_a[0],u_b[w],x_a[0],x_b[w],mi) && \
   recursive_match(u_a[1],u_b[x],x_a[1],x_b[x],mi) && \
   recursive_match(u_a[2],u_b[y],x_a[2],x_b[y],mi) && \
   recursive_match(u_a[3],u_b[z],x_a[3],x_b[z],mi))

static void undo_match(int *start, match_info * mi)
{
  /* remove the matched atom set */
  int *match = mi->matched;
  while(match > start) {
    int a = match[-4];
    int b = match[-3];
    int aa = match[-2];
    int bb = match[-1];
    mi->atom_mark_a[a] = false;
    mi->atom_mark_b[b] = false;
    mi->bond_mark_a[aa] = false;
    mi->bond_mark_b[bb] = false;
    match -= 4;
  }
  mi->matched = start;
}

static int recursive_match(int a, int b, int aa, int bb, match_info * mi)
{
  if (mi->atom_mark_a[a] && mi->atom_mark_b[b]) {
    if (aa >= 0 && bb >= 0 && !(mi->bond_mark_a[aa] || mi->bond_mark_b[bb])) {
      /* note bond */
      mi->atom_mark_a[a] = true;
      mi->atom_mark_b[b] = true;
      mi->bond_mark_a[aa] = true;
      mi->bond_mark_b[bb] = true;
      mi->matched[0] = a;       /* atoms */
      mi->matched[1] = b;
      mi->matched[2] = aa;      /* bonds */
      mi->matched[3] = bb;
      mi->matched += 4;
    }
    return true;
  } else if (mi->atom_mark_a[a] != mi->atom_mark_b[b])
    return false;
  else if(mi->ai_a[a].protons != mi->ai_b[b].protons)
    return false;
  else {
    int ni_a = mi->nbr_a[a];
    int ni_b = mi->nbr_b[b];
    int num_a = mi->nbr_a[ni_a++];
    int num_b = mi->nbr_b[ni_b++];

    if((num_a != num_b) || (num_a > 4)) /* must have the same number of neighbors (four or less) */
      return false;
    else {
      int match_flag = false;
      int u_a[4], u_b[4], x_a[4], x_b[4];
      int n_u_a = 0;
      int n_u_b = 0;
      int *before = mi->matched;
      mi->atom_mark_a[a] = true;
      mi->atom_mark_b[b] = true;
      mi->bond_mark_a[aa] = true;
      mi->bond_mark_b[bb] = true;
      mi->matched[0] = a;       /* atoms */
      mi->matched[1] = b;
      mi->matched[2] = aa;      /* bonds */
      mi->matched[3] = bb;
      mi->matched += 4;

      /* collect unvisited bonds */

      while(num_a--) {
        int i_a = mi->nbr_a[ni_a + 1];
        if(!mi->bond_mark_a[i_a]) {      /* bond not yet visited */
          u_a[n_u_a] = mi->nbr_a[ni_a]; /* atom index */
          x_a[n_u_a++] = i_a;
        }
        ni_a += 2;
      }

      while(num_b--) {
        int i_b = mi->nbr_b[ni_b + 1];
        if(!mi->bond_mark_b[i_b]) {
          u_b[n_u_b] = mi->nbr_b[ni_b];
          x_b[n_u_b++] = i_b;
        }
        ni_b += 2;
      }
      /* NOTE: implicitly relying upon C short-circuit evaluation */
      if(n_u_a == n_u_b) {
        if(!n_u_a)
          match_flag = true;
        else if(n_u_a == 1) {
          match_flag = recursive_match(u_a[0], u_b[0], x_a[0], x_b[0], mi);
        } else if(n_u_a == 2) {
          match_flag =
            (recursive_match(u_a[0], u_b[0], x_a[0], x_b[0], mi) &&
             recursive_match(u_a[1], u_b[1], x_a[1], x_b[1], mi)) ||
            (recursive_match(u_a[0], u_b[1], x_a[0], x_b[1], mi) &&
             recursive_match(u_a[1], u_b[0], x_a[1], x_b[0], mi));
        } else if(n_u_a == 3) {
          match_flag =
            recmat3(0, 1, 2) ||
            recmat3(0, 2, 1) ||
            recmat3(1, 0, 2) || recmat3(1, 2, 0) || recmat3(2, 0, 1) || recmat3(2, 1, 0);
        } else if(n_u_a == 4) {
          match_flag =
            recmat4(0, 1, 2, 3) ||
            recmat4(0, 2, 1, 3) ||
            recmat4(1, 0, 2, 3) ||
            recmat4(1, 2, 0, 3) || recmat4(2, 0, 1, 3) || recmat4(2, 1, 0, 3);
          if(!match_flag) {
            match_flag =
              recmat4(0, 1, 3, 2) ||
              recmat4(0, 2, 3, 1) ||
              recmat4(1, 0, 3, 2) ||
              recmat4(1, 2, 3, 0) || recmat4(2, 0, 3, 1) || recmat4(2, 1, 3, 0);
            if(!match_flag) {
              match_flag =
                recmat4(0, 3, 1, 2) ||
                recmat4(0, 3, 2, 1) ||
                recmat4(1, 3, 0, 2) ||
                recmat4(1, 3, 2, 0) || recmat4(2, 3, 0, 1) || recmat4(2, 3, 1, 0);
              if(!match_flag) {
                match_flag =
                  recmat4(3, 0, 1, 2) ||
                  recmat4(3, 0, 2, 1) ||
                  recmat4(3, 1, 0, 2) ||
                  recmat4(3, 1, 2, 0) || recmat4(3, 2, 0, 1) || recmat4(3, 2, 1, 0);
              }
            }
          }
        }
      }
      if(!match_flag) {         /* clean the match tree */
        undo_match(before, mi);
      }
      return match_flag;
    }
  }
}

int ObjectMoleculeXferValences(ObjectMolecule * Ia, int sele1, int sele2,
                               int target_state, ObjectMolecule * Ib, int sele3,
                               int source_state, int quiet)
{
  int *matched = nullptr;
  int match_found = false;
  PyMOLGlobals *G = Ia->G;
  if(Ia == Ib)
    return false;

  {
    int max_match = Ia->NAtom + Ia->NBond;
    if(max_match < (Ib->NAtom + Ib->NBond))
      max_match = (Ib->NAtom + Ib->NBond);
    matched = pymol::calloc<int>(max_match * 4);
  }

  {
    int a, b;
    AtomInfoType *ai_a = Ia->AtomInfo.data();
    AtomInfoType *ai_b = Ib->AtomInfo.data();
    BondType *bi_a = Ia->Bond.data();
    BondType *bi_b = Ib->Bond.data();

    match_info mi;

    mi.atom_mark_a.resize(Ia->NAtom);
    mi.atom_mark_b.resize(Ib->NAtom);
    mi.bond_mark_a.resize(Ia->NBond);
    mi.bond_mark_b.resize(Ib->NBond);

    // confirm zero-initialization
    assert(std::none_of(mi.atom_mark_a.begin(), mi.atom_mark_a.end(),
        [](bool m) { return m; }));

    mi.ai_a = ai_a;
    mi.ai_b = ai_b;
    mi.bi_a = bi_a;
    mi.bi_b = bi_b;
    mi.nbr_a = Ia->getNeighborArray();
    mi.nbr_b = Ib->getNeighborArray();
    mi.matched = matched;
    for(a = 0; a < Ia->NAtom; a++) {
      if(!mi.atom_mark_a[a]) {
        int a_entry = ai_a[a].selEntry;
        if(SelectorIsMember(G, a_entry, sele1) || SelectorIsMember(G, a_entry, sele2)) {
          for(b = 0; b < Ib->NAtom; b++) {
            if(SelectorIsMember(G, ai_b[b].selEntry, sele3)) {
              if(recursive_match(a, b, -1, -1, &mi)) {
                int *match = mi.matched;
                match_found = true;
                /* now graft bond orders for bonds in matching atoms */

                while(match > matched) {
                  int at_a = match[-4];
                  int at_b = match[-3];
                  int bd_a = match[-2];
                  int bd_b = match[-1];

                  if((bd_a >= 0) && (bd_a >= 0)) {
                    int a1 = bi_a[bd_a].index[0];
                    int a2 = bi_a[bd_a].index[1];

                    int a1_entry = ai_a[a1].selEntry;
                    int a2_entry = ai_a[a2].selEntry;

                    if((SelectorIsMember(G, a1_entry, sele1) &&
                        SelectorIsMember(G, a2_entry, sele2)) ||
                       (SelectorIsMember(G, a2_entry, sele1) &&
                        SelectorIsMember(G, a1_entry, sele2))) {
                      /* only update bonds which actually sit between the two selections */
                      bi_a[bd_a].order = bi_b[bd_b].order;
                      ai_a[at_a].chemFlag = false;
                    }
                  }
                  mi.atom_mark_b[at_b] = false;     /* release source for future matching */
                  if(bd_b >= 0)
                    mi.bond_mark_b[bd_b] = false;
                  match -= 4;
                }
                break;
              }
            }
          }
        }
      }
    }
  }
  FreeP(matched);
  return match_found;
}

void ObjectMoleculeTransformState44f(ObjectMolecule * I, int state, const float *matrix,
                                     int log_trans, int homogenous, int transformed)
{
  int a;
  float tmp_matrix[16];
  CoordSet *cs;
  int use_matrices = SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
  if(use_matrices<0) use_matrices = 0;
  if(!use_matrices) {
    ObjectMoleculeTransformSelection(I, state, -1, matrix, log_trans, I->Name,
                                     homogenous, true);
  } else {
    double dbl_matrix[16];
    if(state == -2)
      state = ObjectGetCurrentState(I, false);
    /* ensure homogenous matrix to preserve programmer sanity */
    if(!homogenous) {
      convertTTTfR44d(matrix, dbl_matrix);
      copy44d44f(dbl_matrix, tmp_matrix);
      matrix = tmp_matrix;
    } else {
      copy44f44d(matrix, dbl_matrix);
    }

    if(state < 0) {             /* all states */
      for(a = 0; a < I->NCSet; a++) {
        cs = I->CSet[a];
        if(cs)
          ObjectStateLeftCombineMatrixR44d(cs, dbl_matrix);
      }
    } else if(state < I->NCSet) {       /* single state */
      cs = I->CSet[state];
      if(cs)
        ObjectStateLeftCombineMatrixR44d(cs, dbl_matrix);
    } else if(I->NCSet == 1) {  /* static singleton state */
      cs = I->CSet[0];
      if(cs && SettingGet_b(I->G, I->Setting.get(), nullptr, cSetting_static_singletons)) {
        ObjectStateLeftCombineMatrixR44d(cs, dbl_matrix);
      }
    }
  }
}


/*========================================================================*/
static int ObjectMoleculeFixSeleHydrogens(ObjectMolecule * I, int sele, int state)
{
  int a;
  int seleFlag = false;
  const AtomInfoType *ai0;
  int ok = true;

  ai0 = I->AtomInfo;
  for(a = 0; a < I->NAtom; a++) {
    if(SelectorIsMember(I->G, ai0->selEntry, sele)) {
      seleFlag = true;
      break;
    }
    ai0++;
  }
  if(seleFlag) {
    seleFlag = false;
    if(!ObjectMoleculeVerifyChemistry(I, state)) {
      ErrMessage(I->G, " AddHydrogens", "missing chemical geometry information.");
    } else {
      ai0 = I->AtomInfo;
      for(a = 0; a < I->NAtom; a++) {
        if(!ai0->isHydrogen()) {    /* only do heavies */
          if(SelectorIsMember(I->G, ai0->selEntry, sele)) {
            for(StateIterator iter(I->G, I->Setting.get(), state, I->NCSet);
                iter.next();) {
              auto cs = I->CSet[iter.state];
              if (!cs)
                continue;

              seleFlag |= ObjectMoleculeSetMissingNeighborCoords(I, cs, a, true);
            }
          }
        }
        ai0++;
      }
    }
    if(seleFlag)
      I->invalidate(cRepAll, cRepInvAll, -1);
  }
  return ok;
}

static const char *skip_fortran(int num, int per_line, const char *p)
{
  int a, b;
  b = 0;
  for(a = 0; a < num; a++) {
    if((++b) == per_line) {
      b = 0;
      p = nextline(p);
    }
  }
  if(b || (!num))
    p = nextline(p);
  return (p);
}

void ObjectMoleculeOpRecInit(ObjectMoleculeOpRec * op)
{
  UtilZeroMem((char *) op, sizeof(ObjectMoleculeOpRec));
}

bool ObjectMoleculeGetNeighborVector(
    ObjectMolecule* I, int atom, int state, float* v_result)
{
  float v_atom[3] = {0.0, 0.0, 0.0};

  auto const* cs = I->getCoordSet(state);
  if (cs == nullptr) {
    return false;
  }

  if (!CoordSetGetAtomVertex(cs, atom, v_atom)) {
    return false;
  }

  for (auto const& neighbor : AtomNeighbors(I, atom)) {
    auto const a2 = neighbor.atm;

    // ignore hydrogens
    if (I->AtomInfo[a2].protons != cAN_H &&
        CoordSetGetAtomVertex(cs, a2, v_result)) {
      return true;
    }
  }

  return false;
}

/**
 * Returns the most proton-rich element with the lowest priority value (OG1
 * before OG2, CG, HB1)
 */
int ObjectMoleculeGetTopNeighbor(PyMOLGlobals * G,
                                 ObjectMolecule * I, int start, int excluded)
{
  int highest_at = -1, highest_prot = 0, lowest_pri = 9999;

  for (auto const& neighbor : AtomNeighbors(I, start)) {
    auto const at = neighbor.atm;
    auto const *ai = I->AtomInfo.data() + at;
    if((highest_at < 0) && (at != excluded)) {
      highest_prot = ai->protons;
      lowest_pri = ai->priority;
      highest_at = at;
    } else if(((ai->protons > highest_prot) ||
               ((ai->protons == highest_prot) && (ai->priority < lowest_pri)))
              && (at != excluded)) {
      highest_prot = ai->protons;
      highest_at = at;
      lowest_pri = ai->priority;
    }
  }
  return highest_at;
}

/**
 * Selection mapping for `load_traj` to only load coordinates for a subset of
 * atoms. Returns a mapping of old to new coordinate indices and modifies the
 * index-related members of `cs`.
 *
 * @param[in] obj Object Molecule for evaluation of the selection
 * @param[in,out] cs Candidate coordinate set (with valid index arrays)
 * @param[in] selection Named selection
 * @return index mapping, or nullptr if `selection` is not valid
 */
std::unique_ptr<int[]> LoadTrajSeleHelper(
    const ObjectMolecule* obj, CoordSet* cs, const char* selection)
{
  auto G = obj->G;
  int sele0 = SelectorIndexByName(G, selection);

  // We can skip "all" (0) here since it's a trivial case
  if (sele0 <= 0) {
    return nullptr;
  }

  auto xref = std::unique_ptr<int[]>(new int[cs->NIndex]);

  int idx_new = 0;

  for (int idx = 0; idx < cs->NIndex; ++idx) {
    auto atm = cs->IdxToAtm[idx];
    if (SelectorIsMember(G, obj->AtomInfo[atm].selEntry, sele0)) {
      cs->IdxToAtm[idx_new] = atm;
      cs->AtmToIdx[atm] = idx_new;
      xref[idx] = idx_new;
      ++idx_new;
    } else {
      cs->AtmToIdx[atm] = -1;
      xref[idx] = -1;
    }
  }

  cs->NIndex = idx_new;
  cs->IdxToAtm.resize(cs->NIndex);
  cs->Coord.resize(cs->NIndex * 3);

  return xref;
}

/*========================================================================*/
ObjectMolecule *ObjectMoleculeLoadTRJFile(PyMOLGlobals * G, ObjectMolecule * I,
                                          const char *fname, int frame, int interval,
                                          int average, int start, int stop, int max,
                                          const char *sele, int image, const float *shift, int quiet)
{
  FILE *f;
  char *buffer;
  const char *p;
  char cc[MAXLINELEN];
  int n_read;
  int to_go;
  int skip_first_line = true;
  int periodic = false;
  int angles = true;
  float f0, f1, f2, *fp;
  float box[3], angle[3];
  float r_cent[3], r_trans[3];
  int r_act, r_val, r_cnt;
  float *r_fp_start = nullptr, *r_fp_stop = nullptr;
  int a, b, c, i;
  int zoom_flag = false;
  int cnt = 0;
  int n_avg = 0;
  int icnt;
  int ncnt = 0;
  float zerovector[3] = { 0.0, 0.0, 0.0 };
  CoordSet *cs = nullptr;

  if (I->DiscreteFlag) {
    PRINTFB(G, FB_ObjectMolecule, FB_Errors)
      " %s: Discrete objects not supported\n", __func__ ENDFB(G);
    return I;
  }

  if(!shift)
    shift = zerovector;
  if(interval < 1)
    interval = 1;

  icnt = interval;
#define BUFSIZE 4194304
#define GETTING_LOW 10000

  f = pymol_fopen(fname, "rb");
  if(!f) {
    ErrMessage(G, __func__, "Unable to open file!");
  } else {
    if(I->CSTmpl) {
      cs = CoordSetCopy(I->CSTmpl);
    } else if (I->NCSet > 0) {
      cs = CoordSetCopy(I->CSet[0]);
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Errors)
        " ObjMolLoadTRJFile: Missing topology" ENDFB(G);
      return (I);
    }

    auto xref = LoadTrajSeleHelper(I, cs, sele);

    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      " ObjMolLoadTRJFile: Loading from \"%s\".\n", fname ENDFB(G);
    buffer = pymol::malloc<char>(BUFSIZE + 1);     /* 1 MB read buffer */
    p = buffer;
    buffer[0] = 0;
    n_read = 0;
    to_go = 0;
    a = 0;
    b = 0;
    c = 0;
    f1 = 0.0;
    f2 = 0.0;
    while(1) {
      to_go = n_read - (p - buffer);
      if(to_go < GETTING_LOW)
        if(!feof(f)) {
          if(to_go)
            memcpy(buffer, p, to_go);
          n_read = fread(buffer + to_go, 1, BUFSIZE - to_go, f);
          n_read = to_go + n_read;
          buffer[n_read] = 0;
          p = buffer;
          if(skip_first_line) {
            p = nextline(p);
            skip_first_line = false;
          }
          to_go = n_read - (p - buffer);
        }
      if(!to_go)
        break;
      p = ncopy(cc, p, 8);
      if((++b) == 10) {
        b = 0;
        p = nextline(p);
      }
      f0 = f1;
      f1 = f2;
      if(sscanf(cc, "%f", &f2) == 1) {
        if((++c) == 3) {
          c = 0;
          if((cnt + 1) >= start) {
            if(icnt <= 1) {
              if(xref) {
                if(xref[a] >= 0)
                  fp = cs->coordPtr(xref[a]);
                else
                  fp = nullptr;
              } else {
                fp = cs->coordPtr(a);
              }
              if(fp) {
                if(n_avg) {
                  *(fp++) += f0;
                  *(fp++) += f1;
                  *(fp++) += f2;
                } else {
                  *(fp++) = f0;
                  *(fp++) = f1;
                  *(fp++) = f2;
                }
              }
            }
          }
          if((++a) == I->NAtom) {

            cnt++;
            a = 0;
            if(b)
              p = nextline(p);
            b = 0;

            if(cs->PeriodicBoxType != CoordSet::NoPeriodicity) {
              /* read periodic box */

              c = 0;
              periodic = true;
              angles = true;

              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &box[0]) != 1)
                periodic = false;
              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &box[1]) != 1)
                periodic = false;
              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &box[2]) != 1)
                periodic = false;

              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &angle[0]) != 1)
                angles = false;

              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &angle[1]) != 1)
                angles = false;

              p = ncopy(cc, p, 8);
              if(sscanf(cc, "%f", &angle[2]) != 1)
                angles = false;
              if(periodic) {
                cs->Symmetry = std::make_unique<CSymmetry>(G);
                cs->Symmetry->Crystal.setDims(box);
                if(angles) {
                  cs->Symmetry->Crystal.setAngles(angle);
                }
                p = nextline(p);
                b = 0;
              }

              if(cs->PeriodicBoxType == CoordSet::Octahedral)
                periodic = false;       /* can't handle this yet... */
            }

            if((stop > 0) && (cnt >= stop))
              break;
            if(cnt >= start) {
              icnt--;
              if(icnt > 0) {
                PRINTFB(G, FB_ObjectMolecule, FB_Details)
                  " ObjectMolecule: skipping set %d...\n", cnt ENDFB(G);
              } else {
                icnt = interval;
                n_avg++;
              }

              if(icnt == interval) {
                if(n_avg < average) {
                  PRINTFB(G, FB_ObjectMolecule, FB_Details)
                    " ObjectMolecule: averaging set %d...\n", cnt ENDFB(G);
                } else {

                  /* compute average */
                  if(n_avg > 1) {
                    fp = cs->Coord.data();
                    for(i = 0; i < cs->NIndex; i++) {
                      *(fp++) /= n_avg;
                      *(fp++) /= n_avg;
                      *(fp++) /= n_avg;
                    }
                  }
                  if(periodic && image) {       
                    /* Perform residue-based period image transformation */
                    i = 0;
                    r_cnt = 0;
                    r_act = 0;  /* 0 unspec, 1=load, 2=image, 3=leave */
                    r_val = -1;
                    while(r_act != 3) {
                      if(i >= cs->NIndex) {
                        if(r_cnt)
                          r_act = 2;
                        else
                          r_act = 3;
                      }
                      if(r_act == 0) {
                        /* start new residue */
                        r_cnt = 0;
                        r_act = 1;      /* now load */
                      }
                      if(r_act == 1) {
                        if(i < cs->NIndex) {

                          /* is there a coordinate for atom? */
                          if(xref) {
                            if(xref[i] >= 0)
                              fp = cs->coordPtr(xref[i]);
                            else
                              fp = nullptr;
                          } else {
                            fp = cs->coordPtr(i);
                          }
                          if(fp) {      /* yes there is... */
                            if(r_cnt) {
                              if(r_val != I->AtomInfo[cs->IdxToAtm[i]].resv) {
                                r_act = 2;      /* end of residue-> time to image */
                              } else {
                                r_cnt++;
                                r_cent[0] += *(fp++);
                                r_cent[1] += *(fp++);
                                r_cent[2] += *(fp++);
                                r_fp_stop = fp; /* stop here */
                                i++;
                              }
                            } else {
                              r_val = I->AtomInfo[cs->IdxToAtm[i]].resv;
                              r_cnt++;
                              r_fp_start = fp;  /* start here */
                              r_cent[0] = *(fp++);
                              r_cent[1] = *(fp++);
                              r_cent[2] = *(fp++);
                              r_fp_stop = fp;   /* stop here */
                              i++;
                            }
                          } else {
                            i++;
                          }
                        } else {
                          r_act = 2;    /* image */
                        }
                      }

                      if(r_act == 2) {  /* time to image */
                        if(r_cnt) {
                          r_cent[0] /= r_cnt;
                          r_cent[1] /= r_cnt;
                          r_cent[2] /= r_cnt;
                          const auto& periodicbox = cs->getSymmetry()->Crystal;
                          transform33f3f(periodicbox.realToFrac(), r_cent, r_cent);
                          r_trans[0] = fmodf(1000.0F + shift[0] + r_cent[0], 1.0F);
                          r_trans[1] = fmodf(1000.0F + shift[1] + r_cent[1], 1.0F);
                          r_trans[2] = fmodf(1000.0F + shift[2] + r_cent[2], 1.0F);
                          r_trans[0] -= r_cent[0];
                          r_trans[1] -= r_cent[1];
                          r_trans[2] -= r_cent[2];
                          transform33f3f(periodicbox.fracToReal(), r_trans, r_trans);
                          fp = r_fp_start;
                          while(fp < r_fp_stop) {
                            *(fp++) += r_trans[0];
                            *(fp++) += r_trans[1];
                            *(fp++) += r_trans[2];
                          }
                        }
                        r_act = 0;      /* reset */
                        r_cnt = 0;
                      }
                    }
                  }

                  /* add new coord set */
                  cs->invalidateRep(cRepAll, cRepInvRep);
                  if(frame < 0)
                    frame = I->NCSet;
                  if(!I->NCSet) {
                    zoom_flag = true;
                  }

                  VLACheck(I->CSet, CoordSet *, frame);
                  if(I->NCSet <= frame)
                    I->NCSet = frame + 1;
                  delete I->CSet[frame];
                  I->CSet[frame] = cs;
                  ncnt++;

                  if(average < 2) {
                    PRINTFB(G, FB_ObjectMolecule, FB_Details)
                      " ObjectMolecule: read set %d into state %d...\n", cnt, frame + 1
                      ENDFB(G);
                  } else {
                    PRINTFB(G, FB_ObjectMolecule, FB_Details)
                      " ObjectMolecule: averaging set %d...\n", cnt ENDFB(G);
                    PRINTFB(G, FB_ObjectMolecule, FB_Details)
                      " ObjectMolecule: average loaded into state %d...\n", frame + 1
                      ENDFB(G);
                  }
                  frame++;
                  cs = CoordSetCopy(cs);
                  n_avg = 0;
                  if((stop > 0) && (cnt >= stop))
                    break;
                  if((max > 0) && (ncnt >= max))
                    break;
                }
              }
            } else {
              PRINTFB(G, FB_ObjectMolecule, FB_Details)
                " ObjectMolecule: skipping set %d...\n", cnt ENDFB(G);
            }
          }
        }
      } else {
        PRINTFB(G, FB_ObjectMolecule, FB_Errors)
          " ObjMolLoadTRJFile-Error: Failed to read expected coordinate value.\n  This traj. does not match the loaded parameter/topology file.\n  Likely cause: either the atom count or the periodic box settings\n  are inconsistent between the two files.\n"
          ENDFB(G);
        break;
      }
    }
    mfree(buffer);
  }
  delete cs;
  SceneChanged(G);
  SceneCountFrames(G);
  if(zoom_flag)
    if(SettingGetGlobal_i(G, cSetting_auto_zoom)) {
      ExecutiveWindowZoom(G, I->Name, 0.0, -1, 0, 0, quiet);        /* auto zoom (all states) */
    }

  return (I);
}

ObjectMolecule *ObjectMoleculeLoadRSTFile(PyMOLGlobals * G, ObjectMolecule * I,
                                          const char *fname, int frame, int quiet, char mode)
{
  /*
   * mode = 0: AMBER coordinate/restart file (one frame only)
   * mode = 1: AMBER trajectory
   * mode = 2: AMBER trajectory with box
   *           http://ambermd.org/formats.html
   */
  int ok = true;
  char *buffer, *p;
  char cc[MAXLINELEN];
  float f0, f1, f2, *fp;
  int a, b, c;
  int zoom_flag = false;
  CoordSet *cs = nullptr;
  char ncolumn = 6; // number of coordinates per line
  char nbyte = 12;  // width of one coordinate

  if(mode > 0) {
    ncolumn = 10;
    nbyte = 8;
  }

#define BUFSIZE 4194304
#define GETTING_LOW 10000

  else {
    if(I->CSTmpl) {
      cs = CoordSetCopy(I->CSTmpl);
    } else if (I->NCSet > 0) {
      cs = CoordSetCopy(I->CSet[0]);
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Errors)
        " ObjMolLoadRSTFile: Missing topology" ENDFB(G);
      return (I);
    }
    CHECKOK(ok, cs);
    if (ok){
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
	" ObjMolLoadRSTFile: Loading from \"%s\".\n", fname ENDFB(G);
      p = buffer = FileGetContents(fname, nullptr);
      if(!buffer)
        ok = ErrMessage(G, __func__, "Unable to open file!");
    }
    if (ok){
      p = nextline(p);
      if (mode == 0) // skip NATOM,TIME
        p = nextline(p);
    }
    a = 0;
    b = 0;
    c = 0;
    f1 = 0.0;
    f2 = 0.0;
    while(ok && *p) {
      p = ncopy(cc, p, nbyte);
      if((++b) == ncolumn) {
        b = 0;
        p = nextline(p);
      }
      f0 = f1;
      f1 = f2;
      if(sscanf(cc, "%f", &f2) == 1) {
        if((++c) == 3) {
          c = 0;
          fp = cs->coordPtr(a);
          *(fp++) = f0;
          *(fp++) = f1;
          *(fp++) = f2;

          if((++a) == I->NAtom) {
            a = 0;
            if(b)
              p = nextline(p);
            if(mode == 2) // skip box
              p = nextline(p);
            b = 0;
            /* add new coord set */
            cs->invalidateRep(cRepAll, cRepInvRep);
            if(frame < 0)
              frame = I->NCSet;
            if(!I->NCSet) {
              zoom_flag = true;
            }

            VLACheck(I->CSet, CoordSet *, frame);
	    CHECKOK(ok, I->CSet);
	    if (ok){
	      if(I->NCSet <= frame)
		I->NCSet = frame + 1;
	      delete I->CSet[frame];
	      I->CSet[frame] = cs;
	    }
            PRINTFB(G, FB_ObjectMolecule, FB_Details)
              " ObjectMolecule: read coordinates into state %d...\n", frame + 1 ENDFB(G);

            if (ok)
              cs = CoordSetCopy(cs);
            CHECKOK(ok, cs);

            if (mode == 0) // restart file has only one frame
              break;

            frame += 1;
          }
        }
      } else {
        PRINTFB(G, FB_ObjectMolecule, FB_Errors)
          " ObjMolLoadRSTFile: atom/coordinate mismatch.\n" ENDFB(G);
        break;
      }
    }
    mfree(buffer);
  }
  delete cs;

  SceneChanged(G);
  SceneCountFrames(G);
  if(zoom_flag){
    if(SettingGetGlobal_i(G, cSetting_auto_zoom)) {
      ExecutiveWindowZoom(G, I->Name, 0.0, -1, 0, 0, quiet);        /* auto zoom (all states) */
    }
  }
  return (I);
}

static const char *findflag(PyMOLGlobals * G, const char *p, const char *flag, const char *format)
{

  char cc[MAXLINELEN];
  char pat[MAXLINELEN] = "%FLAG ";
  int l;

  PRINTFD(G, FB_ObjectMolecule)
    " findflag: flag %s format %s\n", flag, format ENDFD;

  strcat(pat, flag);
  l = strlen(pat);
  while(*p) {
    p = ncopy(cc, p, l);
    if(WordMatch(G, cc, pat, true) < 0) {
      p = nextline(p);
      break;
    }
    p = nextline(p);
    if(!*p) {
      PRINTFB(G, FB_ObjectMolecule, FB_Errors)
        " ObjectMolecule-Error: Unrecognized file format (can't find \"%s\").\n", pat
        ENDFB(G);
    }
  }

  strcpy(pat, "%FORMAT(");
  strcat(pat, format);
  strcat(pat, ")");
  l = strlen(pat);
  while(*p) {
    p = ncopy(cc, p, l);
    if(WordMatch(G, cc, pat, true) < 0) {
      p = nextline(p);
      break;
    }
    p = nextline(p);
    if(!*p) {
      PRINTFB(G, FB_ObjectMolecule, FB_Errors)
        " ObjectMolecule-Error: Unrecognized file format (can't find \"%s\").\n", pat
        ENDFB(G);
    }

  }
  return (p);
}


/*========================================================================*/
static CoordSet *ObjectMoleculeTOPStr2CoordSet(PyMOLGlobals * G, const char *buffer,
                                        AtomInfoType ** atInfoPtr)
{
  const char *p;
  int nAtom;
  int a, b, c, bi, last_i, at_i, aa, rc;
  float *coord = nullptr;
  float *f;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr, *ai;
  BondType *bond = nullptr, *bd;
  int nBond = 0;
  int auto_show = RepGetAutoShowMask(G);
  int amber7 = false;

  WordType title;
  ResName *resn;

  char cc[MAXLINELEN];
  int ok = true;
  int i0, i1, i2;

  /* trajectory parameters */

  int NTYPES, NBONH, MBONA, NTHETH, MTHETA;
  int NPHIH, MPHIA, NHPARM, NPARM, NNB, NRES;
  int NBONA, NTHETA, NPHIA, NUMBND, NUMANG, NPTRA;
  int NATYP, NPHB, IFPERT, NBPER, NGPER, NDPER;
  int MBPER, MGPER, MDPER, IFBOX = 0, NMXRS, IFCAP;
  int NEXTRA, IPOL = 0;
  int wid, col;
  float BETA;
  float BOX1, BOX2, BOX3;

  cset = CoordSetNew(G);

  p = buffer;
  nAtom = 0;
  if(atInfoPtr)
    atInfo = *atInfoPtr;
  if(!atInfo)
    ErrFatal(G, "TOPStr2CoordSet", "need atom information record!");
  /* failsafe for old version.. */

  ncopy(cc, p, 8);
  if(strcmp(cc, "%VERSION") == 0) {
    amber7 = true;
    PRINTFB(G, FB_ObjectMolecule, FB_Details)
      " ObjectMolecule: Attempting to read Amber7 topology file.\n" ENDFB(G);
  } else {
    PRINTFB(G, FB_ObjectMolecule, FB_Details)
      " ObjectMolecule: Assuming this is an Amber6 topology file.\n" ENDFB(G);
  }

  /* read title */
  if(amber7) {
    p = findflag(G, buffer, "TITLE", "20a4");
  }

  p = ncopy(cc, p, 20);
  title[0] = 0;
  sscanf(cc, "%s", title);
  p = nextline(p);

  if(amber7) {

    p = findflag(G, buffer, "POINTERS", "10I8");

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &nAtom) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NTYPES) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NBONH) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MBONA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NTHETH) == 1);

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MTHETA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NPHIH) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MPHIA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NHPARM) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NPARM) == 1);

    p = nextline(p);

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NNB) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NRES) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NBONA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NTHETA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NPHIA) == 1);

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NUMBND) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NUMANG) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NPTRA) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NATYP) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NPHB) == 1);

    p = nextline(p);

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &IFPERT) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NBPER) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NGPER) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NDPER) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MBPER) == 1);

    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MGPER) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &MDPER) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &IFBOX) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NMXRS) == 1);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &IFCAP) == 1);

    p = nextline(p);
    p = ncopy(cc, p, 8);
    ok = ok && (sscanf(cc, "%d", &NEXTRA) == 1);

  } else {

    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &nAtom) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NTYPES) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NBONH) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MBONA) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NTHETH) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MTHETA) == 1);

    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NPHIH) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MPHIA) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NHPARM) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NPARM) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NNB) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NRES) == 1);

    p = nextline(p);

    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NBONA) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NTHETA) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NPHIA) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NUMBND) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NUMANG) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NPTRA) == 1);

    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NATYP) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NPHB) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &IFPERT) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NBPER) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NGPER) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NDPER) == 1);

    p = nextline(p);

    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MBPER) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MGPER) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &MDPER) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &IFBOX) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &NMXRS) == 1);
    p = ncopy(cc, p, 6);
    ok = ok && (sscanf(cc, "%d", &IFCAP) == 1);

    p = ncopy(cc, p, 6);
    if(sscanf(cc, "%d", &NEXTRA) != 1)
      NEXTRA = 0;

  }

  if(!ok) {
    ErrMessage(G, "TOPStrToCoordSet", "Error reading counts lines");
    ok_raise(1);
  } else {
    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      " TOPStr2CoordSet: read counts line nAtom %d NBONA %d NBONH %d\n",
      nAtom, NBONA, NBONH ENDFB(G);
  }

  switch (IFBOX) {
  case 2:
    cset->PeriodicBoxType = CoordSet::Octahedral;
    PRINTFB(G, FB_ObjectMolecule, FB_Details)
      " TOPStrToCoordSet: Warning: can't currently image a truncated octahedron...\n"
      ENDFB(G);
    break;
  case 1:
    cset->PeriodicBoxType = CoordSet::Orthogonal;
    break;
  case 0:
  default:
    cset->PeriodicBoxType = CoordSet::NoPeriodicity;
    break;
  }

  p = nextline(p);

  if(ok) {
    VLACheck(atInfo, AtomInfoType, nAtom);

    if(amber7) {
      p = findflag(G, buffer, "ATOM_NAME", "20a4");
    }
    /* read atoms */

    b = 0;
    for(a = 0; a < nAtom; a++) {
      p = ntrim(cc, p, 4);
      atInfo[a].name = LexIdx(G, cc);
      if((++b) == 20) {
        b = 0;
        p = nextline(p);
      }
    }

    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error reading atom names");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read atom names.\n" ENDFB(G);
    }

    /* read charges */

    if(amber7) {
      p = findflag(G, buffer, "CHARGE", "5E16.8");
    }

    b = 0;
    for(a = 0; a < nAtom; a++) {
      p = ncopy(cc, p, 16);
      ai = atInfo + a;
      if(!sscanf(cc, "%f", &ai->partialCharge))
        ok = false;
      else {
        ai->partialCharge /= 18.2223F;  /* convert to electron charge */
      }
      if((++b) == 5) {
        b = 0;
        p = nextline(p);
      }
    }

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error reading charges");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read charges.\n" ENDFB(G);
    }
    if(b)
      p = nextline(p);

    if(!amber7) {
      /* skip masses */

      p = skip_fortran(nAtom, 5, p);
    }

    /* read LJ atom types */

    if(amber7) {
      p = findflag(G, buffer, "ATOM_TYPE_INDEX", "10I8");
      col = 10;
      wid = 8;
    } else {
      col = 12;
      wid = 6;
    }

    b = 0;
    for(a = 0; a < nAtom; a++) {
      p = ncopy(cc, p, wid);
      ai = atInfo + a;
      if(!sscanf(cc, "%d", &ai->customType))
        ok = false;
      if((++b) == col) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error LJ atom types");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read LJ atom types.\n" ENDFB(G);
    }

    if(!amber7) {
      /* skip excluded atom counts */

      p = skip_fortran(nAtom, 12, p);

      /* skip NB param arrays */

      p = skip_fortran(NTYPES * NTYPES, 12, p);
    }

    /* read residue labels */

    if(amber7) {
      p = findflag(G, buffer, "RESIDUE_LABEL", "20a4");
    }

    resn = pymol::malloc<ResName>(NRES);

    b = 0;
    for(a = 0; a < NRES; a++) {
      p = ncopy(cc, p, 4);
      if(!sscanf(cc, "%s", resn[a]))
        resn[a][0] = 0;
      if((++b) == 20) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error reading residue labels");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read residue labels.\n" ENDFB(G);
    }

    /* read residue assignments */

    if(amber7) {
      p = findflag(G, buffer, "RESIDUE_POINTER", "10I8");
      col = 10;
      wid = 8;
    } else {
      col = 12;
      wid = 6;
    }

    b = 0;
    last_i = 0;
    rc = 0;
    for(a = 0; a < NRES; a++) {
      p = ncopy(cc, p, wid);
      if(sscanf(cc, "%d", &at_i)) {
        if(last_i)
          for(aa = (last_i - 1); aa < (at_i - 1); aa++) {
            ai = atInfo + aa;
            ai->resn = LexIdx(G, resn[a - 1]);
            ai->resv = rc;
          }
        rc++;
        last_i = at_i;
      }
      if((++b) == col) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);
    if(last_i)
      for(aa = (last_i - 1); aa < nAtom; aa++) {
        ai = atInfo + aa;
        ai->resn = LexIdx(G, resn[NRES - 1]);
        ai->resv = rc;
      }
    rc++;

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error reading residues");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read residues.\n" ENDFB(G);
    }

    FreeP(resn);

    if(!amber7) {
      /* skip bond force constants */

      p = skip_fortran(NUMBND, 5, p);

      /* skip bond lengths */

      p = skip_fortran(NUMBND, 5, p);

      /* skip angle force constant */

      p = skip_fortran(NUMANG, 5, p);

      /* skip angle eq */

      p = skip_fortran(NUMANG, 5, p);

      /* skip dihedral force constant */

      p = skip_fortran(NPTRA, 5, p);

      /* skip dihedral periodicity */

      p = skip_fortran(NPTRA, 5, p);

      /* skip dihedral phases */

      p = skip_fortran(NPTRA, 5, p);

      /* skip SOLTYs */

      p = skip_fortran(NATYP, 5, p);

      /* skip LJ terms r12 */

      p = skip_fortran((NTYPES * (NTYPES + 1)) / 2, 5, p);

      /* skip LJ terms r6 */

      p = skip_fortran((NTYPES * (NTYPES + 1)) / 2, 5, p);

    }

    /* read bonds */

    if(amber7) {
      p = findflag(G, buffer, "BONDS_INC_HYDROGEN", "10I8");
      col = 10;
      wid = 8;
    } else {
      col = 12;
      wid = 6;
    }

    nBond = NBONH + NBONA;

    bond = VLACalloc(BondType, nBond);

    bi = 0;

    b = 0;
    c = 0;
    i0 = 0;
    i1 = 0;
    for(a = 0; a < 3 * NBONH; a++) {
      p = ncopy(cc, p, wid);
      i2 = i1;
      i1 = i0;
      if(!sscanf(cc, "%d", &i0))
        ok = false;
      if((++c) == 3) {
        c = 0;
        bd = bond + bi;
        bd->index[0] = (abs(i2) / 3);
        bd->index[1] = (abs(i1) / 3);
        bd->order = 1;
        bi++;
      }
      if((++b) == col) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error hydrogen containing bonds");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read %d hydrogen containing bonds.\n", NBONH ENDFB(G);
    }

    if(amber7) {
      p = findflag(G, buffer, "BONDS_WITHOUT_HYDROGEN", "10I8");
      col = 10;
      wid = 8;
    } else {
      col = 12;
      wid = 6;
    }

    b = 0;
    c = 0;
    for(a = 0; a < 3 * NBONA; a++) {
      p = ncopy(cc, p, wid);
      i2 = i1;
      i1 = i0;
      if(!sscanf(cc, "%d", &i0))
        ok = false;
      if((++c) == 3) {
        c = 0;
        bd = bond + bi;
        bd->index[0] = (abs(i2) / 3);
        bd->index[1] = (abs(i1) / 3);
        bd->order = 1; // PYMOL-2707
        bi++;
      }
      if((++b) == col) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error hydrogen free bonds");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read %d hydrogen free bonds.\n", NBONA ENDFB(G);
    }

    if(!amber7) {

      /* skip hydrogen angles */

      p = skip_fortran(4 * NTHETH, 12, p);

      /* skip non-hydrogen angles */

      p = skip_fortran(4 * NTHETA, 12, p);

      /* skip hydrogen dihedrals */

      p = skip_fortran(5 * NPHIH, 12, p);

      /* skip non hydrogen dihedrals */

      p = skip_fortran(5 * NPHIA, 12, p);

      /* skip nonbonded exclusions */

      p = skip_fortran(NNB, 12, p);

      /* skip hydrogen bonds ASOL */

      p = skip_fortran(NPHB, 5, p);

      /* skip hydrogen bonds BSOL */

      p = skip_fortran(NPHB, 5, p);

      /* skip HBCUT */

      p = skip_fortran(NPHB, 5, p);

    }
    /* read AMBER atom types */

    if(amber7) {
      p = findflag(G, buffer, "AMBER_ATOM_TYPE", "20a4");
    }

    b = 0;
    for(a = 0; a < nAtom; a++) {
      OrthoLineType temp;
      p = ncopy(cc, p, 4);
      ai = atInfo + a;
      if(sscanf(cc, "%s", temp) != 1)
        ok = false;
      else {
        ai->textType = LexIdx(G, temp);
      }
      if((++b) == 20) {
        b = 0;
        p = nextline(p);
      }
    }
    if(b)
      p = nextline(p);

    if(!ok) {
      ErrMessage(G, "TOPStrToCoordSet", "Error reading atom types");
    } else {
      PRINTFB(G, FB_ObjectMolecule, FB_Blather)
        " TOPStr2CoordSet: read atom types.\n" ENDFB(G);
    }

    if(!amber7) {
      /* skip TREE classification */

      p = skip_fortran(nAtom, 20, p);

      /* skip tree joining information */

      p = skip_fortran(nAtom, 12, p);

      /* skip last atom rotated blah blah blah */

      p = skip_fortran(nAtom, 12, p);

    }

    if(IFBOX > 0) {

      int IPTRES, NSPM = 0, NSPSOL;

      if(amber7) {
        p = findflag(G, buffer, "SOLVENT_POINTERS", "3I8");
        wid = 8;
      } else {
        wid = 6;
      }
      p = ncopy(cc, p, wid);
      ok = ok && (sscanf(cc, "%d", &IPTRES) == 1);
      p = ncopy(cc, p, wid);
      ok = ok && (sscanf(cc, "%d", &NSPM) == 1);
      p = ncopy(cc, p, wid);
      ok = ok && (sscanf(cc, "%d", &NSPSOL) == 1);

      ok_assert(1, ok);

      p = nextline(p);

      if(amber7) {
        p = findflag(G, buffer, "ATOMS_PER_MOLECULE", "10I8");
        col = 10;
      } else {
        col = 12;
      }

      /* skip num atoms per box */

      p = skip_fortran(NSPM, col, p);

      if(amber7) {
        p = findflag(G, buffer, "BOX_DIMENSIONS", "5E16.8");
      }
      wid = 16;

      p = ncopy(cc, p, 16);
      ok = ok && (sscanf(cc, "%f", &BETA) == 1);
      p = ncopy(cc, p, 16);
      ok = ok && (sscanf(cc, "%f", &BOX1) == 1);
      p = ncopy(cc, p, 16);
      ok = ok && (sscanf(cc, "%f", &BOX2) == 1);
      p = ncopy(cc, p, 16);
      ok = ok && (sscanf(cc, "%f", &BOX3) == 1);

      if(ok) {
        float angle[3] = {90.f, BETA, 90.f};

        if((BETA > 109.47) && (BETA < 109.48)) {
          cset->PeriodicBoxType = CoordSet::Octahedral;
          angle[0] = 109.47122;
          angle[1] = 109.47122;
          angle[2] = 109.47122;
        } else if(BETA == 60.0) {
          angle[0] = 60.0;   /* rhombic dodecahedron (from ptraj.c) */
          angle[1] = 90.0;
          angle[2] = 60.0;
        }

        cset->Symmetry = std::make_unique<CSymmetry>(G);
        cset->Symmetry->Crystal.setDims(BOX1, BOX2, BOX3);
        cset->Symmetry->Crystal.setAngles(angle);
      }
      /* skip periodic box */

      p = nextline(p);

    }

    if(!amber7) {

      if(IFCAP > 0) {
        p = nextline(p);
        p = nextline(p);
        p = nextline(p);
      }

      if(IFPERT > 0) {

        /* skip perturbed bond atoms */

        p = skip_fortran(2 * NBPER, 12, p);

        /* skip perturbed bond atom pointers */

        p = skip_fortran(2 * NBPER, 12, p);

        /* skip perturbed angles */

        p = skip_fortran(3 * NGPER, 12, p);

        /* skip perturbed angle pointers */

        p = skip_fortran(2 * NGPER, 12, p);

        /* skip perturbed dihedrals */

        p = skip_fortran(4 * NDPER, 12, p);

        /* skip perturbed dihedral pointers */

        p = skip_fortran(2 * NDPER, 12, p);

        /* skip residue names */

        p = skip_fortran(NRES, 20, p);

        /* skip atom names */

        p = skip_fortran(nAtom, 20, p);

        /* skip atom symbols */

        p = skip_fortran(nAtom, 20, p);

        /* skip unused field */

        p = skip_fortran(nAtom, 5, p);

        /* skip perturbed flags */

        p = skip_fortran(nAtom, 12, p);

        /* skip LJ atom flags */

        p = skip_fortran(nAtom, 12, p);

        /* skip perturbed charges */

        p = skip_fortran(nAtom, 5, p);

      }

      if(IPOL > 0) {

        /* skip atomic polarizabilities */

        p = skip_fortran(nAtom, 5, p);

      }

      if((IPOL > 0) && (IFPERT > 0)) {

        /* skip atomic polarizabilities */

        p = skip_fortran(nAtom, 5, p);

      }
    }
    /* for future reference 

       %FLAG LES_NTYP
       %FORMAT(10I8)
       %FLAG LES_TYPE
       %FORMAT(10I8)
       %FLAG LES_FAC
       %FORMAT(5E16.8)
       %FLAG LES_CNUM
       %FORMAT(10I8)
       %FLAG LES_ID
       %FORMAT(10I8)

       Here is the additional information for LES topology formats:
       First, if NPARM ==1, LES entries are in topology (NPARM is the 10th
       entry in the initial list of control parameters); otherwise the standard
       format applies.
       So, with NPARM=1, you just need to read a few more things at the very
       end of topology file:
       LES_NTYP (format: I6) ... one number, number of LES types
       and four arrays:
       LES_TYPE (12I6) ... NATOM integer entries
       LES_FAC (E16.8) ... LES_NTYPxLES_NTYP float entries
       LES_CNUM (12I6) ... NATOM integer entries
       LES_ID (12I6)   ... NATOM integer entries

       and that's it. Your parser must have skipped this information because it
       was at the end of the file. Maybe that's good enough.

     */

    coord = VLAlloc(float, 3 * nAtom);

    f = coord;
    for(a = 0; a < nAtom; a++) {
      *(f++) = 0.0;
      *(f++) = 0.0;
      *(f++) = 0.0;
      ai = atInfo + a;
      ai->id = a + 1;           /* assign 1-based identifiers */
      ai->rank = a;
      AtomInfoAssignParameters(G, ai);
      AtomInfoAssignColors(G, ai);
      ai->visRep = auto_show;
    }
  }
  if(ok) {
    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->TmpBond = pymol::vla_take_ownership(bond);
    cset->NTmpBond = nBond;
  } else {
ok_except1:
    delete cset;
    cset = nullptr;
    ErrMessage(G, __func__, "failed");
  }
  if(atInfoPtr)
    *atInfoPtr = atInfo;

  return (cset);
}


/*========================================================================*/
static ObjectMolecule *ObjectMoleculeReadTOPStr(PyMOLGlobals * G, ObjectMolecule * I,
                                         char *TOPStr, int frame, int discrete)
{
  CoordSet *cset = nullptr;
  pymol::vla<AtomInfoType> atInfo(1);
  int ok = true;
  int isNew = true;
  unsigned int nAtom = 0;

  if(!I)
    isNew = true;
  else
    isNew = false;

  if(ok) {
    if(isNew) {
      I = (ObjectMolecule *) new ObjectMolecule(G, discrete);
      CHECKOK(ok, I);
      if (ok)
        std::swap(atInfo, I->AtomInfo);
    }
    if(ok && isNew) {
      I->Color = AtomInfoUpdateAutoColor(G);
    }

    if (ok)
      cset = ObjectMoleculeTOPStr2CoordSet(G, TOPStr, &atInfo);
    CHECKOK(ok, cset);
  }

  /* include coordinate set */
  if(ok) {
    nAtom = cset->NIndex;

    if(I->DiscreteFlag && atInfo) {
      unsigned int a;
      int fp1 = frame + 1;
      AtomInfoType *ai = atInfo.data();
      for(a = 0; a < nAtom; a++) {
        (ai++)->discrete_state = fp1;
      }
    }

    cset->Obj = I;
    cset->enumIndices();
    cset->invalidateRep(cRepAll, cRepInvRep);
    if(isNew) {
      std::swap(I->AtomInfo, atInfo);
    } else if (ok){
      ok &= ObjectMoleculeMerge(I, std::move(atInfo), cset, false, cAIC_AllMask, true);
    }
    if(isNew)
      I->NAtom = nAtom;
    /* 
       if(frame<0) frame=I->NCSet;
       VLACheck(I->CSet,CoordSet*,frame);
       if(I->NCSet<=frame) I->NCSet=frame+1;
       if(I->CSet[frame]) I->CSet[frame]->fFree(I->CSet[frame]);
       I->CSet[frame] = cset;
     */

    if(ok && isNew)
      ok = ObjectMoleculeConnect(I, cset, false);
    if(cset->Symmetry && (!I->Symmetry)) {
      I->Symmetry.reset(new CSymmetry(*cset->Symmetry));
      CHECKOK(ok, I->Symmetry);
    }

    delete I->CSTmpl;
    I->CSTmpl = cset;           /* save template coordinate set */

    SceneCountFrames(G);
    if (ok)
      ok &= ObjectMoleculeExtendIndices(I, -1);
    if (ok)
      ok &= ObjectMoleculeSort(I);
    if (ok){
      ObjectMoleculeUpdateIDNumbers(I);
      ObjectMoleculeUpdateNonbonded(I);
    }
  }
  if (!ok){
    DeleteP(I)
  }
  return (I);
}

ObjectMolecule *ObjectMoleculeLoadTOPFile(PyMOLGlobals * G, ObjectMolecule * obj,
                                          const char *fname, int frame, int discrete)
{
  ObjectMolecule *I = nullptr;
  char *buffer;

  buffer = FileGetContents(fname, nullptr);

  if(!buffer)
    ErrMessage(G, __func__, "Unable to open file!");
  else {
    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      " %s: Loading from %s.\n", __func__, fname ENDFB(G);

    I = ObjectMoleculeReadTOPStr(G, obj, buffer, frame, discrete);
    mfree(buffer);
  }

  return (I);
}

void ObjectMoleculeSculptClear(ObjectMolecule * I)
{
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: entered.\n", __func__ ENDFD;
  if(I->Sculpt)
    DeleteP(I->Sculpt);
}

void ObjectMoleculeSculptImprint(ObjectMolecule * I, int state, int match_state,
                                 int match_by_segment)
{
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: entered.\n", __func__ ENDFD;

  if(!I->Sculpt)
    I->Sculpt = new CSculpt(I->G);
  SculptMeasureObject(I->Sculpt, I, state, match_state, match_by_segment);
}

float ObjectMoleculeSculptIterate(ObjectMolecule * I, int state, int n_cycle,
                                  float *center)
{
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: entered.\n", __func__ ENDFD;
  if(I->Sculpt) {
    return SculptIterateObject(I->Sculpt, I, state, n_cycle, center);
  } else
    return 0.0F;
}

/**
 * - Assigns new id to all atoms with AtomInfoType.id == -1
 * - Assigns ObjectMolecule.AtomCounter if -1
 *
 * Cost: O(NAtom + NBond)
 */
void ObjectMoleculeUpdateIDNumbers(ObjectMolecule * I)
{
  int a;
  int max;
  AtomInfoType *ai;

  if(I->AtomCounter < 0) {
    max = -1;
    ai = I->AtomInfo.data();
    for(a = 0; a < I->NAtom; a++) {
      if(ai->id > max)
        max = ai->id;
      ai++;
    }
    I->AtomCounter = max + 1;
  }
  ai = I->AtomInfo.data();
  for(a = 0; a < I->NAtom; a++) {
    if(ai->id < 0)
      ai->id = I->AtomCounter++;
    ai++;
  }
}

/*========================================================================*/
int ObjectMoleculeGetPhiPsi(ObjectMolecule * I, int ca, float *phi, float *psi, int state)
{
  int np = -1;
  int cm = -1;
  int c = -1;
  int n = -1;
  int result = false;
  const AtomInfoType *ai;
  float v_ca[3];
  float v_n[3];
  float v_c[3];
  float v_cm[3];
  float v_np[3];
  auto G = I->G;

  ai = I->AtomInfo;

  if(ai[ca].name == G->lex_const.CA) {
    /* find C */
    for (auto const& neighbor : AtomNeighbors(I, ca)) {
      if (ai[neighbor.atm].name == G->lex_const.C) {
        c = neighbor.atm;
        break;
      }
    }

    /* find N */
    for (auto const& neighbor : AtomNeighbors(I, ca)) {
      if (ai[neighbor.atm].name == G->lex_const.N) {
        n = neighbor.atm;
        break;
      }
    }

    /* find NP */
    if(c >= 0) {
      for (auto const& neighbor : AtomNeighbors(I, c)) {
        if (ai[neighbor.atm].name == G->lex_const.N) {
          np = neighbor.atm;
          break;
        }
      }
    }

    /* find CM */
    if(n >= 0) {
      for (auto const& neighbor : AtomNeighbors(I, n)) {
        if (ai[neighbor.atm].name == G->lex_const.C) {
          cm = neighbor.atm;
          break;
        }
      }
    }
    if((ca >= 0) && (np >= 0) && (c >= 0) && (n >= 0) && (cm >= 0)) {
      if(ObjectMoleculeGetAtomVertex(I, state, ca, v_ca) &&
         ObjectMoleculeGetAtomVertex(I, state, n, v_n) &&
         ObjectMoleculeGetAtomVertex(I, state, c, v_c) &&
         ObjectMoleculeGetAtomVertex(I, state, cm, v_cm) &&
         ObjectMoleculeGetAtomVertex(I, state, np, v_np)) {

        (*phi) = rad_to_deg(get_dihedral3f(v_c, v_ca, v_n, v_cm));
        (*psi) = rad_to_deg(get_dihedral3f(v_np, v_c, v_ca, v_n));
        result = true;
      }
    }
  }
  return (result);
}


/*========================================================================*/
int ObjectMoleculeCheckBondSep(ObjectMolecule * I, int a0, int a1, int dist)
{
  int result = false;
  int n0;
  int stack[MAX_BOND_DIST + 1];
  int history[MAX_BOND_DIST + 1];
  int depth = 0;
  int distinct;
  int a;
  if(dist > MAX_BOND_DIST)
    return false;

  auto* const Neighbor = I->getNeighborArray();

  depth = 1;
  history[depth] = a0;
  stack[depth] = Neighbor[a0] + 1;   /* go to first neighbor */
  while(depth) {                /* keep going until we've traversed tree */
    while(Neighbor[stack[depth]] >= 0) {     /* end of branches? go back up one bond */
      n0 = Neighbor[stack[depth]];   /* get current neighbor index */
      stack[depth] += 2;        /* set up next neighbor */
      distinct = true;          /* check to see if current candidate is distinct from ancestors */
      for(a = 1; a < depth; a++) {
        if(history[a] == n0)
          distinct = false;
      }
      if(distinct) {
        if(depth < dist) {      /* are not yet at the proper distance? */
          if(distinct) {
            depth++;
            stack[depth] = Neighbor[n0] + 1; /* then keep moving outward */
            history[depth] = n0;
          }
        } else if(n0 == a1)     /* otherwise, see if we have a match */
          result = true;
      }
    }
    depth--;
  }
  return result;
}


/*========================================================================*/
void ObjectGotoState(pymol::CObject* I, int state)
{
  auto nstates = I->getNFrame();
  if (nstates > 1 || !SettingGet<bool>(I->G, cSetting_static_singletons)) {
    if(state > nstates)
      state = nstates - 1;
    if(state < 0)
      state = nstates - 1;
    SceneSetFrame(I->G, 0, state);
  }
}


/*========================================================================*/
CObjectState* ObjectMolecule::_getObjectState(int state)
{
  return CSet[state];
}


/*========================================================================*/
pymol::copyable_ptr<CSetting>* ObjectMolecule::getSettingHandle(int state)
{
  auto I = this;

  if (state < -1) {
    state = I->getCurrentState();
  }

  if(state < 0) {
    return (&I->Setting);
  } else if(state < I->NCSet) {
    if(I->CSet[state]) {
      return (&I->CSet[state]->Setting);
    } else {
      return (nullptr);
    }
  } else {
    return (nullptr);
  }
}

/*========================================================================*/
CSymmetry const* ObjectMolecule::getSymmetry(int state) const
{
  auto cs = getCoordSet(state);
  if (cs && cs->Symmetry) {
    return cs->Symmetry.get();
  }

  return Symmetry.get();
}

/**
 * @return False if state does not exist
 */
bool ObjectMolecule::setSymmetry(CSymmetry const& symmetry, int state)
{
  bool const all_states = (state == cSelectorUpdateTableAllStates);
  bool success = false;

  if (all_states) {
    Symmetry.reset(new CSymmetry(symmetry));
    success = true;
  }

  for (StateIterator iter(G, Setting.get(), state, NCSet); iter.next();) {
    auto cs = CSet[iter.state];
    if (cs) {
      cs->Symmetry.reset(all_states ? nullptr : new CSymmetry(symmetry));
      cs->invalidateRep(cRepCell, cRepInvRep);
      success = true;
    }
  }

  return success;
}

/*========================================================================*/
pymol::Result<> ObjectMoleculeSetStateTitle(ObjectMolecule * I, int state, const char *text)
{
  auto cs = I->getCoordSet(state);
  if (!cs) {
    return pymol::make_error("Invalid state ", state + 1);
  }
  cs->setTitle(text);
  return {};
}


/*========================================================================*/
const char *ObjectMoleculeGetStateTitle(const ObjectMolecule * I, int state)
{
  auto cs = I->getCoordSet(state);
  if (!cs) {
    PRINTFB(I->G, FB_ObjectMolecule, FB_Errors)
      "Error: invalid state %d\n", state + 1 ENDFB(I->G);
    return nullptr;
  }
  return cs->Name;
}


/*========================================================================*/
void ObjectMoleculeRenderSele(ObjectMolecule* I, int curState, int sele,
    bool vis_only, CGO& selIndicatorsCGO)
{
  PyMOLGlobals *G = I->G;
  CoordSet *cs;
  int a, nIndex;
  const float *coord, *v;
  int flag = true;
  int all_vis = !vis_only;
  int visRep;
  float tmp_matrix[16], v_tmp[3], *matrix = nullptr;
  int use_matrices =
    SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);

  if(use_matrices<0) use_matrices = 0;

  if (SettingGetIfDefined_i(G, I->Setting.get(), cSetting_all_states, &a)) {
    curState = a ? -1 : SettingGet_i(G, I->Setting.get(), nullptr, cSetting_state);
  } else if (SettingGetIfDefined_i(G, I->Setting.get(), cSetting_state, &a)) {
    curState = a - 1;
  }

  if(G->HaveGUI && G->ValidContext) {
    const AtomInfoType *atInfo = I->AtomInfo.data();

    for(StateIterator iter(G, I->Setting.get(), curState, I->NCSet);
        iter.next();) {
      if((cs = I->CSet[iter.state])) {
	    const auto* idx2atm = cs->IdxToAtm.data();
	    nIndex = cs->NIndex;
	    coord = cs->Coord;
	    if(use_matrices && !cs->Matrix.empty()) {
	      copy44d44f(cs->Matrix.data(), tmp_matrix);
	      matrix = tmp_matrix;
	    } else
	      matrix = nullptr;
	    
	    if(I->TTTFlag) {
	      if(!matrix) {
		convertTTTfR44f(I->TTT, tmp_matrix);
	      } else {
		float ttt[16];
		convertTTTfR44f(I->TTT, ttt);
		left_multiply44f44f(ttt, tmp_matrix);
	      }
	      matrix = tmp_matrix;
	    }
	    
	    for(a = 0; a < nIndex; a++) {
	      if(SelectorIsMember(G, atInfo[*(idx2atm++)].selEntry, sele)) {
		if(all_vis)
		  flag = true;
		else {
		  visRep = atInfo[idx2atm[-1]].visRep;
		  flag = false;
		  if(visRep & (cRepCylBit | cRepSphereBit | cRepSurfaceBit |
			       cRepLabelBit | cRepNonbondedSphereBit | cRepCartoonBit |
			       cRepRibbonBit | cRepLineBit | cRepMeshBit |
			       cRepDotBit | cRepNonbondedBit)){
		    flag = true;
		  }
		}
		if(flag) {
		  v = coord + a + a + a;
		  if(matrix) {
		    transform44f3f(matrix, v, v_tmp);
	      CGOVertexv(&selIndicatorsCGO, v_tmp);
		  } else {
	      CGOVertexv(&selIndicatorsCGO, v);
		  }
		}
	      }
	    }
	  }
    }
  }
}

/*========================================================================*/
static CoordSet *ObjectMoleculeXYZStr2CoordSet(PyMOLGlobals * G, const char *buffer,
                                               AtomInfoType ** atInfoPtr, const char **restart)
{
  const char *p, *p_store;
  int nAtom;
  int a, c;
  float *coord = nullptr;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr, *ai;
  char cc[MAXLINELEN];
  int atomCount;
  BondType *bond = nullptr;
  int nBond = 0;
  int b1, b2;
  WordType tmp_name;
  int auto_show = RepGetAutoShowMask(G);
  int tinker_xyz = true;
  int valid_atom;
  int have_n_atom = false;
  BondType *ii;

  p = buffer;
  nAtom = 0;
  atInfo = *atInfoPtr;

  p_store = p;
  p = ncopy(cc, p, MAXLINELEN - 1);
  if(sscanf(cc, "%d", &nAtom) != 1) {
    nAtom = 0;
    tinker_xyz = false;
    p = p_store;
  } else {
    have_n_atom = true;
    p = nskip(p, 2);
    p = ncopy(tmp_name, p, sizeof(WordType) - 1);
    p = nextline(p);
  }

  if(tinker_xyz && nAtom) {     /* test Tinker XYZ formatting assumption */
    const char *pp = p;
    int dummy_int;
    float dummy_float;
    AtomName dummy_name;

    pp = ncopy(cc, pp, 6);
    if(!sscanf(cc, "%d", &dummy_int))
      tinker_xyz = false;       /* id */
    pp = nskip(pp, 2);
    pp = ncopy(cc, pp, 3);
    if(sscanf(cc, "%s", dummy_name) != 1)
      tinker_xyz = false;       /* name */
    pp = ncopy(cc, pp, 12);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      tinker_xyz = false;       /* x */
    pp = ncopy(cc, pp, 12);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      tinker_xyz = false;       /* y */
    pp = ncopy(cc, pp, 12);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      tinker_xyz = false;       /* z */
    pp = ncopy(cc, pp, 6);
    if(sscanf(cc, "%d", &dummy_int) != 1)
      tinker_xyz = false;       /* numeric type */
  }

  if(!tinker_xyz) {
    const char *pp = p;
    int have_atom_line = true;
    float dummy_float;
    AtomName dummy_name;

    pp = ParseWordCopy(cc, pp, sizeof(AtomName) - 1);
    if(sscanf(cc, "%s", dummy_name) != 1)
      have_atom_line = false;   /* name */
    pp = ParseWordCopy(cc, pp, MAXLINELEN - 1);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      have_atom_line = false;   /* x */
    pp = ParseWordCopy(cc, pp, MAXLINELEN - 1);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      have_atom_line = false;   /* y */
    pp = ParseWordCopy(cc, pp, MAXLINELEN - 1);
    if(sscanf(cc, "%f", &dummy_float) != 1)
      have_atom_line = false;   /* z */
    if(!have_atom_line) {       /* copy the comment line into the title field */
      p = ncopy(tmp_name, p, sizeof(WordType) - 1);
      p = nextline(p);
    }
  }

  if(nAtom) {
    coord = VLAlloc(float, 3 * nAtom);
    if(atInfo)
      VLACheck(atInfo, AtomInfoType, nAtom);
  } else {
    coord = VLAlloc(float, 3);
  }

  if(tinker_xyz) {
    nBond = 0;
    bond = VLACalloc(BondType, 6 * nAtom);      /* is this a safe assumption? */
    ii = bond;
  }

  PRINTFB(G, FB_ObjectMolecule, FB_Blather)
    " ObjectMoleculeReadXYZ: Found %i atoms...\n", nAtom ENDFB(G);

  a = 0;
  atomCount = 0;
  while(*p) {
    VLACheck(atInfo, AtomInfoType, atomCount);
    ai = atInfo + atomCount;

    if(!tinker_xyz) {
      valid_atom = true;

      p = ParseWordCopy(cc, p, MAXLINELEN - 1);
      UtilCleanStr(cc);
      if(!cc[0])
        valid_atom = false;
      if(valid_atom) {
        strncpy(ai->elem, cc, cElemNameLen);
        ai->name = LexIdx(G, cc);

        ai->rank = atomCount;
        ai->id = atomCount + 1;

        VLACheck(coord, float, a * 3 + 2);
        p = ParseWordCopy(cc, p, MAXLINELEN - 1);
        if(sscanf(cc, "%f", coord + a) != 1)
          valid_atom = false;
        p = ParseWordCopy(cc, p, MAXLINELEN - 1);
        if(sscanf(cc, "%f", coord + a + 1) != 1)
          valid_atom = false;
        p = ParseWordCopy(cc, p, MAXLINELEN - 1);
        if(sscanf(cc, "%f", coord + a + 2) != 1)
          valid_atom = false;

        ai->resn = LexIdx(G, "UNK");

        ai->alt[0] = 0;
        ai->chain = 0;
        ai->resv = atomCount + 1;

        ai->q = 1.0;
        ai->b = 0.0;

        ai->segi = 0;

        ai->visRep = auto_show;

        /* in the absense of external tinker information, assume hetatm */

        ai->hetatm = 1;

        AtomInfoAssignParameters(G, ai);
        AtomInfoAssignColors(G, ai);
      }
    } else {                    /* tinker XYZ */

      valid_atom = true;

      p = ncopy(cc, p, 6);
      if(!sscanf(cc, "%d", &ai->id))
        break;
      ai->rank = atomCount;

      p = nskip(p, 2);          /* to 12 */
      p = ntrim(cc, p, 3);
      ai->name = LexIdx(G, cc);

      ai->resn = LexIdx(G, "UNK");

      ai->alt[0] = 0;
      ai->chain = 0;

      ai->resv = atomCount + 1;

      valid_atom = true;

      p = ncopy(cc, p, 12);
      sscanf(cc, "%f", coord + a);
      p = ncopy(cc, p, 12);
      sscanf(cc, "%f", coord + (a + 1));
      p = ncopy(cc, p, 12);
      sscanf(cc, "%f", coord + (a + 2));

      ai->q = 1.0;
      ai->b = 0.0;

      ai->segi = 0;
      ai->elem[0] = 0;          /* let atom info guess/infer atom type */

      ai->visRep = auto_show;

      p = ncopy(cc, p, 6);
      sscanf(cc, "%d", &ai->customType);

      /* in the absense of external tinker information, assume hetatm */

      ai->hetatm = 1;

      AtomInfoAssignParameters(G, ai);
      AtomInfoAssignColors(G, ai);

      b1 = atomCount;
      for(c = 0; c < 6; c++) {
        p = ncopy(cc, p, 6);
        if(!cc[0])
          break;
        if(!sscanf(cc, "%d", &b2))
          break;
        if(b1 < (b2 - 1)) {
          VLACheck(bond, BondType, nBond);
          ii = bond + nBond;
          nBond++;
          ii->index[0] = b1;
          ii->index[1] = b2 - 1;
          ii->order = 1;        /* missing bond order information */
          ii++;
        }
      }
    }

    if(valid_atom) {
      PRINTFD(G, FB_ObjectMolecule)
        " ObjectMolecule-DEBUG: %s %s %d %s %8.3f %8.3f %8.3f %6.2f %6.2f %s\n",
         LexStr(G, ai->name), LexStr(G, ai->resn), ai->resv, LexStr(G, ai->chain),
         *(coord + a), *(coord + a + 1), *(coord + a + 2), ai->b, ai->q, LexStr(G, ai->segi) ENDFD;

      a += 3;
      atomCount++;

    }
    p = nextline(p);
    if(have_n_atom && (atomCount >= nAtom)) {
      int dummy;
      ncopy(cc, p, MAXLINELEN - 1);
      if(sscanf(cc, "%d", &dummy) == 1)
        *restart = p;
      break;
    }
  }

  PRINTFB(G, FB_ObjectMolecule, FB_Blather)
    " XYZStr2CoordSet: Read %d bonds.\n", nBond ENDFB(G);

  if(!tinker_xyz)
    nAtom = atomCount;          /* use number of atoms actually read */

  cset = CoordSetNew(G);
  cset->NIndex = nAtom;
  cset->Coord = pymol::vla_take_ownership(coord);
  cset->TmpBond = pymol::vla_take_ownership(bond);
  cset->NTmpBond = nBond;
  strcpy(cset->Name, tmp_name);
  if(atInfoPtr)
    *atInfoPtr = atInfo;
  return (cset);
}


/*========================================================================*/
int ObjectMoleculeAreAtomsBonded(ObjectMolecule * I, int i0, int i1)
{
  int result = false;
  int a;
  const BondType *b;
  b = I->Bond;
  for(a = 0; a < I->NBond; a++) {
    if(i0 == b->index[0]) {
      if(i1 == b->index[1]) {
        result = true;
        break;
      }
    }
    if(i1 == b->index[0]) {
      if(i0 == b->index[1]) {
        result = true;
        break;
      }
    }
    b++;
  }
  return (result);
}


/*========================================================================*/
int ObjectMoleculeRenameAtoms(ObjectMolecule * I, int *flag, int force)
{
  PyMOLGlobals * G = I->G;
  AtomInfoType *ai;
  int a;
  int result;
  if(force) {
    ai = I->AtomInfo.data();
    if(!flag) {
      for(a = 0; a < I->NAtom; a++) {
        LexAssign(G, ai->name, 0);
        ai++;
      }
    } else {
      for(a = 0; a < I->NAtom; a++) {
        if(flag[a])
          LexAssign(G, ai->name, 0);
        ai++;
      }
    }
  }
  result = AtomInfoUniquefyNames(I->G, nullptr, 0, I->AtomInfo.data(), flag, I->NAtom);
  return result;
}


/*========================================================================*/
/**
 * Transform coordinates of `cs` in place and then append it to `tcs`.
 *
 * @param coord_orig Original cs coordinates
 * @param at0 Anchor atom index
 * @param mat1_inv Source coordinate system
 * @param valence_vec Valence vector. If nullptr, don't move. If null-vector, don't merge.
 */
static bool AddCoordinateIntoCoordSet(CoordSet* tcs, CoordSet* cs,
    const float* coord_orig, int at0, const float* mat1_inv,
    const float* valence_vec)
{
  if (!tcs) {
    return true;
  }

  if (valence_vec) {
    if (lengthsq3f(valence_vec) < 0.1) {
      // null-vector
      return true;
    }

    // anchor
    auto idx0 = tcs->atmToIdx(at0);
    assert(idx0 >= 0);
    const float* va0 = tcs->coordPtr(idx0);

    // Target coordinate system
    float matT[16], mat[16];
    identity44f(matT);
    copy3f(valence_vec, matT);
    get_system1f3f(matT, matT + 4, matT + 8);
    copy3(va0, matT + 12);
    transpose44f44f(matT, mat);

    // combine with source coordinate system
    right_multiply44f44f(mat, mat1_inv);

    // transform coordset
    for (int idx = 0; idx < cs->NIndex; idx++) {
      transform44f3f(mat, &coord_orig[3 * idx], cs->coordPtr(idx));
    }
  }

  return CoordSetMerge(tcs->Obj, tcs, cs);
}

/**
 * @param at0 Atom index
 * @param index0 If different from at0: Index of atom to replace (e.g. Hydrogen)
 * @param[out] out (3x1) Normalized open valence vector
 * @return True if valid open valence vector found
 */
static bool GetTargetValenceVector(
    const CoordSet* tcs, int at0, int index0, float* out)
{
  if (!tcs) {
    return false;
  }

  if (at0 != index0) {
    // anchor
    auto ca0 = tcs->atmToIdx(at0);
    if (ca0 < 0) {
      return false;
    }

    // hydrogen (or other replaced atom)
    auto ch0 = tcs->atmToIdx(index0);
    if (ch0 < 0) {
      return false;
    }

    const float* vh0 = tcs->coordPtr(ch0);
    const float* va0 = tcs->coordPtr(ca0);
    subtract3f(vh0, va0, out);
    return true;
  }

  return CoordSetFindOpenValenceVector(tcs, at0, out);
}

/*========================================================================*/
/**
 * Merge src into I.
 *
 * Optionally creates a bond between index0 and index1, or the atoms bound to
 * index0 and index1 if they are "single geometry" atoms like hydrogens.
 *
 * @param index0 Atom index in I
 * @param index1 Atom index in src
 * @param create_bond If false, then don't create a bond, just combine the
 * objects. Implies move_flag=false.
 * @param move_flag If true, then transform the source coordinates to form a
 * reasonable bond geometry.
 */
pymol::Result<> ObjectMoleculeFuse(ObjectMolecule* I, int const index0,
    const ObjectMolecule* src, int const index1, bool create_bond,
    bool move_flag)
{
  constexpr int state1 = 0;
  constexpr bool edit = true;

  auto* G = I->G;

  if (!create_bond) {
    move_flag = false;
  }

  auto const* scs = src->getCoordSet(state1);
  if (!scs) {
    return pymol::Error("no source coordset");
  }

  auto* ai0 = I->AtomInfo.data();
  auto const* ai1 = src->AtomInfo.data();

  /**
   * If we want a bond and atm has cAtomInfoSingle geometry, then return the
   * atom index of its (only) neighbor. Otherwise, return atm.
   */
  auto const get_anchor_atm = [create_bond](
                                  const ObjectMolecule* obj, int atm) {
    if (create_bond) {
      if (obj->AtomInfo[atm].geom == cAtomInfoSingle) {
        auto neighbors = AtomNeighbors(obj, atm);
        if (neighbors.size() == 1) {
          atm = neighbors[0].atm;
        }
      }
    }
    return atm;
  };

  int const at0 = get_anchor_atm(I, index0);
  int const at1 = get_anchor_atm(src, index1);

  assert(!(at0 < 0 || at1 < 0));

  auto const anch1 = scs->atmToIdx(at1);
  if (anch1 < 0) {
    return pymol::Error("no coordinate for source anchor atom");
  }

  /* copy atoms and atom info into a 1:1 direct mapping */
  auto cs = std::make_unique<CoordSet>(G);
  cs->setNIndex(scs->NIndex);
  cs->enumIndices();

  auto nai = pymol::vla<AtomInfoType>(scs->NIndex);

  for (int idx = 0; idx < scs->NIndex; ++idx) {
    copy3f(scs->coordPtr(idx), cs->coordPtr(idx));
    auto atm1 = scs->IdxToAtm[idx];

    AtomInfoCopy(G, ai1 + atm1, nai + idx);

    if (edit) {
      if (atm1 == at1) {
        nai[idx].temp1 = 2; /* clear marks */
      } else {
        nai[idx].temp1 = 0; /* clear marks */
      }
    }
  }

  /* copy internal bond information */
  cs->TmpBond.reserve(src->NBond);
  cs->NTmpBond = 0;
  for (int b = 0; b != src->NBond; ++b) {
    auto const& b1 = src->Bond[b];
    auto a0 = scs->atmToIdx(b1.index[0]);
    auto a1 = scs->atmToIdx(b1.index[1]);
    if (a0 >= 0 && a1 >= 0) {
      auto& b0 = cs->TmpBond[cs->NTmpBond++];
      b0 = b1;
      b0.index[0] = a0;
      b0.index[1] = a1;
    }
  }

  // source coordinate system
  float mat1_inv[16];

  if (create_bond) {
    const float * va1 = scs->coordPtr(anch1);

    if (at0 != index0) {
      ai0[index0].deleteFlag = true;
    }

    identity44f(mat1_inv);

    if (at1 != index1) {
      auto const hydr1 = scs->atmToIdx(index1);
      if (hydr1 == -1) {
        return pymol::Error("no source attachment vector found");
      }
      nai[hydr1].deleteFlag = true;
      const float* vh1 = scs->coordPtr(hydr1);
      subtract3f(va1, vh1, mat1_inv);
    } else {
      auto found = CoordSetFindOpenValenceVector(scs, at1, mat1_inv);
      if (!found) {
        return pymol::Error("no source attachment vector found");
      }
      scale3f(mat1_inv, -1.0F, mat1_inv);
    }

    // bond length
    float const d = AtomInfoGetBondLength(G, ai0 + at0, ai1 + at1);

    // rotation matrix
    get_system1f3f(mat1_inv, mat1_inv + 4, mat1_inv + 8);

    // translation vector
    float mat1_trans[16];
    identity44f(mat1_trans);
    mat1_trans[3] = (mat1_inv[0] * d - va1[0]);
    mat1_trans[7] = (mat1_inv[1] * d - va1[1]);
    mat1_trans[11] = (mat1_inv[2] * d - va1[2]);

    right_multiply44f44f(mat1_inv, mat1_trans);

    /* set up the linking bond */
    cs->TmpLinkBond.resize(1);
    cs->NTmpLinkBond = 1;
    auto* bond = cs->TmpLinkBond.data();
    BondTypeInit2(bond, at0, anch1);
  }

  AtomInfoUniquefyNames(I, nai.data(), cs->NIndex);

  /* set up tags which will enable use to continue editing bond */

  if (edit) {
    for (int atm = 0; atm < I->NAtom; ++atm) {
      ai0[atm].temp1 = 0;
    }
    ai0[at0].temp1 = 1;
  }

  int const state0 = I->DiscreteFlag ? (ai0[at0].discrete_state - 1)
                                     : cSelectorUpdateTableAllStates;
  assert(!I->DiscreteFlag || I->DiscreteCSet[at0] == I->CSet[state0]);

  // Get target valence vectors for all relevant coordinate sets. Do this before
  // merging atoms to not leave the object molecule in an half-done state in
  // case we can't find valence vectors.
  std::vector<std::array<float, 3>> target_vectors;
  if (move_flag) {
    bool found_any_target_vector = false;
    target_vectors.resize(I->NCSet);

    for (StateIterator iter(G, nullptr, state0, I->NCSet); iter.next();) {
      float* vec = target_vectors[iter.state].data();
      if (GetTargetValenceVector(I->CSet[iter.state], at0, index0, vec)) {
        found_any_target_vector = true;
      } else {
        zero3(vec);
      }
    }

    if (!found_any_target_vector) {
      return pymol::Error("no target attachment vector found");
    }
  }

  // will free nai, cs->TmpBond and cs->TmpLinkBond
  // invalidates the ai0 pointer!
  bool ok = ObjectMoleculeMerge(
                I, std::move(nai), cs.get(), false, cAIC_AllMask, true) &&
            ObjectMoleculeExtendIndices(I, state0);
  p_return_val_if_fail(ok, pymol::Error::MEMORY);

  // Get untransformed copy of source coordinates
  pymol::vla<float> coord_orig;
  if (move_flag) {
    coord_orig = cs->Coord;
    p_return_val_if_fail(coord_orig, pymol::Error::MEMORY);
  }

  // Add coordinates into the coordinate set(s)
  for (StateIterator iter(G, nullptr, state0, I->NCSet); iter.next();) {
    const float* const vec =
        move_flag ? target_vectors[iter.state].data() : nullptr;
    ok = AddCoordinateIntoCoordSet(I->CSet[iter.state], cs.get(),
        coord_orig.data(), at0, mat1_inv, vec);
    p_return_val_if_fail(ok, pymol::Error::MEMORY);
  }

  ok = ObjectMoleculeSort(I);
  p_return_val_if_fail(ok, pymol::Error::MEMORY);

  ObjectMoleculeUpdateIDNumbers(I);

  if (at0 != index0 || at1 != index1) {
    ObjectMoleculePurge(I);
  }

  // edit the resulting bond
  if (edit) {
    int atm_pk1 = -1;
    int atm_pk2 = -1;
    for (int atm = 0; atm < I->NAtom; ++atm) {
      if (I->AtomInfo[atm].temp1 == 1) {
        atm_pk2 = atm;
      } else if (I->AtomInfo[atm].temp1 == 2) {
        atm_pk1 = atm;
      }
    }
    if (atm_pk2 >= 0 && atm_pk1 >= 0) {
      auto sele1 = pymol::string_format("%s`%d", I->Name, atm_pk1 + 1);
      auto sele2 = pymol::string_format("%s`%d", I->Name, atm_pk2 + 1);
      EditorSelect(G, sele1.data(), sele2.data(), "", "", false, true, true);
    }
  }

  return {};
}

/*========================================================================*/
int ObjectMoleculeVerifyChemistry(ObjectMolecule * I, int state)
{
  int result = false;
  const AtomInfoType *ai;
  int a;
  int flag;

  if(state < 0) {
    /* use the first defined state */
    for(a = 0; a < I->NCSet; a++) {
      if(I->CSet[a]) {
        state = a;
        break;
      }
    }
  }
  ai = I->AtomInfo;
  flag = true;
  for(a = 0; a < I->NAtom; a++) {
    if(!ai->chemFlag) {
      flag = false;
    }
    ai++;
  }
  if((!flag) && (state >= 0) && (state < I->NCSet)) {
    if(I->CSet[state]) {
      ObjectMoleculeInferChemFromBonds(I, state);
      ObjectMoleculeInferChemFromNeighGeom(I, state);
      ObjectMoleculeInferHBondFromChem(I);
      /*      ObjectMoleculeInferChemForProtein(I,0); */
    }
    flag = true;
    ai = I->AtomInfo;
    for(a = 0; a < I->NAtom; a++) {
      if(!ai->chemFlag) {
        flag = false;
        break;
      }
      ai++;
    }
  }
  if(flag)
    result = true;
  return (result);
}


/*========================================================================*/
int ObjectMoleculeAttach(ObjectMolecule * I, int index,
    pymol::vla<AtomInfoType>&& nai)
{
  int a;
  AtomInfoType *ai;
  BondType* bond;
  float v[3], v0[3], d;
  CoordSet *cs = nullptr;
  int ok = false;

  ai = I->AtomInfo + index;

  ok_assert(1, cs = CoordSetNew(I->G));
  ok_assert(1, cs->Coord = pymol::vla<float>(3));

  cs->NIndex = 1;
  ok_assert(1, cs->TmpLinkBond = pymol::vla<BondType>(1));

  cs->NTmpLinkBond = 1;
  bond = cs->TmpLinkBond.data();
  BondTypeInit2(bond, index, 0);
  cs->enumIndices();

  ok_assert(1, ObjectMoleculePrepareAtom(I, index, nai.data()));
  d = AtomInfoGetBondLength(I->G, ai, nai);

  ok_assert(1, ObjectMoleculeMerge(I, std::move(nai),
        cs, false, cAIC_AllMask, true)); // will free nai and cs->TmpLinkBond
  ok_assert(1, ObjectMoleculeExtendIndices(I, -1));

  for(a = 0; a < I->NCSet; a++) {       /* add atom to each coordinate set */
    if (const auto* cs_a = I->CSet[a]) {
      CoordSetGetAtomVertex(cs_a, index, v0);
      CoordSetFindOpenValenceVector(cs_a, index, v);
      scale3f(v, d, v);
      add3f(v0, v, cs->Coord.data());
      ok_assert(1, CoordSetMerge(I, I->CSet[a], cs));
    }
  }

  ok_assert(1, ObjectMoleculeSort(I));
  ObjectMoleculeUpdateIDNumbers(I);

  ok = true;
ok_except1:
  delete cs;
  return ok;
}


/*========================================================================*/
int ObjectMoleculeFillOpenValences(ObjectMolecule * I, int index)
{
  int a;
  AtomInfoType *ai;
  int result = 0;
  int flag = true;
  float v[3], v0[3], d;
  CoordSet *cs = nullptr;
  int ok = true;

  if((index >= 0) && (index <= I->NAtom)) {
    while(ok) {
      ai = I->AtomInfo + index;
      auto const nn = AtomNeighbors(I, index).size();

      assert(flag);

      if((nn >= ai->valence) || (!flag))
        break;
      flag = false;

      if (ok)
	cs = CoordSetNew(I->G);
      CHECKOK(ok, cs);
      if (ok){
	cs->Coord = pymol::vla<float>(3);
	CHECKOK(ok, cs->Coord);

	cs->NIndex = 1;
	if (ok)
	  cs->TmpLinkBond = pymol::vla<BondType>(1);
	CHECKOK(ok, cs->TmpLinkBond);
	if (ok){
	  cs->NTmpLinkBond = 1;
          auto* bond = cs->TmpLinkBond.data();
          BondTypeInit2(bond, index, 0);
	}
      }
      
      if(ok)
        cs->enumIndices();
      auto atInfo = pymol::vla<AtomInfoType>(1);
      AtomInfoType* nai = atInfo.data();
      if (ok){
	UtilNCopy(nai->elem, "H", 2);
	nai->geom = cAtomInfoSingle;
	nai->valence = 1;
	ok &= ObjectMoleculePrepareAtom(I, index, nai);
	d = AtomInfoGetBondLength(I->G, ai, nai);
	if (ok)
          ok &= ObjectMoleculeMerge(I, std::move(atInfo),
              cs, false, cAIC_AllMask, true);       /* will free nai and cs->TmpLinkBond  */
      }
      if (ok)
	ok &= ObjectMoleculeExtendIndices(I, -1);
      for(a = 0; ok &&  a < I->NCSet; a++) {   /* add atom to each coordinate set */
        if (auto* cs_a = I->CSet[a]) {
          CoordSetGetAtomVertex(cs_a, index, v0);
          CoordSetFindOpenValenceVector(cs_a, index, v);
          scale3f(v, d, v);
          add3f(v0, v, cs->Coord.data());
          ok &= CoordSetMerge(I, cs_a, cs);
        }
      }
      delete cs;
      result++;
      flag = true;
    }
  }
  ObjectMoleculeUpdateIDNumbers(I);
  return (result);
}

#define MaxOcc 100


/*========================================================================*/
/**
 * @param[out] normal (3x1) Normal vector of planar geometry at atom `atm`
 *
 * @return True if the atom has planar geometry and the normal could be computed
 *
 * @pre neighbors are defined
 */
static bool get_planer_normal(const CoordSet* cs, int const atm, float* normal)
{
  int found = false;
  int nOcc = 0;
  float occ[MaxOcc * 3];
  float v0[3], v1[3], v2[3], n0[3];

  if (CoordSetGetAtomVertex(cs, atm, v0)) {
    const auto* I = cs->Obj;

    // look for an attached non-hydrogen as a base
    for (auto const& neighbor : AtomNeighbors(I, atm)) {
      if (CoordSetGetAtomVertex(cs, neighbor.atm, v1)) {
        subtract3f(v1, v0, n0);
        normalize3f(n0);        /* n0's point away from center atom */
        copy3f(n0, occ + 3 * nOcc);
        nOcc++;
        if(nOcc == MaxOcc)      /* safety valve */
          break;
      }
    }
    switch (I->AtomInfo[atm].geom) {
    case cAtomInfoPlanar:
      if(nOcc > 1) {
        cross_product3f(occ, occ + 3, normal);
        if(nOcc > 2) {
          cross_product3f(occ, occ + 6, v2);
          if(dot_product3f(normal, v2) < 0) {
            subtract3f(normal, v2, normal);
          } else {
            add3f(normal, v2, normal);
          }
          cross_product3f(occ + 3, occ + 6, v2);
          if(dot_product3f(normal, v2) < 0) {
            subtract3f(normal, v2, normal);
          } else {
            add3f(normal, v2, normal);
          }
        }
        normalize3f(normal);
        found = true;
      }
      break;
    }
  }
  return found;
}

/*========================================================================*/
/**
 * @param atm Atom index
 * @param[out] v (3x1) Normalized open valence vector
 * @param seek (3x1) Primer for the direction, or nullptr for random priming.
 * @param ignore_index Atom index of neighbor to ignore
 * @return True if valid open valence vector found
 */
bool CoordSetFindOpenValenceVector(
    const CoordSet* cs, int atm, float* v, const float* seek, int ignore_index)
{
  int nOcc = 0;
  float occ[MaxOcc * 3];
  int last_occ = -1;
  float v0[3], v1[3], n0[3] = {0.0F,0.0F,0.0F}, t[3];
  int result = false;
  float y[3], z[3];

  /* default is +X */
  v[0] = 1.0;
  v[1] = 0.0;
  v[2] = 0.0;

  if(cs) {
    const auto* I = cs->Obj;

    if (atm >= 0 && atm <= I->NAtom) {
      if (CoordSetGetAtomVertex(cs, atm, v0)) {
        const auto* ai = I->AtomInfo.data() + atm;

        // look for an attached non-hydrogen as a base
        for (auto const& neighbor : AtomNeighbors(I, atm)) {
          auto const a1 = neighbor.atm;
          if(a1 != ignore_index) {
            if (CoordSetGetAtomVertex(cs, a1, v1)) {
              last_occ = a1;
              subtract3f(v1, v0, n0);
              normalize3f(n0);  /* n0's point away from center atom */
              copy3f(n0, occ + 3 * nOcc);
              nOcc++;
              if(nOcc == MaxOcc)        /* safety valve */
                break;
            }
          }
        }
        if((!nOcc) || (nOcc > 4) || (ai->geom == cAtomInfoNone)) {
          if(!seek)
            get_random3f(v);
          else
            copy3f(seek, v);
          result = true;
        } else {
          switch (nOcc) {
          case 1:              /* only one current occupied position */
            switch (ai->geom) {
            case cAtomInfoTetrahedral:
              if(!seek) {
                get_system1f3f(occ, y, z);
                scale3f(occ, -0.334F, v);
                scale3f(z, 0.943F, t);
                add3f(t, v, v);
              } else {          /* point hydrogen towards sought vector */
                copy3f(seek, z);
                get_system2f3f(occ, z, y);
                scale3f(occ, -0.334F, v);
                scale3f(z, 0.943F, t);
                add3f(t, v, v);
              }
              result = true;
              break;
            case cAtomInfoPlanar:
              {
                if(!seek) {
                  if (last_occ >= 0 && get_planer_normal(cs, last_occ, n0)) {
                    copy3f(n0, y);
                    get_system2f3f(occ, y, z);
                  } else {
                    get_system1f3f(occ, y, z);
                  }
                  scale3f(occ, -0.500F, v);
                  scale3f(z, 0.866F, t);
                  add3f(t, v, v);
                } else {
                  copy3f(seek, z);
                  get_system2f3f(occ, z, y);
                  scale3f(occ, -0.500F, v);
                  scale3f(z, 0.866F, t);
                  add3f(t, v, v);
                }
                result = true;
              }
              break;
            case cAtomInfoLinear:
              scale3f(occ, -1.0F, v);
              result = true;
              break;
            default:
              if(!seek)
                get_random3f(v);
              else
                copy3f(seek, v);
              result = false;
              break;
            }
            break;
          case 2:              /* only two current occupied positions */
            switch (ai->geom) {
            case cAtomInfoTetrahedral:
              add3f(occ, occ + 3, t);
              get_system2f3f(t, occ, z);
              scale3f(t, -1.0F, v);
              if(seek) {
                if(dot_product3f(z, seek) < 0.0F) {
                  invert3f(z);
                }
              }
              scale3f(z, 1.41F, t);
              add3f(t, v, v);
              result = true;
              break;
            case cAtomInfoPlanar:
              add3f(occ, occ + 3, t);
              scale3f(t, -1.0F, v);
              result = true;
              break;
            default:
              if(!seek) {
                add3f(occ, occ + 3, t);
                scale3f(t, -1.0F, v);
                if(length3f(t) < 0.1)
                  get_random3f(v);
              } else
                copy3f(seek, v);
              /* hypervalent */
              result = false;
              break;
            }
            break;
          case 3:              /* only three current occupied positions */
            switch (ai->geom) {
            case cAtomInfoTetrahedral:
              add3f(occ, occ + 3, t);
              add3f(occ + 6, t, t);
              scale3f(t, -1.0F, v);
              result = true;
              break;
            default:
              if(!seek) {
                add3f(occ, occ + 3, t);
                add3f(occ + 6, t, t);
                scale3f(t, -1.0F, v);
                if(length3f(t) < 0.1)
                  get_random3f(v);
              } else
                copy3f(seek, v);
              /* hypervalent */
              result = false;
              break;
            }
            break;
          case 4:
            if(!seek)
              get_random3f(v);
            else
              copy3f(seek, v);
            /* hypervalent */
            result = false;
            break;
          }
        }
      }
    }
  }
  normalize3f(v);
  return (result);
#undef MaxOcc

}


/*========================================================================*/
void ObjectMoleculeCreateSpheroid(ObjectMolecule * I, int average)
{
  CoordSet *cs;
  int a, b, c, a0;
  SphereRec *sp;
  float *v, *v0, *s, *f, ang, min_dist, *max_sq;
  int *i;
  float *center = nullptr;
  float d0[3], n0[3], d1[3], d2[3];
  float p0[3], p1[3], p2[3];
  int t0, t1, t2, bt0, bt1, bt2;
  float dp, l, *fsum = nullptr;
  float spheroid_smooth;
  float spheroid_fill;
  float spheroid_ratio = 0.1F;  /* minimum ratio of width over length */
  float spheroid_minimum = 0.02F;       /* minimum size - to insure valid normals */
  int row, *count = nullptr, base;
  int nRow;
  int first = 0;
  int last = 0;
  int current;
  int cscount;
  int n_state = 0;
  sp = GetSpheroidSphereRec(I->G);

  nRow = I->NAtom * sp->nDot;

  center = pymol::malloc<float>(I->NAtom * 3);
  count = pymol::malloc<int>(I->NAtom);
  fsum = pymol::malloc<float>(nRow);
  max_sq = pymol::malloc<float>(I->NAtom);

  spheroid_smooth = SettingGetGlobal_f(I->G, cSetting_spheroid_smooth);
  spheroid_fill = SettingGetGlobal_f(I->G, cSetting_spheroid_fill);
  /* first compute average coordinate */

  if(average < 1)
    average = I->NCSet;
  current = 0;
  cscount = 0;
  while(current < I->NCSet) {
    if(I->CSet[current]) {
      if(!cscount)
        first = current;
      cscount++;
      last = current + 1;
    }

    if(cscount == average || current == I->NCSet - 1) {
      PRINTFB(I->G, FB_ObjectMolecule, FB_Details)
        " ObjectMolecule: computing spheroid from states %d to %d.\n",
        first + 1, last ENDFB(I->G);

      auto spheroid = std::vector<float>(nRow);

      v = center;
      i = count;
      for(a = 0; a < I->NAtom; a++) {
        *(v++) = 0.0;
        *(v++) = 0.0;
        *(v++) = 0.0;
        *(i++) = 0;
      }

      for(b = first; b < last; b++) {
        cs = I->CSet[b];
        if(cs) {
          v = cs->Coord.data();
          for(a = 0; a < cs->NIndex; a++) {
            a0 = cs->IdxToAtm[a];
            v0 = center + 3 * a0;
            add3f(v, v0, v0);
            (*(count + a0))++;
            v += 3;
          }
        }
      }

      i = count;
      v = center;
      for(a = 0; a < I->NAtom; a++)
        if(*i) {
          (*(v++)) /= (*i);
          (*(v++)) /= (*i);
          (*(v++)) /= (*i++);
        } else {
          v += 3;
          i++;
        }

      /* now go through and compute radial distances */

      f = fsum;
      s = spheroid.data();
      for(a = 0; a < nRow; a++) {
        *(f++) = 0.0;
        *(s++) = 0.0;
      }

      v = max_sq;
      for(a = 0; a < I->NAtom; a++)
        *(v++) = 0.0;

      for(b = first; b < last; b++) {
        cs = I->CSet[b];
        if(cs) {
          v = cs->Coord.data();
          for(a = 0; a < cs->NIndex; a++) {
            a0 = cs->IdxToAtm[a];
            base = (a0 * sp->nDot);
            v0 = center + (3 * a0);
            subtract3f(v, v0, d0);      /* subtract from average */
            l = lengthsq3f(d0);
            if(l > max_sq[a0])
              max_sq[a0] = l;
            if(l > 0.0) {
              float isq = (float) (1.0 / sqrt1d(l));
              scale3f(d0, isq, n0);
              for(c = 0; c < sp->nDot; c++) {   /* average over spokes */
                dp = dot_product3f(sp->dot[c], n0);
                row = base + c;
                if(dp >= 0.0) {
                  ang = (float) ((acos(dp) / spheroid_smooth) * (cPI / 2.0));
                  if(ang > spheroid_fill)
                    ang = spheroid_fill;
                  /* take envelop to zero over that angle */
                  if(ang <= (cPI / 2.0)) {
                    dp = (float) cos(ang);
                    fsum[row] += dp * dp;
                    spheroid[row] += l * dp * dp * dp;
                  }
                }
              }
            }
            v += 3;
          }
        }
      }

      f = fsum;
      s = spheroid.data();
      for(a = 0; a < I->NAtom; a++) {
        min_dist = (float) (spheroid_ratio * sqrt(max_sq[a]));
        if(min_dist < spheroid_minimum)
          min_dist = spheroid_minimum;
        for(b = 0; b < sp->nDot; b++) {
          if(*f > R_SMALL4) {
            (*s) = (float) (sqrt1d((*s) / (*(f++))));   /* we put the "rm" in "rms" */
          } else {
            f++;
          }
          if(*s < min_dist)
            *s = min_dist;
          s++;
        }
      }

      /* set frame 0 coordinates to the average */

      cs = I->CSet[first];
      if(cs) {
        v = cs->Coord.data();
        for(a = 0; a < cs->NIndex; a++) {
          a0 = cs->IdxToAtm[a];
          v0 = center + 3 * a0;
          copy3f(v0, v);
          v += 3;
        }
      }

      /* now compute surface normals */

      auto norm = std::vector<float>(nRow * 3);
      for(a = 0; a < nRow; a++) {
        zero3f(norm.data() + a * 3);
      }
      for(a = 0; a < I->NAtom; a++) {
        base = a * sp->nDot;
        for(b = 0; b < sp->NTri; b++) {
          t0 = sp->Tri[b * 3];
          t1 = sp->Tri[b * 3 + 1];
          t2 = sp->Tri[b * 3 + 2];
          bt0 = base + t0;
          bt1 = base + t1;
          bt2 = base + t2;
          copy3f(sp->dot[t0], p0);
          copy3f(sp->dot[t1], p1);
          copy3f(sp->dot[t2], p2);
          /*      scale3f(sp->dot[t0].v,spheroid[bt0],p0);
             scale3f(sp->dot[t1].v,spheroid[bt1],p1);
             scale3f(sp->dot[t2].v,spheroid[bt2],p2); */
          subtract3f(p1, p0, d1);
          subtract3f(p2, p0, d2);
          cross_product3f(d1, d2, n0);
          normalize3f(n0);
          v = norm.data() + bt0 * 3;
          add3f(n0, v, v);
          v = norm.data() + bt1 * 3;
          add3f(n0, v, v);
          v = norm.data() + bt2 * 3;
          add3f(n0, v, v);
        }
      }

      f = norm.data();
      for(a = 0; a < I->NAtom; a++) {
        base = a * sp->nDot;
        for(b = 0; b < sp->nDot; b++) {
          normalize3f(f);
          f += 3;
        }
      }

      if(I->CSet[first]) {
        I->CSet[first]->Spheroid = std::move(spheroid);
        I->CSet[first]->SpheroidNormal = std::move(norm);
      }

      for(b = first + 1; b < last; b++) {
        delete I->CSet[b];
        I->CSet[b] = nullptr;
      }

      if(n_state != first) {
        I->CSet[n_state] = I->CSet[first];
        I->CSet[first] = nullptr;
      }
      n_state++;

      cscount = 0;
    }
    current++;
  }
  I->NCSet = n_state;
  FreeP(center);
  FreeP(count);
  FreeP(fsum);
  FreeP(max_sq);

  I->invalidate(cRepSphere, cRepInvProp, -1);
}


/*========================================================================*/
void ObjectMoleculeReplaceAtom(ObjectMolecule * I, int index, AtomInfoType&& ai)
{
  if((index >= 0) && (index <= I->NAtom)) {
    I->AtomInfo[index] = std::move(ai);
    I->invalidate(cRepAll, cRepInvAtoms, -1);
    /* could we put in a refinement step here? */
  }
}


/*========================================================================*/
int ObjectMoleculePrepareAtom(ObjectMolecule * I, int index, AtomInfoType * ai,
    bool uniquefy)
{
  /* match existing properties of the old atom */
  AtomInfoType *ai0;
  int ok = true;

  if((index >= 0) && (index <= I->NAtom)) {
    ai0 = I->AtomInfo + index;
    ai->resv = ai0->resv;
    ai->hetatm = ai0->hetatm;
    ai->flags = ai0->flags;

    if (!ai->geom)
      ai->geom = ai0->geom;

    ai->discrete_state = ai0->discrete_state;
    ai->q = ai0->q;
    ai->b = ai0->b;
    strcpy(ai->alt, ai0->alt);
    ai->inscode = ai0->inscode;
    LexAssign(I->G, ai->segi, ai0->segi);
    LexAssign(I->G, ai->chain, ai0->chain);
    LexAssign(I->G, ai->resn, ai0->resn);
    ai->visRep = ai0->visRep;
    ai->id = -1;
    ai->rank = -1;

    AtomInfoAssignParameters(I->G, ai);

    if (uniquefy) {
      AtomInfoUniquefyNames(I->G, I->AtomInfo, I->NAtom, ai, nullptr, 1);
    }

    if((ai->elem[0] == ai0->elem[0]) && (ai->elem[1] == ai0->elem[1]))
      ai->color = ai0->color;
    else if((ai->elem[0] == 'C') && (ai->elem[1] == 0)) {
      int found = false;
      for (auto const& neighbor : AtomNeighbors(I, index)) {
        AtomInfoType const* ai1 = I->AtomInfo.data() + neighbor.atm;
        if(ai1->protons == cAN_C) {
          ai->color = ai1->color;
          found = true;
          break;
        }
      }
      if(ok && !found) {
        /* if no carbon nearby, then color according to the object color */
        ai->color = I->Color;
      }
    } else {
      AtomInfoAssignColors(I->G, ai);
    }
  }
  return ok;
}


/*========================================================================*/
int ObjectMoleculePreposReplAtom(ObjectMolecule * I, int index, AtomInfoType * ai)
{
  float v0[3], v1[3], v[3];
  float d0[3], d, n0[3];
  int cnt;
  float t[3], sum[3];
  int a;
  int ncycle;
  int ok = true;
  if (ok){
    for(a = 0; a < I->NCSet; a++) {
      if(I->CSet[a]) {
	if(ObjectMoleculeGetAtomVertex(I, a, index, v0)) {
	  copy3f(v0, v);          /* default is direct superposition */
	  ncycle = -1;
	  while(ncycle) {
	    cnt = 0;
	    zero3f(sum);
            /* look for an attached non-hydrogen as a base */
            for (auto const& neighbor : AtomNeighbors(I, index)) {
	      auto const a1 = neighbor.atm;
              auto const* ai1 = I->AtomInfo.data() + a1;
              if(ai1->protons != 1)
		if(ObjectMoleculeGetAtomVertex(I, a, a1, v1)) {
		  d = AtomInfoGetBondLength(I->G, ai, ai1);
		  subtract3f(v0, v1, n0);
		  normalize3f(n0);
		  scale3f(n0, d, d0);
		  add3f(d0, v1, t);
		  add3f(t, sum, sum);
		  cnt++;
		}
	    }
	    if(cnt) {
	      scale3f(sum, 1.0F / cnt, sum);
	      copy3f(sum, v0);
	      if((cnt > 1) && (ncycle < 0))
		ncycle = 5;
	    }
	    ncycle = abs(ncycle) - 1;
	  }
	  if(cnt)
	    copy3f(sum, v);
	  ObjectMoleculeSetAtomVertex(I, a, index, v);
	}
      }
    }
  }
  return ok;
}


/*========================================================================*/

#if 1
void ObjectMoleculeSaveUndo(ObjectMolecule * I, int state, int log)
{
  CoordSet *cs;
  PyMOLGlobals *G = I->G;
  FreeP(I->UndoCoord[I->UndoIter]);
  I->UndoState[I->UndoIter] = -1;
  if(state < 0)
    state = 0;
  if(I->NCSet == 1)
    state = 0;
  state = state % I->NCSet;
  cs = I->CSet[state];
  if(cs) {
    I->UndoCoord[I->UndoIter] = pymol::malloc<float>(cs->NIndex * 3);
    memcpy(I->UndoCoord[I->UndoIter], cs->Coord, sizeof(float) * cs->NIndex * 3);
    I->UndoState[I->UndoIter] = state;
    I->UndoNIndex[I->UndoIter] = cs->NIndex;
  }
  I->UndoIter = cUndoMask & (I->UndoIter + 1);
  ExecutiveSetLastObjectEdited(G, I);
  if(log) {
    OrthoLineType line;
    if(SettingGetGlobal_i(I->G, cSetting_logging)) {
      sprintf(line, "cmd.push_undo(\"%s\",%d)\n", I->Name, state + 1);
      PLog(G, line, cPLog_no_flush);
    }
  }

}


/*========================================================================*/
void ObjectMoleculeUndo(ObjectMolecule * I, int dir)
{
  CoordSet *cs;
  int state;

  FreeP(I->UndoCoord[I->UndoIter]);
  I->UndoState[I->UndoIter] = -1;
  state = SceneGetState(I->G);
  if(state < 0)
    state = 0;
  if(I->NCSet == 1)
    state = 0;
  state = state % I->NCSet;
  cs = I->CSet[state];
  if(cs) {
    I->UndoCoord[I->UndoIter] = pymol::malloc<float>(cs->NIndex * 3);
    memcpy(I->UndoCoord[I->UndoIter], cs->Coord, sizeof(float) * cs->NIndex * 3);
    I->UndoState[I->UndoIter] = state;
    I->UndoNIndex[I->UndoIter] = cs->NIndex;
  }

  I->UndoIter = cUndoMask & (I->UndoIter + dir);
  if(!I->UndoCoord[I->UndoIter])
    I->UndoIter = cUndoMask & (I->UndoIter - dir);

  if(I->UndoState[I->UndoIter] >= 0) {
    state = I->UndoState[I->UndoIter];
    if(state < 0)
      state = 0;

    if(I->NCSet == 1)
      state = 0;
    state = state % I->NCSet;
    cs = I->CSet[state];
    if(cs) {
      if(cs->NIndex == I->UndoNIndex[I->UndoIter]) {
        memcpy(cs->Coord.data(), I->UndoCoord[I->UndoIter], sizeof(float) * cs->NIndex * 3);
        I->UndoState[I->UndoIter] = -1;
        FreeP(I->UndoCoord[I->UndoIter]);
        cs->invalidateRep(cRepAll, cRepInvCoord);
        SceneChanged(I->G);
      }
    }
  }
}
#endif

int ObjectMoleculeAddBond(ObjectMolecule * I, int sele0, int sele1, int order, pymol::zstring_view symop)
{
  int a1, a2;
  AtomInfoType *ai1, *ai2;
  int s1, s2;
  int c = 0;

  /* TO DO: optimize for performance -- we shouldn't be doing full
     table scans */

  ai1 = I->AtomInfo.data();
  for(a1 = 0; a1 < I->NAtom; a1++) {
    s1 = ai1->selEntry;
    if(SelectorIsMember(I->G, s1, sele0)) {
      ai2 = I->AtomInfo.data();
      for(a2 = 0; a2 < I->NAtom; a2++) {
        s2 = ai2->selEntry;
        if(SelectorIsMember(I->G, s2, sele1)) {
          if(!I->Bond){
            I->Bond = pymol::vla<BondType>(1);
	  }
          if(I->Bond) {
            BondType* bnd = I->Bond.check(I->NBond);
            BondTypeInit2(bnd, a1, a2, order);

            assert(!bnd->symop_2);
            if (!symop.empty()) {
              bnd->symop_2.reset(symop.c_str());
            }

            I->NBond++;
            c++;
            I->AtomInfo[a1].chemFlag = false;
            I->AtomInfo[a2].chemFlag = false;
            I->AtomInfo[a1].bonded = true;
            I->AtomInfo[a2].bonded = true;
          }
        }
        ai2++;
      }
    }
    ai1++;
  }
  if(c) {
    I->invalidate(cRepAll, cRepInvBondsNoNonbonded, -1);
  }
  return (c);
}

/*========================================================================*/
pymol::Result<> ObjectMoleculeAddBondByIndices(
    ObjectMolecule* I, unsigned atm1, unsigned atm2, int order)
{
  if (atm1 >= I->NAtom || atm2 >= I->NAtom) {
    return pymol::make_error("atom index out of bounds");
  }

  if (!I->Bond) {
    I->Bond = pymol::vla<BondType>(1);
  } else {
    I->Bond.check(I->NBond);
  }

  if (!I->Bond) {
    return pymol::Error::MEMORY;
  }

  auto& bnd = I->Bond[I->NBond++];
  BondTypeInit2(&bnd, atm1, atm2, order);

  I->AtomInfo[atm1].chemFlag = false;
  I->AtomInfo[atm2].chemFlag = false;
  I->AtomInfo[atm1].bonded = true;
  I->AtomInfo[atm2].bonded = true;

  I->invalidate(cRepAll, cRepInvBondsNoNonbonded, -1);

  return {};
}

/*========================================================================*/
int ObjectMoleculeAdjustBonds(ObjectMolecule * I, int sele0, int sele1, int mode,
                              int order, pymol::zstring_view symop)
{
  auto const G = I->G;
  int a0, a1;
  int cnt = 0;
  BondType *b0;
  int both;
  int a;

  if(I->Bond) {
    b0 = I->Bond.data();
    for(a = 0; a < I->NBond; a++) {
      a0 = b0->index[0];
      a1 = b0->index[1];

      both = 0;

      if (SelectorIsMember(G, I->AtomInfo[a0].selEntry, sele0) &&
          SelectorIsMember(G, I->AtomInfo[a1].selEntry, sele1)) {
        both = 2;
      } else if ( //
          SelectorIsMember(G, I->AtomInfo[a1].selEntry, sele0) &&
          SelectorIsMember(G, I->AtomInfo[a0].selEntry, sele1)) {
        std::swap(a0, a1);
        both = 2;
      }

      if(both == 2) {
        cnt++;
        switch (mode) {
        case 0:                /* cycle */
          switch(SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_editor_bond_cycle_mode)) {
          case 1: /* 1 arom 2 3 */
            switch(b0->order) {
            case 1:
              b0->order = 4;
              break;
            case 4:
              b0->order = 2;
              break;
            case 2:
              b0->order = 3;
              break;
            default:
              b0->order = 1;
              break;
            }
            break;
          case 2: /* 1 2 3 arom */
            b0->order++;
            if(b0->order > 4)
              b0->order = 1;
            break;
          default: /* old way -> 1 2 3 */
            b0->order++;
            if(b0->order > 3)
              b0->order = 1;
            break;
          }
          I->AtomInfo[a0].chemFlag = false;
          I->AtomInfo[a1].chemFlag = false;
          break;
        case 1:                /* set */
          b0->order = order;
          I->AtomInfo[a0].chemFlag = false;
          I->AtomInfo[a1].chemFlag = false;
          break;
        }

        if (!symop.empty()) {
          b0->symop_2.reset(symop.c_str());
        }
      }
      b0++;
    }
    if(cnt) {
      I->invalidate(cRepLine, cRepInvBonds, -1);
      I->invalidate(cRepCyl, cRepInvBonds, -1);
      I->invalidate(cRepNonbonded, cRepInvBonds, -1);
      I->invalidate(cRepNonbondedSphere, cRepInvBonds, -1);
      I->invalidate(cRepRibbon, cRepInvBonds, -1);
      I->invalidate(cRepCartoon, cRepInvBonds, -1);
    }
  }

  return (cnt);
}


/*========================================================================*/
int ObjectMoleculeRemoveBonds(ObjectMolecule * I, int sele0, int sele1)
{
  int a0, a1;
  int offset = 0;
  BondType *b0, *b1;
  int both;
  int s;
  int a;

  if(I->Bond) {
    offset = 0;
    b0 = I->Bond.data();
    b1 = I->Bond.data();
    for(a = 0; a < I->NBond; a++) {
      a0 = b0->index[0];
      a1 = b0->index[1];

      both = 0;
      s = I->AtomInfo[a0].selEntry;
      if(SelectorIsMember(I->G, s, sele0))
        both++;
      s = I->AtomInfo[a1].selEntry;
      if(SelectorIsMember(I->G, s, sele1))
        both++;
      if(both < 2) {            /* reverse combo */
        both = 0;
        s = I->AtomInfo[a1].selEntry;
        if(SelectorIsMember(I->G, s, sele0))
          both++;
        s = I->AtomInfo[a0].selEntry;
        if(SelectorIsMember(I->G, s, sele1))
          both++;
      }

      if(both == 2) {
        AtomInfoPurgeBond(I->G, b0);
        offset--;
        b0++;
        I->AtomInfo[a0].chemFlag = false;
        I->AtomInfo[a1].chemFlag = false;
      } else if(offset) {
        *(b1++) = *(b0++);      /* copy bond info */
      } else {
        *(b1++) = *(b0++);      /* copy bond info */
      }
    }
    if(offset) {
      I->NBond += offset;
      VLASize(I->Bond, BondType, I->NBond);
      I->invalidate(cRepLine, cRepInvBonds, -1);
      I->invalidate(cRepCyl, cRepInvBonds, -1);
      I->invalidate(cRepNonbonded, cRepInvBonds, -1);
      I->invalidate(cRepNonbondedSphere, cRepInvBonds, -1);
      I->invalidate(cRepRibbon, cRepInvBonds, -1);
      I->invalidate(cRepCartoon, cRepInvBonds, -1);
    }
  }

  return (-offset);
}


/*========================================================================*/
void ObjectMoleculePurge(ObjectMolecule * I)
{
  PyMOLGlobals *G = I->G;
  int offset = 0;

  // remove the object selection and free up any selection entries.
  // note that we don't delete atom selection members -- those may be needed in the new object.
  SelectorDelete(G, I->Name);

  // old-to-new mapping
  auto oldToNew = std::vector<int>(size_t(I->NAtom), -1);
  for (int atm = 0; atm < I->NAtom; ++atm) {
    auto &ai0 = I->AtomInfo[atm];
    if(ai0.deleteFlag) {
      AtomInfoPurge(G, &ai0);
      offset--;
      assert(oldToNew[atm] == -1);
    } else {
      if(offset) {
        I->AtomInfo[atm + offset] = std::move(ai0);
      }
      oldToNew[atm] = atm + offset;
    }
  }
  if(offset) {
    I->NAtom += offset;
    I->AtomInfo.resize(I->NAtom);
    for (int a = 0; a < I->NCSet; ++a) {
      if (auto cs = I->CSet[a])
        CoordSetAdjustAtmIdx(cs, oldToNew.data());
    }
    if (I->CSTmpl) {
      CoordSetAdjustAtmIdx(I->CSTmpl, oldToNew.data());
    }
  }

  I->updateAtmToIdx();

  // step 4, bonds

  offset = 0;
  auto b0 = I->Bond.data();
  auto b1 = I->Bond.data();
  for (int b = 0; b < I->NBond; ++b) {
    auto a0 = b0->index[0];
    auto a1 = b0->index[1];
    if(a0 < 0 || a1 < 0 || (oldToNew[a0] < 0) || (oldToNew[a1] < 0)) {
      /* deleting bond */
      AtomInfoPurgeBond(I->G, b0);
      offset--;
      b0++;
    } else {
      if(offset) {
	*b1 = *b0;
      }
      b1->index[0] = oldToNew[a0];      /* copy bond info */
      b1->index[1] = oldToNew[a1];
      b0++;
      b1++;
    }
  }
  if(offset) {
    I->NBond += offset;
    VLASize(I->Bond, BondType, I->NBond);
  }

  I->invalidate(cRepAll, cRepInvAtoms, -1);
}


/*========================================================================*/
/**
 * Determines hybridization from coordinates in those few cases where it is
 * unambiguous.
 */
int ObjectMoleculeGetAtomGeometry(const ObjectMolecule* I, int state, int at)
{

  int result = -1;
  float v0[3], v1[3], v2[3], v3[3];
  float d1[3], d2[3], d3[3];
  float cp1[3], cp2[3], cp3[3];
  float avg;
  float dp;
  auto const neighbors = AtomNeighbors(I, at);
  auto const nn = neighbors.size();
  if(nn == 4)
    result = cAtomInfoTetrahedral;
  else if(nn == 3) {
    /* check cross products */
    ObjectMoleculeGetAtomVertex(I, state, at, v0);
    ObjectMoleculeGetAtomVertex(I, state, neighbors[0].atm, v1);
    ObjectMoleculeGetAtomVertex(I, state, neighbors[1].atm, v2);
    ObjectMoleculeGetAtomVertex(I, state, neighbors[2].atm, v3);
    subtract3f(v1, v0, d1);
    subtract3f(v2, v0, d2);
    subtract3f(v3, v0, d3);
    cross_product3f(d1, d2, cp1);
    cross_product3f(d2, d3, cp2);
    cross_product3f(d3, d1, cp3);
    normalize3f(cp1);
    normalize3f(cp2);
    normalize3f(cp3);
    avg = (dot_product3f(cp1, cp2) +
           dot_product3f(cp2, cp3) + dot_product3f(cp3, cp1)) / 3.0F;
    if(avg > 0.75)
      result = cAtomInfoPlanar;
    else
      result = cAtomInfoTetrahedral;
  } else if(nn == 2) {
    ObjectMoleculeGetAtomVertex(I, state, at, v0);
    ObjectMoleculeGetAtomVertex(I, state, neighbors[0].atm, v1);
    ObjectMoleculeGetAtomVertex(I, state, neighbors[1].atm, v2);
    subtract3f(v1, v0, d1);
    subtract3f(v2, v0, d2);
    normalize3f(d1);
    normalize3f(d2);
    dp = dot_product3f(d1, d2);
    if(dp < -0.75)
      result = cAtomInfoLinear;
  }
  return (result);
}


/*========================================================================*/
static float compute_avg_center_dot_cross_fn(ObjectMolecule * I, CoordSet * cs,
                                             int n_atom, int *atix)
{
  float result = 0.0F;
  float *v[9];
  int missing_flag = false;
  {
    int a, i;
    for(i = 0; i < n_atom; i++) {
      int a1 = atix[i];
      a = cs->atmToIdx(a1);
      if(a < 0) {
        missing_flag = true;
        break;
      } else {
        v[i] = cs->coordPtr(a);
      }
    }
  }
  if(!missing_flag) {
    int i;
    float d10[3], d20[3], cp[8][3];
    float avg = 0.0;

    v[n_atom] = v[1];
    for(i = 1; i < n_atom; i++) {
      subtract3f(v[i], v[0], d10);
      subtract3f(v[i + 1], v[0], d20);
      normalize3f(d10);
      normalize3f(d20);
      cross_product3f(d10, d20, cp[i]);
      normalize3f(cp[i]);
      if(i > 1) {
        if(dot_product3f(cp[i - 1], cp[i]) < 0.0)
          invert3f(cp[i]);
      }
    }
    copy3f(cp[1], cp[n_atom]);
    for(i = 1; i < n_atom; i++) {
      avg += dot_product3f(cp[i], cp[i + 1]);
    }
    result = avg / (n_atom - 1);
  }
  return result;
}

static int verify_planer_bonds(const ObjectMolecule* I, const CoordSet* cs,
    int n_atom, const int* atix, const int* neighbor, const float* dir,
    float cutoff)
{
  int a, i;
  for(i = 0; i < n_atom; i++) {
    int a1 = atix[i];
    a = cs->atmToIdx(a1);
    if(a >= 0) {
      const float* v0 = cs->coordPtr(a);
      int n = neighbor[a1] + 1;
      int a2;
      while((a2 = neighbor[n]) >= 0) {
        n += 2;
        a = cs->atmToIdx(a2);
        if(a >= 0) {
          const float* v1 = cs->coordPtr(a);
          float d10[] = { 0.f, 0.f, 0.f };
          subtract3f(v1, v0, d10);
          normalize3f(d10);
          {
	    float dot = fabs(dot_product3f(d10, dir));
	    if(dot > cutoff) {
             switch (I->AtomInfo[a1].protons) {
              case cAN_C:
              case cAN_N:
              case cAN_O:
              case cAN_S:
                switch (I->AtomInfo[a2].protons) {
                case cAN_C:
                case cAN_N:
                case cAN_O:
                case cAN_S:
                  return 0;
                  break;
                }
                break;
              }
            }
          }
        }
      }
    }
  }
  return 1;
}

static float compute_avg_ring_dot_cross_fn(ObjectMolecule * I, CoordSet * cs,
                                           int n_atom, int *atix, float *dir)
{
  float result = 0.0F;
  float *v[9];
  int missing_flag = false;
  {
    int a, i;
    for(i = 0; i < n_atom; i++) {
      int a1 = atix[i];
      a = cs->atmToIdx(a1);
      if(a < 0) {
        missing_flag = true;
        break;
      } else {
        v[i] = cs->coordPtr(a);
      }
    }
  }
  if(!missing_flag) {
    int i;
    float d10[3], d21[3], cp[8][3];
    float avg = 0.0;

    v[n_atom] = v[0];
    v[n_atom + 1] = v[1];
    for(i = 0; i < n_atom; i++) {
      subtract3f(v[i + 0], v[i + 1], d10);
      subtract3f(v[i + 2], v[i + 1], d21);
      normalize3f(d10);
      normalize3f(d21);
      cross_product3f(d10, d21, cp[i]);
      normalize3f(cp[i]);
      if(i) {
        if(dot_product3f(cp[i - 1], cp[i]) < 0.0)
          invert3f(cp[i]);
      }
      add3f(cp[i], dir, dir);
    }
    copy3f(cp[0], cp[n_atom]);
    for(i = 0; i < n_atom; i++) {
      avg += dot_product3f(cp[i], cp[i + 1]);
    }
    result = avg / n_atom;
  }
  normalize3f(dir);
  return result;
}

typedef struct {
  int cyclic, planer, aromatic;
} ObservedInfo;

void ObjectMoleculeGuessValences(ObjectMolecule * I, int state, int *flag1, int *flag2,
                                 int reset)
{
  /* this a hacked 80% solution ...it will get things wrong, but it is
     better than nothing! */

  const float planer_cutoff = 0.96F;
  CoordSet *cs = nullptr;
  ObservedInfo *obs_atom = nullptr;
  ObservedInfo *obs_bond = nullptr;
  int *flag = nullptr;
  int warning1 = 0, warning2 = 0;

/* WORKAROUND of a possible -funroll-loops inlining optimizer bug in gcc 3.3.3 */

  float (*compute_avg_center_dot_cross) (ObjectMolecule *, CoordSet *, int, int *);
  float (*compute_avg_ring_dot_cross) (ObjectMolecule *, CoordSet *, int, int *, float *);

  compute_avg_center_dot_cross = compute_avg_center_dot_cross_fn;
  compute_avg_ring_dot_cross = compute_avg_ring_dot_cross_fn;


/* end WORKAROUND */

  if((state >= 0) && (state < I->NCSet)) {
    cs = I->CSet[state];
  }
  if(cs) {
    obs_atom = pymol::calloc<ObservedInfo>(I->NAtom);
    obs_bond = pymol::calloc<ObservedInfo>(I->NBond);
  }
  flag = pymol::calloc<int>(I->NAtom);
  if(flag) {
    if(!flag1) {
      int a, *flag_a = flag;
      const AtomInfoType *ai = I->AtomInfo.data();
      /* default behavior: only reset hetatm valences */
      for(a = 0; a < I->NAtom; a++) {
        *(flag_a++) = (ai++)->hetatm;
      }
    } else if(flag1 && flag2) {
      int a, *flag_a = flag, *flag1_a = flag1, *flag2_a = flag2;
      for(a = 0; a < I->NAtom; a++) {
        *(flag_a++) = (*(flag1_a++) || *(flag2_a++));
      }
    } else if(flag1) {
      int a, *flag_a = flag, *flag1_a = flag1;
      for(a = 0; a < I->NAtom; a++) {
        *(flag_a++) = *(flag1_a++);
      }
    }
  }
  if(!flag1)
    flag1 = flag;
  if(!flag2)
    flag2 = flag;
  if(reset) {
    /* reset chemistry information and bond orders for selected atoms */
    {
      int a;
      AtomInfoType *ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        if(flag[a])
          ai->chemFlag = 0;
        ai++;
      }
    }
    {
      int b;
      BondType *bi = I->Bond.data();
      for(b = 0; b < I->NBond; b++) {
        int at0 = bi->index[0];
        int at1 = bi->index[1];
        if((flag1[at0] && flag2[at1]) || (flag1[at1] && flag2[at0]))
          bi->order = 1;
        bi++;
      }
    }
  }
  if(cs && obs_bond && obs_atom && flag && flag1 && flag2) {
    int a;
    int const* const neighbor = I->getNeighborArray();
    AtomInfoType *atomInfo = I->AtomInfo.data();
    BondType *bondInfo = I->Bond.data();

    for(a = 0; a < I->NAtom; a++) {
      AtomInfoType *ai = atomInfo + a;
      if((!ai->chemFlag) && (flag[a])) {
        /*  determine whether or not atom participates in a planer system with 5 or 6 atoms */

        {
          int mem[9];
          int nbr[7];
          const int ESCAPE_MAX = 500;

          const int* atmToIdx = I->DiscreteFlag ? nullptr : cs->AtmToIdx.data();

          int escape_count = ESCAPE_MAX;    /* don't get bogged down with structures 
                                           that have unreasonable connectivity */
          mem[0] = a;
          nbr[0] = neighbor[mem[0]] + 1;
          while(((mem[1] = neighbor[nbr[0]]) >= 0) &&
                ((!atmToIdx) || (atmToIdx[mem[0]] >= 0))) {
            nbr[1] = neighbor[mem[1]] + 1;
            while(((mem[2] = neighbor[nbr[1]]) >= 0) &&
                  ((!atmToIdx) || (atmToIdx[mem[1]] >= 0))) {
              if(mem[2] != mem[0]) {
                nbr[2] = neighbor[mem[2]] + 1;
                while(((mem[3] = neighbor[nbr[2]]) >= 0) &&
                      ((!atmToIdx) || (atmToIdx[mem[2]] >= 0))) {
                  if(mem[3] != mem[1]) {
                    nbr[3] = neighbor[mem[3]] + 1;
                    while(((mem[4] = neighbor[nbr[3]]) >= 0) &&
                          ((!atmToIdx) || (atmToIdx[mem[3]] >= 0))) {
                      if((mem[4] != mem[2]) && (mem[4] != mem[1]) && (mem[4] != mem[0])) {
                        nbr[4] = neighbor[mem[4]] + 1;
                        while(((mem[5] = neighbor[nbr[4]]) >= 0) &&
                              ((!atmToIdx) || (atmToIdx[mem[4]] >= 0))) {
                          if(!(escape_count--))
                            goto escape;
                          if((mem[5] != mem[3]) && (mem[5] != mem[2])
                             && (mem[5] != mem[1])) {
                            if(mem[5] == mem[0]) {      /* five-cycle */
                              int i;
                              float dir[] = { 0.f, 0.f, 0.f } ;
                              float avg_dot_cross =
                                compute_avg_ring_dot_cross(I, cs, 5, mem, dir);
                              for(i = 0; i < 5; i++) {
                                obs_atom[mem[i]].cyclic = true;
                                obs_bond[neighbor[nbr[0] + 1]].cyclic = true;
                              }
                              if(avg_dot_cross > planer_cutoff) {
                                if(verify_planer_bonds
                                   (I, cs, 5, mem, neighbor, dir, 0.35F)) {
                                  for(i = 0; i < 5; i++) {
                                    obs_atom[mem[i]].planer = true;
                                    obs_bond[neighbor[nbr[i] + 1]].planer = true;
                                  }
                                }
                              }
                            }

                            nbr[5] = neighbor[mem[5]] + 1;
                            while(((mem[6] = neighbor[nbr[5]]) >= 0) &&
                                  ((!atmToIdx) || (atmToIdx[mem[5]] >= 0))) {
                              if((mem[6] != mem[4]) && (mem[6] != mem[3])
                                 && (mem[6] != mem[2]) && (mem[6] != mem[1])) {
                                if(mem[6] == mem[0]) {  /* six-cycle */
                                  int i;
                                  float dir[3] = { 0.f, 0.f, 0.f } ;
                                  float avg_dot_cross =
                                    compute_avg_ring_dot_cross(I, cs, 6, mem, dir);
                                  for(i = 0; i < 6; i++) {
                                    obs_atom[mem[i]].cyclic = true;
                                    obs_bond[neighbor[nbr[i] + 1]].cyclic = true;
                                  }
                                  if(avg_dot_cross > planer_cutoff) {
                                    if(verify_planer_bonds
                                       (I, cs, 6, mem, neighbor, dir, 0.35F)) {
                                      for(i = 0; i < 6; i++) {
                                        obs_atom[mem[i]].planer = true;
                                        obs_bond[neighbor[nbr[i] + 1]].planer = true;
                                      }
                                    }
                                  }
                                }
                              }
                              nbr[5] += 2;
                            }
                          }
                          nbr[4] += 2;
                        }
                      }
                      nbr[3] += 2;
                    }
                  }
                  nbr[2] += 2;
                }
              }
              nbr[1] += 2;
            }
            nbr[0] += 2;
          }

        escape:
          escape_count = ESCAPE_MAX;    /* don't get bogged down with structures 
                                           that have unreasonable connectivity */
	  warning1 = 1;
        }
      }
    }

    for(a = 0; a < I->NAtom; a++) {
      AtomInfoType *ai = atomInfo + a;
      if((!ai->chemFlag) && flag[a]) {
        ObservedInfo *ob_at = obs_atom + a;

        {
          switch (ai->protons) {
          case cAN_P:
          case cAN_S:
          case cAN_N:
          case cAN_C:
            {
              int atm[5];
              int bnd[5];
              int n = neighbor[a];
              int nn = neighbor[n++];
              if(nn > 4)
                nn = 4;
              atm[0] = a;
              {
                int i;
                for(i = 1; i <= nn; i++) {
                  atm[i] = neighbor[n];
                  bnd[i] = neighbor[n + 1];
                  n += 2;
                }
              }
              {
                int o1_at = -1, o2_at = -1, o3_at = -1, o4_at = -1;
                int o1_bd = 0, o2_bd = 0, o3_bd = 0, o4_bd = 0;
                float o1_len = 0.0F, o2_len = 0.0F;
                float n1_v[3] = { 0.0F, 0.0F, 0.0F };
                int n1_at = -1, n2_at = -1, n3_at = -1;
                int n1_bd = 0, n2_bd = 0, n3_bd = 0;
                float n1_len = 0.0F, n2_len = 0.0F, n3_len = 0.0F;
                int c1_at = -1, c2_at = -1;
                float c1_v[3] = { 0.0F, 0.0F, 0.0F };
                float *v0 = nullptr;

                {
                  int idx0 = cs->atmToIdx(a);
                  if(idx0 >= 0)
                    v0 = cs->coordPtr(idx0);
                }
                {
                  int i;
                  for(i = 1; i <= nn; i++) {
                    float *v1 = nullptr;

                    {
                      int idx1 = cs->atmToIdx(atm[i]);
                      if(idx1 >= 0)
                        v1 = cs->coordPtr(idx1);
                    }
                    if(v0 && v1) {
                      float diff[3];
                      subtract3f(v1, v0, diff);
                      {

                        float bond_len = length3f(diff);

                        switch (atomInfo[atm[i]].protons) {
                        case cAN_C:
                          if(c1_at < 0) {
                            c1_at = atm[i];
                            copy3f(diff, c1_v);
                          } else if(c2_at < 0) {
                            c2_at = atm[i];
                          }
                          break;
                        case cAN_O:
                          if(o1_at < 0) {
                            o1_at = atm[i];
                            o1_len = bond_len;
                            o1_bd = bnd[i];
                          } else if(o2_at < 0) {
                            o2_at = atm[i];
                            o2_len = bond_len;
                            o2_bd = bnd[i];
                          } else if(o3_at < 0) {
                            o3_at = atm[i];
                            o3_bd = bnd[i];
                          } else if(o4_at < 0) {
                            o4_at = atm[i];
                            o4_bd = bnd[i];
                          }
                          break;
                        case cAN_N:
                          if(n1_at < 0) {
                            copy3f(diff, n1_v);
                            n1_at = atm[i];
                            n1_len = bond_len;
                            n1_bd = bnd[i];
                          } else if(n2_at < 0) {
                            n2_at = atm[i];
                            n2_len = bond_len;
                            n2_bd = bnd[i];
                          } else if(n3_at < 0) {
                            n3_at = atm[i];
                            n3_len = bond_len;
                            n3_bd = bnd[i];
                          }
                          break;
                        }
                      }
                    }
                  }
                }
                {
                  float avg_dot_cross = 0.0F;

                  switch (ai->protons) {
                  case cAN_C:  /* planer carbons */
                    if(nn == 3) {
                      avg_dot_cross = compute_avg_center_dot_cross(I, cs, 4, atm);

                      if(avg_dot_cross > planer_cutoff) {

                        if((n1_at >= 0) && (o1_at >= 0) && (o2_at < 0)) {
                          /* simple amide? */
                          if(o1_len < 1.38F) {
                            if(neighbor[neighbor[o1_at]] == 1)
                              if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                                bondInfo[o1_bd].order = 2;
                          }
                        } else if((n1_at >= 0) && (o1_at >= 0) && (o2_at >= 0)) {
                          /* carbamyl */
                          if((o1_len < 1.38F) && (neighbor[neighbor[o1_at]] == 1) &&
                             (o2_len < 1.38F) && (neighbor[neighbor[o2_at]] == 1)) {
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 4;
                            if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                              bondInfo[o2_bd].order = 4;
                          } else if((o1_len < 1.38F) && (neighbor[neighbor[o1_at]] == 1)) {
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 2;
                          } else if((o2_len < 1.38F) && (neighbor[neighbor[o2_at]] == 1))
                            if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                              bondInfo[o2_bd].order = 2;
                        } else if((n1_at < 0) && (o1_at >= 0) && (o2_at < 0)) {
                          /* ketone */
                          if((o1_len < 1.31F) && (neighbor[neighbor[o1_at]] == 1)) {
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 2;
                          }
                        } else if((o1_at >= 0) && (o2_at >= 0) && (n1_at < 0)) {
                          /* simple carboxylate? */
                          if((o1_len < 1.38F) && (o2_len < 1.38F) &&
                             (neighbor[neighbor[o1_at]] == 1) &&
                             (neighbor[neighbor[o2_at]] == 1)) {
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 4;
                            if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                              bondInfo[o2_bd].order = 4;
                          } else if((o1_len < 1.38F) && (neighbor[neighbor[o1_at]] == 1)) {     /* esters */
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 2;
                          } else if((o2_len < 1.38F) && (neighbor[neighbor[o2_at]] == 1)) {
                            if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                              bondInfo[o2_bd].order = 2;
                          }
                        } else if((n1_at >= 0) && (n2_at >= 0) && (n3_at < 0)
                                  && (c1_at >= 0) && (n1_len < 1.43F) && (n2_len < 1.43F)
                                  && obs_atom[c1_at].planer && obs_atom[c1_at].cyclic
                                  && (!ob_at->cyclic) && (!obs_atom[n1_at].cyclic)
                                  && (!obs_atom[n2_at].cyclic)) {
                          if((flag1[a] && flag2[n1_at]) || (flag2[a] && flag1[n1_at]))
                            bondInfo[n1_bd].order = 4;
                          atomInfo[n1_at].valence = 3;
                          atomInfo[n1_at].geom = cAtomInfoPlanar;
                          atomInfo[n1_at].chemFlag = 2;
                          if((flag1[a] && flag2[n2_at]) || (flag2[a] && flag1[n2_at]))
                            bondInfo[n2_bd].order = 4;
                          atomInfo[n2_at].valence = 3;
                          atomInfo[n2_at].geom = cAtomInfoPlanar;
                          atomInfo[n2_at].chemFlag = 2;
                        } else if((n1_at >= 0) && (n2_at >= 0) && (n3_at >= 0)) {
                          /* guanido with no hydrogens */
                          if((n1_len < 1.44F) && (n2_len < 1.44F) && (n3_len < 1.44F)) {
                            if((neighbor[neighbor[n1_at]] == 1) &&
                               (neighbor[neighbor[n2_at]] == 1) &&
                               (neighbor[neighbor[n3_at]] >= 2)) {
                              if((flag1[a] && flag2[n1_at]) || (flag2[a] && flag1[n1_at]))
                                bondInfo[n1_bd].order = 4;
                              atomInfo[n1_at].valence = 3;
                              atomInfo[n1_at].geom = cAtomInfoPlanar;
                              atomInfo[n1_at].chemFlag = 2;
                              if((flag1[a] && flag2[n2_at]) || (flag2[a] && flag1[n2_at]))
                                bondInfo[n2_bd].order = 4;
                              atomInfo[n2_at].valence = 3;
                              atomInfo[n2_at].geom = cAtomInfoPlanar;
                              atomInfo[n2_at].chemFlag = 2;
                            } else if((neighbor[neighbor[n1_at]] == 1) &&
                                      (neighbor[neighbor[n2_at]] >= 2) &&
                                      (neighbor[neighbor[n3_at]] == 1)) {
                              if((flag1[a] && flag2[n1_at]) || (flag2[a] && flag1[n1_at]))
                                bondInfo[n1_bd].order = 4;
                              atomInfo[n1_at].valence = 3;
                              atomInfo[n1_at].geom = cAtomInfoPlanar;
                              atomInfo[n1_at].chemFlag = 2;
                              if((flag1[a] && flag2[n3_at]) || (flag2[a] && flag1[n3_at]))
                                bondInfo[n3_bd].order = 4;
                              atomInfo[n3_at].valence = 3;
                              atomInfo[n3_at].geom = cAtomInfoPlanar;
                              atomInfo[n3_at].chemFlag = 2;
                            } else if((neighbor[neighbor[n1_at]] >= 2) &&
                                      (neighbor[neighbor[n2_at]] == 1) &&
                                      (neighbor[neighbor[n3_at]] == 1)) {
                              if((flag1[a] && flag2[n2_at]) || (flag2[a] && flag1[n2_at]))
                                bondInfo[n2_bd].order = 4;
                              atomInfo[n2_at].valence = 3;
                              atomInfo[n2_at].geom = cAtomInfoPlanar;
                              atomInfo[n2_at].chemFlag = 2;
                              if((flag1[a] && flag2[n3_at]) || (flag2[a] && flag1[n3_at]))
                                bondInfo[n3_bd].order = 4;
                              atomInfo[n3_at].valence = 3;
                              atomInfo[n3_at].geom = cAtomInfoPlanar;
                              atomInfo[n3_at].chemFlag = 2;
                            }
                          }
                        }
                      }
                    }
                    /* any carbon */

                    /* handle imines and nitriles */

                    if((nn >= 2) && (nn <= 3) && (n1_at >= 0) && (o1_at < 0) &&
                       (n2_at < 0) && (n1_len < 1.36F) &&
                       (!ob_at->cyclic) && (!obs_bond[n1_bd].cyclic)
                       && (!obs_atom[n1_at].planer) && ((nn == 2)
                                                        || ((nn == 3)
                                                            && (avg_dot_cross >
                                                                planer_cutoff)))) {

                      float n1_dot_cross = 1.0F;
                      int n2 = neighbor[n1_at];
                      int nn2 = neighbor[n2++];

                      {         /* check nitrogen planarity */
                        int atm2[5];
                        if(nn2 > 2) {
                          nn2 = 3;
                          atm2[0] = n1_at;
                          {
                            int i2;
                            for(i2 = 1; i2 <= nn2; i2++) {
                              atm2[i2] = neighbor[n2];
                              n2 += 2;
                            }
                          }
                          n1_dot_cross = compute_avg_center_dot_cross(I, cs, 4, atm2);
                        }
                      }
                      if(n1_dot_cross > planer_cutoff) {
                        if((flag1[a] && flag2[n1_at]) || (flag2[a] && flag1[n1_at]))
                          bondInfo[n1_bd].order = 2;
                        if((n1_len < 1.24F) && (c1_at >= 0) && (nn2 == 1)) {
                          normalize3f(n1_v);
                          normalize3f(c1_v);
                          if(dot_product3f(n1_v, c1_v) < -0.9) {
                            if((flag1[a] && flag2[n1_at]) || (flag2[a] && flag1[n1_at]))
                              bondInfo[n1_bd].order = 3;
                          }
                        }
                      }
                    }
                    break;
                  case cAN_N:
                    if(nn == 3) {
                      avg_dot_cross = compute_avg_center_dot_cross(I, cs, 4, atm);

                      if((avg_dot_cross > planer_cutoff)) {
                        if((o1_at >= 0) && (o2_at >= 0) && (o3_at < 0)) {
                          /* nitro */
                          if(neighbor[neighbor[o1_at]] == 1) {
                            if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                              bondInfo[o1_bd].order = 4;
                          }
                          if(neighbor[neighbor[o2_at]] == 1) {
                            if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                              bondInfo[o2_bd].order = 4;
                          }
                        }
                      }
                    }
                    break;
                  case cAN_S:
                  case cAN_P:
                    if((o1_at >= 0) && (o2_at >= 0) && (o3_at >= 0) && (o4_at >= 0)) {
                      /* sulfate, phosphate */
                      int o1 = -1, o2 = -1, o3 = -1;
                      int a1 = 0;
                      if(neighbor[neighbor[o1_at]] == 1) {
                        o1 = o1_bd;
                        a1 = o1_at;
                      }
                      if(neighbor[neighbor[o2_at]] == 1) {
                        if(o1 < 0) {
                          o1 = o2_bd;
                          a1 = o2_at;
                        } else if(o2 < 0) {
                          o2 = o2_bd;
                        }
                      }
                      if(neighbor[neighbor[o3_at]] == 1) {
                        if(o1 < 0) {
                          o1 = o3_bd;
                          a1 = o3_at;
                        } else if(o2 < 0)
                          o2 = o3_bd;
                        else if(o3 < 0)
                          o3 = o3_bd;
                      }
                      if(neighbor[neighbor[o4_at]] == 1) {
                        if(o1 < 0) {
                          o1 = o4_bd;
                          a1 = o4_at;
                        } else if(o2 < 0)
                          o2 = o4_bd;
                        else if(o3 < 0)
                          o3 = o4_bd;
                      }
                      if(o2 >= 0) {
                        if((flag1[a] && flag2[a1]) || (flag2[a] && flag1[a1]))
                          bondInfo[o1].order = 2;
                        if(o2 == o2_bd) {
                          atomInfo[o2_at].formalCharge = -1;
                        } else if(o2 == o3_bd) {
                          atomInfo[o3_at].formalCharge = -1;
                        } else if(o2 == o4_bd) {
                          atomInfo[o4_at].formalCharge = -1;
                        }
                      }
                    } else if((o1_at >= 0) && (o2_at >= 0) && (o3_at >= 0) && (o4_at < 0)) {
                      /* sulfonamide */
                      int o1 = -1, o2 = -1;
                      int a1 = 0, a2 = 0;
                      if(neighbor[neighbor[o1_at]] == 1) {
                        o1 = o1_bd;
                        a1 = o1_at;
                      }
                      if(neighbor[neighbor[o2_at]] == 1) {
                        if(o1 < 0) {
                          o1 = o2_bd;
                          a1 = o2_at;
                        } else if(o2 < 0) {
                          o2 = o2_bd;
                          a2 = o2_at;
                        }
                      }
                      if(neighbor[neighbor[o3_at]] == 1) {
                        if(o1 < 0) {
                          o1 = o3_bd;
                          a1 = o3_at;
                        } else if(o2 < 0) {
                          o2 = o3_bd;
                          a2 = o3_at;
                        }
                      }
                      if(o1 >= 0) {
                        if((flag1[a] && flag2[a1]) || (flag2[a] && flag1[a1]))
                          bondInfo[o1].order = 2;
                      }
                      if(o2 >= 0) {
                        if((flag1[a] && flag2[a2]) || (flag2[a] && flag1[a2]))
                          bondInfo[o2].order = 2;
                      }
                    } else if((o1_at >= 0) && (o2_at >= 0) && (o3_at < 0)) {
                      /* sulphone */
                      if(neighbor[neighbor[o1_at]] == 1) {
                        if((flag1[a] && flag2[o1_at]) || (flag2[a] && flag1[o1_at]))
                          bondInfo[o1_bd].order = 2;
                      }
                      if(neighbor[neighbor[o2_at]] == 1) {
                        if((flag1[a] && flag2[o2_at]) || (flag2[a] && flag1[o2_at]))
                          bondInfo[o2_bd].order = 2;
                      }
                    }
                    break;
                  }
                }
              }
            }
          }
          /* this sets aromatic bonds for cyclic planer systems */

          if(ob_at->cyclic && ob_at->planer) {

            switch (ai->protons) {
            case cAN_C:
            case cAN_N:
            case cAN_O:
            case cAN_S:
              {
                int n, a0, b0;
                n = neighbor[a] + 1;
                while(1) {
                  a0 = neighbor[n];
                  if(a0 < 0)
                    break;
                  b0 = neighbor[n + 1];
                  n += 2;
                  if(obs_atom[a0].cyclic && obs_atom[a0].planer && obs_bond[b0].cyclic) {
                    obs_atom[a0].aromatic = true;
                    switch (I->AtomInfo[a0].protons) {
                    case cAN_C:
                    case cAN_N:
                    case cAN_O:
                    case cAN_S:
                      if((flag1[a] && flag2[a0]) || (flag2[a] && flag1[a0]))
                        I->Bond[b0].order = 4;
                      break;
                    }
                  }
                }
              }
              break;
            }
          }
        }
      }
    }

    /* now try to address some simple cases with aromatic nitrogens */
    for(a = 0; a < I->NAtom; a++) {
      AtomInfoType *ai = atomInfo + a;
      if((!ai->chemFlag) && (ai->protons == cAN_N) &&
         (ai->formalCharge == 0) && flag[a] &&
         obs_atom[a].cyclic && obs_atom[a].aromatic) {

        int n = neighbor[a];
        int nn = neighbor[n++];

        if(nn == 2) {           /* only two explicit neighbors */

          int mem[9];
          int nbr[7];
          const int ESCAPE_MAX = 500;

          const int* atmToIdx = I->DiscreteFlag ? nullptr : cs->AtmToIdx.data();

          int escape_count = ESCAPE_MAX;    /* don't get bogged down with structures 
                                           that have unreasonable connectivity */
          mem[0] = a;
          nbr[0] = neighbor[mem[0]] + 1;
          while(((mem[1] = neighbor[nbr[0]]) >= 0) &&
                ((!atmToIdx) || (atmToIdx[mem[0]] >= 0))) {
            nbr[1] = neighbor[mem[1]] + 1;
            while(((mem[2] = neighbor[nbr[1]]) >= 0) &&
                  ((!atmToIdx) || (atmToIdx[mem[1]] >= 0))) {
              if(mem[2] != mem[0]) {
                nbr[2] = neighbor[mem[2]] + 1;
                while(((mem[3] = neighbor[nbr[2]]) >= 0) &&
                      ((!atmToIdx) || (atmToIdx[mem[2]] >= 0))) {
                  if(mem[3] != mem[1]) {
                    nbr[3] = neighbor[mem[3]] + 1;
                    while(((mem[4] = neighbor[nbr[3]]) >= 0) &&
                          ((!atmToIdx) || (atmToIdx[mem[3]] >= 0))) {
                      if((mem[4] != mem[2]) && (mem[4] != mem[1]) && (mem[4] != mem[0])) {
                        nbr[4] = neighbor[mem[4]] + 1;
                        while(((mem[5] = neighbor[nbr[4]]) >= 0) &&
                              ((!atmToIdx) || (atmToIdx[mem[4]] >= 0))) {
                          if(!(escape_count--))
                            goto escape2; /* BUG FIX: need a new escape2, instead of 
					     mistakenly going back to the first escape,
					     which is in the loop above */
                          if((mem[5] != mem[3]) && (mem[5] != mem[2])
                             && (mem[5] != mem[1])) {
                            if(mem[5] == mem[0] && (!ai->chemFlag)) {

                              /* unassigned aromatic nitrogen-containing five-cycle */

                              /* c1ccnc1 becomes c1ccn[H]c1 */

                              if((atomInfo[mem[1]].protons == cAN_C) &&
                                 (atomInfo[mem[2]].protons == cAN_C) &&
                                 (atomInfo[mem[3]].protons == cAN_C) &&
                                 (atomInfo[mem[4]].protons == cAN_C) &&
                                 obs_atom[mem[1]].aromatic &&
                                 obs_atom[mem[2]].aromatic &&
                                 obs_atom[mem[3]].aromatic && obs_atom[mem[4]].aromatic) {
                                ai->valence = 3;
                                ai->chemFlag = 2;
                                ai->geom = cAtomInfoPlanar;
                              }

                              /* c1ncnc1 becomes c1n[H]cnc1 */

                              if((atomInfo[mem[1]].protons == cAN_C) &&
                                 (atomInfo[mem[2]].protons == cAN_N) &&
                                 (atomInfo[mem[2]].formalCharge == 0) &&
                                 (!atomInfo[mem[2]].chemFlag) &&
                                 (atomInfo[mem[3]].protons == cAN_C) &&
                                 (atomInfo[mem[4]].protons == cAN_C) &&
                                 obs_atom[mem[1]].aromatic &&
                                 obs_atom[mem[2]].aromatic &&
                                 obs_atom[mem[3]].aromatic && obs_atom[mem[4]].aromatic) {

                                int n2 = neighbor[mem[2]];
                                int nn2 = neighbor[n2++];
                                if(nn2 == 2) {  /* second nitrogen also ambiguous */
                                  ai->valence = 3;
                                  ai->chemFlag = 2;
                                  ai->geom = cAtomInfoPlanar;
                                }
                              }

                              /* c1cnnc1 becomes c1cn[H]nc1 */

                              if((atomInfo[mem[1]].protons == cAN_N) &&
                                 (atomInfo[mem[1]].formalCharge == 0) &&
                                 (!atomInfo[mem[1]].chemFlag) &&
                                 (atomInfo[mem[2]].protons == cAN_C) &&
                                 (atomInfo[mem[3]].protons == cAN_C) &&
                                 (atomInfo[mem[4]].protons == cAN_C) &&
                                 obs_atom[mem[1]].aromatic &&
                                 obs_atom[mem[2]].aromatic &&
                                 obs_atom[mem[3]].aromatic && obs_atom[mem[4]].aromatic) {

                                int n2 = neighbor[mem[1]];
                                int nn2 = neighbor[n2++];
                                if(nn2 == 2) {  /* second nitrogen also ambiguous */
                                  ai->valence = 3;
                                  ai->chemFlag = 2;
                                  ai->geom = cAtomInfoPlanar;
                                }
                              }

                            }
                          }
                          nbr[4] += 2;
                        }
                      }
                      nbr[3] += 2;
                    }
                  }
                  nbr[2] += 2;
                }
              }
              nbr[1] += 2;
            }
            nbr[0] += 2;
          }
        escape2:           /* BUG FIX: Need separate escape for this loop */
          escape_count = ESCAPE_MAX;    /* don't get bogged down with structures 
                                           that have unreasonable connectivity */
	  warning2 = 1;
        }
      }
    }
  }
  if (warning1 || warning2){
	  PRINTFB(I->G, FB_ObjectMolecule, FB_Blather)
	    " %s(%d,%d): Unreasonable connectivity in heteroatom,\n  unsuccessful in guessing valences.\n", __func__, warning1, warning2
	     ENDFB(I->G);
  }
  FreeP(obs_bond);
  FreeP(obs_atom);
  FreeP(flag);
}


/*========================================================================*/

void ObjectMoleculeInferChemForProtein(ObjectMolecule * I, int state)
{
  /* Infers chemical relations for a molecules under protein assumptions.
   * 
   * NOTE: this routine needs an all-atom model (with hydrogens!)
   * and it will make mistakes on non-protein atoms (if they haven't
   * already been assigned)
   */

  int changedFlag = true;

  /* first, try to find all amids and acids */
  while(changedFlag) {
    changedFlag = false;
    for (int a = 0; a < I->NAtom; a++) {
      auto const* const ai = I->AtomInfo.data() + a;
      if(ai->chemFlag) {
        if(ai->geom == cAtomInfoPlanar)
          if(ai->protons == cAN_C) {
            auto const neighbors = AtomNeighbors(I, a);
            if (neighbors.size() > 1) {
              AtomInfoType* ai1 = nullptr;
              for (auto const& neighbor : neighbors) {
                auto* const ai0 = I->AtomInfo.data() + neighbor.atm;
                if((ai0->protons == cAN_O) && (!ai0->chemFlag)) {
                  ai1 = ai0;    /* found candidate carbonyl */
                  break;
                }
              }
              if (ai1) {
                for (auto const& neighbor : neighbors) {
                  auto* const ai0 = I->AtomInfo.data() + neighbor.atm;
                  if (ai0 != ai1) {
                    if(ai0->protons == cAN_O) {
                      if(!ai0->chemFlag) {
                        ai0->chemFlag = true;   /* acid */
                        ai0->geom = cAtomInfoPlanar;
                        ai0->valence = 1;
                        ai1->chemFlag = true;
                        ai1->geom = cAtomInfoPlanar;
                        ai1->valence = 1;
                        changedFlag = true;
                        break;
                      }
                    } else if(ai0->protons == cAN_N) {
                      if(!ai0->chemFlag) {
                        ai0->chemFlag = true;   /* amide N */
                        ai0->geom = cAtomInfoPlanar;
                        ai0->valence = 3;
                        ai1->chemFlag = true;   /* amide =O */
                        ai1->geom = cAtomInfoPlanar;
                        ai1->valence = 1;
                        changedFlag = true;
                        break;
                      } else if(ai0->geom == cAtomInfoPlanar) {
                        ai1->chemFlag = true;   /* amide =O */
                        ai1->geom = cAtomInfoPlanar;
                        ai1->valence = 1;
                        changedFlag = true;
                        break;
                      }
                    }
                  }
                }
              }
            }
          }
      }
    }
  }
  /* then handle aldehydes and amines (partial amides - both missing a valence) */

  changedFlag = true;
  while(changedFlag) {
    changedFlag = false;
    for (int a = 0; a < I->NAtom; a++) {
      auto* const ai = I->AtomInfo.data() + a;
      if(!ai->chemFlag) {
        if(ai->protons == cAN_C) {
          auto const neighbors = AtomNeighbors(I, a);
          if (neighbors.size() > 1) {
            for (auto const& neighbor : neighbors) {
              auto* const ai0 = I->AtomInfo.data() + neighbor.atm;
              if((ai0->protons == cAN_O) && (!ai0->chemFlag)) { /* =O */
                ai->chemFlag = true;
                ai->geom = cAtomInfoPlanar;
                ai->valence = 1;
                ai0->chemFlag = true;
                ai0->geom = cAtomInfoPlanar;
                ai0->valence = 3;
                changedFlag = true;
                break;
              }
            }
          }
        } else if(ai->protons == cAN_N) {
          if((!ai->chemFlag) || ai->geom != cAtomInfoLinear) {
            if(ai->formalCharge == 0) {
              ai->chemFlag = true;
              ai->geom = cAtomInfoPlanar;
              ai->valence = 3;
            }
          }
        }
      }
    }
  }

}


/*========================================================================*/
/**
 * Assigns:
 * - geom
 * - valence
 * - chemFlag
 */
void ObjectMoleculeInferChemFromNeighGeom(ObjectMolecule * I, int state)
{
  /* infers chemical relations from neighbors and geometry 
   * NOTE: very limited in scope */

  int a;
  int changedFlag = true;
  int geom;
  int carbonVal[10];

  AtomInfoType *ai, *ai2;

  carbonVal[cAtomInfoTetrahedral] = 4;
  carbonVal[cAtomInfoPlanar] = 3;
  carbonVal[cAtomInfoLinear] = 2;

  while(changedFlag) {
    changedFlag = false;
    for(a = 0; a < I->NAtom; a++) {
      ai = I->AtomInfo + a;
      if(!ai->chemFlag) {
        geom = ObjectMoleculeGetAtomGeometry(I, state, a);
        switch (ai->protons) {
        case cAN_K:
          ai->chemFlag = 1;
          ai->geom = cAtomInfoNone;
          ai->valence = 0;
          break;
        case cAN_H:
        case cAN_F:
        case cAN_I:
        case cAN_Br:
          ai->chemFlag = 1;
          ai->geom = cAtomInfoSingle;
          ai->valence = 1;
          break;
        case cAN_O: {
          auto const neighbors = AtomNeighbors(I, a);
          auto const nn = neighbors.size();
          if(nn != 1) {         /* water, hydroxy, ether */
            ai->chemFlag = 1;
            ai->geom = cAtomInfoTetrahedral;
            ai->valence = 2;
          } else {              /* hydroxy or carbonyl? check carbon geometry */
            ai2 = I->AtomInfo.data() + neighbors[0].atm;
            if(ai2->chemFlag) {
              if((ai2->geom == cAtomInfoTetrahedral) || (ai2->geom == cAtomInfoLinear)) {
                ai->chemFlag = 1;       /* hydroxy */
                ai->geom = cAtomInfoTetrahedral;
                ai->valence = 2;
              }
            }
          }
          break;
        }
        case cAN_C:
          if(geom >= 0) {
            ai->geom = geom;
            ai->valence = carbonVal[geom];
            ai->chemFlag = true;
          } else {
            auto const neighbors = AtomNeighbors(I, a);
            auto const nn = neighbors.size();
            if(nn == 1) {       /* only one neighbor */
              ai2 = I->AtomInfo.data() + neighbors[0].atm;
              if(ai2->chemFlag && (ai2->geom == cAtomInfoTetrahedral)) {
                ai->chemFlag = true;    /* singleton carbon bonded to tetC must be tetC */
                ai->geom = cAtomInfoTetrahedral;
                ai->valence = 4;
              }
            }
          }
          break;
        case cAN_N:
          if(geom == cAtomInfoPlanar) {
            ai->chemFlag = true;
            ai->geom = cAtomInfoPlanar;
            ai->valence = 3;
          } else if(geom == cAtomInfoTetrahedral) {
            ai->chemFlag = true;
            ai->geom = cAtomInfoTetrahedral;
            ai->valence = 4;
          }
          break;
        case cAN_S: {
          auto const nn = AtomNeighbors(I, a).size();
          if(nn == 4) {         /* sulfone */
            ai->chemFlag = true;
            ai->geom = cAtomInfoTetrahedral;
            ai->valence = 4;
          } else if(nn == 3) {  /* suloxide */
            ai->chemFlag = true;
            ai->geom = cAtomInfoTetrahedral;
            ai->valence = 3;
          } else if(nn == 2) {  /* thioether */
            ai->chemFlag = true;
            ai->geom = cAtomInfoTetrahedral;
            ai->valence = 2;
          }
          break;
        }
        case cAN_Cl:
          ai->chemFlag = 1;
          if(ai->formalCharge == 0) {
            ai->geom = cAtomInfoSingle;
            ai->valence = 1;
          } else {
            ai->geom = cAtomInfoNone;
            ai->valence = 0;
          }
          break;
        }
        if(ai->chemFlag)
          changedFlag = true;
      }
    }
  }
}


/*========================================================================*/
/**
 * Assigns, based on valence, geom, formalCharge, and bonded atoms:
 * - hb_donor
 * - hb_acceptor
 */
void ObjectMoleculeInferHBondFromChem(ObjectMolecule * I)
{
  int a;
  AtomInfoType *ai;
  int a1;
  int has_hydro;
  /* initialize accumulators on uncategorized atoms */

  const lexborrow_t lex_pseudo = LexBorrow(I->G, "pseudo");

  ai = I->AtomInfo.data();
  for(a = 0; a < I->NAtom; a++) {
    auto const neighbors = AtomNeighbors(I, a);
    int nn = neighbors.size();
    ai->hb_donor = false;
    ai->hb_acceptor = false;

    has_hydro = (nn < ai->valence);     /* implicit hydrogens? */

    if(!has_hydro) {
      /* explicit hydrogens? */
      switch (ai->protons) {
      case cAN_N:
      case cAN_O:
        for (auto const& neighbor : neighbors) {
          a1 = neighbor.atm;
          if(I->AtomInfo[a1].protons == 1) {
            has_hydro = true;
            break;
          }
          if (I->AtomInfo[a1].name == lex_pseudo && --nn < ai->valence) {
            has_hydro = true;
            break;
          }
        }
        break;
      }
    }

    switch (ai->protons) {
      /* cat-ions, lewis acids etc. */
    case cAN_Fe:
    case cAN_Ca:
    case cAN_Cu:
    case cAN_K:
    case cAN_Na:
    case cAN_Mg:
    case cAN_Zn:
    case cAN_Hg:
    case cAN_Sr:
    case cAN_Ba:
      ai->hb_donor = true;
      break;
    case cAN_N:
      if(has_hydro)
        ai->hb_donor = true;
      else {
        int delocalized = false;
        int has_double_bond = false;
        int neighbor_has_double_bond = false;
        int mem[3];
        int nbr[3];
        int const* neighbor = I->getNeighborArray();

        mem[0] = a;
        nbr[0] = neighbor[mem[0]] + 1;
        while(((mem[1] = neighbor[nbr[0]]) >= 0)) {
          int b_order = I->Bond[neighbor[nbr[0] + 1]].order;
          if(b_order > 1) {     /* any double/triple/aromatic bonds? */
            delocalized = true;
          }
          if(b_order == 2) {
            has_double_bond = true;
          }
          nbr[1] = neighbor[mem[1]] + 1;
          while(((mem[2] = neighbor[nbr[1]]) >= 0)) {
            if(mem[2] != mem[0]) {
              int b_order2 = I->Bond[neighbor[nbr[1] + 1]].order;
              if(b_order2 == 2) {
                neighbor_has_double_bond = true;
              }
            }
            nbr[1] += 2;
          }
          nbr[0] += 2;
        }
        if((ai->formalCharge <= 0) && delocalized && (nn < 3)) {
          /* delocalized nitrogen can likely serve as an acceptor */
          ai->hb_acceptor = true;
        }
        if(delocalized && (neighbor_has_double_bond) && (!has_double_bond) &&
           (ai->geom == cAtomInfoPlanar) && (nn == 2) && (ai->formalCharge >= 0)) {
          /* there's a fair chance of a resonance structure with this nitrogen as a donor */
          ai->hb_donor = true;
        }
        if((ai->geom != cAtomInfoPlanar) && (nn == 3) && (ai->formalCharge >= 0)
           && (!delocalized)) {
          /* tertiary amine case -- assume potential donor */
          ai->hb_donor = true;
        }
      }
      break;
    case cAN_O:
      if(ai->formalCharge <= 0)
        ai->hb_acceptor = true;
      if(has_hydro)
        ai->hb_donor = true;
      else {
        int has_double_bond = false;
        int neighbor_has_aromatic_bond = false;
        int mem[3];
        int nbr[3];
        auto* const neighbor = I->getNeighborArray();

        mem[0] = a;
        nbr[0] = neighbor[mem[0]] + 1;
        while(((mem[1] = neighbor[nbr[0]]) >= 0)) {
          int b_order = I->Bond[neighbor[nbr[0] + 1]].order;
          if(b_order == 2) {
            has_double_bond = true;
          }
          nbr[1] = neighbor[mem[1]] + 1;
          while(((mem[2] = neighbor[nbr[1]]) >= 0)) {
            if(mem[2] != mem[0]) {
              int b_order2 = I->Bond[neighbor[nbr[1] + 1]].order;
              if(b_order2 == 4) {
                neighbor_has_aromatic_bond = true;
              }
            }
            nbr[1] += 2;
          }
          nbr[0] += 2;
        }

        if(has_double_bond && neighbor_has_aromatic_bond && (ai->formalCharge >= 0)) {
          /* allow for phenolic resonance structures (and the like) */
          ai->hb_donor = true;
        }
      }
      break;
    }
    ai++;
  }

}


/*========================================================================*/
/**
 * Assigns:
 * - geom
 * - valence
 * - chemFlag
 */
void ObjectMoleculeInferChemFromBonds(ObjectMolecule * I, int state)
{

  int a, b;
  const BondType *b0;
  AtomInfoType *ai, *ai0, *ai1 = nullptr;
  int a0, a1;
  int expect, order;
  int changedFlag;
  /* initialize accumulators on uncategorized atoms */

  ai = I->AtomInfo.data();
  for(a = 0; a < I->NAtom; a++) {
    if(!ai->chemFlag) {
      ai->geom = 0;
      ai->valence = 0;
    }
    ai++;
  }

  // Ignore "pseudo" atoms (Desmond virtual sites)
  const lexborrow_t lex_pseudo = LexBorrow(I->G, "pseudo");
  std::vector<unsigned char> pseudo_neighbor_count(
      lex_pseudo == LEX_BORROW_NOTFOUND ? 0 : I->NAtom);

  /* find maximum bond order for each atom */

  b0 = I->Bond;
  for(b = 0; b < I->NBond; b++) {
    a0 = b0->index[0];
    a1 = b0->index[1];
    ai0 = I->AtomInfo + a0;
    ai1 = I->AtomInfo + a1;
    order = b0->order;
    b0++;

    // count "pseudo" neighbors
    if (!pseudo_neighbor_count.empty()) {
      if (ai0->name == lex_pseudo) {
        pseudo_neighbor_count[a1] += 1;
      }
      if (ai1->name == lex_pseudo) {
        pseudo_neighbor_count[a0] += 1;
      }
    }

    if(!ai0->chemFlag) {
      if(order > ai0->geom)
        ai0->geom = order;
      ai0->valence += order;
    }
    if(!ai1->chemFlag) {
      if(order > ai1->geom)
        ai1->geom = order;
      ai1->valence += order;
    }
    if(order == 3) {
      /* override existing chemistry * this is a temp fix to a pressing problem...
         we need to rethink the chemisty assignment ordering (should bond
         information come first? */
      ai0->geom = cAtomInfoLinear;
      ai1->geom = cAtomInfoLinear;
      if(ai0->chemFlag != 2) {
        switch (ai0->protons) {
        case cAN_C:
          ai0->valence = 2;
          break;
        default:
          ai0->valence = 1;
        }
        ai0->chemFlag = true;
      }
      if(ai1->chemFlag != 2) {
        switch (ai1->protons) {
        case cAN_C:
          ai1->valence = 2;
          break;
        default:
          ai1->valence = 1;
        }
        ai1->chemFlag = true;
      }
    } else if(order == 4) {
      ai0->geom = cAtomInfoPlanar;
      ai1->geom = cAtomInfoPlanar;
      if(ai0->chemFlag != 2) {
        switch (ai0->protons) {
        case cAN_O:
          ai0->valence = 1;
          break;
        case cAN_N:
          ai0->valence = (ai0->formalCharge == 1) ? 3 : 2; // TODO wrong for neutral -NH-
          break;
        case cAN_C:
          ai0->valence = 3;
          break;
        case cAN_S:
          ai0->valence = 2;
          break;
        default:
          ai0->valence = 4;
        }
        ai0->chemFlag = true;
      }
      if(ai1->chemFlag != 2) {
        switch (ai1->protons) {
        case cAN_O:
          ai1->valence = 1;
          break;
        case cAN_N:
          ai1->valence = (ai1->formalCharge == 1) ? 3 : 2; // TODO wrong for neutral -NH-
          break;
        case cAN_C:
          ai1->valence = 3;
          break;
        default:
          ai1->valence = 1;
        }
        ai1->chemFlag = true;
      }
    }
  }

  /* now set up valences and geometries */

  ai = I->AtomInfo.data();
  for(a = 0; a < I->NAtom; a++) {
    if(!ai->chemFlag) {
      expect = AtomInfoGetExpectedValence(I->G, ai);
      int nn = AtomNeighbors(I, a).size();

      // don't count "pseudo" atom neighbors
      if (!pseudo_neighbor_count.empty()) {
        nn -= pseudo_neighbor_count[a];
      }

      if(ai->geom == 3) {
        ai->geom = cAtomInfoLinear;
        switch (ai->protons) {
        case cAN_C:
          ai->valence = 2;
          break;
        default:
          ai->valence = 1;
        }
        ai->chemFlag = true;
      } else {
        if(expect < 0)
          expect = -expect;     /* for now, just ignore this issue */
        /*      printf("%d %d %d %d\n",ai->geom,ai->valence,nn,expect); */
        if(ai->valence == expect) {     /* sum of bond orders equals valence */
          ai->chemFlag = true;
          ai->valence = nn;
          switch (ai->geom) {   /* max bond order observed */
          case 0:
            ai->geom = cAtomInfoNone;
            break;
          case 2:
            ai->geom = cAtomInfoPlanar;
            break;
          case 3:
            ai->geom = cAtomInfoLinear;
            break;
          default:
            if(expect == 1)
              ai->geom = cAtomInfoSingle;
            else
              ai->geom = cAtomInfoTetrahedral;
            break;
          }
        } else if(ai->valence < expect) {       /* missing a bond */
          ai->chemFlag = true;
          ai->valence = nn + (expect - ai->valence);
          switch (ai->geom) {
          case 2:
            ai->geom = cAtomInfoPlanar;
            break;
          case 3:
            ai->geom = cAtomInfoLinear;
            break;
          default:
            if(expect == 1)
              ai->geom = cAtomInfoSingle;
            else
              ai->geom = cAtomInfoTetrahedral;
            break;
          }
        } else if(ai->valence > expect) {
          ai->chemFlag = true;
          ai->valence = nn;
          switch (ai->geom) {
          case 2:
            ai->geom = cAtomInfoPlanar;
            break;
          case 3:
            ai->geom = cAtomInfoLinear;
            break;
          default:
            if(expect == 1)
              ai->geom = cAtomInfoSingle;
            else
              ai->geom = cAtomInfoTetrahedral;
            break;
          }
          if(nn > 3)
            ai->geom = cAtomInfoTetrahedral;
        }
      }
    }
    ai++;
  }

  /* now go through and make sure conjugated amines are planer */
  changedFlag = true;
  while(changedFlag) {
    changedFlag = false;
    ai = I->AtomInfo.data();
    for(a = 0; a < I->NAtom; a++) {
      if(ai->chemFlag) {
        if(ai->protons == cAN_N)
          if(ai->formalCharge < 1)
            if(ai->geom == cAtomInfoTetrahedral) {
              /* search for uncharged tetrahedral nitrogen */
              for (auto const& neighbor : AtomNeighbors(I, a)) {
                auto const* ai0 = I->AtomInfo.data() + neighbor.atm;
                if((ai0->chemFlag) && (ai0->geom == cAtomInfoPlanar) &&
                   ((ai0->protons == cAN_C) || (ai0->protons == cAN_N))) {
                  ai->geom = cAtomInfoPlanar;   /* found probable delocalization */
                  if(ai->formalCharge == 0)
                    ai->valence = 3;      /* just in case... */
                  changedFlag = true;
                  break;
                }
              }
            }
      }
      ai++;
    }
  }

  /* now go through and make sure conjugated anions are planer */
  changedFlag = true;
  while(changedFlag) {
    changedFlag = false;
    ai = I->AtomInfo.data();
    for(a = 0; a < I->NAtom; a++) {
      if(ai->chemFlag) {
        if(ai->protons == cAN_O)
          if(ai->formalCharge == -1)
            if((ai->geom == cAtomInfoTetrahedral) || (ai->geom == cAtomInfoSingle)) {
              /* search for anionic tetrahedral oxygen */
              for (auto const& neighbor : AtomNeighbors(I, a)) {
                auto const* ai0 = I->AtomInfo + neighbor.atm;
                if((ai0->chemFlag) && (ai0->geom == cAtomInfoPlanar) &&
                   ((ai0->protons == cAN_C) || (ai0->protons == cAN_N))) {
                  ai->geom = cAtomInfoPlanar;   /* found probable delocalization */
                  changedFlag = true;
                  break;
                }
              }
            }
      }
      ai++;
    }
  }

}


/*========================================================================*/
int ObjectMoleculeTransformSelection(ObjectMolecule * I, int state,
                                     int sele, const float *matrix, int log,
                                     const char *sname, int homogenous, int global)
{
  /* called from "translate [5,5,5], objSele" */
  /* if sele == -1, then the whole object state is transformed */
  PyMOLGlobals *G = I->G;
  int a, s;
  int flag = false;
  CoordSet *cs;
  const AtomInfoType *ai;
  int logging;
  int all_states = false, inp_state;
  int ok = true;
  float homo_matrix[16], tmp_matrix[16];
  const float* input_matrix = matrix;

  inp_state = state;
  if(state == -2)
    state = ObjectGetCurrentState(I, false);
  if(state < 0) {
    all_states = true;
    state = -1;
  }
  PRINTFD(G, FB_ObjectMolecule)
    "ObjMolTransSele-Debug: state %d\n", state ENDFD;
  while(1) {
    if(all_states) {
      state++;
      if(state >= I->NCSet)
        break;
    }
    if(state < I->NCSet) {
      cs = I->CSet[state];
      if(cs) {
        int use_matrices = SettingGet_i(G, I->Setting.get(),
                                        nullptr, cSetting_matrix_mode);
        if(use_matrices<0) use_matrices = 0;

        if(global &&!homogenous) {      /* convert matrix to homogenous */
          convertTTTfR44f(matrix, homo_matrix);
          matrix = homo_matrix;
          input_matrix = homo_matrix;
          homogenous = true;
        }

        if(global &&((use_matrices && !cs->Matrix.empty()) || I->TTTFlag)) {
          /* if input coordinates are in the global system,
             they may need to be converted to local coordinates */

          matrix = input_matrix;

          /* global to object */

          if(I->TTTFlag) {
            float ttt[16];
            if(matrix != tmp_matrix) {
              copy44f(matrix, tmp_matrix);
            }
            convertTTTfR44f(I->TTT, ttt);
            {
              float ttt_inv[16];
              invert_special44f44f(ttt, ttt_inv);
              left_multiply44f44f(ttt_inv, tmp_matrix);
              right_multiply44f44f(tmp_matrix, ttt);
            }
            matrix = tmp_matrix;
          }

          /* object to state */

          if(use_matrices) {
            if(!cs->Matrix.empty()) {
              double tmp_double[16], tmp_inv[16];
              copy44f44d(matrix, tmp_double);
              invert_special44d44d(cs->Matrix.data(), tmp_inv);
              left_multiply44d44d(tmp_inv, tmp_double);
              right_multiply44d44d(tmp_double, cs->Matrix.data());
              copy44d44f(tmp_double, tmp_matrix);
              matrix = tmp_matrix;
            }
          }
        }

        if(sele >= 0) {         /* transforming select atoms */
          ai = I->AtomInfo;
          for(a = 0; a < I->NAtom; a++) {
            s = ai->selEntry;
            if(!(ai->protekted == cAtomProtected_explicit))
              if(SelectorIsMember(G, s, sele)) {
                if(homogenous)
                  CoordSetTransformAtomR44f(cs, a, matrix);
                else
                  CoordSetTransformAtomTTTf(cs, a, matrix);
                flag = true;
              }
            ai++;
          }
        } else {                /* transforming whole coordinate set */
          if(!use_matrices) {
            ai = I->AtomInfo;
            for(a = 0; a < I->NAtom; a++) {
              if(!(ai->protekted == cAtomProtected_explicit)) {
                if(homogenous)
                  CoordSetTransformAtomR44f(cs, a, matrix);
                else
                  CoordSetTransformAtomTTTf(cs, a, matrix);
              }
              ai++;
            }
            flag = true;
            CoordSetRecordTxfApplied(cs, matrix, homogenous);
          } else {
            ObjectMoleculeTransformState44f(I, state, matrix, false, homogenous, false);
          }
        }
        if(flag) {
          cs->invalidateRep(cRepAll, cRepInvCoord);
          ExecutiveUpdateCoordDepends(G, I);
        }
      }
    }
    if(!all_states)
      break;
  }

  if(log) {
    OrthoLineType line;
    WordType sele_str = ",'";
    logging = SettingGetGlobal_i(G, cSetting_logging);
    if(sele >= 0) {
      strcat(sele_str, sname);
    }
    strcat(sele_str, "'");
    switch (logging) {
    case cPLog_pml:
      sprintf(line,
              "_ cmd.transform_object('%s',[\\\n_ %15.9f,%15.9f,%15.9f,%15.9f,\\\n_ %15.9f,%15.9f,%15.9f,%15.9f,\\\n_ %15.9f,%15.9f,%15.9f,%15.9f,\\\n_ %15.9f,%15.9f,%15.9f,%15.9f\\\n_     ],%d,%d%s,%d)\n",
              I->Name,
              matrix[0], matrix[1], matrix[2], matrix[3],
              matrix[4], matrix[5], matrix[6], matrix[7],
              matrix[8], matrix[9], matrix[10], matrix[11],
              matrix[12], matrix[13], matrix[14], matrix[15],
              inp_state + 1, 0, sele_str, homogenous);
      PLog(G, line, cPLog_no_flush);
      break;
    case cPLog_pym:

      sprintf(line,
              "cmd.transform_object('%s',[\n%15.9f,%15.9f,%15.9f,%15.9f,\n%15.9f,%15.9f,%15.9f,%15.9f,\n%15.9f,%15.9f,%15.9f,%15.9f,\n%15.9f,%15.9f,%15.9f,%15.9f\n],%d,%d%s,%d)\n",
              I->Name,
              matrix[0], matrix[1], matrix[2], matrix[3],
              matrix[4], matrix[5], matrix[6], matrix[7],
              matrix[8], matrix[9], matrix[10], matrix[11],
              matrix[12], matrix[13], matrix[14], matrix[15],
              inp_state + 1, 0, sele_str, homogenous);
      PLog(G, line, cPLog_no_flush);
      break;
    default:
      break;
    }
  }
  return (ok);
}


/*========================================================================*/
int ObjectMoleculeGetAtomIndex(const ObjectMolecule* I, SelectorID_t sele)
{
  if(sele < 0)
    return (-1);

  for (int a = 0; a < I->NAtom; a++) {
    auto const s = I->AtomInfo[a].selEntry;
    if(SelectorIsMember(I->G, s, sele))
      return (a);
  }
  return (-1);
}


/*========================================================================*/
/**
 * Update all AtomInfoType.bonded
 *
 * Cost: O(NAtom + NBond)
 */
void ObjectMoleculeUpdateNonbonded(ObjectMolecule * I)
{
  int a;
  const BondType *b;
  AtomInfoType *ai;
  int nAtom = I->NAtom;
  int nBond = I->NBond;

  ai = I->AtomInfo.data();

  for(a = 0; a < nAtom; a++)
    (ai++)->bonded = false;

  b = I->Bond;
  ai = I->AtomInfo.data();
  for(a = 0; a < nBond; a++) {
    ai[b->index[0]].bonded = true;
    ai[b->index[1]].bonded = true;
    b++;
  }
}


/**
 * Get the raw ObjectMolecule::Neighbor storage structure, which is generated
 * from the ObjectMolecule::Bond array. This structure is cached, to clear the
 * cache, call ObjectMolecule::invalidate(level=cRepInvBonds).
 *
 * Direct usage of this array should be avoided, use the AtomNeighbors() class
 * instead which is a higher-level and more readable interface to the same
 * data.
 *
 * Changed in PyMOL 2.1.1: Ignore zero-order bonds (PYMOL-3025)
 *
 * @return Pointer to cached array, or nullptr if memory allocation failed
 *
 * @verbatim
     0       list offset for atom 0 = n
     1       list offset for atom 1 = n + m + 1
     ...
     n-1     list offset for atom n-1

     n       count for atom 0 
     n+1     neighbor of atom 0
     n+2     bond index
     n+3     neighbor of atom 0
     n+4     bond index
     ...
     n+m     -1 terminator for atom 0

     n+m+1   count for atom 1
     n+m+2   neighbor of atom 1
     n+m+3   bond index
     n+m+4   neighbor of atom 1
     n+m+5   bond index
     etc.

     NOTE: all atoms have an offset and a terminator whether or not they have any bonds:

     Here's an example of an ALA
     [ offet for atom1, offset for atom2, ..., offset for atom9, \
     {10, 16, 26, 32, 36, 46, 50, 54, 58, 62, \
     (atom1)  nbonds=2, neighbor index1=5, bond index=1, neighbor index2=1, bond index=0, null terminator=-1 \
     2, 5, 1, 1, 0, -1, 
     (atom2) nbonds=4, nbr index1=6, bond id=4; nbr idx2=4, bond idx=3; nbr index3=2, bond idx=2; nbr index=0, bond index=0, null=-1 \
     4, 6, 4, 4, 3, 2, 2, 0, 0, -1,
     (atom3) ...
     2, 3, 5, 1, 2, -1, 1, 2, 5, -1,
     4, 9, 8, 8, 7, 7, 6, 1, 3, -1,
     1, 0, 1, -1,
     1, 1, 4, -1,
     1, 4, 6, -1,
     1, 4, 7, -1,
     1, 4, 8, -1 }
   @endverbatim

   */
int const* ObjectMolecule::getNeighborArray() const
{
  auto const I = this;

  /* If no neighbors have been calculated, fill in the table */
  if(!I->Neighbor) {

    /* Create/check the VLA */
    auto const size = (I->NAtom * 3) + (I->NBond * 4);

    const_cast<ObjectMolecule*>(this)->Neighbor.reset(new int[size]);
    auto* const neighbor = this->Neighbor.get();

    p_return_val_if_fail(neighbor, nullptr);

    /* initialize; zero out neighbors */
    std::fill_n(neighbor, I->NAtom, 0);

    /* count neighbors for each atom */
    const auto* bnd = I->Bond.data();
    for(int b = 0; b < I->NBond; b++) {
      if (bnd->order && !bnd->hasSymOp()) {
        neighbor[bnd->index[0]]++;
        neighbor[bnd->index[1]]++;
      }
      bnd++;
    }

    /* set up offsets and list terminators */
    auto c = I->NAtom;
    for (int a = 0; a < I->NAtom; a++) {
      auto const d = neighbor[a];       /* get number of neighbors */
      neighbor[c] = d;       /* store neighbor count */
      neighbor[a] = c + d + d + 1;   /* set initial position to end of list, we'll fill backwards */
      neighbor[neighbor[a]] = -1; /* store terminator */
      c += d + d + 2;
    }

    /* now load neighbors in a sequential list for each atom (reverse order) */
    bnd = I->Bond.data();
    for(int b = 0; b < I->NBond; b++, ++bnd) {
      auto const l0 = bnd->index[0];
      auto const l1 = bnd->index[1];

      if (!bnd->order || bnd->hasSymOp())
        continue;

      neighbor[l0]--;
      neighbor[neighbor[l0]] = b; /* store bond indices (for I->Bond) */
      neighbor[l0]--;
      neighbor[neighbor[l0]] = l1;        /* store neighbor references (I->AtomInfo, etc.) */

      neighbor[l1]--;
      neighbor[neighbor[l1]] = b; /* store bond indices (for I->Bond) */
      neighbor[l1]--;
      neighbor[neighbor[l1]] = l0;        /* store neighbor references (I->AtomInfo, etc.) */
    }

    // adjust down to point to the count, not the first entry
    for (int a = 0; a < I->NAtom; a++) {
      if (neighbor[a] >= 0)
        --neighbor[a];
    }
  }
  return this->Neighbor.get();
}


/*========================================================================*/
#ifndef _PYMOL_NOPY
static CoordSet *ObjectMoleculeChemPyModel2CoordSet(PyMOLGlobals * G,
                                                    PyObject * model,
                                                    AtomInfoType ** atInfoPtr)
{
  int nAtom, nBond;
  int a, c;
  float *coord = nullptr;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr, *ai;
  float *f;
  BondType *ii, *bond = nullptr;
  int ok = true;
  int auto_show = RepGetAutoShowMask(G);
  int hetatm;
  int ignore_ids;
  PyObject *atomList = nullptr;
  PyObject *bondList = nullptr;
  PyObject *atom = nullptr;
  PyObject *bnd = nullptr;
  PyObject *index = nullptr;
  PyObject *crd = nullptr;
  PyObject *tmp = nullptr;

  ignore_ids = !SettingGetGlobal_b(G, cSetting_preserve_chempy_ids);

  nAtom = 0;
  nBond = 0;
  if(atInfoPtr)
    atInfo = *atInfoPtr;

  atomList = PyObject_GetAttrString(model, "atom");
  if(atomList && PyList_Check(atomList))
    nAtom = PyList_Size(atomList);
  else
    ok = ErrMessage(G, __func__, "can't get atom list");

  if(ok) {
    coord = VLAlloc(float, 3 * nAtom);
    if(atInfo)
      VLACheck(atInfo, AtomInfoType, nAtom);
  }

  if(ok) {

    f = coord;
    for(a = 0; a < nAtom; a++) {

      atom = PyList_GetItem(atomList, a);
      if(!atom)
        ok = ErrMessage(G, __func__, "can't get atom");
      crd = PyObject_GetAttrString(atom, "coord");
      if(!crd)
        ok = ErrMessage(G, __func__, "can't get coordinates");
      else {
        for(c = 0; c < 3; c++) {
          tmp = PySequence_GetItem(crd, c);
          if(tmp)
            ok = PConvPyObjectToFloat(tmp, f++);
          Py_XDECREF(tmp);
          if(!ok) {
            ErrMessage(G, __func__, "can't read coordinates");
            break;
          }
        }
      }
      Py_XDECREF(crd);

      ai = atInfo + a;
      ai->id = a;               /* chempy models are zero-based */
      if(!ignore_ids) {
        if(ok) {                /* get chempy atom id if extant */
          if(PTruthCallStr(atom, "has", "id")) {
            tmp = PyObject_GetAttrString(atom, "id");
            if(tmp)
              ok = PConvPyObjectToInt(tmp, &ai->id);
            if(!ok)
              ErrMessage(G, __func__,
                         "can't read atom identifier");
            Py_XDECREF(tmp);
          } else {
            ai->id = -1;
          }
        }
      }
      ai->rank = a;             /* ranks are always zero-based */

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "name");
        if(tmp) {
          WordType tmp_word;

          ok = PConvPyObjectToStrMaxClean(tmp, tmp_word, sizeof(WordType) - 1);
          AtomInfoCleanAtomName(tmp_word);
          ai->name = LexIdx(G, tmp_word);
        }
        if(!ok)
          ErrMessage(G, __func__, "can't read name");
        Py_XDECREF(tmp);
      }

      if(ok) {
        ai->textType = 0;
        if(PTruthCallStr(atom, "has", "text_type")) {
          tmp = PyObject_GetAttrString(atom, "text_type");
          if(tmp) {
            OrthoLineType temp;
            ok = PConvPyObjectToStrMaxClean(tmp, temp, sizeof(OrthoLineType) - 1);
            ai->textType = LexIdx(G, temp);
          }
          if(!ok)
            ErrMessage(G, __func__, "can't read text_type");
          Py_XDECREF(tmp);
        }
      }

      if(ok) {
        ai->custom = 0;
        if(PTruthCallStr(atom, "has", "custom")) {
          tmp = PyObject_GetAttrString(atom, "custom");
          if(tmp) {
            OrthoLineType temp;
            ok = PConvPyObjectToStrMaxClean(tmp, temp, sizeof(OrthoLineType) - 1);
            ai->custom = LexIdx(G, temp);
          }
          if(!ok)
            ErrMessage(G, __func__, "can't read custom");
          Py_XDECREF(tmp);
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "vdw")) {
          tmp = PyObject_GetAttrString(atom, "vdw");
          if(tmp)
            ok = PConvPyObjectToFloat(tmp, &ai->vdw);
          if(!ok)
            ErrMessage(G, __func__, "can't read vdw radius");
          Py_XDECREF(tmp);
        } else {
          ai->vdw = 0.0f;
        }
      }
      if(ok) {
        if(PTruthCallStr(atom, "has", "bohr")) {
          tmp = PyObject_GetAttrString(atom, "bohr");
          if(tmp)
            ok = PConvPyObjectToFloat(tmp, &ai->elec_radius);
          if(!ok)
            ErrMessage(G, __func__,
                       "can't read elec. radius");
          Py_XDECREF(tmp);
        } else {
          ai->elec_radius = 0.0F;
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "stereo")) {
          tmp = PyObject_GetAttrString(atom, "stereo");
          if(tmp) {
            char tmp_char;
            ok = PConvPyObjectToChar(tmp, &tmp_char);
            ai->stereo = tmp_char;
          }
          if(!ok)
            ErrMessage(G, __func__, "can't read stereo");
          Py_XDECREF(tmp);
        } else {
          ai->stereo = 0;
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "numeric_type")) {
          tmp = PyObject_GetAttrString(atom, "numeric_type");
          if(tmp)
            ok = PConvPyObjectToInt(tmp, &ai->customType);
          if(!ok)
            ErrMessage(G, __func__,
                       "can't read numeric_type");
          Py_XDECREF(tmp);
        } else {
          ai->customType = cAtomInfoNoType;
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "formal_charge")) {
          tmp = PyObject_GetAttrString(atom, "formal_charge");
          if(tmp)
            ok = PConvPyObjectToChar(tmp, (char *) &ai->formalCharge);
          if(!ok)
            ErrMessage(G, __func__,
                       "can't read formal_charge");
          Py_XDECREF(tmp);
        } else {
          ai->formalCharge = 0;
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "partial_charge")) {
          tmp = PyObject_GetAttrString(atom, "partial_charge");
          if(tmp)
            ok = PConvPyObjectToFloat(tmp, &ai->partialCharge);
          if(!ok)
            ErrMessage(G, __func__,
                       "can't read partial_charge");
          Py_XDECREF(tmp);
        } else {
          ai->partialCharge = 0.0;
        }
      }

      if(ok) {
        if(PTruthCallStr(atom, "has", "flags")) {
          tmp = PyObject_GetAttrString(atom, "flags");
          if(tmp)
            ok = PConvPyObjectToInt(tmp, (int *) &ai->flags);
          if(!ok)
            ErrMessage(G, __func__, "can't read flags");
          Py_XDECREF(tmp);
        } else {
          ai->flags = 0;
        }
      }

      if(ok) {
        char buf[8] = "";
        tmp = PyObject_GetAttrString(atom, "resn");
        if(tmp)
          ok = PConvPyObjectToStrMaxClean(tmp, buf, sizeof(buf) - 1);
        ai->resn = LexIdx(G, buf);
        if(!ok)
          ErrMessage(G, __func__, "can't read resn");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "ins_code");
        if(tmp) {
          ResIdent tmp_ins_code;
          ok = PConvPyObjectToStrMaxClean(tmp, tmp_ins_code, sizeof(ResIdent) - 1);
          if(!ok)
            ErrMessage(G, __func__, "can't read ins_code");
          else if(tmp_ins_code[0] != '?') {
            ai->setInscode(tmp_ins_code[0]);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok) {
        if(PTruthCallStr(atom, "has", "resi_number")) {
          tmp = PyObject_GetAttrString(atom, "resi_number");
          if(tmp)
            ok = PConvPyObjectToInt(tmp, &ai->resv);
          if(!ok)
            ErrMessage(G, __func__, "can't read resi_number");
          Py_XDECREF(tmp);
        }
      }

      if(ok) {
        OrthoLineType temp = "";
        tmp = PyObject_GetAttrString(atom, "segi");
        if(tmp)
          PConvPyObjectToStrMaxClean(tmp, temp, sizeof(OrthoLineType) - 1);
        ai->segi = LexIdx(G, temp);
        if(!ok)
          ErrMessage(G, __func__, "can't read segi");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "b");
        if(tmp)
          ok = PConvPyObjectToFloat(tmp, &ai->b);
        if(!ok)
          ErrMessage(G, __func__, "can't read b value");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "u");
        if(tmp) {
          ok = PConvPyObjectToFloat(tmp, &ai->b);
          if(!ok)
            ErrMessage(G, __func__, "can't read u value");
          else
            ai->b *= (8 * cPI * cPI);   /* B-factor = 8 pi^2 U-factor */
        } else {
          assert(PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_AttributeError));
          PyErr_Clear();
        }
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "u_aniso");
        if(tmp) {
          float u[6];
          if(PConvPyListToFloatArrayInPlace(tmp, u, 6)) {
            // only allocate if not all zero
            if(u[0] || u[1] || u[2] || u[3] || u[4] || u[5])
              std::copy_n(u, 6, ai->get_anisou());
          }
          Py_DECREF(tmp);
        } else {
          assert(PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_AttributeError));
          PyErr_Clear();
        }
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "q");
        if(tmp)
          ok = PConvPyObjectToFloat(tmp, &ai->q);
        if(!ok)
          ErrMessage(G, __func__, "can't read occupancy");
        Py_XDECREF(tmp);
      }

      if(ok) {
        OrthoLineType temp = "";
        tmp = PyObject_GetAttrString(atom, "chain");
        if(tmp)
          PConvPyObjectToStrMaxClean(tmp, temp, sizeof(OrthoLineType) - 1);
        ai->chain = LexIdx(G, temp);
        if(!ok)
          ErrMessage(G, __func__, "can't read chain");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "hetatm");
        if(tmp)
          ok = PConvPyObjectToInt(tmp, &hetatm);
        if(!ok)
          ErrMessage(G, __func__, "can't read hetatm");
        else {
          ai->hetatm = hetatm;
          if(!PTruthCallStr(atom, "has", "flags")) {
            if(ai->hetatm)
              ai->flags = cAtomFlag_ignore;
          }
        }
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "alt");
        if(tmp)
          ok = PConvPyObjectToStrMaxClean(tmp, ai->alt, sizeof(Chain) - 1);
        if(!ok)
          ErrMessage(G, __func__,
                     "can't read alternate conformation");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "symbol");
        if(tmp)
          ok = PConvPyObjectToStrMaxClean(tmp, ai->elem, sizeof(ElemName) - 1);
        if(!ok)
          ErrMessage(G, __func__, "can't read symbol");
        Py_XDECREF(tmp);
      }

      if(ok) {
        tmp = PyObject_GetAttrString(atom, "ss");
        if(tmp)
          ok = PConvPyObjectToStrMaxClean(tmp, ai->ssType, sizeof(SSType) - 1);
        if(!ok)
          ErrMessage(G, __func__,
                     "can't read secondary structure");
        Py_XDECREF(tmp);
      }

      if(ok && PyObject_HasAttrString(atom, "label")) {
        tmp = PyObject_GetAttrString(atom, "label");
        if(tmp) {
          if (PyString_Check(tmp))
            ai->label = LexIdx(G, PyString_AsString(tmp));
        }
        Py_XDECREF(tmp);
      }

      if(ok && PyObject_HasAttrString(atom, "sphere_scale")) {
        tmp = PyObject_GetAttrString(atom, "sphere_scale");
        if(tmp) {
          float value;
          if(PConvPyFloatToFloat(tmp, &value)) {
            SettingSet(G, cSetting_sphere_scale, value, ai);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok && PyObject_HasAttrString(atom, "cartoon_color")) {
        tmp = PyObject_GetAttrString(atom, "cartoon_color");
        if(tmp) {
          int color_index;
          ok = PConvPyObjectToInt(tmp, &color_index);
          if(!ok)
            ErrMessage(G, __func__, "bad cartoon color info");
          else {
            SettingSet(G, cSetting_cartoon_color, color_index, ai);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok && PyObject_HasAttrString(atom, "cartoon_transparency")) {
        tmp = PyObject_GetAttrString(atom, "cartoon_transparency");
        if(tmp) {
          float alpha_val;
          ok = PConvPyObjectToFloat(tmp, &alpha_val);
          if(!ok)
            ErrMessage(G, __FUNCTION__, "bad alpha value");
          else {
            SettingSet(G, cSetting_cartoon_transparency, alpha_val, ai);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok && PyObject_HasAttrString(atom, "cartoon_trgb")) {
        tmp = PyObject_GetAttrString(atom, "cartoon_trgb");
        if(tmp) {
          unsigned int trgb;
          ok = PConvPyObjectToInt(tmp, (signed int *) &trgb);
          if(!ok)
            ErrMessage(G, __func__, "bad cartoon color info");
          else {
            char color_name[24];
            sprintf(color_name, "0x%08x", trgb);
            SettingSet(G, cSetting_cartoon_color, ColorGetIndex(G, color_name), ai);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok && PyObject_HasAttrString(atom, "label_trgb")) {
        tmp = PyObject_GetAttrString(atom, "label_trgb");
        if(tmp) {
          unsigned int trgb;
          ok = PConvPyObjectToInt(tmp, (signed int *) &trgb);
          if(!ok)
            ErrMessage(G, __func__, "bad label color info");
          else {
            char color_name[24];
            sprintf(color_name, "0x%08x", trgb);
            SettingSet(G, cSetting_label_color, ColorGetIndex(G, color_name), ai);
          }
        }
        Py_XDECREF(tmp);
      }
      if(ok && PyObject_HasAttrString(atom, "ribbon_color")) {
        tmp = PyObject_GetAttrString(atom, "ribbon_color");
        if(tmp) {
          int color_index;
          ok = PConvPyObjectToInt(tmp, &color_index);
          if(!ok)
            ErrMessage(G, __func__, "bad ribbon color info");
          else {
            SettingSet(G, cSetting_ribbon_color, color_index, ai);
          }
        }
        Py_XDECREF(tmp);
      }

      if(ok && PyObject_HasAttrString(atom, "ribbon_trgb")) {
        tmp = PyObject_GetAttrString(atom, "ribbon_trgb");
        if(tmp) {
          unsigned int trgb;
          ok = PConvPyObjectToInt(tmp, (signed int *) &trgb);
          if(!ok)
            ErrMessage(G, __func__, "bad cartoon color info");
          else {
            char color_name[24];
            sprintf(color_name, "0x%08x", trgb);
            SettingSet(G, cSetting_ribbon_color, ColorGetIndex(G, color_name), ai);
          }
        }
        Py_XDECREF(tmp);
      }

      atInfo[a].visRep = auto_show;

      if(ok && PyObject_HasAttrString(atom, "visible")) {
        unsigned int vis;
        tmp = PyObject_GetAttrString(atom, "visible");
        if(tmp) {
          ok = PConvPyObjectToInt(tmp, (signed int *) &vis);
          if(!ok)
            ErrMessage(G, __func__, "bad visibility info");
          else {
            atInfo[a].visRep = vis;

            if (!(ai->flags & cAtomFlag_class)) {
              ai->flags |= cAtomFlag_inorganic; // suppress auto_show_classified
            }
          }
        }
        Py_XDECREF(tmp);
      }

      if(ok && atInfo) {
        AtomInfoAssignParameters(G, ai);
        AtomInfoAssignColors(G, ai);
      }

      if(ok && PyObject_HasAttrString(atom, "trgb")) {
        /* were we given a Transparency-Red-Blue-Green value? */
        unsigned int trgb;
        tmp = PyObject_GetAttrString(atom, "trgb");
        if(tmp) {
          ok = PConvPyObjectToInt(tmp, (signed int *) &trgb);
          if(!ok)
            ErrMessage(G, __func__, "bad color info");
          else {
            char color_name[24];
            sprintf(color_name, "0x%08x", trgb);
	    atInfo[a].color = ColorGetIndex(G, color_name);
          }
          Py_DECREF(tmp);
        }
      }

      if(!ok)
        break;

      assert(!PyErr_Occurred());
    }
  }

  if(nAtom) {
    bondList = PyObject_GetAttrString(model, "bond");
    if(bondList && PyList_Check(bondList))
      nBond = PyList_Size(bondList);
    else
      ok = ErrMessage(G, __func__, "can't get bond list");

    if(ok) {
      bond = VLACalloc(BondType, nBond);
      ii = bond;
      for(a = 0; a < nBond; a++) {
        bnd = PyList_GetItem(bondList, a);
        if(!bnd)
          ok = ErrMessage(G, __func__, "can't get bond");
        index = PyObject_GetAttrString(bnd, "index");
        if(!index)
          ok =
            ErrMessage(G, __func__, "can't get bond indices");
        else {
          for(c = 0; c < 2; c++) {
            tmp = PyList_GetItem(index, c);
            if(tmp)
              ok = PConvPyObjectToInt(tmp, &ii->index[c]);
            if(!ok) {
              ErrMessage(G, __func__,
                         "can't read coordinates");
              break;
            }
          }
        }
        if(ok) {
          int order = 0;
          tmp = PyObject_GetAttrString(bnd, "order");
          if(tmp)
            ok = PConvPyObjectToInt(tmp, &order);
          if(!ok)
            ErrMessage(G, __func__, "can't read bond order");
          Py_XDECREF(tmp);
          ii->order = order;
        }

        auto const set_symop = [&](const char* key) {
          if (ok && PyObject_HasAttrString(bnd, key)) {
            ok = false;
            if (auto tmp = PyObject_GetAttrString(bnd, key)) {
              if (auto const* code = PyUnicode_AsUTF8(tmp)) {
                ii->symop_2.reset(code);
                ok = true;
              }
              Py_DECREF(tmp);
            }
          }
        };

        set_symop("symmetry_2");

        if(ok && PyObject_HasAttrString(bnd, "stick_radius")) {
          tmp = PyObject_GetAttrString(bnd, "stick_radius");
          if(tmp) {
            float value;
            if(PConvPyFloatToFloat(tmp, &value)) {
              SettingSet(G, cSetting_stick_radius, value, ii);
            }
          }
          Py_XDECREF(tmp);
        }
        if(ok && PyObject_HasAttrString(bnd, "valence")) {
          tmp = PyObject_GetAttrString(bnd, "valence");
          if(tmp) {
            int value;
            if(PConvPyIntToInt(tmp, &value)) {
              SettingSet(G, cSetting_valence, value, ii);
            }
          }
          Py_XDECREF(tmp);
        }

        Py_XDECREF(index);
        ii++;
      }
    }
  }

  Py_XDECREF(atomList);
  Py_XDECREF(bondList);

  if(ok) {
    cset = CoordSetNew(G);
    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->NTmpBond = nBond;
    cset->TmpBond = pymol::vla_take_ownership(bond);

#ifdef _PYMOL_IP_PROPERTIES
#endif
  } else {
    VLAFreeP(bond);
    VLAFreeP(coord);
  }
  if(atInfoPtr)
    *atInfoPtr = atInfo;

  if(PyErr_Occurred())
    PyErr_Print();
  return (cset);
}
#endif


/*========================================================================*/
ObjectMolecule *ObjectMoleculeLoadChemPyModel(PyMOLGlobals * G,
                                              ObjectMolecule * I,
                                              PyObject * model, int frame, int discrete)
{
#ifdef _PYMOL_NOPY
  return nullptr;
#else
  CoordSet *cset = nullptr;
  auto atInfo = pymol::vla<AtomInfoType>(10);
  int ok = true;
  int isNew = true;
  unsigned int nAtom = 0;
  int fractional = false;
  int connect_mode = -1;
  int auto_bond = false;
  PyObject *tmp, *mol;

  if(!I)
    isNew = true;
  else
    isNew = false;

  if(ok) {

    if(isNew) {
      I = (ObjectMolecule *) new ObjectMolecule(G, discrete);
      std::swap(atInfo, I->AtomInfo);
      isNew = true;
    } else {
      if (discrete > -1)
        ObjectMoleculeSetDiscrete(G, I, discrete);
    }

    if(isNew) {
      I->Color = AtomInfoUpdateAutoColor(G);
    }
    cset = ObjectMoleculeChemPyModel2CoordSet(G, model, &atInfo);

    if(!cset)
      ok = false;
    else {
      mol = PyObject_GetAttrString(model, "molecule");
      if(mol) {
        if(PyObject_HasAttrString(mol, "title")) {
          tmp = PyObject_GetAttrString(mol, "title");
          if(tmp) {
            UtilNCopy(cset->Name, PyString_AsString(tmp), sizeof(WordType));
            Py_DECREF(tmp);
            if(!strcmp(cset->Name, "untitled")) /* ignore untitled */
              cset->Name[0] = 0;
          }
        }
        Py_DECREF(mol);
      }
      if(PyObject_HasAttrString(model, "spheroid") &&
         PyObject_HasAttrString(model, "spheroid_normals")) {
        tmp = PyObject_GetAttrString(model, "spheroid");
        if(tmp) {
          PConvFromPyObject(G, tmp, cset->Spheroid);
          Py_DECREF(tmp);
        }
        tmp = PyObject_GetAttrString(model, "spheroid_normals");
        if(tmp) {
          PConvFromPyObject(G, tmp, cset->SpheroidNormal);
          Py_DECREF(tmp);
        }
      }
      if(PyObject_HasAttrString(model, "spacegroup") &&
         PyObject_HasAttrString(model, "cell")) {
        CSymmetry *symmetry = new CSymmetry(G);
        if(symmetry) {
          tmp = PyObject_GetAttrString(model, "spacegroup");
          if(tmp) {
            const char *tmp_str = nullptr;
            if(PConvPyStrToStrPtr(tmp, &tmp_str)) {
              symmetry->setSpaceGroup(tmp_str);
            }
            Py_DECREF(tmp);
          }
          tmp = PyObject_GetAttrString(model, "cell");
          if(tmp) {
            float cell[6];
            if(PConvPyListToFloatArrayInPlace(tmp, cell, 6)) {
              symmetry->Crystal.setDims(cell);
              symmetry->Crystal.setAngles(cell + 3);
            }
            Py_DECREF(tmp);
          }
          cset->Symmetry = std::unique_ptr<CSymmetry>(symmetry);
        }
      }
      if(PyObject_HasAttrString(model, "fractional")) {
        tmp = PyObject_GetAttrString(model, "fractional");
        if(tmp) {
          int tmp_int = 0;
          if(PConvPyIntToInt(tmp, &tmp_int)) {
            fractional = tmp_int;
          }
          Py_DECREF(tmp);
        }
      }
      if(PyObject_HasAttrString(model, "connect_mode")) {
        tmp = PyObject_GetAttrString(model, "connect_mode");
        if(tmp) {
          int tmp_int = 0;
          if(PConvPyIntToInt(tmp, &tmp_int)) {
            auto_bond = true;
            connect_mode = tmp_int;
          }
          Py_DECREF(tmp);
        }
      }
      nAtom = cset->NIndex;
    }
  }
  /* include coordinate set */
  if(ok) {
    if(frame < 0)
      frame = I->NCSet;

    if(I->DiscreteFlag && atInfo) {
      unsigned int a;
      int fp1 = frame + 1;
      AtomInfoType *ai = atInfo.data();
      for(a = 0; a < nAtom; a++) {
        (ai++)->discrete_state = fp1;
      }
    }

    cset->Obj = I;
    cset->enumIndices();
    cset->invalidateRep(cRepAll, cRepInvRep);
    if(isNew) {
      std::swap(I->AtomInfo, atInfo);
    } else {
      ObjectMoleculeMerge(I, std::move(atInfo), cset, false, cAIC_AllMask, true);
    }
    if(isNew)
      I->NAtom = nAtom;
    VLACheck(I->CSet, CoordSet *, frame);
    if(I->NCSet <= frame)
      I->NCSet = frame + 1;
    delete I->CSet[frame];
    I->CSet[frame] = cset;
    if (fractional && cset->Symmetry) {
      CoordSetFracToReal(cset, &cset->Symmetry->Crystal);
    }
    if(ok && isNew)
      ok &= ObjectMoleculeConnect(I, cset, auto_bond, connect_mode);
    if(cset->Symmetry && (!I->Symmetry)) {
      I->Symmetry.reset(new CSymmetry(*cset->Symmetry));
    }
    SceneCountFrames(G);
    if (ok)
      ok &= ObjectMoleculeExtendIndices(I, frame);
    if (ok)
      ok &= ObjectMoleculeSort(I);
    if (ok){
      ObjectMoleculeUpdateIDNumbers(I);
      ObjectMoleculeUpdateNonbonded(I);
    }
  }
  return (I);
#endif
}


/*========================================================================*/
/**
 * Update coordinates of an exisiting coordset or insert/append a new
 * coordset.
 *
 * coords:      flat coordinate array of length NIndex * 3
 * coords_len:  must be NIndex * 3
 * frame:       coordset index
 */
ObjectMolecule *ObjectMoleculeLoadCoords(PyMOLGlobals * G, ObjectMolecule * I,
                                         const float * coords, int coords_len, int frame)
{
  CoordSet *cset = nullptr;
  int a;
  bool is_new = false;

  if(frame < 0) {
    frame = I->NCSet;
  } else if (frame < I->NCSet) {
    cset = I->CSet[frame];
  }

  if (!cset) {
    // template coordinate set, if available
    cset = I->CSTmpl;

    // find any coordinate set
    for(a = 0; !cset && a < I->NCSet; ++a)
      cset = I->CSet[a];
    ok_assert(1, cset);
    cset = CoordSetCopy(cset);
    is_new = true;
  }

  // check atom count
  if(coords_len != cset->NIndex * 3) {
    ErrMessage(G, "LoadCoords", "atom count mismatch");
    ok_raise(1);
  }

  // copy coordinates
  for(a = 0; a < coords_len; ++a) {
    cset->Coord[a] = coords[a];
  }

  cset->invalidateRep(cRepAll, cRepInvRep);

  // include coordinate set
  if (is_new) {
    VLACheck(I->CSet, CoordSet *, frame);
    if(I->NCSet <= frame)
      I->NCSet = frame + 1;
    I->CSet[frame] = cset;
    SceneCountFrames(G);
  }

  // success
  return (I);

  // error handling
ok_except1:
  if(is_new && cset)
    delete cset;
  ErrMessage(G, "LoadCoords", "failed");
  return nullptr;
}

/*========================================================================*/
/* see above... but look up object by name
 */
ObjectMolecule *ObjectMoleculeLoadCoords(PyMOLGlobals * G, const char * name,
                                         const float * coords, int coords_len, int frame)
{
  pymol::CObject * cobj = ExecutiveFindObjectByName(G, name);

  if(!cobj || cobj->type != cObjectMolecule) {
    ErrMessage(G, "LoadCoords", "named object molecule not found.");
    return nullptr;
  }

  return ObjectMoleculeLoadCoords(G, (ObjectMolecule *) cobj,
      coords, coords_len, frame);
}


/*========================================================================*/
/* see above... but take coordinates from Python list
 *
 * coords: 2d Python float sequence with shape (NIndex, 3)
 */
ObjectMolecule *ObjectMoleculeLoadCoords(PyMOLGlobals * G, ObjectMolecule * I,
                                         PyObject * coords, int frame)
{
#ifdef _PYMOL_NOPY
  return nullptr;
#else
  CoordSet *cset = nullptr;
  int a, b, l;
  PyObject *v, *w;
  float *f;
  bool is_new = false;

  if(!PySequence_Check(coords)) {
    ErrMessage(G, "LoadCoords", "passed argument is not a sequence");
    ok_raise(1);
  }

  if(frame < 0) {
    frame = I->NCSet;
  } else if (frame < I->NCSet) {
    cset = I->CSet[frame];
  }

  if (!cset) {
    // template coordinate set, if available
    cset = I->CSTmpl;

    // find any coordinate set
    for(a = 0; !cset && a < I->NCSet; ++a)
      cset = I->CSet[a];
    ok_assert(1, cset);
    cset = CoordSetCopy(cset);
    is_new = true;
  }

  // check atom count
  l = PySequence_Size(coords);
  if(l != cset->NIndex) {
    ErrMessage(G, "LoadCoords", "atom count mismatch");
    ok_raise(1);
  }

  // copy coordinates
  f = cset->Coord.data();
  for(a = 0; a < l; a++) {
    v = PySequence_ITEM(coords, a);

    for(b = 0; b < 3; b++) {
      if(!(w = PySequence_GetItem(v, b)))
        break;

      f[a * 3 + b] = (float) PyFloat_AsDouble(w);
      Py_DECREF(w);
    }

    Py_DECREF(v);
    ok_assert(2, !PyErr_Occurred());
  }

  cset->invalidateRep(cRepAll, cRepInvRep);

  // include coordinate set
  if (is_new) {
    VLACheck(I->CSet, CoordSet *, frame);
    if(I->NCSet <= frame)
      I->NCSet = frame + 1;
    I->CSet[frame] = cset;
    SceneCountFrames(G);
  }

  // success
  return (I);

  // error handling
ok_except2:
  PyErr_Print();
ok_except1:
  if(is_new && cset)
    delete cset;
  ErrMessage(G, "LoadCoords", "failed");
  return nullptr;
#endif
}


/*========================================================================*/
int ObjectMoleculeExtendIndices(ObjectMolecule * I, int state)
{
  int a;
  CoordSet *cs;

  if(I->DiscreteFlag && (state >= 0)) {
    /* if object is discrete, then we don't need to extend each state,
       just the current one (which updates object DiscreteAtmToIdx) */
    cs = I->CSTmpl;
    ok_assert(1, (!cs) || cs->extendIndices(I->NAtom));
    if(state < I->NCSet) {
      cs = I->CSet[state];
      ok_assert(1, (!cs) || cs->extendIndices(I->NAtom));
    }
  } else {                      /* do all states */
    for(a = -1; a < I->NCSet; a++) {
      cs = (a < 0) ? I->CSTmpl : I->CSet[a];
      ok_assert(1, (!cs) || cs->extendIndices(I->NAtom));
    }
  }
  return true;
ok_except1:
  return false;
}


/*========================================================================*/

static CoordSet *ObjectMoleculeMOLStr2CoordSet(PyMOLGlobals * G, const char *buffer,
                                               AtomInfoType ** atInfoPtr, const char **restart)
{
  const char *p;
  int nAtom, nBond;
  int a, cnt, atm, chg;
  float *coord = nullptr;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr;
  char cc[MAXLINELEN], cc1[MAXLINELEN], resn[MAXLINELEN] = "UNK";
  float *f;
  BondType *ii;
  BondType *bond = nullptr;
  int ok = true;
  int auto_show = RepGetAutoShowMask(G);
  WordType nameTmp;

  p = buffer;
  nAtom = 0;
  if(atInfoPtr)
    atInfo = *atInfoPtr;

  /*  p=ParseWordCopy(nameTmp,p,sizeof(WordType)-1); */
  p = ParseNCopy(nameTmp, p, sizeof(WordType) - 1);
  PRINTFB(G, FB_ObjectMolecule, FB_Blather)
    " ObjMolMOLStr2CoordSet: title '%s'\n", nameTmp ENDFB(G)
    p = nextline(p);
  p = nextline(p);
  p = nextline(p);

  if(ok) {
    p = ncopy(cc, p, 3);
    if(sscanf(cc, "%d", &nAtom) != 1)
      ok = ErrMessage(G, "ReadMOLFile", "bad atom count");
  }

  if(ok) {
    p = ncopy(cc, p, 3);
    if(sscanf(cc, "%d", &nBond) != 1)
      ok = ErrMessage(G, "ReadMOLFile", "bad bond count");
  }

  if(ok) {
    coord = VLAlloc(float, 3 * nAtom);
    if(atInfo)
      VLACheck(atInfo, AtomInfoType, nAtom);
  }

  p = nextline(p);

  /* read coordinates and atom names */

  if(ok) {
    f = coord;
    for(a = 0; a < nAtom; a++) {
      if(ok) {
        p = ncopy(cc, p, 10);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad coordinate");
      }
      if(ok) {
        p = ncopy(cc, p, 10);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad coordinate");
      }
      if(ok) {
        p = ncopy(cc, p, 10);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad coordinate");
      }
      if(ok) {
        p = nskip(p, 1);
        p = ntrim(cc, p, 3);
        strncpy(atInfo[a].elem, cc, cElemNameLen);
        atInfo[a].name = LexIdx(G, cc);
        atInfo[a].visRep = auto_show;
      }
      if(ok) {
        int tmp_int;
        p = nskip(p, 2);
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%hhi", &atInfo[a].formalCharge) == 1) {
          if(atInfo[a].formalCharge) {
            atInfo[a].formalCharge = 4 - atInfo[a].formalCharge;
          }
        }
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%d", &tmp_int) != 1)
          atInfo[a].stereo = 0;
        else
          atInfo[a].stereo = tmp_int;
      }
      if(ok && atInfo) {
        atInfo[a].id = a + 1;
        atInfo[a].rank = a;
        atInfo[a].resn = LexIdx(G, resn);
        atInfo[a].hetatm = true;
        AtomInfoAssignParameters(G, atInfo + a);
        AtomInfoAssignColors(G, atInfo + a);
        atInfo[a].alt[0] = 0;
        atInfo[a].segi = 0;
        atInfo[a].inscode = 0;
      }
      p = nextline(p);
      if(!ok)
        break;
    }
  }
  if(ok) {
    bond = VLACalloc(BondType, nBond);
    ii = bond;
    for(a = 0; a < nBond; a++) {
      if(ok) {
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%d", &ii->index[0]) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad bond atom");
      }

      if(ok) {
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%d", &ii->index[1]) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad bond atom");
      }

      if(ok) {
        int order = 0;
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%d", &order) != 1)
          ok = ErrMessage(G, "ReadMOLFile", "bad bond order");
        ii->order = order;
      }
      if(ok) {
        p = ncopy(cc, p, 3);
        // stereo
      }
      ii++;
      if(!ok)
        break;
      p = nextline(p);
    }
    ii = bond;
    for(a = 0; a < nBond; a++) {
      ii->index[0]--;           /* adjust bond indexs down one */
      ii->index[1]--;
      ii++;
    }
  }
  while(*p) {                   /* read M  CHG records */
    auto p_line = p;
    p = ncopy(cc, p, 6);
    if(cc[5] == ' ')
      cc[5] = 0;
    if((!strcmp(cc, "M  END")) || (!strcmp(cc, "M END"))) {
      /* denotes end of MOL block */
      break;
    }
    if((!strcmp(cc, "M  CHG")) || (!strcmp(cc, "M CHG"))) {
      p = ncopy(cc, p, 3);
      if(sscanf(cc, "%d", &cnt) == 1) {
        while(cnt--) {
          p = ncopy(cc, p, 4);
          p = ncopy(cc1, p, 4);
          if(!((*cc) || (*cc1)))
            break;
          if((sscanf(cc, "%d", &atm) == 1) && (sscanf(cc1, "%d", &chg) == 1)) {
            atm--;
            if((atm >= 0) && (atm < nAtom))
              atInfo[atm].formalCharge = chg;
          }
        }
      }
    } else if (!strcmp(cc, "M  V30")) {
      p = MOLV3000Parse(G, p_line, atInfo, bond, coord, nAtom, nBond);
      if (!p) {
        ok = false;
        break;
      }
      continue;
    }
    p = nextline(p);
  }
  if(ok) {
    (*restart) = p;
    cset = CoordSetNew(G);
    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->NTmpBond = nBond;
    cset->TmpBond = pymol::vla_take_ownership(bond);
    strcpy(cset->Name, nameTmp);
  } else {
    VLAFreeP(bond);
    VLAFreeP(coord);
    (*restart) = nullptr;
  }
  if(atInfoPtr)
    *atInfoPtr = atInfo;
  return (cset);
}


/*========================================================================*/

static CoordSet *ObjectMoleculeSDF2Str2CoordSet(PyMOLGlobals * G, const char *buffer,
                                                AtomInfoType ** atInfoPtr,
                                                const char **next_mol)
{
  char cc[MAXLINELEN];
  const char *p;
  CoordSet *result = nullptr;
  result = ObjectMoleculeMOLStr2CoordSet(G, buffer, atInfoPtr, next_mol);
  p = *next_mol;
  if(p) {
    while(*p) {                 /* we simply need to skip until we've read past the end of the SDF record */
      p = ncopy(cc, p, 4);
      p = nextline(p);
      if(!strcmp(cc, "$$$$")) { /* SDF record separator... */
        break;
      }
    }
    if(!*p)
      p = nullptr;
  }
  *next_mol = p;
  return result;
}

/**
 * Get the sum of bond orders for every atom. For this purpose, aromatic
 * bonds will be considered either single or double bond, based on
 * the entire system (heuristic method, room for improvement).
 */
static
std::vector<signed char> get_bond_order_sums(ObjectMolecule * obj) {
  std::vector<signed char> valences(obj->NAtom);
  std::vector<signed char> freevalences(obj->NAtom);
  std::vector<signed char> orders(obj->NBond);

  // bond order sums as if all aromatic bonds were single bonds
  for (int b = 0; b < obj->NBond; ++b) {
    auto bond = obj->Bond + b;
    auto order = bond->order;
    orders[b] = order;
    if (order > 3)
      order = 1;
    valences[bond->index[0]] += order;
    valences[bond->index[1]] += order;
  }

  // determine free valences as if all aromatic bonds were single bonds
  for (int atm = 0; atm < obj->NAtom; ++atm) {
    int tmp = 0;
    switch (obj->AtomInfo[atm].protons) {
      case cAN_C:             tmp = 4; break;
      case cAN_N: case cAN_P: tmp = 5; break;
      case cAN_O: case cAN_S: tmp = 6; break;
    }
    if (tmp) {
      tmp -= valences[atm];
      if (tmp) {
        freevalences[atm] = (tmp - 1) % 2 + 1;
      }
    }
  }

  // (This is a heuristic, gets most of the ZINC dataset correct)
  // Do two passes over all aromatic bonds.
  // 1st pass: assign double bonds between atoms with `freevalence == 1`
  // 2nd pass: assign double bonds between atoms with `freevalence >= 1`
  for (int secondpass = 0; secondpass < 2; ++secondpass) {
    for (int b = 0; b < obj->NBond; ++b) {
      if (orders[b] == 4) {
        auto atm1 = obj->Bond[b].index[0];
        auto atm2 = obj->Bond[b].index[1];
        if (secondpass ?
            (freevalences[atm1] >= 1 && freevalences[atm2] >= 1 ) :
            (freevalences[atm1] == 1 && freevalences[atm2] == 1)) {
          freevalences[atm1] = 0;
          freevalences[atm2] = 0;
          valences[atm1] += 1;
          valences[atm2] += 1;
          orders[b] = 2;
        }
      }
    }
  }

  return valences;
}

/**
 * For each atom, set `formalCharge` based on mol2 `textType`
 *
 * See also: getMOL2Type() in layer2/Mol2Typing.cpp
 */
static void ObjectMoleculeMOL2SetFormalCharges(PyMOLGlobals *G, ObjectMolecule *obj){

  // check if structure has explicit hydrogens
  bool has_hydrogens = false;
  for (int at = 0; at < obj->NAtom; ++at) {
    auto ai = obj->AtomInfo + at;
    if (ai->isHydrogen()) {
      has_hydrogens = true;
      break;
    }
  }

  if (!has_hydrogens) {
    // could eventually do PDB nomenclature charge assignment here...
    return;
  }

  // Unfortunately, aromatic bonds (type 4) can't be used to determine
  // formal charges. The following uses a heuristic to guess bond orders
  // for aromatic bonds.
  auto valences = get_bond_order_sums(obj);

  // (period currently incompatible with G->lex_const)
  auto lex_N_4 = LexBorrow(G, "N.4");

  for (int at = 0; at < obj->NAtom; ++at) {
    int fcharge = 0;
    auto ai = obj->AtomInfo + at;

    if (ai->protons == cAN_N) {
      if (ai->textType == lex_N_4) {
        fcharge = 1;
      } else if (valences[at] == 2) {
        fcharge = -1;
      } else if (valences[at] == 4) {
        fcharge = 1;
      }
    } else if (ai->protons == cAN_O) {
      if (valences[at] == 1) {
        fcharge = -1;
      }
    }

    ai->formalCharge = fcharge;
  }
}

/*========================================================================*/

static CoordSet *ObjectMoleculeMOL2Str2CoordSet(PyMOLGlobals * G,
                                                const char *buffer,
                                                AtomInfoType ** atInfoPtr,
                                                const char **next_mol)
{
  const char *p;
  int nAtom, nBond, nSubst, nFeat, nSet;
  int a;
  float *coord = nullptr;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr;
  char cc[MAXLINELEN];
  float *f;
  const char *last_p;
  BondType *ii;
  BondType *bond = nullptr;
  int ok = true;
  int auto_show = RepGetAutoShowMask(G);
  int have_molecule = false;
  WordType nameTmp;
  bool has_non_hetatm = false;

  // Maestro writes <0> as subst_name for waters
  const lexidx_t empty_subst_name = LexIdx(G, "<0>");

  p = buffer;
  nAtom = 0;
  if(atInfoPtr)
    atInfo = *atInfoPtr;

  while((*p) && ok) {

    last_p = p;
    /* top level -- looking for an RTI, assuming p points to the beginning of a line */
    p = ParseWordCopy(cc, p, MAXLINELEN);
    if(cc[0] == '@') {
      if(WordMatchExact(G, cc, "@<TRIPOS>MOLECULE", true)
         || WordMatchExact(G, cc, "@MOLECULE", true)) {
        if(have_molecule) {
          *next_mol = last_p;
          break;                /* next record of multi-mol2 */
        }
        p = ParseNextLine(p);
        p = ParseNTrim(nameTmp, p, sizeof(WordType) - 1);       /* get mol name */
        p = ParseNextLine(p);
        p = ParseWordCopy(cc, p, MAXLINELEN);
        if(sscanf(cc, "%d", &nAtom) != 1)
          ok = ErrMessage(G, "ReadMOL2File", "bad atom count");
        else {
          coord = VLAlloc(float, 3 * nAtom);
          if(atInfo)
            VLACheck(atInfo, AtomInfoType, nAtom);

          p = ParseWordCopy(cc, p, MAXLINELEN);
          if(sscanf(cc, "%d", &nBond) != 1) {
            nBond = 0;
          }
          p = ParseWordCopy(cc, p, MAXLINELEN);
          if(sscanf(cc, "%d", &nSubst) != 1) {
            nSubst = 0;
          }
          p = ParseWordCopy(cc, p, MAXLINELEN);
          if(sscanf(cc, "%d", &nFeat) != 1) {
            nFeat = 0;
          }
          p = ParseWordCopy(cc, p, MAXLINELEN);
          if(sscanf(cc, "%d", &nSet) != 1) {
            nSet = 0;
          }

        }
        p = ParseNextLine(p);
        p = ParseNextLine(p);
        have_molecule = true;
      } else if(WordMatchExact(G, cc, "@<TRIPOS>ATOM", true)
                || WordMatchExact(G, cc, "@ATOM", true)) {
        if(!have_molecule) {
          ok = ErrMessage(G, "ReadMOL2File", "@ATOM before @MOLECULE!");
          break;
        }
        p = ParseNextLine(p);
        f = coord;
        for(a = 0; a < nAtom; a++) {
          AtomInfoType *ai = atInfo + a;

          if(ok) {
            p = ParseWordCopy(cc, p, MAXLINELEN);
            if(sscanf(cc, "%d", &ai->id) != 1) {
              ok = ErrMessage(G, "ReadMOL2File", "bad atom id");
	    }
          }
          if(ok) {
            p = ParseWordCopy(cc, p, MAXLINELEN);
            cc[cAtomNameLen] = 0;
            UtilCleanStr(cc);
            if(cc[0])
              LexAssign(G, ai->name, cc);
            else
              ok = ErrMessage(G, "ReadMOL2File", "bad atom name");
          }
          if(ok) {
            p = ParseWordCopy(cc, p, MAXLINELEN);
            if(sscanf(cc, "%f", f++) != 1)
              ok = ErrMessage(G, "ReadMOL2File", "bad x coordinate");
          }
          if(ok) {
            p = ParseWordCopy(cc, p, MAXLINELEN);
            if(sscanf(cc, "%f", f++) != 1)
              ok = ErrMessage(G, "ReadMOL2File", "bad y coordinate");
          }
          if(ok) {
            p = ParseWordCopy(cc, p, MAXLINELEN);
            if(sscanf(cc, "%f", f++) != 1)
              ok = ErrMessage(G, "ReadMOL2File", "bad z coordinate");
          }
          if(ok) {
            OrthoLineType temp;
            p = ParseWordCopy(cc, p, OrthoLineLength - 1);
            if(sscanf(cc, "%s", temp) != 1)
              ok = ErrMessage(G, "ReadMOL2File", "bad atom type");
            else {              /* convert atom type to elem symbol */
              char *tt = temp;
              char *el = ai->elem;
              int elc = 0;
              while(*tt && ((*tt) != '.')) {
                *(el++) = *(tt++);
                elc++;
                if(elc > cElemNameLen)
                  break;
              }
              *el = 0;
              if(el[2])
                el[0] = 0;

              ai->textType = LexIdx(G, temp);
            }
          }
          if(ok) {
            char cc_resi[8];
            p = ParseWordCopy(cc_resi, p, sizeof(cc_resi) - 1);
            if(cc_resi[0]) {         /* subst_id is residue identifier */
              ai->setResi(cc_resi);
              p = ParseWordCopy(cc, p, MAXLINELEN);
              if(cc[0]) {
                if (nSubst == 0) {
                  // without substructure information (e.g. OpenBabel exported MOL2):
                  // if subst_name includes the subst_id (e.g. 5 ALA5) then strip the number
                  int len_resi = strlen(cc_resi);
                  int len_resn = strlen(cc);
                  if (len_resn > len_resi) {
                    if (strcmp(cc_resi, cc + len_resn - len_resi) == 0) {
                      cc[len_resn - len_resi] = 0;
                    }
                  }
                }

                LexAssign(G, ai->resn, cc);

                p = ParseWordCopy(cc, p, MAXLINELEN);
                if(cc[0]) {
                  if(sscanf(cc, "%f", &ai->partialCharge) != 1)
                    ok = ErrMessage(G, "ReadMOL2File", "bad atom charge");
                }

                // status_bit
                p = ParseWordCopy(cc, p, MAXLINELEN);
                if (cc[0]) {
                  // for water, substitute resn "<0>" with "HOH" for
                  // correct classification as "solvent"
                  if (ai->resn == empty_subst_name && strstr(cc, "WATER")) {
                    LexAssign(G, ai->resn, G->lex_const.HOH);
                  }
                }
              }
            }
          }
          p = ParseNextLine(p);

          ai->visRep = auto_show;

          ai->id = a + 1;
          ai->rank = a;
          ai->hetatm = true;
          AtomInfoAssignParameters(G, atInfo + a);
          AtomInfoAssignColors(G, atInfo + a);
          ai->alt[0] = 0;
          ai->segi = 0;

        }
      } else if(WordMatchExact(G, cc, "@<TRIPOS>SET", true)
                || WordMatchExact(G, cc, "@SET", true)) {
        if(!have_molecule) {
          ok = ErrMessage(G, "ReadMOL2File", "@SET before @MOLECULE!");
          break;
        }
        p = ParseNextLine(p);
        if(ok) {
          for(a = 0; a < nSet; a++) {
            if(ok) {
              char ss = 0;
              p = ParseWordCopy(cc, p, 50);
              cc[5] = 0;
              if(!strcmp("HELIX", cc))
                ss = 'H';
              if(!strcmp("SHEET", cc))
                ss = 'S';
              p = ParseWordCopy(cc, p, 50);
              if(!strcmp("STATIC", cc)) {
                int nMember;

                p = ParseNextLine(p);
                p = ParseWordCopy(cc, p, 20);
                if(sscanf(cc, "%d", &nMember) != 1)
                  ok = ErrMessage(G, "ReadMOL2File", "bad member count");
                else {
                  while(ok && (nMember > 0)) {
                    p = ParseWordCopy(cc, p, 20);
                    if((!cc[0]) && (!*p)) {
                      ok = false;
                      break;
                    }
                    if(cc[0] != '\\') {
                      int atom_id;
                      if(sscanf(cc, "%d", &atom_id) != 1) {
                        ok = ErrMessage(G, "ReadMOL2File", "bad member");
                      } else {
                        atom_id--;
                        if((atom_id >= 0) && (atom_id < nAtom)) {
                          atInfo[atom_id].ssType[0] = ss;
                          atInfo[atom_id].ssType[1] = 0;
                        }
                        nMember--;
                      }
                    } else {
                      p = ParseNextLine(p);
                    }
                  }
                }
                p = ParseNextLine(p);
              } else {
                p = ParseNextLine(p);
                p = ParseNextLine(p);
              }
            }
          }
        }
      } else if(WordMatchExact(G, cc, "@<TRIPOS>BOND", true)
                || WordMatchExact(G, cc, "@BOND", true)) {
        if(!have_molecule) {
          ok = ErrMessage(G, "ReadMOL2File", "@BOND before @MOLECULE!");
          break;
        }
        p = ParseNextLine(p);

        if(ok) {
          bond = VLACalloc(BondType, nBond);
          ii = bond;
          for(a = 0; a < nBond; a++) {
            if(ok) {
              p = ParseWordCopy(cc, p, 20);
            }

            if(ok) {
              p = ParseWordCopy(cc, p, 20);
              if(sscanf(cc, "%d", &ii->index[0]) != 1)
                ok = ErrMessage(G, "ReadMOL2File", "bad source atom id");
            }

            if(ok) {
              p = ParseWordCopy(cc, p, 20);
              if(sscanf(cc, "%d", &ii->index[1]) != 1)
                ok = ErrMessage(G, "ReadMOL2File", "bad target atom id");
            }

            if(ok) {
              p = ParseWordCopy(cc, p, 20);
              if(!cc[1]) {
                switch (cc[0]) {
                case '1':
                  ii->order = 1;
                  break;
                case '2':
                  ii->order = 2;
                  break;
                case '3':
                  ii->order = 3;
                  break;
                case '4':
                  ii->order = 4;
                  break;
                }
              } else if(WordMatchExact(G, "ar", cc, true)) {
                ii->order = 4;
              } else if(WordMatchExact(G, "am", cc, true)) {
                ii->order = 1;
              } else if(WordMatchExact(G, "un", cc, true)) {
                ii->order = 1;
              } else if(WordMatchExact(G, "nc", cc, true)) {
                ii->order = 0;  /* is this legal in PyMOL? */
              } else if(WordMatchExact(G, "du", cc, true)) {
                ii->order = 1;
              } else
                ok = ErrMessage(G, "ReadMOL2File", "bad bond type");
            }
            ii++;
            if(!ok)
              break;
            p = ParseNextLine(p);
          }
          ii = bond;
          for(a = 0; a < nBond; a++) {
            ii->index[0]--;     /* adjust bond indexs down one */
            ii->index[1]--;
            ii++;
          }
        }
      } else if(WordMatchExact(G, cc, "@<TRIPOS>SUBSTRUCTURE", true)
                || WordMatchExact(G, cc, "@SUBSTRUCTURE", true)) {
        if(!have_molecule) {
          ok = ErrMessage(G, "ReadMOL2File", "@SUBSTSRUCTURE before @MOLECULE!");
          break;
        }
        p = ParseNextLine(p);
        if(ok) {
          WordType subst_name;
          SegIdent segment;     /* what MOL2 calls chain */
          lexidx_t chain = 0;
          WordType subst_type;
          ResIdent resi;
          ResName resn;
          int id, dict_type, root, resv;
          int end_line, seg_flag, subst_flag, resi_flag;
          int chain_flag, resn_flag;
          std::unordered_map<int, std::vector<int>> resv_map;

          {
            int b;
            AtomInfoType *ai = atInfo;
            for(b = 0; b < nAtom; b++) {
              resv_map[ai->resv].push_back(b);
              ai++;
            }
          }

          for(a = 0; a < nSubst; a++) {
            bool hetatm = true;
            segment[0] = 0;
            subst_name[0] = 0;
            LexAssign(G, chain, 0);
            resn[0] = 0;
            resi[0] = 0;
            end_line = false;
            seg_flag = false;
            subst_flag = false;

            resi_flag = false;
            chain_flag = false;
            resn_flag = false;

            if(ok) {
              const char *save_p = p;
              p = ParseWordCopy(cc, p, 20);
              if(sscanf(cc, "%d", &id) != 1) {
                if(cc[0] != '@')
                  ok = ErrMessage(G, "ReadMOL2File", "bad substructure id");
                else {
                  p = save_p;
                  break;        /* missing substructure... */
                }
              }
            }
            if(ok) {
              p = ParseWordCopy(cc, p, sizeof(WordType) - 1);
              if(sscanf(cc, "%s", subst_name) != 1)
                ok = ErrMessage(G, "ReadMOL2File", "bad substructure name");
            }
            if(ok) {
              p = ParseWordCopy(cc, p, 20);
              if(sscanf(cc, "%d", &root) != 1)
                ok = ErrMessage(G, "ReadMOL2File", "bad target root atom id");
              else
                root--;         /* convert 1-based to 0-based */
            }

            /* optional data */
            if(ok && (!end_line)) {
              p = ParseWordCopy(cc, p, sizeof(WordType) - 1);
              if(sscanf(cc, "%s", subst_type) != 1) {
                end_line = true;
                subst_type[0] = 0;
              } else {
                subst_flag = 1;
              }
            }

            if(ok && (!end_line)) {
              p = ParseWordCopy(cc, p, 20);
              if(sscanf(cc, "%d", &dict_type) != 1)
                end_line = true;
            }

            if(ok && (!end_line)) {
              p = ParseWordCopy(cc, p, sizeof(WordType) - 1);
              cc[cSegiLen] = 0;
              if(sscanf(cc, "%s", segment) != 1) {
                end_line = true;
                segment[0] = 0;
              } else if(strcmp(segment, "****")) {
                seg_flag = true;
                if(!segment[1]) {       /* if segment is single letter, then also assign to chain field */
                  LexAssign(G, chain, segment);
                  chain_flag = true;
                }
              }
            }

            if(ok && (!end_line)) {
              p = ParseWordCopy(cc, p, sizeof(WordType) - 1);
              cc[cResnLen] = 0;
              if(!strcmp(cc, "****")) { /* whoops -- no residue name... */
                char *pp;
                UtilNCopy(resn, subst_name, sizeof(ResName));
                pp = resn;
                /* any numbers? */
                while(*pp) {
                  if(((*pp) >= '0') && ((*pp) <= '9'))
                    break;
                  pp++;
                }
                /* trim them off */
                while(pp >= resn) {
                  if(((*pp) >= '0') && ((*pp) <= '9'))
                    *pp = 0;
                  else
                    break;
                  pp--;
                }

                if(resn[0])
                  resn_flag = true;
              } else {
                if(sscanf(cc, "%s", resn) != 1) {
                  end_line = true;
                  resn[0] = 0;
                } else {
                  resn_flag = true;
                }
              }
            }

            if(ok && (root >= 0) && (root < nAtom)) {

              if((strlen(subst_name) == 4) &&
                 ((subst_name[0] >= '1') && (subst_name[0] <= '9')) &&
                 ((((subst_name[1] >= 'A') && (subst_name[1] <= 'Z')) ||
                   ((subst_name[1] >= 'a') && (subst_name[1] <= 'z'))) ||
                  (((subst_name[2] >= 'A') && (subst_name[2] <= 'Z')) ||
                   ((subst_name[2] >= 'a') && (subst_name[2] <= 'z'))) ||
                  (((subst_name[3] >= 'A') && (subst_name[3] <= 'Z')) ||
                   ((subst_name[3] >= 'a') && (subst_name[3] <= 'z'))))) {

                /* if subst_name looks like a PDB code, then it isn't a residue identifier
                   so do nothing... */
              } else {

                if(!subst_flag) {

                  /* Merck stuffs the chain ID and the residue ID in the
                     subst_name field, so handle that case first */

                  /* get the residue value */

                  ParseIntCopy(cc, subst_name, 20);
                  if(sscanf(cc, "%d", &resv) == 1) {
                    /* we have the resv, so now get the chain if there is one */
                    char *pp = subst_name;
                    if(!((pp[0] >= '0') && (pp[0] <= '9'))) {
                      char tmp[2] = {pp[0], 0};
                      LexAssign(G, chain, tmp);
                      chain_flag = true;
                      pp++;
                    }
                    UtilNCopy(resi, pp, cResiLen);      /* now get the textual residue identifier */

                    if(resi[0]) {
                      resi_flag = true;
                    }
                  }
                } else {
                  /* normal mode: assume that the substructure ID is a vanilla residue identifier */

                  char *pp = subst_name;

                  if(resn_flag) {       /* if we have a resn, then remove it from the subst_name */
                    char *qq = resn;
                    while((*pp) && (*qq)) {
                      if(*pp == *qq) {
                        pp++;
                        qq++;
                      } else
                        break;
                    }
                  }

                  ParseIntCopy(cc, pp, 20);
                  if(sscanf(cc, "%d", &resv) == 1) {
                    UtilNCopy(resi, pp, cResiLen);      /* now get the textual residue identifier */
                    if(resi[0]) {
                      resi_flag = true;
                    }
                  }

                  if (strcmp(subst_type, "RESIDUE") == 0) {
                    hetatm = false;
                    has_non_hetatm = true;
                  }
                }
              }

              /* for now, assume that atom ids are 1-based and sequential */

              if(ok) {
                if(resi_flag || chain_flag || resn_flag || seg_flag) {
                  auto it = resv_map.find(id);
                  if (it != resv_map.end()) {
                    for (auto b : it->second) {
                      assert(b >= 0 && b < nAtom);
                  {
                        AtomInfoType *ai = atInfo + b;
                        if(resi_flag)
                          ai->setResi(resi);
                        if(chain_flag) {
                          LexAssign(G, ai->chain, chain);
                        }
                        if(resn_flag)
                          LexAssign(G, ai->resn, resn);
                        if(seg_flag)
                          LexAssign(G, ai->segi, segment);
                        ai->hetatm = hetatm;
                      }
                    }
                  }
                }
              }
            }
            if(!ok)
              break;
            p = ParseNextLine(p);
          }
          LexDec(G, chain);
        }
      } else
        p = ParseNextLine(p);
    } else
      p = ParseNextLine(p);
  }

  LexDec(G, empty_subst_name);

  if (has_non_hetatm) {
    // contains "residues" (assume polymer) so ignore hetatm for surfacing
    for (auto ai = atInfo, ai_end = atInfo + nAtom; ai != ai_end; ++ai) {
      if (ai->hetatm) {
        ai->flags |= cAtomFlag_ignore;
      }
    }
  }

  if(ok) {
    cset = CoordSetNew(G);
    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->NTmpBond = nBond;
    cset->TmpBond = pymol::vla_take_ownership(bond);
    strcpy(cset->Name, nameTmp);
  } else {
    VLAFreeP(bond);
    VLAFreeP(coord);
  }
  if(atInfoPtr)
    *atInfoPtr = atInfo;
  return (cset);
}

/**
 * Read one molecule in MOL2, MOL, SDF, MMD or XYZ format from the string pointed
 * to by (*next_entry). All these formats (except MOL) support multiple
 * concatenated entries in one file. If multiplex=1, then read only one
 * molecule (one state) and set the next_entry pointer to the beginning of the
 * next entry. Otherwise, read a multi-state molecule.
 */
ObjectMolecule *ObjectMoleculeReadStr(PyMOLGlobals * G, ObjectMolecule * I,
                                      const char **next_entry,
                                      cLoadType_t content_format, int frame,
                                      int discrete, int quiet, int multiplex,
                                      char *new_name,
				      short loadpropertiesall, OVLexicon *loadproplex)
{
  int ok = true;
  CoordSet *cset = nullptr;
  pymol::vla<AtomInfoType> atInfo;
  int isNew;
  int nAtom;
  const char *restart = nullptr, *start = *next_entry;
  int repeatFlag = true;
  int successCnt = 0;
  char tmpName[WordLength];
  int deferred_tasks = false;
  int skip_out;
  int connect = false;
  int set_formal_charges = false;
  *next_entry = nullptr;
  int aic_mask = cAIC_MOLMask;

  while(repeatFlag) {
    repeatFlag = false;
    skip_out = false;

    if(!I)
      isNew = true;
    else
      isNew = false;

    if(isNew) {
      I = (ObjectMolecule *) new ObjectMolecule(G, (discrete > 0));
      std::swap(atInfo, I->AtomInfo);
    } else {
      atInfo = pymol::vla<AtomInfoType>(10);
    }

    if(isNew) {
      I->Color = AtomInfoUpdateAutoColor(G);
    }

    restart = nullptr;
    switch (content_format) {
    case cLoadTypeMOL2:
    case cLoadTypeMOL2Str:
      cset = ObjectMoleculeMOL2Str2CoordSet(G, start, &atInfo, &restart);
      if (cset){
	set_formal_charges = true;
      }
      break;
    case cLoadTypeMOL:
    case cLoadTypeMOLStr:
      cset = ObjectMoleculeMOLStr2CoordSet(G, start, &atInfo, &restart);
      restart = nullptr;
      break;
    case cLoadTypeSDF2:
    case cLoadTypeSDF2Str:
      cset = ObjectMoleculeSDF2Str2CoordSet(G, start, &atInfo, &restart);
      break;
    case cLoadTypeXYZ:
    case cLoadTypeXYZStr:
      cset = ObjectMoleculeXYZStr2CoordSet(G, start, &atInfo, &restart);
      if(!cset->TmpBond)
        connect = true;
      break;
    case cLoadTypeMMD:
    case cLoadTypeMMDStr:
      cset = ObjectMoleculeMMDStr2CoordSet(G, start, &atInfo, &restart);
      aic_mask = cAIC_MMDMask;
      break;
    }


    if(!cset) {
      if(!isNew)
        VLAFreeP(atInfo);
      if(!successCnt) {
	if (isNew)
          std::swap(I->AtomInfo, atInfo);
        DeleteP(I);
        ok = false;
      } else {
        skip_out = true;
      }
    }

    if(ok && !skip_out) {

      if((discrete>0 && !I->DiscreteFlag) ||  // should set to discrete if explicitly defined
	 ((discrete < 0) && (restart) && isNew && (multiplex <= 0))) {
        /* if default discrete behavior and new object, with
           multi-coordinate set file, and not multiplex, then set
           discrete */

        ObjectMoleculeSetDiscrete(G, I, true);
      }

      if(frame < 0)
        frame = I->NCSet;
      if(I->NCSet <= frame)
        I->NCSet = frame + 1;
      VLACheck(I->CSet, CoordSet *, frame);

      nAtom = cset->NIndex;

      if(I->DiscreteFlag && atInfo) {
        int a;
        int fp1 = frame + 1;
        AtomInfoType *ai = atInfo.data();
        for(a = 0; a < nAtom; a++) {
          (ai++)->discrete_state = fp1;
        }
      }

      if(multiplex > 0)
        UtilNCopy(tmpName, cset->Name, WordLength);

      cset->Obj = I;
      cset->enumIndices();
      cset->invalidateRep(cRepAll, cRepInvRep);
      if(isNew) {
        std::swap(I->AtomInfo, atInfo);
      } else {
        ObjectMoleculeMerge(I, std::move(atInfo), cset, false, aic_mask, false);
        /* NOTE: will release atInfo */
      }

      if(isNew)
        I->NAtom = nAtom;
      if(frame < 0)
        frame = I->NCSet;
      VLACheck(I->CSet, CoordSet *, frame);
      if(I->NCSet <= frame)
        I->NCSet = frame + 1;
      delete I->CSet[frame];
      I->CSet[frame] = cset;

      if(ok && isNew)
        ok &= ObjectMoleculeConnect(I, cset, connect);

      if (ok)
	ok &= ObjectMoleculeExtendIndices(I, frame);
      if (ok)
	ok &= ObjectMoleculeSort(I);

      deferred_tasks = true;
      successCnt++;
      if(!quiet) {
        if(successCnt > 1) {
          if(successCnt == 2) {
            PRINTFB(G, FB_ObjectMolecule, FB_Actions)
              " %s: read through molecule %d.\n", __func__, 1 ENDFB(G);
          }
          if(UtilShouldWePrintQuantity(successCnt)) {
            PRINTFB(G, FB_ObjectMolecule, FB_Actions)
              " %s: read through molecule %d.\n", __func__, successCnt ENDFB(G);
          }
        }
      }
      if(multiplex > 0) {
        UtilNCopy(new_name, tmpName, WordLength);
        if(restart) {
          *next_entry = restart;
        }
      } else if(restart) {
        repeatFlag = true;
        start = restart;
        frame = frame + 1;
      }
    }
  }
  if(deferred_tasks && I) {     /* defer time-consuming tasks until all states have been loaded */
    if (set_formal_charges){
      ObjectMoleculeMOL2SetFormalCharges(G, I);
    }
    SceneCountFrames(G);
    I->invalidate(cRepAll, cRepInvAll, -1);
    ObjectMoleculeUpdateIDNumbers(I);
    ObjectMoleculeUpdateNonbonded(I);
  }
  return (I);
}


/*========================================================================*/
typedef int CompareFn(PyMOLGlobals *, const AtomInfoType *, const AtomInfoType *);
int ObjectMoleculeMerge(ObjectMolecule * I, pymol::vla<AtomInfoType>&& ai,
			CoordSet * cs, int bondSearchFlag, int aic_mask, int invalidate)
{
  PyMOLGlobals *G = I->G;
  int *index, *outdex;
  int a, b, lb = 0, ac;
  int c, nb, a1, a2;
  int found;
  int nAt, nBd, nBond;
  int expansionFlag = false;
  int ok = true;

  const auto n_index = cs->getNIndex();
  const auto oldNAtom = I->NAtom;
  const auto oldNBond = I->NBond;

  /* first, sort the coodinate set */

  index = AtomInfoGetSortedIndex(G, I, ai, n_index, &outdex);
  CHECKOK(ok, index);
  if (!ok)
    return false;

  auto ai2 = pymol::vla<AtomInfoType>(n_index);
  if (ok){
    for(a = 0; a < n_index; a++)
      ai2[a] = std::move(ai[index[a]]);      /* creates a sorted list of atom info records */
  }
  ai = std::move(ai2);

  /* now, match it up with the current object's atomic information */

  if (ok){
    for(a = 0; a < n_index; a++) {
      index[a] = -1;
    }
  }

  if (ok) {
    const auto n_atom = I->NAtom;
    const AtomInfoType* const atInfo = I->AtomInfo.data();
    CompareFn *fCompare;

    if(SettingGetGlobal_b(G, cSetting_pdb_hetatm_sort)) {
      fCompare = AtomInfoCompareIgnoreRank;
    } else {
      fCompare = AtomInfoCompareIgnoreRankHet;
    }

    c = 0;
    b = 0;
    lb = 0;

    for(a = 0; a < n_index; a++) {
      int reverse = false;
      const auto* const ai_a = ai + a;
      found = false;
      if(!I->DiscreteFlag) {    /* don't even try matching for discrete objects */
        while(b < n_atom) {
          ac = (fCompare(G, ai_a, atInfo + b));
          if(!ac) {
            found = true;
            break;
          } else if(ac < 0) {   /* atom is before current, so try going the other way */
            reverse = true;
            break;
          } else if(!b) {
            int idx;
            ac = (fCompare(G, ai_a, atInfo + n_atom - 1));
            if(ac > 0) {        /* atom is after all atoms in list, so don't even bother searching */
              break;
            }
            /* next, try to jump to an appropriate position in the list */
            idx = ((a * n_atom) / n_index) - 1;
            if((idx > 0) && (b != idx) && (idx < n_atom)) {
              ac = (fCompare(G, ai_a, atInfo + idx));
              if(ac > 0)
                b = idx;
            }
          }
          lb = b;               /* last b before atom */
          b++;
        }
        if(reverse && !found) { /* searching going backwards... */
          while(b >= 0) {
            ac = (fCompare(G, ai_a, atInfo + b));
            if(!ac) {
              found = true;
              break;
            } else if(ac > 0) { /* atom after current -- no good */
              break;
            }
            lb = b;
            b--;
          }
          if(b < 0)
            b = 0;
        }
      }
      if(found) {
        index[a] = b;           /* store real atom index b for a in index[a] */
        b++;
      } else {
        index[a] = I->NAtom + c;        /* otherwise, this is a new atom */
        c++;
        b = lb;
      }
    }
  }

  /* first, reassign atom info for matched atoms */

  /* allocate additional space */
  if (ok){
    if(c) {
      expansionFlag = true;
      nAt = I->NAtom + c;
    } else {
      nAt = I->NAtom;
    }
    if(expansionFlag) {
      VLACheck(I->AtomInfo, AtomInfoType, nAt);
      CHECKOK(ok, I->AtomInfo);
    }
  }

  if (ok){
    for(a = 0; a < n_index; a++) {     /* a is in original file space */
      a1 = outdex[cs->IdxToAtm[a]];    /* a1 is in sorted atom info space */
      a2 = index[a1];
      cs->IdxToAtm[a] = a2;       /* a2 is in object space */
      if(a2 < oldNAtom)
        AtomInfoCombine(G, I->AtomInfo + a2, std::move(ai[a1]), aic_mask);
      else
	I->AtomInfo[a2] = std::move(ai[a1]);
    }
  }

  if(ok && I->DiscreteFlag) {
    ok = I->setNDiscrete(nAt);
  }

  I->NAtom = nAt;

  if (ok){
    cs->updateNonDiscreteAtmToIdx(nAt);
  }

  VLAFreeP(ai);                 /* note that we're trusting AtomInfoCombine to have 
                                   already purged all atom-dependent storage (such as strings) */

  AtomInfoFreeSortedIndexes(G, &index, &outdex);

  /* now find and integrate and any new bonds */
  if(ok && expansionFlag) {           /* expansion flag means we have introduced at least 1 new atom */
    pymol::vla<BondType> bond;
    ok &= ObjectMoleculeConnect(I, nBond, bond, cs, bondSearchFlag, -1);
    if(nBond) {
      index = pymol::malloc<int>(nBond);
      CHECKOK(ok, index);
      c = 0;
      b = 0;
      nb = 0;
      if (ok){
	for(a = 0; a < nBond; a++) {      /* iterate over new bonds */
	  found = false;
	  if(!I->DiscreteFlag) {  /* don't even try matching for discrete objects */
	    b = nb;               /* pick up where we left off */
	    while(b < I->NBond) {
	      ac = BondCompare(bond + a, I->Bond + b);
	      if(!ac) {           /* zero is a match */
		found = true;
		break;
	      } else if(ac < 0) { /* gone past position of this bond */
		break;
	      } else if(!b) {
		int idx;
		ac = BondCompare(bond + a, I->Bond + I->NBond - 1);
		
		if(ac > 0)        /* bond is after all bonds in list, so don't even bother searching */
		  break;
		/* next, try to jump to an appropriate position in the list */
		idx = ((a * I->NBond) / nBond) - 1;
		if((idx > 0) && (b != idx) && (idx < I->NBond)) {
		  ac = BondCompare(bond + a, I->Bond + idx);
		  if(ac > 0)
		    b = idx;
		}
	      }
	      b++;                /* no match yet, keep looking */
	    }
	  }
	  if(found) {
	    index[a] = b;         /* existing bond... */
	    nb = b + 1;
	  } else {                /* this is a new bond, save index and increment */
	    index[a] = I->NBond + c;
	    c++;
	  }
	}
      }
      /* first, reassign atom info for matched atoms */
      if(c) {
        /* allocate additional space */
        nBd = I->NBond + c;

        if(!I->Bond)
          I->Bond = pymol::vla<BondType>(1);
	CHECKOK(ok, I->Bond);
	if (ok)
	  VLACheck(I->Bond, BondType, nBd);
	CHECKOK(ok, I->Bond);
        for(a = 0; a < nBond; a++) {    /* copy the new bonds */
          a2 = index[a];
          if(a2 >= I->NBond) {
            I->Bond[a2] = bond[a];
          }
        }
        I->NBond = nBd;
      }
      FreeP(index);
    }
  }
  if(invalidate) {
    if(oldNAtom) {
      if(oldNAtom == I->NAtom) {
        if(oldNBond != I->NBond) {
          I->invalidate(cRepAll, cRepInvBonds, -1);
        }
      } else {
        I->invalidate(cRepAll, cRepInvAtoms, -1);
      }
    }
  }
  return ok;
}


/*========================================================================*/
/**
 * @param state Object state or -2 for current state
 * @return nullptr if there is no CoordSet for the given state
 */
CoordSet* ObjectMolecule::getCoordSet(int state)
{
  return static_cast<CoordSet*>(getObjectState(state));
}
const CoordSet* ObjectMolecule::getCoordSet(int state) const
{
  return static_cast<const CoordSet*>(getObjectState(state));
}


/*========================================================================*/
void ObjectMoleculeTransformTTTf(ObjectMolecule * I, float *ttt, int frame)
{
  int b;
  CoordSet *cs;
  for(b = 0; b < I->NCSet; b++) {
    if((frame < 0) || (frame == b)) {
      cs = I->CSet[b];
      if(cs) {
        cs->invalidateRep(cRepAll, cRepInvCoord);
        MatrixTransformTTTfN3f(cs->NIndex, cs->Coord.data(), ttt, cs->Coord.data());
        CoordSetRecordTxfApplied(cs, ttt, false);
      }
    }
  }
}


/*========================================================================*/
bool ObjectMoleculeSeleOp(ObjectMolecule * I, int sele, ObjectMoleculeOpRec * op)
{
  float *coord;
  int a, b, s;
  int c, t_i;
  int a1 = 0, ind;
  float rms;
  float v1[3], v2, *vv1, *vv2, *vt, *vt1, *vt2;
  int hit_flag = false;
  int ok = true;
  int cnt;
  int skip_flag;
  int match_flag = false;
  int offset;
  int priority;
  int use_matrices = false;
  CoordSet *cs;
  AtomInfoType *ai, *ai0;
  PyMOLGlobals *G = I->G;
#ifndef _PYMOL_NOPY
  PyObject* expr_co = nullptr;
  int compileType = Py_single_input;
#endif
#ifdef _WEBGL
#endif
  PRINTFD(G, FB_ObjectMolecule)
    " %s-DEBUG: sele %d op->code %d\n", __func__, sele, op->code ENDFD;
  if(sele >= 0) {
    const char *errstr = "Alter";
    /* always run on entry */
    switch (op->code) {
    case OMOP_LABL:
      errstr = "Label";
      if (op->i2 != cExecutiveLabelEvalOn){
	break;
      }
#ifndef _PYMOL_NOPY
      compileType = Py_eval_input;
#endif
    case OMOP_ALTR:
    case OMOP_AlterState:
      // assume blocked interpreter

      if (op->s1 && op->s1[0]){
#ifndef _PYMOL_NOPY
	expr_co = Py_CompileString(op->s1, "", compileType);
	if(expr_co == nullptr) {
	  ok = ErrMessage(G, errstr, "failed to compile expression");
	}
#else
#ifndef _WEBGL
        ok = ErrMessage(G, errstr, "failed to compile expression");
#endif
#endif
      }
      /* PBlockAndUnlockAPI() is not safe.
       * what if "v" is invalidated by another thread? */
      break;
    }
    switch (op->code) {
    case OMOP_ReferenceStore:
    case OMOP_ReferenceRecall:
    case OMOP_ReferenceValidate:
    case OMOP_ReferenceSwap:
      for(b = 0; b < I->NCSet; b++) {
        if(I->CSet[b]) {
          if((b == op->i1) || (op->i1 < 0)) {
            int inv_flag = false;
            cs = I->CSet[b];
            if(cs && CoordSetValidateRefPos(cs)) {
              for(a = 0; a < I->NAtom; a++) {
                s = I->AtomInfo[a].selEntry;
                if(SelectorIsMember(G, s, sele)) {
                  ind = cs->atmToIdx(a);
                  if(ind >= 0) {
                    float *v = cs->coordPtr(ind);
                    RefPosType *rp = cs->RefPos + ind;
                    switch (op->code) {
                    case OMOP_ReferenceStore:
                      copy3f(v, rp->coord);
                      rp->specified = true;
                      break;
                    case OMOP_ReferenceRecall:
                      if(rp->specified) {
                        copy3f(rp->coord, v);
                        inv_flag = true;
                      }
                      break;
                    case OMOP_ReferenceValidate:
                      if(!rp->specified) {
                        copy3f(v, rp->coord);
                        rp->specified = true;
                      }
                      break;
                    case OMOP_ReferenceSwap:
                      if(rp->specified) {
                        copy3f(rp->coord, v1);
                        copy3f(v, rp->coord);
                        copy3f(v1, v);
                        inv_flag = true;
                      }
                      break;
                    }
                    op->i2++;
                  }
                }
              }
            }
            if(inv_flag && cs) {
              cs->invalidateRep(cRepAll, cRepInvRep);
            }
          }
        }
      }
      break;
    case OMOP_AddHydrogens:
      if (ok) {
          ok &= ObjectMoleculeAddSeleHydrogensRefactored(I, sele, op->i1);
      }
      break;
    case OMOP_FixHydrogens:
      if (ok)
	ok &= ObjectMoleculeFixSeleHydrogens(I, sele, -1);      /* state? */
      break;
    case OMOP_RevalenceFromSource:
    case OMOP_RevalenceByGuessing:
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        if(SelectorIsMember(G, ai->selEntry, sele)) {
          hit_flag = true;
          break;
        }
        ai++;
      }
      break;
    case OMOP_PrepareFromTemplate:
      ai0 = op->ai;             /* template atom */
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          ai = I->AtomInfo + a;
          if(op->i1 != 3) {
            ai->hetatm = ai0->hetatm;
            ai->flags = ai0->flags;
            LexAssign(G, ai->chain, ai0->chain);
            strcpy(ai->alt, ai0->alt);
            LexAssign(G, ai->segi, ai0->segi);
          }
          if(op->i1 == 1) {     /* mode 1, merge residue information */
            ai->inscode = ai0->inscode;
            ai->resv = ai0->resv;
            LexAssign(G, ai->resn, ai0->resn);
          }
          AtomInfoAssignColors(G, ai);
          if(op->i3) {
            if((ai->elem[0] == ai0->elem[0]) && (ai->elem[1] == ai0->elem[1]))
              ai->color = ai0->color;
            else if(ai->protons == cAN_C) {
              ai->color = op->i4;
            }
          }
          ai->visRep = ai0->visRep;
          ai->id = -1;
          ai->rank = -1;
          op->i2++;
        }
      }
      break;
    case OMOP_Sort:
      for(a = 0; ok && a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          if (ok)
	    ok &= ObjectMoleculeSort(I);
          break;
        }
      }
      break;
    case OMOP_SetAtomicSetting:
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        if(SelectorIsMember(G, ai->selEntry, sele)) {
          int uid = AtomInfoCheckUniqueID(G, ai);
          ai->has_setting = true;
          if (SettingUniqueSetTypedValue(G, uid, op->i1, op->i2, op->ii1))
            op->i4++;
        }
        ai++;
      }
      break;
    case OMOP_Pop:
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          if(SelectorMoveMember(G, s, sele, op->i1)) {
            op->i2--;
            op->i3++;
          }
          if(!op->i2)
            break;
        }
      }
      break;
    case OMOP_AVRT:            /* average vertex coordinate */
    case OMOP_StateVRT:        /* state vertex coordinate */
      {
        int op_i2 = op->i2;
        int obj_TTTFlag = I->TTTFlag;
        int b_end = I->NCSet;
        if (op->code == OMOP_StateVRT && op->i1 < b_end) {
          b_end = op->i1 + 1;
        }
        if(op_i2) {
          use_matrices =
            SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
          if(use_matrices<0) use_matrices = 0;
        }
        for(a = 0; a < I->NAtom; a++) {
          s = I->AtomInfo[a].selEntry;
          if(!(priority = SelectorIsMember(G, s, sele)))
            continue;

          cnt = 0;

          // all states for AVRT, one state for StateVRT (don't use
          // StateIterator which depends on settings)
          for(b = op->i1; b < b_end; ++b) {
            if(!(cs = I->CSet[b]))
              continue;

            if((a1 = cs->atmToIdx(a)) == -1)
              continue;

            if(!cnt) {
              VLACheck(op->vv1, float, (op->nvv1 * 3) + 2);
            }
            cnt++;
            vv2 = cs->coordPtr(a1);

            if(op_i2) {   /* do we want transformed coordinates? */
              if(use_matrices) {
                if(!cs->Matrix.empty()) {    /* state transformation */
                  transform44d3f(cs->Matrix.data(), vv2, v1);
                  vv2 = v1;
                }
              }
              if(obj_TTTFlag) {
                transformTTT44f3f(I->TTT, vv2, v1);
                vv2 = v1;
              }
            }

            // sum up coordinates over states
            vv1 = op->vv1 + (op->nvv1 * 3);
            add3f(vv1, vv2, vv1);
          }

          // number of summed up coordinates (states) for this atom
          VLACheck(op->vc1, int, op->nvv1);
          op->vc1[op->nvv1] = cnt;

          // ordered_selections
          if(op->vp1) {
            VLACheck(op->vp1, int, op->nvv1);
            op->vp1[op->nvv1] = priority;
          }

          // atom pointer VLA
          if(op->ai1VLA) {
            VLACheck(op->ai1VLA, AtomInfoType *, op->nvv1);
            op->ai1VLA[op->nvv1] = I->AtomInfo + a;
            I->AtomInfo[a].temp1 = a;
            /* KLUDGE ALERT!!! storing atom index in the temp1 field... */
          }

          // number of atoms in selection (incl. the ones with no coordinates)
          op->nvv1++;
        }
      }
      break;
    case OMOP_SFIT:            /* state fitting within a single object */
      vt = pymol::malloc<float>(3 * op->nvv2);  /* temporary (matching) target vertex pointers */
      cnt = 0;
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          cnt++;
          break;
        }
      }
      if(cnt) {                 /* only perform action for selected object */

        for(b = 0; b < I->NCSet; b++) {
          rms = -1.0;
          vt1 = vt;             /* reset target vertex pointers */
          vt2 = op->vv2;
          t_i = 0;              /* original target vertex index */
          if(I->CSet[b] && (b != op->i2)) {
            op->nvv1 = 0;
            for(a = 0; a < I->NAtom; a++) {
              s = I->AtomInfo[a].selEntry;
              if(SelectorIsMember(G, s, sele)) {
                a1 = I->CSet[b]->atmToIdx(a);
                if(a1 >= 0) {

                  match_flag = false;
                  while(t_i < op->nvv2) {
                    if(op->i1VLA[t_i] == a) {   /* same atom? */
                      match_flag = true;
                      break;
                    }
                    if(op->i1VLA[t_i] < a) {    /* catch up? */
                      t_i++;
                      vt2 += 3;
                    } else
                      break;
                  }
                  if(match_flag) {
                    VLACheck(op->vv1, float, (op->nvv1 * 3) + 2);
                    vv2 = I->CSet[b]->coordPtr(a1);
                    vv1 = op->vv1 + (op->nvv1 * 3);
                    *(vv1++) = *(vv2++);
                    *(vv1++) = *(vv2++);
                    *(vv1++) = *(vv2++);
                    *(vt1++) = *(vt2);
                    *(vt1++) = *(vt2 + 1);
                    *(vt1++) = *(vt2 + 2);
                    op->nvv1++;
                  }
                }
              }
            }
            if(op->nvv1 != op->nvv2) {
              PRINTFB(G, FB_Executive, FB_Warnings)
                "Executive-Warning: Missing atoms in state %d (%d instead of %d).\n",
                b + 1, op->nvv1, op->nvv2 ENDFB(G);
            }
            if(op->nvv1) {
              if(op->i1 != 0)   /* fitting flag */
                rms = MatrixFitRMSTTTf(G, op->nvv1, op->vv1, vt, nullptr, op->ttt);
              else
                rms = MatrixGetRMS(G, op->nvv1, op->vv1, vt, nullptr);
              if(op->i1 == 2) {
                ObjectMoleculeTransformTTTf(I, op->ttt, b);

                if(op->i3) {
                  const float divisor = (float) op->i3;
                  const float premult = (float) op->i3 - 1.0F;

                  /* mix flag is set, so average the prior target
                     coordinates with these coordinates */

                  vt2 = op->vv2;
                  t_i = 0;      /* original target vertex index */
                  for(a = 0; a < I->NAtom; a++) {
                    s = I->AtomInfo[a].selEntry;
                    if(SelectorIsMember(G, s, sele)) {
                      a1 = I->CSet[b]->atmToIdx(a);
                      if(a1 >= 0) {

                        match_flag = false;
                        while(t_i < op->nvv2) {
                          if(op->i1VLA[t_i] == a) {     /* same atom? */
                            match_flag = true;
                            break;
                          }
                          if(op->i1VLA[t_i] < a) {      /* catch up? */
                            t_i++;
                            vt2 += 3;
                          } else
                            break;
                        }
                        if(match_flag) {
                          vv2 = I->CSet[b]->coordPtr(a1);
                          *(vt2) = ((premult * (*vt2)) + *(vv2++)) / divisor;
                          *(vt2 + 1) = ((premult * (*(vt2 + 1))) + *(vv2++)) / divisor;
                          *(vt2 + 2) = ((premult * (*(vt2 + 2))) + *(vv2++)) / divisor;
                        }
                      }
                    }
                  }
                }
              }
            } else {
              PRINTFB(G, FB_Executive, FB_Warnings)
                "Executive-Warning: No matches found for state %d.\n", b + 1 ENDFB(G);
            }
          }
          VLACheck(op->f1VLA, float, b);
          op->f1VLA[b] = rms;
        }
        VLASize(op->f1VLA, float, I->NCSet);    /* NOTE this action is object-specific! */
      }
      FreeP(vt);
      break;
    case OMOP_OnOff:
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          hit_flag = true;
          break;
        }
      }
      break;
    case OMOP_SaveUndo:        /* save undo */
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          hit_flag = true;
          break;
        }
      }
      break;
    case OMOP_IdentifyObjects: /* identify atoms */
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          VLACheck(op->i1VLA, int, op->i1);
          op->i1VLA[op->i1] = I->AtomInfo[a].id;
          if (op->obj1VLA != nullptr) {
            VLACheck(op->obj1VLA, ObjectMolecule*, op->i1);
            op->obj1VLA[op->i1] = I;
          }
          op->i1++;
        }
      }
      break;
    case OMOP_Index:           /* identify atoms */
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          VLACheck(op->i1VLA, int, op->i1);
          op->i1VLA[op->i1] = a;        /* NOTE: need to incr by 1 before python */
          VLACheck(op->obj1VLA, ObjectMolecule *, op->i1);
          op->obj1VLA[op->i1] = I;
          op->i1++;
        }
      }
      break;
    case OMOP_GetObjects:      /* identify atoms */
      for(a = 0; a < I->NAtom; a++) {
        s = I->AtomInfo[a].selEntry;
        if(SelectorIsMember(G, s, sele)) {
          VLACheck(op->obj1VLA, ObjectMolecule *, op->i1);
          op->obj1VLA[op->i1] = I;
          op->i1++;
          break;
        }
      }
      break;
    case OMOP_CountAtoms:      /* count atoms in object, in selection */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele))
          op->i1++;
        ai++;
      }
      break;
    case OMOP_PhiPsi:
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          VLACheck(op->i1VLA, int, op->i1);
          op->i1VLA[op->i1] = a;
          VLACheck(op->obj1VLA, ObjectMolecule *, op->i1);
          op->obj1VLA[op->i1] = I;
          VLACheck(op->f1VLA, float, op->i1);
          VLACheck(op->f2VLA, float, op->i1);
          if(ObjectMoleculeGetPhiPsi
             (I, a, op->f1VLA + op->i1, op->f2VLA + op->i1, op->i2))
            op->i1++;
        }
        ai++;
      }
      break;
    case OMOP_Cartoon:         /* adjust cartoon type */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          if (ai->cartoon!=op->i1){
            op->i3++;
          }
          ai->cartoon = op->i1;
          op->i2++;
        }
        ai++;
      }
      break;
    case OMOP_Protect:         /* protect atoms from movement */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          ai->protekted = op->i1;
          op->i2++;
        }
        ai++;
      }
      break;
    case OMOP_Mask:            /* protect atoms from selection */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          ai->masked = op->i1;
          op->i2++;
        }
        ai++;
      }
      break;
    case OMOP_SetB:            /* set B-value */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          ai->b = op->f1;
          op->i2++;
        }
        ai++;
      }
      break;
    case OMOP_Remove:          /* flag atoms for deletion */
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
	ai->deleteFlag = false;
	s = ai->selEntry;
	if(SelectorIsMember(G, s, sele)) {
	  ai->deleteFlag = true;
	  op->i1++;
	}
	ai++;
      }
      break;
    case OMOP_GetChains:
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          // pointer hack
          ((std::set<lexidx_t> *) (void*) op->ii1)->insert(ai->chain);
          op->i1++;
        }
        ai++;
      }
      break;

    case OMOP_Spectrum:
      ai = I->AtomInfo.data();
      ai0 = nullptr;
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          skip_flag = false;
          if(op->i4 && ai0)     /* byres and we've done a residue */
            if(AtomInfoSameResidue(G, ai, ai0))
              skip_flag = true;
          if(!skip_flag) {
            c = (int) (0.49999 + op->i1 * (op->ff1[op->i3] - op->f1) / op->f2);
            if(c < 0)
              c = 0;
            if(c >= op->i1)
              c = op->i1 - 1;
            ai->color = op->ii1[c];

            /*               printf("%8.3 %8.3\n",ai->partial_charge, */
            if(op->i4) {        /* byres */
              offset = -1;
              while((a + offset) >= 0) {
                ai0 = I->AtomInfo + a + offset;
                if(AtomInfoSameResidue(G, ai, ai0)) {
                  ai0->color = op->ii1[c];
                  hit_flag = true;
                } else
                  break;
                offset--;
              }
              offset = 1;
              while((a + offset) < I->NAtom) {
                ai0 = I->AtomInfo + a + offset;
                if(AtomInfoSameResidue(G, ai, ai0)) {
                  ai0->color = op->ii1[c];
                  hit_flag = true;
                } else
                  break;
                offset++;
              }
            }
            ai0 = ai;
          }
          op->i3++;

        }
        ai++;
      }
      break;

    case OMOP_SingleStateVertices:     /* same as OMOP_VERT for a single state */
      ai = I->AtomInfo.data();
      if(op->cs1 < I->NCSet) {
        if(I->CSet[op->cs1]) {
          b = op->cs1;
          for(a = 0; a < I->NAtom; a++) {
            s = ai->selEntry;
            if(SelectorIsMember(G, s, sele)) {
              op->i1++;
              a1 = I->CSet[b]->atmToIdx(a);
              if(a1 >= 0) {
                VLACheck(op->vv1, float, (op->nvv1 * 3) + 2);
                vv2 = I->CSet[b]->coordPtr(a1);
                vv1 = op->vv1 + (op->nvv1 * 3);
                *(vv1++) = *(vv2++);
                *(vv1++) = *(vv2++);
                *(vv1++) = *(vv2++);
                op->nvv1++;
              }
            }
            ai++;
          }
        }
      }
      break;
    case OMOP_CSetIdxGetAndFlag:
      ai = I->AtomInfo.data();
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          for(b = op->cs1; b <= op->cs2; b++) {
            offset = b - op->cs1;
            if(b < I->NCSet) {
              if(I->CSet[b]) {
                a1 = I->CSet[b]->atmToIdx(a);
                if(a1 >= 0) {
                  op->ii1[op->i1 * offset + op->i2] = 1;        /* presence flag */
                  vv1 = op->vv1 + 3 * (op->i1 * offset + op->i2);       /* atom-based offset */
                  vv2 = I->CSet[b]->coordPtr(a1);
                  *(vv1++) = *(vv2++);
                  *(vv1++) = *(vv2++);
                  *(vv1++) = *(vv2++);
                  op->nvv1++;
                }
              }
            }
          }
          op->i2++;             /* atom index field for atoms within selection... */
        }
        ai++;
      }
      break;
    case OMOP_CSetIdxSetFlagged:
      ai = I->AtomInfo.data();
      hit_flag = false;
      for(a = 0; a < I->NAtom; a++) {
        s = ai->selEntry;
        if(SelectorIsMember(G, s, sele)) {
          for(b = op->cs1; b <= op->cs2; b++) {
            offset = b - op->cs1;
            if(b < I->NCSet) {
              if(I->CSet[b]) {
                a1 = I->CSet[b]->atmToIdx(a);
                if(a1 >= 0) {
                  if(op->ii1[op->i1 * offset + op->i2]) {       /* copy flag */
                    vv1 = op->vv1 + 3 * (op->i1 * offset + op->i2);     /* atom-based offset */
                    vv2 = I->CSet[b]->coordPtr(a1);
                    *(vv2++) = *(vv1++);
                    *(vv2++) = *(vv1++);
                    *(vv2++) = *(vv1++);
                    op->nvv1++;
                    hit_flag = true;
                  }
                }
              }
            }
          }
          op->i2++;             /* atom index field for atoms within selection... */
        }
        ai++;
      }
      break;
    case OMOP_SUMC:            /* performance optimized to speed center & zoom actions */
      {
				/* given a selection, sum up all the coordinates (for centering) */
        float *op_v1 = op->v1;
        int op_i1 = op->i1;
        int op_i2 = op->i2;
        int obj_TTTFlag = I->TTTFlag;
        int i_NCSet = I->NCSet;
        int i_NAtom = I->NAtom;
        int i_DiscreteFlag = I->DiscreteFlag;
        CoordSet* const* i_CSet = I->CSet.data();
        if(op_i2) {
          use_matrices =
            SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
          if(use_matrices<0) use_matrices = 0;
        }
        ai = I->AtomInfo.data();
        for(a = 0; a < i_NAtom; a++) {
          s = ai->selEntry;
					/* for each atom, if this current atom is in the selection */
          if(SelectorIsMember(G, s, sele)) {
						/* loop over all atoms; regardless of state */
            for(b = 0; b < i_NCSet; b++) {
              if(i_DiscreteFlag) {
                if((cs = I->DiscreteCSet[a]))
                  a1 = I->DiscreteAtmToIdx[a];
              } else {
                if((cs = i_CSet[b]))
                  a1 = cs->AtmToIdx[a];
              }
							/* if valid coordinate set and atom info for this atom */
              if(cs && (a1 >= 0)) {
                coord = cs->coordPtr(a1);
                if(op_i2) {     /* do we want transformed coordinates? */
                  if(use_matrices) {
                    if(!cs->Matrix.empty()) {      /* state transformation */
                      transform44d3f(cs->Matrix.data(), coord, v1);
                      coord = v1;
                    }
                  }
                  if(obj_TTTFlag) {
                    transformTTT44f3f(I->TTT, coord, v1);
                    coord = v1;
                  }
                }
								/* op_v1 += coord */
                add3f(op_v1, coord, op_v1);
								/* count += 1 */
                op_i1++;
              }
              if(i_DiscreteFlag)
                break;
            }
          }
					/* next atom */
          ai++;
        }
        op->i1 = op_i1;
      }
      break;
    case OMOP_MNMX:            /* performance optimized to speed center & zoom actions */
      {
        float *op_v1 = op->v1;
        float *op_v2 = op->v2;
        int op_i1 = op->i1;
        int op_i2 = op->i2;
        int obj_TTTFlag = I->TTTFlag;
        int i_NCSet = I->NCSet;
        int i_NAtom = I->NAtom;
        int i_DiscreteFlag = I->DiscreteFlag;
        CoordSet* const* i_CSet = I->CSet.data();
        if(op_i2) {
          use_matrices =
            SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
          if(use_matrices<0) use_matrices = 0;
        }
        ai = I->AtomInfo.data();
        for(a = 0; a < i_NAtom; a++) {
          s = ai->selEntry;
          if(SelectorIsMember(G, s, sele)) {
            for(b = 0; b < i_NCSet; b++) {
              if(i_DiscreteFlag) {
                if((cs = I->DiscreteCSet[a]))
                  a1 = I->DiscreteAtmToIdx[a];
              } else {
                if((cs = i_CSet[b]))
                  a1 = cs->AtmToIdx[a];
              }
              if(cs && (a1 >= 0)) {
                coord = cs->coordPtr(a1);
                if(op_i2) {     /* do we want transformed coordinates? */
                  if(use_matrices) {
                    if(!cs->Matrix.empty()) {      /* state transformation */
                      transform44d3f(cs->Matrix.data(), coord, v1);
                      coord = v1;
                    }
                  }
                  if(obj_TTTFlag) {
                    transformTTT44f3f(I->TTT, coord, v1);
                    coord = v1;
                  }
                }
                if(op_i1) {
                  if(op_v1[0] > coord[0])
                    op_v1[0] = coord[0];
                  if(op_v1[1] > coord[1])
                    op_v1[1] = coord[1];
                  if(op_v1[2] > coord[2])
                    op_v1[2] = coord[2];
                  if(op_v2[0] < coord[0])
                    op_v2[0] = coord[0];
                  if(op_v2[1] < coord[1])
                    op_v2[1] = coord[1];
                  if(op_v2[2] < coord[2])
                    op_v2[2] = coord[2];
                } else {
                  op_v1[0] = coord[0];
                  op_v1[1] = coord[1];
                  op_v1[2] = coord[2];
                  op_v2[0] = coord[0];
                  op_v2[1] = coord[1];
                  op_v2[2] = coord[2];
                }
                op_i1++;
              }
              if(i_DiscreteFlag)
                break;
            }
          }
          ai++;
        }
        op->i1 = op_i1;
      }
      break;
    default:
      {
        int inv_flag;
#ifdef _PYMOL_IP_EXTRAS
	int use_stereo = 0, use_text_type = 0;
#endif

        switch (op->code) {
        case OMOP_INVA:
          /* set up an important optimization... */
          for(b = 0; b < I->NCSet; b++) {
            cs = I->CSet[b];
            if(cs)
              cs->objMolOpInvalidated = false;
          }
          break;
        }
        use_matrices = SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
        if(use_matrices<0) use_matrices = 0;
        ai = I->AtomInfo.data();

#ifdef _PYMOL_IP_EXTRAS
        // use stereo or text_type ?
        // only do this for "label2" command (better logic in WrapperObjectSubScript)
	if (op->code == OMOP_LABL && op->i2 == cExecutiveLabelEvalAlt) {
	  use_stereo = PLabelExprUsesVariable(G, op->s1, "stereo");
	  use_text_type = PLabelExprUsesVariable(G, op->s1, "text_type");
	}

#ifdef NO_MMLIBS
        if (use_stereo) {
          PRINTFB(G, FB_ObjectMolecule, FB_Warnings)
            " NO_MMLIBS-Warning: stereochemistry not supported in this PyMOL build.\n" ENDFB(G);
        }
        if (use_text_type) {
          PRINTFB(G, FB_ObjectMolecule, FB_Warnings)
            " NO_MMLIBS-Warning: automatic 'text_type' assignment not supported in this PyMOL build.\n" ENDFB(G);
        }
#endif
#endif

        for(a = 0; a < I->NAtom; a++) {
          switch (op->code) {
          case OMOP_Flag:
            ai->flags &= op->i2;        /* clear flag using mask */
            op->i4++;
            /* no break here is intentional!  */
          case OMOP_FlagSet:
          case OMOP_FlagClear:
          case OMOP_COLR:      /* normal atom based loops */
          case OMOP_VISI:
          case OMOP_CheckVis:
          case OMOP_TTTF:
          case OMOP_ALTR:
          case OMOP_LABL:
          case OMOP_AlterState:
            s = ai->selEntry;
            if(SelectorIsMember(G, s, sele)) {
              switch (op->code) {
              case OMOP_Flag:
              case OMOP_FlagSet:
                ai->flags |= op->i1;    /* set flag */
                op->i3++;
                break;
              case OMOP_FlagClear:
                ai->flags &= op->i2;    /* clear flag */
                op->i3++;
                break;
              case OMOP_VISI:
                switch (op->i2) {
                case cVis_HIDE:
                  ai->visRep &= ~(op->i1);
                  I->visRep &= ~(op->i1); // cell
                  break;
                case cVis_SHOW:
                  ai->visRep |= op->i1 & cRepsAtomMask;
                  I->visRep |= op->i1 & cRepsObjectMask; // cell
                  break;
                case cVis_AS:
                  ai->visRep = op->i1 & cRepsAtomMask;
                  I->visRep = op->i1 & cRepsObjectMask; // cell
                  break;
                }
                break;
              case OMOP_CheckVis:
                if((ai->visRep & op->i1)) {
                  op->i2 = true;
                }
                break;
              case OMOP_COLR:
                if(op->i1 == cColorAtomic)
                  ai->color = AtomInfoGetColor(G, ai);
                else
                  ai->color = op->i1;
                hit_flag = true;
                op->i2++;
                break;
              case OMOP_TTTF:
                hit_flag = true;
                break;
              case OMOP_LABL:
                if(ok) {
                  if(!op->s1[0]) {
                    if(ai->label) {
			op->i1--; /* negative if unlabelling */
                      LexAssign(G, ai->label, 0);
                    }
                    ai->visRep &= ~cRepLabelBit;
                    hit_flag = true;
                  } else {
                    switch (op->i2) {
                    case cExecutiveLabelEvalOn:
		      {
			/* python label expression evaluation */
			CoordSet *cs = nullptr;
			if(I->DiscreteFlag && I->DiscreteCSet) {
			  cs = I->DiscreteCSet[a];
			} else if (I->NCSet == 1){
			  cs = I->CSet[0];
			}
#ifndef _PYMOL_NOPY
			if(PLabelAtom(I->G, I, cs, expr_co, a)) {
			  if (ai->label){
			    op->i1++; /* only if the string has been set, report labelled */
			  }
			  ai->visRep |= cRepLabelBit;
			  hit_flag = true;
			} else {
			  ok = false;
			}
#else
			ok = false;
#endif
		      }
                      break;
                    case cExecutiveLabelEvalAlt:
		      {
			if(PLabelAtomAlt(I->G, &I->AtomInfo[a], I->Name, op->s1, a)) {
			  if (ai->label){
			    op->i1++; /* only if the string has been set, report labelled */
			  }
			  ai->visRep |= cRepLabelBit;
			  hit_flag = true;
			} else {
			  ok = false;
			}
		      }
                      break;
                    case cExecutiveLabelEvalOff:
                      {
                        /* simple string label text */
                        AtomInfoType *ai = I->AtomInfo + a;
                        LexDec(G, ai->label);
                        ai->label = LexIdx(G, op->s1);
                      }
                      break;
                    }
                  }
                }
                break;
              case OMOP_ALTR:
                if(ok) {
		  CoordSet *cs = nullptr;
		  if(I->DiscreteFlag && I->DiscreteCSet) {
		    cs = I->DiscreteCSet[a];
		  } else if (I->NCSet == 1){
		    cs = I->CSet[0];
		  }
#ifndef _PYMOL_NOPY
                  if(PAlterAtom
                     (I->G, I, cs, expr_co, op->i2, a,
                      op->py_ob1))
                    op->i1++;
                  else
#endif
#ifdef _WEBGL
#endif
                    ok = false;
                }
                break;
              case OMOP_AlterState:
                if(ok) {
                  if(op->i2 < I->NCSet) {
                    cs = I->CSet[op->i2];
                    if(cs) {
                      a1 = cs->atmToIdx(a);
                      if(a1 >= 0) {
#ifndef _PYMOL_NOPY
                        if(PAlterAtomState(I->G, expr_co, op->i3,
                                           I, cs, a, a1, op->i2, op->py_ob1)) {
                          op->i1++;
                          hit_flag = true;
                        } else
#endif
#ifdef _WEBGL
#endif
                          ok = false;
                      }
                    }
                  }
                }
                break;
              }
              break;
            }
            break;

            /* coord-set based properties, iterating only a single coordinate set */
          case OMOP_CSetMinMax:
          case OMOP_CSetCameraMinMax:
          case OMOP_CSetMaxDistToPt:
          case OMOP_CSetSumSqDistToPt:
          case OMOP_CSetSumVertices:
          case OMOP_CSetMoment:
            cs = nullptr;
            if((op->cs1 >= 0) && (op->cs1 < I->NCSet)) {
              cs = I->CSet[op->cs1];
            } else if(op->include_static_singletons) {
              if((I->NCSet == 1)
                 && (SettingGet_b(G, nullptr, I->Setting.get(), cSetting_static_singletons))) {
                cs = I->CSet[0];        /*treat static singletons as present in each state */
              }
            }

            if(cs) {
              s = ai->selEntry;
              if(SelectorIsMember(G, s, sele)) {
                switch (op->code) {
                case OMOP_CSetSumVertices:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    coord = cs->coordPtr(a1);
                    if(op->i2) {        /* do we want transformed coordinates? */
                      if(use_matrices) {
                        if(!cs->Matrix.empty()) {  /* state transformation */
                          transform44d3f(cs->Matrix.data(), coord, v1);
                          coord = v1;
                        }
                      }
                      if(I->TTTFlag) {
                        transformTTT44f3f(I->TTT, coord, v1);
                        coord = v1;
                      }
                    }
                    add3f(op->v1, coord, op->v1);
                    op->i1++;
                  }
                  break;
                case OMOP_CSetMinMax:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    coord = cs->coordPtr(a1);
                    if(op->i2) {        /* do we want transformed coordinates? */
                      if(use_matrices) {
                        if(!cs->Matrix.empty()) {  /* state transformation */
                          transform44d3f(cs->Matrix.data(), coord, v1);
                          coord = v1;
                        }
                      }
                      if(I->TTTFlag) {
                        transformTTT44f3f(I->TTT, coord, v1);
                        coord = v1;
                      }
                    }
                    if(op->i1) {
                      for(c = 0; c < 3; c++) {
                        if(*(op->v1 + c) > *(coord + c))
                          *(op->v1 + c) = *(coord + c);
                        if(*(op->v2 + c) < *(coord + c))
                          *(op->v2 + c) = *(coord + c);
                      }
                    } else {
                      for(c = 0; c < 3; c++) {
                        *(op->v1 + c) = *(coord + c);
                        *(op->v2 + c) = *(coord + c);
                      }
                    }
                    op->i1++;
                  }
                  break;
                case OMOP_CSetCameraMinMax:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    coord = cs->coordPtr(a1);
                    if(op->i2) {        /* do we want transformed coordinates? */
                      if(use_matrices) {
                        if(!cs->Matrix.empty()) {  /* state transformation */
                          transform44d3f(cs->Matrix.data(), coord, v1);
                          coord = v1;
                        }
                      }
                      if(I->TTTFlag) {
                        transformTTT44f3f(I->TTT, coord, v1);
                        coord = v1;
                      }
                    }
                    MatrixTransformC44fAs33f3f(op->mat1, coord, v1);
                    /* convert to view-space */
                    coord = v1;
                    if(op->i1) {
                      for(c = 0; c < 3; c++) {
                        if(*(op->v1 + c) > *(coord + c))
                          *(op->v1 + c) = *(coord + c);
                        if(*(op->v2 + c) < *(coord + c))
                          *(op->v2 + c) = *(coord + c);
                      }
                    } else {
                      for(c = 0; c < 3; c++) {
                        *(op->v1 + c) = *(coord + c);
                        *(op->v2 + c) = *(coord + c);
                      }
                    }
                    op->i1++;
                  }
                  break;
                case OMOP_CSetSumSqDistToPt:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    float dist;
                    coord = cs->coordPtr(a1);
                    if(op->i2) {        /* do we want transformed coordinates? */
                      if(use_matrices) {
                        if(!cs->Matrix.empty()) {  /* state transformation */
                          transform44d3f(cs->Matrix.data(), coord, v1);
                          coord = v1;
                        }
                      }
                      if(I->TTTFlag) {
                        transformTTT44f3f(I->TTT, coord, v1);
                        coord = v1;
                      }
                    }
                    dist = (float) diff3f(op->v1, coord);
                    op->d1 += dist * dist;
                    op->i1++;
                  }
                  break;
                case OMOP_CSetMaxDistToPt:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    float dist;
                    coord = cs->coordPtr(a1);
                    if(op->i2) {        /* do we want transformed coordinates? */
                      if(use_matrices) {
                        if(!cs->Matrix.empty()) {  /* state transformation */
                          transform44d3f(cs->Matrix.data(), coord, v1);
                          coord = v1;
                        }
                      }
                      if(I->TTTFlag) {
                        transformTTT44f3f(I->TTT, coord, v1);
                        coord = v1;
                      }
                    }
                    dist = (float) diff3f(op->v1, coord);
                    if(dist > op->f1)
                      op->f1 = dist;
                    op->i1++;
                  }
                  break;
                case OMOP_CSetMoment:
                  a1 = cs->atmToIdx(a);
                  if(a1 >= 0) {
                    subtract3f(cs->coordPtr(a1), op->v1, v1);
                    v2 = v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2];
                    op->d[0][0] += v2 - v1[0] * v1[0];
                    op->d[0][1] += -v1[0] * v1[1];
                    op->d[0][2] += -v1[0] * v1[2];
                    op->d[1][0] += -v1[1] * v1[0];
                    op->d[1][1] += v2 - v1[1] * v1[1];
                    op->d[1][2] += -v1[1] * v1[2];
                    op->d[2][0] += -v1[2] * v1[0];
                    op->d[2][1] += -v1[2] * v1[1];
                    op->d[2][2] += v2 - v1[2] * v1[2];
                  }
                  break;

                }
              }
            }
            break;
          default:
            /* coord-set based properties, iterating as all coordsets within atoms */
            for(b = 0; b < I->NCSet; b++) {
              if(I->DiscreteFlag) {
                cs = I->DiscreteCSet[a];
              } else {
                cs = I->CSet[b];
              }
              if(cs) {
                s = ai->selEntry;
                inv_flag = false;
                if(SelectorIsMember(G, s, sele)) {
                  switch (op->code) {
                  case OMOP_CameraMinMax:
                    a1 = cs->atmToIdx(a);
                    if(a1 >= 0) {
                      coord = cs->coordPtr(a1);
                      if(op->i2) {      /* do we want transformed coordinates? */
                        if(use_matrices) {
                          if(!cs->Matrix.empty()) {        /* state transformation */
                            transform44d3f(cs->Matrix.data(), coord, v1);
                            coord = v1;
                          }
                        }
                        if(I->TTTFlag) {
                          transformTTT44f3f(I->TTT, coord, v1);
                          coord = v1;
                        }
                      }
                      MatrixTransformC44fAs33f3f(op->mat1, coord, v1);
                      /* convert to view-space */
                      coord = v1;
                      if(op->i1) {
                        for(c = 0; c < 3; c++) {
                          if(*(op->v1 + c) > *(coord + c))
                            *(op->v1 + c) = *(coord + c);
                          if(*(op->v2 + c) < *(coord + c))
                            *(op->v2 + c) = *(coord + c);
                        }
                      } else {
                        for(c = 0; c < 3; c++) {
                          *(op->v1 + c) = *(coord + c);
                          *(op->v2 + c) = *(coord + c);
                        }
                      }
                      op->i1++;
                    }
                    break;
                  case OMOP_MaxDistToPt:
                    a1 = cs->atmToIdx(a);
                    if(a1 >= 0) {
                      float dist;
                      coord = cs->coordPtr(a1);
                      if(op->i2) {      /* do we want transformed coordinates? */
                        if(use_matrices) {
                          if(!cs->Matrix.empty()) {        /* state transformation */
                            transform44d3f(cs->Matrix.data(), coord, v1);
                            coord = v1;
                          }
                        }
                        if(I->TTTFlag) {
                          transformTTT44f3f(I->TTT, coord, v1);
                          coord = v1;
                        }
                      }
                      dist = (float) diff3f(coord, op->v1);
                      if(dist > op->f1)
                        op->f1 = dist;
                      op->i1++;
                    }
                    break;
                  case OMOP_INVA:
                    if(!cs->objMolOpInvalidated) {
                      /* performance optimization: avoid repeatedly
                         calling invalidate on the same coord sets */
                      a1 = cs->atmToIdx(a);
                      if(a1 >= 0)       /* selection touches this coordinate set */
                        inv_flag = true;        /* so set the invalidation flag */
                    }
                    break;
                  case OMOP_VERT:
										/* get the atom index whether it's discrete or not */
                    a1 = cs->atmToIdx(a);
                    if(a1 >= 0) {
											/* if a1 is a valid atom index, then copy it's xyz coordinates
											 * into vv1; increment the counter, nvv1 */
                      VLACheck(op->vv1, float, (op->nvv1 * 3) + 2);
                      vv2 = cs->coordPtr(a1);
                      vv1 = op->vv1 + (op->nvv1 * 3);
                      *(vv1++) = *(vv2++);
                      *(vv1++) = *(vv2++);
                      *(vv1++) = *(vv2++);
                      op->nvv1++;
                    }
                    break;
                  case OMOP_SVRT:
                    /* gives us only vertices for a specific coordinate set */
                    if(b == op->i1) {
                      a1 = cs->atmToIdx(a);
                      if(a1 >= 0) {
                        VLACheck(op->vv1, float, (op->nvv1 * 3) + 2);
                        VLACheck(op->i1VLA, int, op->nvv1);
                        op->i1VLA[op->nvv1] = a;        /* save atom index for later comparisons */
                        vv2 = cs->coordPtr(a1);
                        vv1 = op->vv1 + (op->nvv1 * 3);
                        *(vv1++) = *(vv2++);
                        *(vv1++) = *(vv2++);
                        *(vv1++) = *(vv2++);
                        op->nvv1++;
                      }
                    }
                    break;
                  case OMOP_MOME:
                    /* Moment of inertia tensor - unweighted - assumes v1 is center of molecule */
                    a1 = cs->atmToIdx(a);
                    if(a1 >= 0) {
                      subtract3f(cs->coordPtr(a1), op->v1, v1);
                      v2 = v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2];
                      op->d[0][0] += v2 - v1[0] * v1[0];
                      op->d[0][1] += -v1[0] * v1[1];
                      op->d[0][2] += -v1[0] * v1[2];
                      op->d[1][0] += -v1[1] * v1[0];
                      op->d[1][1] += v2 - v1[1] * v1[1];
                      op->d[1][2] += -v1[1] * v1[2];
                      op->d[2][0] += -v1[2] * v1[0];
                      op->d[2][1] += -v1[2] * v1[1];
                      op->d[2][2] += v2 - v1[2] * v1[2];
                    }
                    break;
                  }
                }
                switch (op->code) {
                  /* full coord-set based */
                case OMOP_INVA:
                  /* shouldn't this be calling the object invalidation routine instead? */
                  if(inv_flag) {
                    cs->objMolOpInvalidated = true;
                    for (auto d = cRep_t(0); d < cRepCnt; ++d) {
                      if ((1 << d) & op->i1) {
                        cs->invalidateRep(d, cRepInv_t(op->i2));
                      }
                    }
                  }
                  break;
                }
                if(I->DiscreteFlag) {
                  /* don't iterate every coordinate set for discrete objects! */
                  break;
                }
              }
            }                   /* end coordset section */
            break;
          }
          ai++;
        }
      }                         /* case: default */
      break;
    }
    if(hit_flag) {
      switch (op->code) {
      case OMOP_COLR:
        ExecutiveUpdateColorDepends(I->G, I);
        break;
      case OMOP_TTTF:
        ObjectMoleculeTransformTTTf(I, op->ttt, -1);
        break;
      case OMOP_LABL:
        I->invalidate(cRepLabel, cRepInvText, -1);
        break;
      case OMOP_AlterState:    /* overly coarse - doing all states, could do just 1 */
        if(!op->i3) {           /* not read_only? */
          I->invalidate(cRepAll, cRepInvRep, -1);
          SceneChanged(G);
        }
        break;
      case OMOP_CSetIdxSetFlagged:
        I->invalidate(cRepAll, cRepInvRep, -1);
        SceneChanged(G);
        break;
      case OMOP_SaveUndo:
        op->i2 = true;
        ObjectMoleculeSaveUndo(I, op->i1, false);
        break;
      case OMOP_OnOff:
        ExecutiveSetObjVisib(G, I->Name, op->i1, false);
        break;
      case OMOP_RevalenceFromSource:
        if(ObjectMoleculeXferValences(I, op->i1, op->i2,
                                      op->i3, op->obj3, op->i4, op->i5, op->i6)) {
          ObjectMoleculeVerifyChemistry(I, op->i3);
          I->invalidate(cRepAll, cRepInvBonds, op->i3);
        }
        break;
      case OMOP_RevalenceByGuessing:
        {
          int *flag1 = pymol::calloc<int>(I->NAtom);
          int *flag2 = pymol::calloc<int>(I->NAtom);
          if(flag1 && flag2) {
            int a;
            int *f1 = flag1;
            int *f2 = flag2;
            const AtomInfoType *ai = I->AtomInfo.data();
            for(a = 0; a < I->NAtom; a++) {
              *(f1++) = SelectorIsMember(G, ai->selEntry, op->i1);
              *(f2++) = SelectorIsMember(G, ai->selEntry, op->i2);
              ai++;
            }
            {
              int target_state = op->i3;
              if(target_state < 0)
                target_state = 0;       /* TO DO */
              ObjectMoleculeGuessValences(I, target_state, flag1, flag2, op->i4);
              ObjectMoleculeVerifyChemistry(I, target_state);
              I->invalidate(cRepAll, cRepInvBonds, target_state);
            }
            FreeP(flag1);
            FreeP(flag2);
          }
        }
        break;
      }
    }

    /* always run on exit... */
    switch (op->code) {
    case OMOP_LABL:
      if (op->i2 != cExecutiveLabelEvalOn){
	break;
      }
    case OMOP_ALTR:
    case OMOP_AlterState:
#ifndef _PYMOL_NOPY
      Py_XDECREF(expr_co);
#endif
      break;
    }
    /* */
  }

  return ok;
}


/*========================================================================*/
void ObjectMoleculeGetAtomSele(const ObjectMolecule * I, int index, char *buffer)
{
  PyMOLGlobals * G = I->G;
  assert(index < I->NAtom);
  auto* ai = I->AtomInfo + index;
  char inscode_str[2] = { ai->inscode, '\0' };

  snprintf(buffer, OrthoLineLength, "/%s/%s/%s/%s`%d%s/%s`%s", I->Name,
      LexStr(G, ai->segi),
      LexStr(G, ai->chain),
      LexStr(G, ai->resn), ai->resv, inscode_str,
      LexStr(G, ai->name), ai->alt);
}

/**
 * Get Atom selection string
 * @param I target Object Molecule
 * @param index atom index of I
 */

std::string ObjectMoleculeGetAtomSele(const ObjectMolecule * I, int index)
{
  PyMOLGlobals * G = I->G;
  assert(index < I->NAtom);
  auto* ai = I->AtomInfo + index;
  char inscode_str[2] = { ai->inscode, '\0' };
  std::string buffer = pymol::string_format("/%s/%s/%s/%s`%d%s/%s`%s", I->Name,
      LexStr(G, ai->segi),
      LexStr(G, ai->chain),
      LexStr(G, ai->resn), ai->resv, inscode_str,
      LexStr(G, ai->name), ai->alt);
  return buffer;
}

/*========================================================================*/

/**
 * Get Atom selection string
 * @param I target Object Molecule
 * @param index atom index of I
 */

std::string ObjectMolecule::describeElement(int index) const
{
  auto I = this;
  auto buffer = ObjectMoleculeGetAtomSele(I, index);
  if(!I->AtomInfo[index].alt[0]) {
    // don't include the trailing backtick
    buffer.pop_back();
  }
  return buffer;
}


/*========================================================================*/

std::string ObjectMoleculeGetAtomSeleLog(const ObjectMolecule* I, int index, int quote)
{
  OrthoLineType buffer;
  ObjectMoleculeGetAtomSeleLog(I, index, buffer, quote);
  return buffer;
}

void ObjectMoleculeGetAtomSeleLog(const ObjectMolecule * I, int index, char *buffer, int quote)
{
  char *p = quote ? buffer + 1 : buffer;

  if(SettingGetGlobal_b(I->G, cSetting_robust_logs)) {
    ObjectMoleculeGetAtomSele(I, index, p);
  } else {
    sprintf(p, "(%s`%d)", I->Name, index + 1);
  }

  if (quote) {
    int len = strlen(p);
    buffer[0] = buffer[len + 1] = '"';
    buffer[len + 2] = 0;
  }
}

std::string ObjectMoleculeGetAtomSeleFast(const ObjectMolecule* I, int index)
{
  auto G = I->G;
  auto* ai = I->AtomInfo + index;
  char inscodestr[2] = {ai->inscode, 0};
  return pymol::string_format("(/'%s'/'%s'/'%s'/'%s'`%d%s/'%s'`'%s')", I->Name,
      LexStr(G, ai->segi), LexStr(G, ai->chain), LexStr(G, ai->resn), ai->resv,
      inscodestr, ai->name, ai->alt);
}

/*========================================================================*/
int ObjectMolecule::getNFrame() const
{
  return NCSet;
}

struct _CCoordSetUpdateThreadInfo {
  CoordSet *cs;
  int a;
};

void CoordSetUpdateThread(CCoordSetUpdateThreadInfo * T)
{
  if(T->cs) {
    T->cs->update(T->a);
  }
}

#ifndef _PYMOL_NOPY
static void ObjMolCoordSetUpdateSpawn(PyMOLGlobals * G,
                                      CCoordSetUpdateThreadInfo * Thread, int n_thread,
                                      int n_total)
{
  if(n_total == 1) {
    CoordSetUpdateThread(Thread);
  } else if(n_total) {
    int blocked;
    PyObject *info_list;
    int a, n = 0;
    blocked = PAutoBlock(G);

    PRINTFB(G, FB_Scene, FB_Blather)
      " Scene: updating coordinate sets with %d threads...\n", n_thread ENDFB(G);
    info_list = PyList_New(n_total);
    for(a = 0; a < n_total; a++) {
      PyList_SetItem(info_list, a, PyCapsule_New(Thread + a, nullptr, nullptr));
      n++;
    }
    PXDecRef(PYOBJECT_CALLMETHOD
             (G->P_inst->cmd, "_coordset_update_spawn", "Oi", info_list, n_thread));
    Py_DECREF(info_list);
    PAutoUnblock(G, blocked);
  }
}
#endif


/*========================================================================*/
void ObjectMolecule::update()
{
  auto I = this;
  int a; /*, ok; */

  OrthoBusyPrime(G);
  /* if the cached representation is invalid, reset state */
  if(!I->RepVisCacheValid) {
    /* note which representations are active */
    /* for each atom in each coordset, blank out the representation cache */
    if(I->NCSet > 1) {
      const AtomInfoType *ai = I->AtomInfo.data();
      I->RepVisCache = 0;
      for(a = 0; a < I->NAtom; a++) {
        I->RepVisCache |= ai->visRep;
        ai++;
      }
    } else {
      I->RepVisCache = cRepBitmask;     /* if only one coordinate set, then
                                         * there's no benefit to pre-filtering
                                         * the representations... */
    }
    I->RepVisCacheValid = true;
  }
  {
    /* determine the start/stop states */
    int start = 0;
    int stop = I->NCSet;
    /* set start and stop given an object */
    ObjectAdjustStateRebuildRange(I, &start, &stop);
    if((I->NCSet == 1)
       && (SettingGet_b(G, I->Setting.get(), nullptr, cSetting_static_singletons))) {
      start = 0;
      stop = 1;
    }
    if(stop > I->NCSet)
      stop = I->NCSet;

    /* single and multithreaded coord set updates */
    {
#ifndef _PYMOL_NOPY
      int n_thread = SettingGetGlobal_i(G, cSetting_max_threads);
      int multithread = SettingGetGlobal_i(G, cSetting_async_builds);

      if(multithread && (n_thread) && (stop - start) > 1) {
        int cnt = 0;

        /* must precalculate to avoid race-condition since this isn't
           mutexed yet and neighbors are needed by cartoons */
        this->getNeighborArray();

        for(a = start; a < stop; a++)
          if((a<I->NCSet) && I->CSet[a])
            cnt++;
        {
          CCoordSetUpdateThreadInfo *thread_info = pymol::malloc<CCoordSetUpdateThreadInfo>(cnt);
          if(thread_info) {
            cnt = 0;
            for(a = start; a < stop; a++) {
              if((a<I->NCSet) && I->CSet[a]) {
                thread_info[cnt].cs = I->CSet[a];
                thread_info[cnt].a = a;
                cnt++;
              }
            }
            ObjMolCoordSetUpdateSpawn(G, thread_info, n_thread, cnt);
            FreeP(thread_info);
          }

        }

      } else
#endif
      {                         /* single thread */
        for(a = start; a < stop; a++) {
          if((a<I->NCSet) && I->CSet[a] && (!G->Interrupt)) {
	    /* status bar */
            OrthoBusySlow(G, a, I->NCSet);
            PRINTFB(G, FB_ObjectMolecule, FB_Blather)
              " ObjectMolecule-DEBUG: updating representations for state %d of \"%s\".\n",
              a + 1, I->Name ENDFB(G);
            I->CSet[a]->update(a);
          }
        }
      }
    }
  } /* end block */

  PRINTFD(G, FB_ObjectMolecule)
    " ObjectMolecule: updates complete for object %s.\n", I->Name ENDFD;
}

/*========================================================================*/
void ObjectMolecule::invalidate(cRep_t rep, cRepInv_t level, int state)
{
  auto I = this;
  int a;
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: entered. rep: %d level: %d\n", __func__, rep, level ENDFD;

  auto const level_actual = level;

  // Remove the "purge" bit
  level = static_cast<decltype(level)>(level & ~cRepInvPurgeMask);

  if(level >= cRepInvVisib) {
    I->RepVisCacheValid = false;
  }

  if (level >= cRepInvBondsNoNonbonded) {
    if (level < cRepInvBonds) {
      level = cRepInvBonds;
    } else {
      ObjectMoleculeUpdateNonbonded(I);
    }
  }

  if(level >= cRepInvBonds) {
    this->Neighbor.reset();
    if(I->Sculpt) {
      DeleteP(I->Sculpt);
    }
    if(level >= cRepInvAtoms) {
      SelectorUpdateObjectSele(I->G, I);
    }
  }
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: invalidating representations...\n", __func__ ENDFD;

  if ( level >= cRepInvColor ) { 
    /* after label, this gets called, so we shouldn't invalidate types b/c PYMOL-317
       not sure if that is the exact level we should cut this off at
       1/28/13 BB: changed from >cRepInvText to >=cRepInvColor for invalidating surface
       colors during gadget invalidation */
    
    int start = 0;
    int stop = I->NCSet;

    if(state >= 0) {
      start = state;
      stop = state + 1;
    }
    if(stop > I->NCSet)
      stop = I->NCSet;
    for(a = start; a < stop; a++) {
      CoordSet *cset = 0;
      cset = I->CSet[a];
      if(cset) {
        cset->invalidateRep(rep, level_actual);
      }
    }
  }
  
  PRINTFD(I->G, FB_ObjectMolecule)
    " %s: leaving...\n", __func__ ENDFD;

}

void ObjectMoleculeInvalidateAtomType(ObjectMolecule *I, int state){
  CoordSet *cset = 0;
  int ai, atm;
  AtomInfoType *at;
  cset = I->CSet[state];
  if (state < 0){
    for (ai=0; ai < I->NAtom; ai++){
      at = &I->AtomInfo[ai];
      at->textType = 0;
    }
  } else {
    for (ai=0; ai < cset->NIndex; ai++){
      atm = cset->IdxToAtm[ai];
      if (atm>=0){
	at = &I->AtomInfo[ai];
	at->textType = 0;
      }
    }
  }
}

/*========================================================================*/
int ObjectMoleculeMoveAtom(ObjectMolecule * I, int state, int index, const float *v, int mode,
                           int log)
{
  int result = 0;
  PyMOLGlobals *G = I->G;
  CoordSet *cs;
  if(!(I->AtomInfo[index].protekted == cAtomProtected_explicit)) {
    if(state < 0)
      state = 0;
    if(I->NCSet == 1)
      state = 0;
    state = state % I->NCSet;
    if((!I->CSet[state]) && (SettingGet_b(G, I->Setting.get(), nullptr, cSetting_all_states)))
      state = 0;
    cs = I->CSet[state];
    if(cs) {
      result = CoordSetMoveAtom(I->CSet[state], index, v, mode);
      cs->invalidateRep(cRepAll, cRepInvCoord);
      ExecutiveUpdateCoordDepends(G, I);
    }
  }
  if(log) {
    OrthoLineType line, buffer;
    if(SettingGetGlobal_i(G, cSetting_logging)) {
      ObjectMoleculeGetAtomSele(I, index, buffer);
      sprintf(line, "cmd.translate_atom(\"%s\",%15.9f,%15.9f,%15.9f,%d,%d,%d)\n",
              buffer, v[0], v[1], v[2], state + 1, mode, 0);
      PLog(G, line, cPLog_no_flush);
    }
  }
  return (result);
}


/*========================================================================*/
int ObjectMoleculeMoveAtomLabel(ObjectMolecule * I, int state, int index, float *v, int log, float *diff)
{
  int result = 0;
  CoordSet *cs;
  if(!(I->AtomInfo[index].protekted == cAtomProtected_explicit)) {
    if(state < 0)
      state = 0;
    if(I->NCSet == 1)
      state = 0;
    state = state % I->NCSet;
    if((!I->CSet[state])
       && (SettingGet_b(I->G, I->Setting.get(), nullptr, cSetting_all_states)))
      state = 0;
    cs = I->CSet[state];
    if(cs) {
      result = CoordSetMoveAtomLabel(I->CSet[state], index, v, diff);
      cs->invalidateRep(cRepLabel, cRepInvCoord);
    }
  }
  return (result);
}

				 
/*========================================================================*/
int ObjectMoleculeInitBondPath(ObjectMolecule * I, ObjectMoleculeBPRec * bp)
{
  int a;
  bp->dist = pymol::malloc<int>(I->NAtom);
  bp->list = pymol::malloc<int>(I->NAtom);
  for(a = 0; a < I->NAtom; a++)
    bp->dist[a] = -1;
  bp->n_atom = 0;
  return 1;
}


/*========================================================================*/
int ObjectMoleculePurgeBondPath(ObjectMolecule * I, ObjectMoleculeBPRec * bp)
{
  FreeP(bp->dist);
  FreeP(bp->list);
  return 1;
}


/*========================================================================*/
int ObjectMoleculeGetBondPaths(ObjectMolecule * I, int atom,
                               int max, ObjectMoleculeBPRec * bp)
{
  /* returns list of bond counts from atom to all others 
     dist and list must be vla array pointers or nullptr */

  int b_cnt = 0;

  /* reinitialize dist array (if we've done at least one pass) */

  for (int a = 0; a < bp->n_atom; a++)
    bp->dist[bp->list[a]] = -1;

  bp->n_atom = 0;
  bp->dist[atom] = 0;
  bp->list[bp->n_atom] = atom;
  bp->n_atom++;

  int cur = 0;
  while(1) {
    b_cnt++;
    if(b_cnt > max)
      break;

    unsigned n_cur = bp->n_atom - cur;

    /* iterate through all current atoms */

    if(!n_cur)
      break;
    while(n_cur--) {
      int const a1 = bp->list[cur++];
      for (auto const& neighbor : AtomNeighbors(I, a1)) {
        auto const a2 = neighbor.atm;
        if(bp->dist[a2] < 0) {  /* for each atom not yet sampled... */
          bp->dist[a2] = b_cnt;
          bp->list[bp->n_atom] = a2;
          bp->n_atom++;
        }
      }
    }
  }
  return (bp->n_atom);
}


/*========================================================================*/
int ***ObjectMoleculeGetBondPrint(ObjectMolecule * I, int max_bond, int max_type,
                                  int *dim)
{
  int a, b, i, c;
  int at1, at2;
  int ***result = nullptr;
  ObjectMoleculeBPRec bp;

  dim[0] = max_type + 1;
  dim[1] = max_type + 1;
  dim[2] = max_bond + 1;

  result = (int ***) UtilArrayCalloc((unsigned int *) dim, 3, sizeof(int));

  ObjectMoleculeInitBondPath(I, &bp);
  for(a = 0; a < I->NAtom; a++) {
    at1 = I->AtomInfo[a].customType;
    if((at1 >= 0) && (at1 <= max_type)) {
      ObjectMoleculeGetBondPaths(I, a, max_bond, &bp);
      for(b = 0; b < bp.n_atom; b++) {
        i = bp.list[b];
        at2 = I->AtomInfo[i].customType;
        if((at2 >= 0) && (at2 <= max_type)) {
          c = bp.dist[i];
          result[at1][at2][c]++;
        }
      }
    }
  }
  ObjectMoleculePurgeBondPath(I, &bp);
  return (result);
}


/*========================================================================*/
float ObjectMoleculeGetAvgHBondVector(ObjectMolecule * I, int atom,
                                      int state, float *v, float *incoming)


/* computes average / optima hydrogen bonding vector for an atom */
{
  float result = 0.0;
  int vec_cnt = 0;
  float v_atom[3], v_neigh[3], v_diff[3], v_acc[3] = { 0.0, 0.0, 0.0 };
  int sp2_flag = false;
  int order;

  auto const* cs = I->getCoordSet(state);
  if(cs) {
    if (CoordSetGetAtomVertex(cs, atom, v_atom)) { /* atom exists in this C-set */
      for (auto const& neighbor : AtomNeighbors(I, atom)) {
        auto const a2 = neighbor.atm;
        order = I->Bond[neighbor.bond].order;
        if((order == 2) || (order == 4)) {
          sp2_flag = true;
        }

    const auto& ai = I->AtomInfo[atom];

    bool isH = I->AtomInfo[a2].protons == cAN_H;

    if (!isH || (ai.protons == cAN_O && isH))
      { /* ignore hydrogens unless water*/
        if (CoordSetGetAtomVertex(cs, a2, v_neigh)) {
          subtract3f(v_atom, v_neigh, v_diff);
          normalize3f(v_diff);
          add3f(v_diff, v_acc, v_acc);
          vec_cnt++;
        }
      }
    }
      if(vec_cnt) {
        result = (float) length3f(v_acc);
        result = result / vec_cnt;
        normalize23f(v_acc, v);
      } else {
        copy3f(v_acc, v);
      }

      if(incoming && (vec_cnt == 1) && (fabs(dot_product3f(v, incoming)) < 0.99F)) {
        /* if we know where the donor is, and the acceptor can
           potentially rotate the lone pair, then we should optimally
           orient the acceptor, if possible */
        AtomInfoType *ai = I->AtomInfo + atom;
        float v_perp[3];
        float v_tmp1[3], v_tmp2[3];
        if(((ai->protons == cAN_O) && (!sp2_flag)) ||   /* C-O-H */
           ((ai->protons == cAN_N) && (sp2_flag))) {    /* C=N-H */

          remove_component3f(incoming, v, v_perp);
          normalize3f(v_perp);
          scale3f(v, 0.333644F, v_tmp1);
          scale3f(v_perp, 0.942699F, v_tmp2);
          add3f(v_tmp1, v_tmp2, v_tmp2);
          subtract3f(v, v_tmp2, v);
          normalize3f(v);
        }
      }
    }
  }
  return (result);
}


/*========================================================================*/
int ObjectMoleculeGetAtomVertex(const ObjectMolecule * I, int state, int index, float *v)
{
  int result = 0;
  if(state < 0)
    state = SettingGet_i(I->G, nullptr, I->Setting.get(), cSetting_state) - 1;
  if(state < 0)
    state = SceneGetState(I->G);
  if(I->NCSet == 1)
    state = 0;                  /* static singletons always active here it seems */
  state = state % I->NCSet;
  if((!I->CSet[state])
     && (SettingGet_b(I->G, I->Setting.get(), nullptr, cSetting_all_states)))
    state = 0;
  if(I->CSet[state])
    result = CoordSetGetAtomVertex(I->CSet[state], index, v);

  return (result);
}


/*========================================================================*/
int ObjectMoleculeGetAtomTxfVertex(const ObjectMolecule * I, int state, int index, float *v)
{
  int result = 0;
  const CoordSet* cs = nullptr;
  if (I->DiscreteFlag){
    cs = I->DiscreteCSet[index];
  }
  if(state < 0){
    state = SettingGet_i(I->G, nullptr, I->Setting.get(), cSetting_state) - 1;
  }
  if(state < 0)
    state = SceneGetState(I->G);
  if(I->NCSet == 1)
    state = 0;                  /* static singletons always active here it seems */
  state = state % I->NCSet;
  {
    if (!cs)
      cs = I->CSet[state];
    if((!cs) && (SettingGet_b(I->G, I->Setting.get(), nullptr, cSetting_all_states))) {
      state = 0;
      cs = I->CSet[state];
    }
    if(cs) {
      result = CoordSetGetAtomTxfVertex(cs, index, v);
    }
  }
  return (result);
}

/*========================================================================*/
int ObjectMoleculeSetAtomVertex(ObjectMolecule * I, int state, int index, float *v)
{
  int result = 0;
  if(state < 0)
    state = SettingGet_i(I->G, nullptr, I->Setting.get(), cSetting_state) - 1;
  if(state < 0)
    state = SceneGetState(I->G);
  if(I->NCSet == 1)
    state = 0;
  state = state % I->NCSet;
  if((!I->CSet[state])
     && (SettingGet_b(I->G, I->Setting.get(), nullptr, cSetting_all_states)))
    state = 0;
  if(I->CSet[state])
    result = CoordSetSetAtomVertex(I->CSet[state], index, v);
  return (result);
}


/*========================================================================*/
void ObjectMolecule::render(RenderInfo * info)
{
  auto I = this;
  int state = info->state;
  const RenderPass pass = info->pass;
  CoordSet *cs;
  int pop_matrix = false;
  int use_matrices = SettingGet_i(I->G, I->Setting.get(), nullptr, cSetting_matrix_mode);
  if(use_matrices<0) use_matrices = 0;
  PRINTFD(I->G, FB_ObjectMolecule)
    " ObjectMolecule: rendering %s pass %d...\n", I->Name, static_cast<int>(pass) ENDFD;

  ObjectPrepareContext(I, info);

  for(StateIterator iter(G, I->Setting.get(), state, I->NCSet); iter.next();) {
    cs = I->CSet[iter.state];
    if(cs) {
      if(use_matrices)
        pop_matrix = ObjectStatePushAndApplyMatrix(cs, info);
      cs->render(info);
      if(pop_matrix)
        ObjectStatePopMatrix(cs, info);
    }
  }
  PRINTFD(I->G, FB_ObjectMolecule)
    " ObjectMolecule: rendering complete for object %s.\n", I->Name ENDFD;
}


/*========================================================================*/
void ObjectMoleculeDummyUpdate(ObjectMolecule * I, int mode)
{
  switch (mode) {
  case cObjectMoleculeDummyOrigin:
    SceneOriginGet(I->G, I->CSet[0]->Coord.data());
    break;
  case cObjectMoleculeDummyCenter:
    SceneGetCenter(I->G, I->CSet[0]->Coord.data());
    break;
  }
}


/*========================================================================*/
ObjectMolecule *ObjectMoleculeDummyNew(PyMOLGlobals * G, int type)
{
  auto I = new ObjectMolecule(G, false);
  I->AtomInfo.resize(I->NAtom = 1);

  auto* cset = new CoordSet(G);
  cset->Obj = I;
  cset->setNIndex(1);
  cset->enumIndices();

  I->CSet.resize(I->NCSet = 1);
  I->CSet[0] = cset;

  return I;
}


/*========================================================================*/

ObjectMolecule::ObjectMolecule(PyMOLGlobals * G, int discreteFlag) : pymol::CObject(G)
{
  auto I = this;
  int a;
  I->type = cObjectMolecule;
  I->CSet = pymol::vla<CoordSet*>(10); /* auto-zero */
  I->DiscreteFlag = discreteFlag;
  if(I->DiscreteFlag) {         /* discrete objects don't share atoms between states */
    I->DiscreteAtmToIdx = pymol::vla<int>(0);
    I->DiscreteCSet = pymol::vla<CoordSet*>(0);
  } else {
    I->DiscreteAtmToIdx = nullptr;
    I->DiscreteCSet = nullptr;
  }
  I->AtomInfo = pymol::vla<AtomInfoType>(10);
  for(a = 0; a <= cUndoMask; a++) {
    I->UndoCoord[a] = nullptr;
    I->UndoState[a] = -1;
  }
  I->UndoIter = 0;
}


/*========================================================================*/
void ObjectMoleculeCopyNoAlloc(const ObjectMolecule* obj, ObjectMolecule* I)
{
  PyMOLGlobals * G = const_cast<PyMOLGlobals*>(obj->G);

  int a;
  BondType *i0;
  const BondType *i1;
  (*I) = (*obj);
  I->Sculpt = nullptr;
  I->Setting.reset(SettingCopyAll(G, obj->Setting.get(), nullptr));

  I->ViewElem = nullptr;
  I->gridSlotSelIndicatorsCGO = nullptr;

  for(a = 0; a <= cUndoMask; a++)
    I->UndoCoord[a] = nullptr;
  I->CSet = pymol::vla<CoordSet*>(I->NCSet);   /* auto-zero */
  for(a = 0; a < I->NCSet; a++) {
    I->CSet[a] = CoordSetCopy(obj->CSet[a]);
    if (I->CSet[a])
      I->CSet[a]->Obj = I;
  }

  if(obj->CSTmpl)
    I->CSTmpl = CoordSetCopy(obj->CSTmpl);

  if (obj->DiscreteFlag){
    int sz = VLAGetSize(obj->DiscreteAtmToIdx);
    I->DiscreteAtmToIdx = VLACopy2(obj->DiscreteAtmToIdx);
    I->DiscreteCSet = pymol::vla<CoordSet*>(sz);
    I->updateAtmToIdx();
  }
  I->Bond = pymol::vla<BondType>(I->NBond);
  i0 = I->Bond.data();
  i1 = obj->Bond.data();
  for(a = 0; a < I->NBond; a++) {
    AtomInfoBondCopy(G, i1++, i0++);
  }

  // unfortunately, the AtomInfoType is not copy-constructable yet
  // assert copy done correct
  if (I->AtomInfo.size() != obj->AtomInfo.size()) {
    throw "AtomInfo copy failed";
  }
  AtomInfoType *a0 = I->AtomInfo.data();
  const AtomInfoType* a1 = obj->AtomInfo.data();
  memset(a0, 0, sizeof(AtomInfoType) * I->NAtom);
  for(a = 0; a < I->NAtom; a++)
    AtomInfoCopy(G, a1++, a0++);
}

ObjectMolecule *ObjectMoleculeCopy(const ObjectMolecule * obj)
{
  PyMOLGlobals * G = const_cast<PyMOLGlobals*>(obj->G);
  auto I = new ObjectMolecule(G, obj->DiscreteFlag);
  ObjectMoleculeCopyNoAlloc(obj, I);
  return (I);

}

/*========================================================================*/
/**
 * Set the order of coordinate sets with an index array
 */
int ObjectMoleculeSetStateOrder(ObjectMolecule * I, int * order, int len) {
  int a;
  CoordSet ** csets = VLAlloc(CoordSet *, I->NCSet);

  ok_assert(1, len == I->NCSet);

  // invalidate
  I->invalidate(cRepAll, cRepInvAll, -1);

  // new coord set array
  for(a = 0; a < I->NCSet; a++) {
    int i = order[a];
    ok_assert(1, 0 <= i && i < I->NCSet);
    csets[a] = I->CSet[i];
  }

  VLAFreeP(I->CSet);
  I->CSet = pymol::vla_take_ownership(csets);

  return true;
ok_except1:
  ErrMessage(I->G, "ObjectMoleculeSetStateOrder", "failed");
  VLAFreeP(csets);
  return false;
}

/*========================================================================*/
pymol::Result<> ObjectMoleculeDeleteStates(ObjectMolecule* I, const std::vector<int>& states)
{
  auto& CSet = I->CSet;

  // first pass, check for invalid states
  for (auto state : states) {
    if (state < 0 || state >= I->NCSet) {
      auto msg = pymol::string_format("Invalid state index: %d", state);
      I->G->Feedback->addColored(msg.c_str(), FB_Errors);
      return {};
    }
  }

  // second pass, delete states
  for (auto it = states.rbegin(); it != states.rend(); ++it) {
    int state = *it;
    DeleteP(CSet[state]);
    CSet.erase(state, 1);
  }
  I->NCSet -= states.size();
  CSet.resize(I->NCSet);

  // Update Rep States
  for (int i = 0; i < I->NCSet; ++i) {
    if (CSet[i]) {
      auto& cset = *CSet[i];
      for (int r = 0; r < cRepCnt; ++r) {
        if (cset.Rep[r] && cset.Rep[r]->context.state != 0) {
          cset.Rep[r]->context.state = i;
        }
      }
    }
  }

  return {};
}

/*========================================================================*/
ObjectMolecule::~ObjectMolecule()
{
  auto I = this;
  int a;
  SelectorPurgeObjectMembers(I->G, I);
  for(a = 0; a < I->NCSet; a++){
    if(I->CSet[a]) {
      delete I->CSet[a];
      I->CSet[a] = nullptr;
    }
  }
  VLAFreeP(I->DiscreteAtmToIdx);
  VLAFreeP(I->DiscreteCSet);
  VLAFreeP(I->CSet);
  I->m_ciffile.reset(); // free data

  {
    int nAtom = I->NAtom;
    AtomInfoType *ai = I->AtomInfo.data();

    for(a = 0; a < nAtom; a++) {
      AtomInfoPurge(I->G, ai);
      ai++;
    }
    VLAFreeP(I->AtomInfo);
  }
  {
    int nBond = I->NBond;
    BondType *bi = I->Bond.data();

    for(a = 0; a < nBond; a++) {
      AtomInfoPurgeBond(I->G, bi);
      bi++;
    }
    VLAFreeP(I->Bond);
  }
  for(a = 0; a <= cUndoMask; a++)
    FreeP(I->UndoCoord[a]);
  if(I->Sculpt)
    DeleteP(I->Sculpt);
  delete I->CSTmpl;
}

/*========================================================================*/
ObjectMolecule *ObjectMoleculeReadPDBStr(PyMOLGlobals * G, ObjectMolecule * I,
                                         const char *PDBStr, int state, int discrete,
                                         char *pdb_name,
                                         const char **next_pdb, PDBInfoRec * pdb_info,
                                         int quiet, int *model_number)
{
  CoordSet *cset = nullptr;
  pymol::vla<AtomInfoType> atInfo;
  int ok = true;
  int isNew = true;
  unsigned int nAtom = 0;
  const char *start, *restart = nullptr;
  int repeatFlag = true;
  int successCnt = 0;
  unsigned int aic_mask = cAIC_PDBMask;

  SegIdent segi_override = "";  /* saved segi for corrupted NMR pdb files */

  start = PDBStr;
  while(repeatFlag) {
    repeatFlag = false;

    if(!I)
      isNew = true;
    else
      isNew = false;

    if(ok) {
      if(isNew) {
        I = (ObjectMolecule *) new ObjectMolecule(G, discrete);
	CHECKOK(ok, I);
	if (ok)
          std::swap(atInfo, I->AtomInfo);
        isNew = true;
      } else {
        atInfo = pymol::vla<AtomInfoType>(10);
	CHECKOK(ok, atInfo);
        isNew = false;
      }
      if(ok && isNew) {
        I->Color = AtomInfoUpdateAutoColor(G);

        if (pdb_info->variant == PDB_VARIANT_VDB ||
            pdb_info->variant == PDB_VARIANT_PQR) {
          // pqr files have no chain identifier by default
          // vdb files have same chain identifiers in all symmetry copies
          SettingSet(cSetting_retain_order, 1, I);
        }
      }
      if (ok)
	cset = ObjectMoleculePDBStr2CoordSet(G, start, &atInfo, &restart,
					     segi_override, pdb_name,
					     next_pdb, pdb_info, quiet, model_number);
      CHECKOK(ok, cset);
      if (ok){
	nAtom = cset->NIndex;
      }
    }
    if(ok && pdb_name && (*next_pdb)) {
      /* problematic scenario? */
    }

    /* include coordinate set */
    if(ok) {
      if(I->DiscreteFlag && atInfo) {
        unsigned int a;
        int fp1 = state + 1;
        AtomInfoType *ai = atInfo.data();
        for(a = 0; a < nAtom; a++) {
          (ai++)->discrete_state = fp1;
        }
      }

      cset->Obj = I;
      cset->enumIndices();
      cset->invalidateRep(cRepAll, cRepInvRep);
      if(isNew) {
        std::swap(I->AtomInfo, atInfo);
      } else {
        ok &= ObjectMoleculeMerge(I, std::move(atInfo), cset, true, aic_mask, true);
        /* NOTE: will release atInfo */
      }
      if(isNew)
        I->NAtom = nAtom;
      if(state < 0)
        state = I->NCSet;
      if(*model_number > 0) {
        if(SettingGetGlobal_b(G, cSetting_pdb_honor_model_number))
          state = *model_number - 1;
      }
      VLACheck(I->CSet, CoordSet *, state);
      CHECKOK(ok, I->CSet);
      if(ok){
	if(I->NCSet <= state)
	  I->NCSet = state + 1;
	delete I->CSet[state];
	I->CSet[state] = cset;
      }
      if(ok && isNew)
        ok &= ObjectMoleculeConnect(I, cset);
      if(ok && cset->Symmetry) {
        I->Symmetry.reset(new CSymmetry(*cset->Symmetry));
      }
      if (I->Symmetry) {
          /* check scale records */
          if(pdb_info &&
             pdb_info->scale.flag[0] &&
             pdb_info->scale.flag[1] && pdb_info->scale.flag[2]) {

            float *sca = pdb_info->scale.matrix;
            sca[15] = 1.0F;

            CoordSetInsureOrthogonal(G, cset, sca, &I->Symmetry->Crystal, quiet);
          }
      }
      SceneCountFrames(G);
      if (ok)
	ok &= ObjectMoleculeExtendIndices(I, state);
      if (ok)
	ok &= ObjectMoleculeSort(I);
      if (ok){
	ObjectMoleculeUpdateIDNumbers(I);
	ObjectMoleculeUpdateNonbonded(I);
	ObjectMoleculeAutoDisableAtomNameWildcard(I);
      }
      if(SettingGetGlobal_b(G, cSetting_pdb_hetatm_guess_valences)) {
        ObjectMoleculeGuessValences(I, state, nullptr, nullptr, false);
      }

      successCnt++;
      if(!quiet) {
        if(successCnt > 1) {
          if(successCnt == 2) {
            PRINTFB(G, FB_ObjectMolecule, FB_Actions)
              " %s: read MODEL %d\n", __func__, 1 ENDFB(G);
          }
          PRINTFB(G, FB_ObjectMolecule, FB_Actions)
            " %s: read MODEL %d\n", __func__, successCnt ENDFB(G);
        }
      }
    }
    if(restart) {
      repeatFlag = true;
      start = restart;
      state = state + 1;
    }
  }
  if (!ok && isNew){
    DeleteP(I);
  }
  return (I);
}


/*========================================================================*/
static
CoordSet *ObjectMoleculeMMDStr2CoordSet(PyMOLGlobals * G, const char *buffer,
                                        AtomInfoType ** atInfoPtr, const char **restart)
{
  const char *p;
  int nAtom, nBond;
  int a, c, bPart, bOrder;
  float *coord = nullptr;
  CoordSet *cset = nullptr;
  AtomInfoType *atInfo = nullptr, *ai;
  char cc[MAXLINELEN];
  WordType title;

  float *f;
  BondType *ii, *bond = nullptr;
  int ok = true;
  int auto_show = RepGetAutoShowMask(G);

  p = buffer;
  nAtom = 0;
  if(atInfoPtr)
    atInfo = *atInfoPtr;

  if(ok) {
    p = ncopy(cc, p, 6);  // could this be too small for large molecules with #atoms > 999999 ?
    if(sscanf(cc, "%d", &nAtom) != 1)
      ok = ErrMessage(G, "ReadMMDFile", "bad atom count");
  }

  if(ok) {
    coord = VLAlloc(float, 3 * nAtom);
    if(atInfo)
      VLACheck(atInfo, AtomInfoType, nAtom);
  }

  if(!atInfo) {
    ErrFatal(G, "MMDStr2CoordSet", "need atom information record!");
    /* failsafe for old version.. */
  }

  nBond = 0;
  if(ok) {
    bond = VLACalloc(BondType, 6 * nAtom);
  }

  p = ParseWordCopy(title, p, sizeof(WordType) - 1);
  UtilCleanStr(title);

  p = nextline(p);

  /* read coordinates and atom names */

  if(ok) {
    f = coord;
    ii = bond;
    for(a = 0; a < nAtom; a++) {
      ai = atInfo + a;

      ai->id = a + 1;
      ai->rank = a;
      if(ok) {
        p = ncopy(cc, p, 4);
        if(sscanf(cc, "%d", &ai->customType) != 1)
          ok = ErrMessage(G, "ReadMMDFile", "bad atom type");
      }
      if(ok) {
        if(ai->customType <= 14)
          strcpy(ai->elem, "C");
        else if(ai->customType <= 23)
          strcpy(ai->elem, "O");
        else if(ai->customType <= 40)
          strcpy(ai->elem, "N");
        else if(ai->customType <= 48)
          strcpy(ai->elem, "H");
        else if(ai->customType <= 52)
          strcpy(ai->elem, "S");
        else if(ai->customType <= 53)
          strcpy(ai->elem, "P");
        else if(ai->customType <= 55)
          strcpy(ai->elem, "B");
        else if(ai->customType <= 56)
          strcpy(ai->elem, "F");
        else if(ai->customType <= 57)
          strcpy(ai->elem, "Cl");
        else if(ai->customType <= 58)
          strcpy(ai->elem, "Br");
        else if(ai->customType <= 59)
          strcpy(ai->elem, "I");
        else if(ai->customType <= 60)
          strcpy(ai->elem, "Si");
        else if(ai->customType <= 61)
          strcpy(ai->elem, "Du");
        else if(ai->customType <= 62)
          strcpy(ai->elem, "Z0");
        else if(ai->customType <= 63)
          strcpy(ai->elem, "Lp");
        else
          ai->elem[0] = 0;
        /* else strcpy(ai->elem,"?"); WLD 090305 -- guess instead. */
      }
      for(c = 0; c < 6; c++) {
        if(ok) {
          p = ncopy(cc, p, 8);
          if(sscanf(cc, "%d%d", &bPart, &bOrder) != 2)
            ok = ErrMessage(G, "ReadMMDFile", "bad bond record");
          else {
            if(bPart && bOrder && (a < (bPart - 1))) {
              nBond++;
              ii->index[0] = a;
              ii->index[1] = bPart - 1;
              ii->order = bOrder;
              ii++;
            }
          }
        }
      }
      if(ok) {
        p = ncopy(cc, p, 12);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMMDFile", "bad coordinate");
      }
      if(ok) {
        p = ncopy(cc, p, 12);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMMDFile", "bad coordinate");
      }
      if(ok) {
        p = ncopy(cc, p, 12);
        if(sscanf(cc, "%f", f++) != 1)
          ok = ErrMessage(G, "ReadMMDFile", "bad coordinate");
      }
      if(ok) {
        p = nskip(p, 1);
        p = ncopy(cc, p, 5);
        ai->setResi(cc);

        // single-letter code
        p = nskip(p, 1);

        // chain
        p = ncopy(cc, p, 1);
        LexAssign(G, ai->chain, cc);
      }
      if(ok) {
        p = nskip(p, 4);
        p = ncopy(cc, p, 9);
        if(sscanf(cc, "%f", &ai->partialCharge) != 1)
          ok = ErrMessage(G, "ReadMMDFile", "bad charge");
      }
      if(ok) {
        p = nskip(p, 10);
        p = ncopy(cc, p, 3);
        UtilCleanStr(cc);
        LexAssign(G, ai->resn, cc);
        ai->hetatm = true;
      }

      ai->segi = 0;
      ai->alt[0] = 0;

      if(ok) {
        p = nskip(p, 2);
        p = ntrim(cc, p, 4);
        if(!cc[0]) {
          sprintf(cc, "%s%02d", ai->elem, a + 1);
        }
        ai->name = LexIdx(G, cc);

        ai->visRep = auto_show;
      }
      if(ok) {
        AtomInfoAssignParameters(G, ai);
        AtomInfoAssignColors(G, ai);
      }
      if(!ok)
        break;
      p = nextline(p);
    }
  }
  if(ok)
    VLASize(bond, BondType, nBond);
  if(ok) {
    cset = CoordSetNew(G);
    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->NTmpBond = nBond;
    cset->TmpBond = pymol::vla_take_ownership(bond);
    strcpy(cset->Name, title);
  } else {
    VLAFreeP(bond);
    VLAFreeP(coord);
  }
  if(atInfoPtr)
    *atInfoPtr = atInfo;
  *restart = *p ? p : nullptr;
  return (cset);
}

#ifdef _PYMOL_IP_EXTRAS
#endif

#ifdef _PYMOL_IP_PROPERTIES
#endif

void AtomInfoSettingGenerateSideEffects(PyMOLGlobals * G, ObjectMolecule *obj, int index, int id){
  switch(index){
  case cSetting_label_position:
  case cSetting_label_placement_offset:
  case cSetting_label_screen_point:
  case cSetting_label_relative_mode:
    obj->invalidate(cRepLabel, cRepInvCoord, -1);
  }
}

static int AtomInfoInOrder(PyMOLGlobals * G, const AtomInfoType * atom, int atom1, int atom2)
{
  return (AtomInfoCompare(G, atom + atom1, atom + atom2) <= 0);
}

static int AtomInfoInOrderIgnoreHet(PyMOLGlobals * G, const AtomInfoType * atom,
    int atom1, int atom2)
{
  return (AtomInfoCompareIgnoreHet(G, atom + atom1, atom + atom2) <= 0);
}

static int AtomInfoInOrigOrder(PyMOLGlobals * G, const AtomInfoType * atom,
    int atom1, int atom2)
{
  if(atom[atom1].rank == atom[atom2].rank)
    return (AtomInfoCompare(G, atom + atom1, atom + atom2) <= 0);
  return (atom[atom1].rank < atom[atom2].rank);
}

int *AtomInfoGetSortedIndex(PyMOLGlobals * G,
    const ObjectMolecule* obj,
    const AtomInfoType* rec, int n, int **outdex)
{
  int *index;
  int a;
  const CSetting* setting = nullptr;

  ok_assert(1, index = pymol::malloc<int>(n + 1));
  ok_assert(1, (*outdex) = pymol::malloc<int>(n + 1));

  if(obj && obj->DiscreteFlag) {
    for(a = 0; a < n; a++)
      index[a] = a;
  } else {
    if(obj)
      setting = obj->Setting.get();

    UtilSortIndexGlobals(G, n, rec, index, (UtilOrderFnGlobals *) (
        SettingGet_b(G, setting, nullptr, cSetting_retain_order) ?
          AtomInfoInOrigOrder :
        SettingGet_b(G, setting, nullptr, cSetting_pdb_hetatm_sort) ?
          AtomInfoInOrder :
          AtomInfoInOrderIgnoreHet));
  }

  for(a = 0; a < n; a++)
    (*outdex)[index[a]] = a;

  return index;

ok_except1:
  FreeP(index);
  return nullptr;
}

/**
 * Set the size of the DiscreteAtmToIdx and DiscreteCSet VLAs and pad
 * them with -1/NULL if necessary.
 */
bool ObjectMolecule::setNDiscrete(int natom) {
  int n = VLAGetSize(DiscreteAtmToIdx);

  if (n == natom)
    return true;

  VLASize(DiscreteAtmToIdx, int, natom);
  VLASize(DiscreteCSet, CoordSet*, natom);

  if (!DiscreteAtmToIdx || !DiscreteCSet)
    return false;

  for (int i = n; i < natom; ++i) {
    DiscreteAtmToIdx[i] = -1;
    DiscreteCSet[i] = nullptr;
  }

  return true;
}

/**
 * Update the AtmToIdx or DiscreteAtmToIdx/DiscreteCSet VLAs from the
 * IdxToAtm arrays
 */
bool ObjectMolecule::updateAtmToIdx() {
  if (DiscreteFlag) {
    ok_assert(1, setNDiscrete(NAtom));
  }

  for (int i = -1; i < NCSet; ++i) {
    CoordSet * cset = (i < 0) ? CSTmpl : CSet[i];

    if (!cset)
      continue;

    if (!DiscreteFlag) {
      cset->updateNonDiscreteAtmToIdx(NAtom);
    } else {
      for (int idx = 0; idx < cset->NIndex; ++idx) {
        int atm = cset->IdxToAtm[idx];
        assert(atm < NAtom);
        DiscreteAtmToIdx[atm] = idx;
        DiscreteCSet[atm] = cset;
        AtomInfo[atm].discrete_state = i + 1;
      }
    }
  }

  return true;
ok_except1:
  return false;
}

/**
 * Check if this atom has coordinates in any state
 */
bool ObjectMolecule::atomHasAnyCoordinates(size_t atm) const
{
  for (size_t i = 0; i < NCSet; ++i) {
    auto cset = CSet[i];
    if (cset && cset->atmToIdx(atm) != -1) {
      return true;
    }
  }

  return false;
}

pymol::CObject* ObjectMolecule::clone() const
{
  return ObjectMoleculeCopy(this);
}

AtomNeighbors::AtomNeighbors(const ObjectMolecule* I, int atm)
{
  auto* Neighbor = I->getNeighborArray();

  m_neighbor = Neighbor + Neighbor[atm];
}
