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

#ifdef PYMOL_OPENMP
#include <omp.h>
#endif

#include"Version.h"
#include"os_python.h"

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

#include"Base.h"
#include"Parse.h"
#include"Err.h"
#include"Vector.h"
#include"PConv.h"
#include"ObjectMolecule.h"
#include"Feedback.h"
#include"Util.h"
#include"Util2.h"
#include"AtomInfo.h"
#include"Selector.h"
#include"SymOpPConv.h"
#include"ObjectDist.h"
#include"Executive.h"
#include"P.h"
#include"ObjectCGO.h"
#include"Scene.h"
#include "Lex.h"

#include"AtomInfoHistory.h"
#include"BondTypeHistory.h"

#ifdef _PYMOL_IP_PROPERTIES
#include"Property.h"
#endif

#include "pymol/zstring_view.h"

#include <functional>
#include <iostream>
#include <map>

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

#define cResvMask 0x7FFF

#define cMaxOther 6

#define strstartswith p_strstartswith

static int strstartswithword(const char * s, const char * word) {
  while(*word)
    if(*s++ != *word++)
      return false;
  switch(*s) {
    case ' ':
    case '\t':
    case '\r':
    case '\n':
    case '\0':
      return true;
  }
  return false;
}

static void AddOrthoOutputIfMatchesTags(PyMOLGlobals * G, int n_tags,
    int nAtom, const char* const* tag_start, const char *p, char *cc, int quiet)
{
  if(n_tags && !quiet && !(nAtom > 0 && strstartswith(p, "HEADER"))) { 
    // HEADER is the mark for a new object, this is a new object, and
    // gets processed on the next pass, when nAtom=0
    int tc = 0;
    for(; tc < n_tags; tc++) {
      if(!strstartswithword(p, tag_start[tc]))
	continue;
      ParseNTrimRight(cc, p, MAXLINELEN - 1);
      OrthoAddOutput(G, cc);
      OrthoNewLine(G, NULL, true);
      break;
    }
  }
}

typedef struct {
  int n_cyclic_arom, cyclic_arom[cMaxOther];
  int n_arom, arom[cMaxOther];
  int n_high_val, high_val[cMaxOther];
  int n_cyclic, cyclic[cMaxOther];
  int n_planer, planer[cMaxOther];
  int n_rest, rest[cMaxOther];
  int score;
} OtherRec;

static int populate_other(OtherRec * other, int at,
    const AtomInfoType* ai,
    const BondType* bd,
    const ObjectMolecule* obj)
{
  int five_cycle = false;
  int six_cycle = false;

  {
    // don't get bogged down with structures that have unreasonable connectivity
    const int ESCAPE_MAX = 500;
    int escape_count = ESCAPE_MAX;

    auto const mem0_atm = bd->index[0];
    auto const mem1_atm = bd->index[1];

    for (auto const& mem2 : AtomNeighbors(obj, mem1_atm)) {
      if (mem2.atm == mem0_atm) {
        continue;
      }

      for (auto const& mem3 : AtomNeighbors(obj, mem2.atm)) {
        if (mem3.atm == mem1_atm) {
          continue;
        }

        for (auto const& mem4 : AtomNeighbors(obj, mem3.atm)) {
          if (mem4.atm == mem2.atm || mem4.atm == mem1_atm ||
              mem4.atm == mem0_atm) {
            continue;
          }

          for (auto const& mem5 : AtomNeighbors(obj, mem4.atm)) {
            if (!(escape_count--)) {
              goto escape;
            }

            if (mem5.atm == mem3.atm || mem5.atm == mem2.atm ||
                mem5.atm == mem1_atm) {
              continue;
            }

            if (mem5.atm == mem0_atm) { /* five-cycle */
              five_cycle = true;

              if (six_cycle) {
                goto escape;
              }
            }

            for (auto const& mem6 : AtomNeighbors(obj, mem5.atm)) {
              if (mem6.atm == mem4.atm || mem6.atm == mem3.atm ||
                  mem6.atm == mem2.atm || mem6.atm == mem1_atm) {
                continue;
              }

              if (mem6.atm == mem0_atm) { /* six-cycle */
                six_cycle = true;

                if (five_cycle) {
                  goto escape;
                }

                break;
              }
            }
          }
        }
      }
    }
  }
escape:

  if(bd->order == 4) {          /* aromatic */
    if((five_cycle || six_cycle) && (other->n_cyclic_arom < cMaxOther)) {
      other->cyclic_arom[other->n_cyclic_arom++] = at;
      if(five_cycle && six_cycle)
        other->score += 34;
      else if(five_cycle)
        other->score += 33;
      else
        other->score += 32;
      return 1;
    } else if(other->n_arom < cMaxOther) {
      other->arom[other->n_arom++] = at;
      other->score += 64;
      return 1;
    }
  }
  if(bd->order > 1) {
    if(other->n_high_val < cMaxOther) {
      other->high_val[other->n_high_val++] = at;
      other->score += 16;
      return 1;
    }
  }
  if(five_cycle || six_cycle) {
    if(other->n_cyclic < cMaxOther) {
      other->cyclic[other->n_cyclic++] = at;
      other->score += 8;
      return 1;
    }
  }
  if(ai->geom == cAtomInfoPlanar) {
    if(other->n_planer < cMaxOther) {
      other->planer[other->n_planer++] = at;
      other->score += 4;
      return 1;
    }
  }
  if(other->n_rest < cMaxOther) {
    other->rest[other->n_rest++] = at;
    other->score += 1;
    return 1;
  }
  return 0;
}

static int append_index(int *result, int offset, int a1, int a2, int score, int ar_count)
{
  int c;
  c = result[a1];
  while(c < offset) {
    if(result[c] == a2) {       /* already entered */
      if(result[c + 1] < score) {
        result[c + 1] = score;
        result[c + 2] = ar_count;
      }
      return offset;
    }
    c += 3;
  }
  result[offset++] = a2;
  result[offset++] = score;
  result[offset++] = ar_count;
  return offset;
}

int ObjectMoleculeAddPseudoatom(ObjectMolecule * I, int sele_index, const char *name,
                                const char *resn, const char *resi, const char *chain,
                                const char *segi, const char *elem, float vdw,
                                int hetatm, float b, float q, const char *label,
                                const float *pos, int color, int state, int mode, int quiet)
{
  PyMOLGlobals *G = I->G;
  int start_state = 0, stop_state = 0;
  int extant_only = false;
  int ai_merged = false;
  float pos_array[3] = { 0.0F, 0.0F, 0.0F };
  int ok = true;

  pymol::vla<AtomInfoType> atInfo(1);
  AtomInfoType* ai = atInfo.data();

  // FIXME this should be cStateCurrent
  if (state == cStateAll) {
    state = I->getCurrentState();
  }

  if(state >= 0) {              /* specific state */
    start_state = state;
    stop_state = state + 1;
  } else {                      /* all states */
    if(sele_index >= 0) {
      start_state = 0;
      stop_state = SelectorCountStates(G, sele_index);

      // Here, -3 does not mean cSelectorUpdateTableEffectiveStates. It means
      // all states present in `sele_index` AND `I`. State != -3 means all
      // states in `sele_index`, so it would create new states in `I` if they
      // don't exist yet.
      if(state == -3)
        extant_only = true;
    } else {
      start_state = 0;
      stop_state = ExecutiveCountStates(G, cKeywordAll);
      if(stop_state < 1)
        stop_state = 1;
    }
  }
  {
    /* match existing properties of the old atom */
    ai->setResi(resi);
    ai->hetatm = hetatm;
    ai->geom = cAtomInfoNone;
    ai->q = q;
    ai->b = b;
    ai->chain = LexIdx(G, chain);
    ai->segi = LexIdx(G, segi);
    ai->resn = LexIdx(G, resn);
    ai->name = LexIdx(G, name);
    strcpy(ai->elem, elem);
    ai->id = -1;
    ai->rank = -1;
    if(vdw >= 0.0F)
      ai->vdw = vdw;
    else
      ai->vdw = 1.0F;
    if(label[0]) {
      ai->label = LexIdx(G, label);
      ai->visRep = cRepLabelBit;
    } else {
      ai->visRep = RepGetAutoShowMask(G);
    }

    ai->flags |= cAtomFlag_inorganic; // suppress auto_show_classified

    if(color < 0) {
      AtomInfoAssignColors(I->G, ai);
      if((ai->elem[0] == 'C') && (ai->elem[1] == 0))
        /* carbons are always colored according to the object color */
        ai->color = I->Color;
    } else {
      ai->color = color;
    }
    AtomInfoAssignParameters(I->G, ai);
    AtomInfoUniquefyNames(I->G, I->AtomInfo, I->NAtom, ai, NULL, 1);
    if(!quiet) {
      PRINTFB(G, FB_ObjectMolecule, FB_Actions)
        " ObjMol: created %s/%s/%s/%s`%d%c/%s\n",
        I->Name, LexStr(G, ai->segi), LexStr(G, ai->chain),
        LexStr(G, ai->resn), ai->resv, ai->getInscode(true),
        LexStr(G, ai->name) ENDFB(G);
    }
  }

  CoordSet *cset = CoordSetNew(G);
  cset->NIndex = 1;
  cset->Coord = pymol::vla<float>(3);
  cset->Obj = I;
  cset->enumIndices();

  for(state = start_state; state < stop_state; state++) {


    if((extant_only && (state < I->NCSet) && I->CSet[state]) || !extant_only) {

      if(sele_index >= 0) {
        ObjectMoleculeOpRec op;
        ObjectMoleculeOpRecInit(&op);
        op.code = OMOP_CSetSumVertices;
        op.cs1 = state;

        ExecutiveObjMolSeleOp(I->G, sele_index, &op);

        if(op.i1) {
          float factor = 1.0F / op.i1;
          scale3f(op.v1, factor, pos_array);
          pos = pos_array;

          if(vdw < 0.0F) {
            switch (mode) {
            case 1:
              ObjectMoleculeOpRecInit(&op);
              op.code = OMOP_CSetMaxDistToPt;
              copy3f(pos_array, op.v1);
              op.cs1 = state;
              ExecutiveObjMolSeleOp(I->G, sele_index, &op);
              vdw = op.f1;
              break;
            case 2:
              ObjectMoleculeOpRecInit(&op);
              op.code = OMOP_CSetSumSqDistToPt;
              copy3f(pos_array, op.v1);
              op.cs1 = state;
              ExecutiveObjMolSeleOp(I->G, sele_index, &op);
              vdw = sqrt1f(op.d1 / op.i1);
              break;
            case 0:
            default:
              vdw = 0.5F;
              break;
            }
            if(vdw >= 0.0F)
              ai->vdw = vdw;        /* NOTE: only uses vdw from first state selection... */
          }
        } else {
          pos = NULL;           /* skip this state */
        }
      } else if(!pos) {
        SceneGetCenter(I->G, pos_array);
        pos = pos_array;
      }

      if(pos) {                 /* only add coordinate to state if we have position for it */

        float *coord = cset->Coord.data();

        copy3f(pos, coord);

        if(!ai_merged) {
	  if (ok)
	    ok &= ObjectMoleculeMerge(I, std::move(atInfo), cset, false, cAIC_AllMask, true);      /* NOTE: will release atInfo */
          if (ok)
	    ok &= ObjectMoleculeExtendIndices(I, -1);
          ai_merged = true;
        }
        if(state >= I->NCSet) {
          VLACheck(I->CSet, CoordSet *, state);
          I->NCSet = state + 1;
        }
        if(!I->CSet[state]) {
          /* new coordinate set */
          I->CSet[state] = CoordSetCopy(cset);
        } else {
          /* merge coordinate set */
	  if (ok)
	    ok &= CoordSetMerge(I, I->CSet[state], cset);
        }
      }
    }
  }

  delete cset;

  if(ai_merged) {
    if (ok)
      ok &= ObjectMoleculeSort(I);
    ObjectMoleculeUpdateIDNumbers(I);
    ObjectMoleculeUpdateNonbonded(I);
    I->invalidate(cRepAll, cRepInvAtoms, -1);
  } else {
    VLAFreeP(atInfo);
  }
  return (ok);
}

int *ObjectMoleculeGetPrioritizedOtherIndexList(ObjectMolecule * I, CoordSet * cs)
{
  int a, b;
  int b1, b2, a1, a2, a3;
  OtherRec *o;
  OtherRec *other = pymol::calloc<OtherRec>(cs->NIndex);
  int *result = NULL;
  int offset;
  int n_alloc = 0;
  const BondType *bd;
  int ok = true;

  CHECKOK(ok, other);

  bd = I->Bond;
  for(a = 0; ok && a < I->NBond; a++) {
    b1 = bd->index[0];
    b2 = bd->index[1];
    a1 = cs->atmToIdx(b1);
    a2 = cs->atmToIdx(b2);
    if((a1 >= 0) && (a2 >= 0)) {
      n_alloc += populate_other(other + a1, a2, I->AtomInfo + b2, bd, I);
      n_alloc += populate_other(other + a2, a1, I->AtomInfo + b1, bd, I);
    }
    bd++;
    ok &= !I->G->Interrupt;
  }
  if (ok){
    n_alloc = 3 * (n_alloc + cs->NIndex);
    o = other;
    result = pymol::malloc<int>(n_alloc);
    CHECKOK(ok, result);
  }
  if (ok){
    for(a = 0; a < cs->NIndex; a++) {
      result[a] = -1;
    }
    offset = cs->NIndex;
    bd = I->Bond;
  }
  for(a = 0; ok && a < I->NBond; a++) {
    b1 = bd->index[0];
    b2 = bd->index[1];
    a1 = cs->atmToIdx(b1);
    a2 = cs->atmToIdx(b2);
    if((a1 >= 0) && (a2 >= 0)) {
      if(result[a1] < 0) {
        o = other + a1;
        result[a1] = offset;
        for(b = 0; b < o->n_cyclic_arom; b++) {
          a3 = o->cyclic_arom[b];
          offset = append_index(result, offset, a1, a3, 128 + other[a3].score, 1);
        }
        for(b = 0; b < o->n_arom; b++) {
          a3 = o->arom[b];
          offset = append_index(result, offset, a1, a3, 64 + other[a3].score, 1);
        }
        for(b = 0; b < o->n_high_val; b++) {
          a3 = o->high_val[b];
          offset = append_index(result, offset, a1, a3, 16 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_cyclic; b++) {
          a3 = o->cyclic[b];
          offset = append_index(result, offset, a1, a3, 8 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_planer; b++) {
          a3 = o->planer[b];
          offset = append_index(result, offset, a1, a3, 2 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_rest; b++) {
          a3 = o->rest[b];
          offset = append_index(result, offset, a1, a3, 1 + other[a3].score, 0);
        }
        result[offset++] = -1;
      }

      if(result[a2] < 0) {
        o = other + a2;
        result[a2] = offset;
        for(b = 0; b < o->n_cyclic_arom; b++) {
          a3 = o->cyclic_arom[b];
          offset = append_index(result, offset, a2, a3, 128 + other[a3].score, 1);
        }
        for(b = 0; b < o->n_arom; b++) {
          a3 = o->arom[b];
          offset = append_index(result, offset, a2, a3, 64 + other[a3].score, 1);
        }
        for(b = 0; b < o->n_high_val; b++) {
          a3 = o->high_val[b];
          offset = append_index(result, offset, a2, a3, 16 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_cyclic; b++) {
          a3 = o->cyclic[b];
          offset = append_index(result, offset, a2, a3, 8 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_planer; b++) {
          a3 = o->planer[b];
          offset = append_index(result, offset, a2, a3, 2 + other[a3].score, 0);
        }
        for(b = 0; b < o->n_rest; b++) {
          a3 = o->rest[b];
          offset = append_index(result, offset, a2, a3, 1 + other[a3].score, 0);
        }
        result[offset++] = -1;
      }

    }
    bd++;
    ok &= !I->G->Interrupt;
  }
  FreeP(other);
  return result;
}

int ObjectMoleculeGetNearestBlendedColor(ObjectMolecule * I, const float *point,
                                         float cutoff, int state, float *dist,
                                         float *color, int sub_vdw)
{
  int result = -1;
  float tot_weight = 0.0F;
  float cutoff2 = cutoff * cutoff;
  float nearest = -1.0F;

  color[0] = 0.0F;
  color[1] = 0.0F;
  color[2] = 0.0F;

  assert(state != -1 /* all states */);
  auto* cs = I->getCoordSet(state);

  {
    if(cs) {
      MapType *map;
      CoordSetUpdateCoord2IdxMap(cs, cutoff);
      if(sub_vdw) {
        cutoff -= MAX_VDW;
        cutoff2 = cutoff * cutoff;
      }
      nearest = cutoff2;
      if((map = cs->Coord2Idx)) {
        int a, b, c, d, e, f, j;
        float test;
        float *v;
        MapLocus(map, point, &a, &b, &c);
        for(d = a - 1; d <= a + 1; d++)
          for(e = b - 1; e <= b + 1; e++)
            for(f = c - 1; f <= c + 1; f++) {
              j = *(MapFirst(map, d, e, f));
              while(j >= 0) {
                v = cs->coordPtr(j);
                test = diffsq3f(v, point);
                if(sub_vdw) {
                  test = sqrt1f(test);
                  test -= I->AtomInfo[cs->IdxToAtm[j]].vdw;
                  if(test < 0.0F)
                    test = 0.0F;
                  test = test * test;
                }
                if(test < cutoff2) {
                  float weight = cutoff - sqrt1f(test);
                  const float *at_col = ColorGet(I->G, I->AtomInfo[cs->IdxToAtm[j]].color);
                  color[0] += at_col[0] * weight;
                  color[1] += at_col[1] * weight;
                  color[2] += at_col[2] * weight;
                  tot_weight += weight;
                }
                if(test <= nearest) {
                  result = j;
                  nearest = test;
                }
                j = MapNext(map, j);
              }
            }
      } else {
        int j;
        float test;
        const float* v = cs->Coord.data();
        for(j = 0; j < cs->NIndex; j++) {
          test = diffsq3f(v, point);
          if(sub_vdw) {
            test = sqrt1f(test);
            test -= I->AtomInfo[cs->IdxToAtm[j]].vdw;
            if(test < 0.0F)
              test = 0.0F;
            test = test * test;
          }
          if(test < cutoff2) {
            float weight = cutoff - sqrt1f(test);
            const float *at_col = ColorGet(I->G, I->AtomInfo[cs->IdxToAtm[j]].color);
            color[0] += at_col[0] * weight;
            color[1] += at_col[1] * weight;
            color[2] += at_col[2] * weight;
            tot_weight += weight;
          }
          if(test <= nearest) {
            result = j;
            nearest = test;
          }
          v += 3;
        }
      }
      if(result >= 0)
        result = cs->IdxToAtm[result];
    }
  }
  if(dist) {
    if(result >= 0) {
      *dist = sqrt1f(nearest);
      if(tot_weight > 0.0F) {
        color[0] /= tot_weight;
        color[1] /= tot_weight;
        color[2] /= tot_weight;
      }
    } else {
      *dist = -1.0F;
    }
  }
  return result;
}

int ObjectMoleculeGetNearestAtomIndex(ObjectMolecule * I, const float *point, float cutoff,
                                      int state, float *dist)
{
  int result = -1;
  float nearest = -1.0F;

  assert(state != -1 /* all states */);
  auto* cs = I->getCoordSet(state);

  {
    if(cs) {
      MapType *map;
      CoordSetUpdateCoord2IdxMap(cs, cutoff);
      nearest = cutoff * cutoff;
      if((map = cs->Coord2Idx)) {
        int a, b, c, d, e, f, j;
        float test;
        float *v;
        MapLocus(map, point, &a, &b, &c);
        for(d = a - 1; d <= a + 1; d++)
          for(e = b - 1; e <= b + 1; e++)
            for(f = c - 1; f <= c + 1; f++) {
              j = *(MapFirst(map, d, e, f));
              while(j >= 0) {
                v = cs->coordPtr(j);
                test = diffsq3f(v, point);
                if(test <= nearest) {
                  result = j;
                  nearest = test;
                }
                j = MapNext(map, j);
              }
            }
      } else {
        int j;
        float test;
        const float* v = cs->Coord.data();
        for(j = 0; j < cs->NIndex; j++) {
          test = diffsq3f(v, point);
          if(test <= nearest) {
            result = j;
            nearest = test;
          }
          v += 3;
        }
      }
      if(result >= 0)
        result = cs->IdxToAtm[result];
    }
  }
  if(dist) {
    if(result >= 0) {
      *dist = sqrt1f(nearest);
    } else {
      *dist = -1.0F;
    }
  }
  return result;
}

int ObjectMoleculeGetPrioritizedOther(const int *other, int a1, int a2, int *double_sided)
{
  int a3 = -1;
  int lvl = -1, ck, ck_lvl;
  int offset;
  int ar_count = 0;

  a3 = -1;
  lvl = -1;
  if(a1 >= 0) {
    offset = other[a1];
    if(offset >= 0) {
      while(1) {
        ck = other[offset];
        if(ck != a2) {
          if(ck >= 0) {
            ck_lvl = other[offset + 1];
            if(ck_lvl > lvl) {
              a3 = ck;
              lvl = ck_lvl;
            }
            ar_count += other[offset + 2];
          } else
            break;
        }
        offset += 3;
      }
    }
  }
  if(a2 >= 0) {
    offset = other[a2];
    if(offset >= 0) {
      while(1) {
        ck = other[offset];
        if(ck != a1) {
          if(ck >= 0) {
            ck_lvl = other[offset + 1];
            if(ck_lvl > lvl) {
              a3 = ck;
              lvl = ck_lvl;
            }
            ar_count += other[offset + 2];
          } else
            break;
        }
        offset += 3;
      }
    }
  }

  if(double_sided) {
    if(ar_count == 4)
      *double_sided = true;
    else
      *double_sided = false;
  }
  return a3;
}

/* Check if atom is bonded to an atom with given name
 *
 * param same_res:
 *    0 = must not be in same residue
 *    1 = must be in same residue
 *   -1 = don't check residue
 */
int ObjectMoleculeIsAtomBondedToName(ObjectMolecule * obj, int a0, const char *name, int same_res)
{
  PyMOLGlobals * G = obj->G;
  AtomInfoType const* const ai0 = obj->AtomInfo.data() + a0;

  if(a0 >= 0) {
    for (auto const& neighbor : AtomNeighbors(obj, a0)) {
      auto const* const ai2 = obj->AtomInfo.data() + neighbor.atm;
      if(WordMatchExact(G, LexStr(G, ai2->name), name, true) &&
          (same_res < 0 || (same_res == AtomInfoSameResidue(G, ai0, ai2))))
        return true;
    }
  }
  return false;
}

int ObjectMoleculeAreAtomsBonded2(ObjectMolecule * obj0, int a0, ObjectMolecule * obj1,
                                  int a1)
{
  if (obj0 == obj1 && a0 >= 0) {
    assert(a1 >= 0);
    for (auto const& neighbor : AtomNeighbors(obj0, a0)) {
      if (a1 == neighbor.atm)
        return true;
    }
  }
  return false;
}

bool ObjectMoleculeIsAtomBondedToSele(
    const ObjectMolecule* I, int atm, SelectorID_t sele)
{
  if (atm < I->NAtom) {
    for (auto const& neighbor : AtomNeighbors(I, atm)) {
      if (SelectorIsMember(I->G, I->AtomInfo[neighbor.atm].selEntry, sele)) {
        return true;
      }
    }
  }
  return false;
}

/**
 * Based on PDB nomenclature (resn, name), do:
 *
 * 1) If `ai1` or `ai2` is a known charged PDB atom, assign `formalCharge`
 *    and set `chemFlag` to false
 *
 * 2) If `ai1` and `ai2` are connected by a double bond, set (*bond_order) = 2
 */
static void assign_pdb_known_residue(PyMOLGlobals * G, AtomInfoType * ai1,
                                     AtomInfoType * ai2, int *bond_order)
{
  int order = *(bond_order);
  auto const name1 = pymol::zstring_view(LexStr(G, ai1->name));
  auto const name2 = pymol::zstring_view(LexStr(G, ai2->name));
  auto const resn1 = pymol::zstring_view(LexStr(G, ai1->resn));

  /* nasty high-speed hack to get bond valences and formal charges 
     for standard residues */
  if (((name1 == "C" && name2 == "O") || (name2 == "C" && name1 == "O")) &&
      AtomInfoKnownProteinResName(resn1.c_str())) {
    order = 2;
  } else if(name2 == "C" && name1 == "OXT") {
    ai1->formalCharge = -1;
    ai1->chemFlag = false;
  } else if(name1 == "C" && name2 == "OXT") {
    ai2->formalCharge = -1;
    ai2->chemFlag = false;
  } else {
    switch (resn1[0]) {
    case 'A':
      switch (resn1[1]) {
      case 'R':
        switch (resn1[2]) {
        case 'G':              /* ARG... */
          switch (resn1[3]) {
          case 0:
          case 'P':            /*  ARG, ARGP */
            if (name1 == "NH1") {
              ai1->formalCharge = 1;
              ai1->chemFlag = false;
            } else if (name2 == "NH1") {
              ai2->formalCharge = 1;
              ai2->chemFlag = false;
            }
            break;
          }
          if((name1 == "CZ" && name2 == "NH1") ||
             (name2 == "CZ" && name1 == "NH1"))
            order = 2;
          break;
        }
        break;
      case 'S':
        switch (resn1[2]) {
        case 'P':              /* ASP... */
          switch (resn1[3]) {
          case 0:
          case 'M':            /* ASP, ASPM minus assumption */
            if (name1 == "OD2") {
              ai1->formalCharge = -1;
              ai1->chemFlag = false;
            } else if (name2 == "OD2") {
              ai2->formalCharge = -1;
              ai2->chemFlag = false;
            }
            break;
          }
          if((name1 == "CG" && name2 == "OD1") ||
             (name2 == "CG" && name1 == "OD1"))
            order = 2;
          break;
        case 'N':              /* ASN  */
          if((name1 == "CG" && name2 == "OD1") ||
             (name2 == "CG" && name1 == "OD1"))
            order = 2;
          break;
        }
        break;
      case 0:
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }
        if((name1 == "C8" && name2 == "N7") ||
           (name2 == "C8" && name1 == "N7"))
          order = 2;
        else if((name1 == "C4" && name2 == "C5") ||
                (name2 == "C4" && name1 == "C5"))
          order = 2;

        else if((name1 == "C6" && name2 == "N1") ||
                (name2 == "C6" && name1 == "N1"))
          order = 2;
        else if((name1 == "C2" && name2 == "N3") ||
                (name2 == "C2" && name1 == "N3"))
          order = 2;
        else
          if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
              (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
        break;
      }
      break;
    case 'C':
      if(resn1[1] == 0) {
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }
        if((name1 == "C2" && name2 == "O2") ||
           (name2 == "C2" && name1 == "O2"))
          order = 2;
        else if((name1 == "C4" && name2 == "N3") ||
                (name2 == "C4" && name1 == "N3"))
          order = 2;

        else if((name1 == "C5" && name2 == "C6") ||
                (name2 == "C5" && name1 == "C6"))
          order = 2;
        else
          if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
              (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
      }
      break;
    case 'D':                  /* deoxy nucleic acids */
      switch (resn1[1]) {
      case 'A':
        if(resn1[2] == 0) {
          if((name1 == "O2P" || name1 == "OP2")) {
            ai1->formalCharge = -1;
            ai1->chemFlag = false;
          } else if((name2 == "O2P" || name2 == "OP2")) {
            ai2->formalCharge = -1;
            ai2->chemFlag = false;
          }
          if((name1 == "C8" && name2 == "N7") ||
             (name2 == "C8" && name1 == "N7"))
            order = 2;
          else if((name1 == "C4" && name2 == "C5") ||
                  (name2 == "C4" && name1 == "C5"))
            order = 2;

          else if((name1 == "C6" && name2 == "N1") ||
                  (name2 == "C6" && name1 == "N1"))
            order = 2;
          else if((name1 == "C2" && name2 == "N3") ||
                  (name2 == "C2" && name1 == "N3"))
            order = 2;
          else
            if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
                (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
            order = 2;
        }
        break;
      case 'C':
        if(resn1[2] == 0) {
          if((name1 == "O2P" || name1 == "OP2")) {
            ai1->formalCharge = -1;
            ai1->chemFlag = false;
          } else if((name2 == "O2P" || name2 == "OP2")) {
            ai2->formalCharge = -1;
            ai2->chemFlag = false;
          }
          if((name1 == "C2" && name2 == "O2") ||
             (name2 == "C2" && name1 == "O2"))
            order = 2;
          else if((name1 == "C4" && name2 == "N3") ||
                  (name2 == "C4" && name1 == "N3"))
            order = 2;

          else if((name1 == "C5" && name2 == "C6") ||
                  (name2 == "C5" && name1 == "C6"))
            order = 2;
          else
            if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
                (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
            order = 2;
        }
        break;
      case 'T':
        if(resn1[2] == 0) {
          if((name1 == "O2P" || name1 == "OP2"))
            ai1->formalCharge = -1;
          else if((name2 == "O2P" || name2 == "OP2"))
            ai2->formalCharge = -1;

          if((name1 == "C2" && name2 == "O2") ||
             (name2 == "C2" && name1 == "O2"))
            order = 2;
          else if((name1 == "C4" && name2 == "O4") ||
                  (name2 == "C4" && name1 == "O4"))
            order = 2;

          else if((name1 == "C5" && name2 == "C6") ||
                  (name2 == "C5" && name1 == "C6"))
            order = 2;
          else
            if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
                (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
            order = 2;
        }
        break;
      case 'G':
        if(resn1[2] == 0) {
          if((name1 == "O2P" || name1 == "OP2")) {
            ai1->formalCharge = -1;
            ai1->chemFlag = false;
          } else if((name2 == "O2P" || name2 == "OP2")) {
            ai2->formalCharge = -1;
            ai2->chemFlag = false;
          }
          if((name1 == "C6" && name2 == "O6") ||
             (name2 == "C6" && name1 == "O6"))
            order = 2;
          else if((name1 == "C2" && name2 == "N3") ||
                  (name2 == "C2" && name1 == "N3"))
            order = 2;
          else if((name1 == "C8" && name2 == "N7") ||
                  (name2 == "C8" && name1 == "N7"))
            order = 2;
          else if((name1 == "C4" && name2 == "C5") ||
                  (name2 == "C4" && name1 == "C5"))
            order = 2;
          else
            if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
                (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
            order = 2;
        }
        break;
      case 'U':
        if(resn1[2] == 0) {
          if((name1 == "O2P" || name1 == "OP2")) {
            ai1->formalCharge = -1;
            ai1->chemFlag = false;
          } else if((name2 == "O2P" || name2 == "OP2")) {
            ai2->formalCharge = -1;
            ai2->chemFlag = false;
          }

          if((name1 == "C2" && name2 == "O2") ||
             (name2 == "C2" && name1 == "O2"))
            order = 2;
          else if((name1 == "C4" && name2 == "O4") ||
                  (name2 == "C4" && name1 == "O4"))
            order = 2;

          else if((name1 == "C5" && name2 == "C6") ||
                  (name2 == "C5" && name1 == "C6"))
            order = 2;
          else
            if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
                (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
            order = 2;
        }
        break;
      }
      break;
    case 'G':
      switch (resn1[1]) {
      case 'L':
        switch (resn1[2]) {
        case 'U':              /* GLU missing GLUN, GLUH, GLH handling */
          switch (resn1[3]) {
          case 0:
          case 'M':            /* minus */
            if (name1 == "OE2") {
              ai1->formalCharge = -1;
              ai1->chemFlag = false;
            } else if (name2 == "OE2") {
              ai2->formalCharge = -1;
              ai2->chemFlag = false;
            }
            break;
          }
          if((name1 == "CD" && name2 == "OE1") ||
             (name2 == "CD" && name1 == "OE1"))
            order = 2;
          break;
        case 'N':              /* GLN or GLU */
          if((name1 == "CD" && name2 == "OE1") ||
             (name2 == "CD" && name1 == "OE1"))
            order = 2;
          break;
        }
        break;
      case 0:
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }

        if((name1 == "C6" && name2 == "O6") ||
           (name2 == "C6" && name1 == "O6"))
          order = 2;
        else if((name1 == "C2" && name2 == "N3") ||
                (name2 == "C2" && name1 == "N3"))
          order = 2;
        else if((name1 == "C8" && name2 == "N7") ||
                (name2 == "C8" && name1 == "N7"))
          order = 2;
        else if((name1 == "C4" && name2 == "C5") ||
                (name2 == "C4" && name1 == "C5"))
          order = 2;
        else
          if((name1 == "P"
              && (name2 == "O1P" || name2 == "OP1"))
             || (name2 == "P"
                 && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
        break;
      }
      break;
    case 'H':
      switch (resn1[1]) {
      case 'I':
        switch (resn1[2]) {
        case 'P':
          if (name1 == "ND1") {
            ai1->formalCharge = 1;
            ai1->chemFlag = false;
          } else if (name2 == "ND1") {
            ai2->formalCharge = 1;
            ai2->chemFlag = false;
          }
          if((name1 == "CG" && name2 == "CD2") ||
             (name2 == "CG" && name1 == "CD2"))
            order = 2;
          else if((name1 == "CE1" && name2 == "ND1") ||
                  (name2 == "CE1" && name1 == "ND1"))
            order = 2;
          break;
        case 'S':
          switch (resn1[3]) {
          case 'A':            /* HISA Gromacs */
          case 'D':            /* HISD Quanta */
            if((name1 == "CG" && name2 == "CD2") ||
               (name2 == "CG" && name1 == "CD2"))
              order = 2;
            else if((name1 == "CE1" && name2 == "NE2") ||
                    (name2 == "CE1" && name1 == "NE2"))
              order = 2;
            break;
          case 0:              /* plain HIS */
          case 'B':            /* HISB Gromacs */
          case 'E':            /* HISE Quanta */
            if((name1 == "CG" && name2 == "CD2") ||
               (name2 == "CG" && name1 == "CD2"))
              order = 2;
            else if((name1 == "CE1" && name2 == "ND1") ||
                    (name2 == "CE1" && name1 == "ND1"))
              order = 2;
            break;
          case 'H':            /* HISH Gromacs */
          case 'P':            /* HISP Quanta */
            if (name1 == "ND1") {
              ai1->formalCharge = 1;
              ai1->chemFlag = false;
            } else if (name2 == "ND1") {
              ai2->formalCharge = 1;
              ai2->chemFlag = false;
            }
            if((name1 == "CG" && name2 == "CD2") ||
               (name2 == "CG" && name1 == "CD2"))
              order = 2;
            else if((name1 == "CE1" && name2 == "ND1") ||
                    (name2 == "CE1" && name1 == "ND1"))
              order = 2;
            break;
          }
          break;
        case 'E':              /* HIE */
          if((name1 == "CG" && name2 == "CD2") ||
             (name2 == "CG" && name1 == "CD2"))
            order = 2;
          else if((name1 == "CE1" && name2 == "ND1") ||
                  (name2 == "CE1" && name1 == "ND1"))
            order = 2;
          break;
        case 'D':              /* HID */
          if((name1 == "CG" && name2 == "CD2") ||
             (name2 == "CG" && name1 == "CD2"))
            order = 2;
          else if((name1 == "CE1" && name2 == "NE2") ||
                  (name2 == "CE1" && name1 == "NE2"))
            order = 2;
          break;
        }
        break;
      }
      break;
    case 'I':
      if(resn1[1] == 0) {
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }
        if((name1 == "C8" && name2 == "N7") ||
           (name2 == "C8" && name1 == "N7"))
          order = 2;
        else if((name1 == "C4" && name2 == "C5") ||
                (name2 == "C4" && name1 == "C5"))
          order = 2;

        else if((name1 == "C6" && name2 == "N1") ||
                (name2 == "C6" && name1 == "N1"))
          order = 2;
        else if((name1 == "C2" && name2 == "N3") ||
                (name2 == "C2" && name1 == "N3"))
          order = 2;
        else
          if((name1 == "P"
              && (name2 == "O1P" || name2 == "OP1"))
             || (name2 == "P"
                 && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
      }
      break;
    case 'P':
      switch (resn1[1]) {
      case 'H':                /* PHE */
        if(resn1[2] == 'E') {
          if((name1 == "CG" && name2 == "CD1") ||
             (name2 == "CG" && name1 == "CD1"))
            order = 2;
          else if((name1 == "CZ" && name2 == "CE1") ||
                  (name2 == "CZ" && name1 == "CE1"))
            order = 2;

          else if((name1 == "CE2" && name2 == "CD2") ||
                  (name2 == "CE2" && name1 == "CD2"))
            order = 2;
          break;
        }
      }
      break;
    case 'L':
      switch (resn1[1]) {
      case 'Y':
        switch (resn1[2]) {
        case 'S':              /* LYS. */
          switch (resn1[3]) {
          case 0:
          case 'P':            /* LYS, LYSP */
            if (name1 == "NZ") {
              ai1->formalCharge = 1;
              ai1->chemFlag = false;
            } else if (name2 == "NZ") {
              ai2->formalCharge = 1;
              ai2->chemFlag = false;
            }
            break;
          }
          break;
        }
        break;
      }
      break;
    case 'T':
      switch (resn1[1]) {
      case 'Y':                /* TYR */
        if(resn1[2] == 'R') {
          if((name1 == "CG" && name2 == "CD1") ||
             (name2 == "CG" && name1 == "CD1"))
            order = 2;
          else if((name1 == "CZ" && name2 == "CE1") ||
                  (name2 == "CZ" && name1 == "CE1"))
            order = 2;

          else if((name1 == "CE2" && name2 == "CD2") ||
                  (name2 == "CE2" && name1 == "CD2"))
            order = 2;
          break;
        }
        break;
      case 'R':
        if(resn1[2] == 'P') {
          if((name1 == "CG" && name2 == "CD1") ||
             (name2 == "CG" && name1 == "CD1"))
            order = 2;
          else if((name1 == "CZ3" && name2 == "CE3") ||
                  (name2 == "CZ3" && name1 == "CE3"))
            order = 2;
          else if((name1 == "CZ2" && name2 == "CH2") ||
                  (name2 == "CZ2" && name1 == "CH2"))
            order = 2;
          else if((name1 == "CE2" && name2 == "CD2") ||
                  (name2 == "CE2" && name1 == "CD2"))
            order = 2;
          break;
        }
        break;
      case 0:
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }

        if((name1 == "C2" && name2 == "O2") ||
           (name2 == "C2" && name1 == "O2"))
          order = 2;
        else if((name1 == "C4" && name2 == "O4") ||
                (name2 == "C4" && name1 == "O4"))
          order = 2;

        else if((name1 == "C5" && name2 == "C6") ||
                (name2 == "C5" && name1 == "C6"))
          order = 2;
        else
          if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
              (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
        break;
      }
      break;
    case 'U':
      if(resn1[1] == 0) {
        if((name1 == "O2P" || name1 == "OP2")) {
          ai1->formalCharge = -1;
          ai1->chemFlag = false;
        } else if((name2 == "O2P" || name2 == "OP2")) {
          ai2->formalCharge = -1;
          ai2->chemFlag = false;
        }
        if((name1 == "C2" && name2 == "O2") ||
           (name2 == "C2" && name1 == "O2"))
          order = 2;
        else if((name1 == "C4" && name2 == "O4") ||
                (name2 == "C4" && name1 == "O4"))
          order = 2;

        else if((name1 == "C5" && name2 == "C6") ||
                (name2 == "C5" && name1 == "C6"))
          order = 2;
        else
          if ((name1 == "P" && (name2 == "O1P" || name2 == "OP1")) ||
              (name2 == "P" && (name1 == "O1P" || name1 == "OP1")))
          order = 2;
      }
      break;
    }
  }
  *(bond_order) = order;
}

void ObjectMoleculeFixChemistry(ObjectMolecule * I, int sele1, int sele2, int invalidate)
{
  int b;
  int flag = false;
  int s1, s2;
  AtomInfoType *ai1, *ai2;
  int order;
  BondType* bond = I->Bond.data();
  for(b = 0; b < I->NBond; b++) {
    flag = false;
    ai1 = I->AtomInfo + bond->index[0];
    ai2 = I->AtomInfo + bond->index[1];
    s1 = ai1->selEntry;
    s2 = ai2->selEntry;

    if((SelectorIsMember(I->G, s1, sele1) &&
        SelectorIsMember(I->G, s2, sele2)) ||
       (SelectorIsMember(I->G, s2, sele1) && SelectorIsMember(I->G, s1, sele2))) {
      order = -1;
      if(strlen(LexStr(I->G, ai1->resn)) < 4) {       /* Standard disconnected PDB residue */
        if(AtomInfoSameResidue(I->G, ai1, ai2)) {
          assign_pdb_known_residue(I->G, ai1, ai2, &order);
        }
      }
      if(order > 0) {
        bond->order = order;
        ai1->chemFlag = false;
        ai2->chemFlag = false;
        flag = true;
      } else if(invalidate) {
        ai1->chemFlag = false;
        ai2->chemFlag = false;
        flag = true;
      }
    }
    bond++;
  }
  if(flag) {
    I->invalidate(cRepAll, cRepInvAll, -1);
    SceneChanged(I->G);
  }
}

void ObjMolPairwiseInit(ObjMolPairwise * pairwise)
{
  UtilZeroMem((char *) pairwise, sizeof(ObjMolPairwise));
  pairwise->trg_vla = VLAlloc(int, 10);
  pairwise->mbl_vla = VLAlloc(int, 10);
}

void ObjMolPairwisePurge(ObjMolPairwise * pairwise)
{
  VLAFreeP(pairwise->trg_vla);
  VLAFreeP(pairwise->mbl_vla);
}

int ObjectMoleculeConvertIDsToIndices(ObjectMolecule * I, int *id, int n_id)
{
  /* return true if all IDs are unique, false if otherwise */

  int min_id, max_id, range, *lookup = NULL;
  int unique = true;

  /* this routine only works if IDs cover a reasonable range --
     should rewrite using a hash table */

  if(I->NAtom) {

    /* determine range */

    {
      int a, cur_id;
      cur_id = I->AtomInfo[0].id;
      min_id = cur_id;
      max_id = cur_id;
      for(a = 1; a < I->NAtom; a++) {
        cur_id = I->AtomInfo[a].id;
        if(min_id > cur_id)
          min_id = cur_id;
        if(max_id < cur_id)
          max_id = cur_id;
      }
    }

    /* create cross-reference table */

    {
      int a, offset;

      range = max_id - min_id + 1;
      lookup = pymol::calloc<int>(range);
      for(a = 0; a < I->NAtom; a++) {
        offset = I->AtomInfo[a].id - min_id;
        if(!lookup[offset])
          lookup[offset] = a + 1;
        else
          unique = false;
      }
    }

    /* iterate through IDs and replace with indices or -1 */

    {
      int i, offset, lkup;

      for(i = 0; i < n_id; i++) {
        offset = id[i] - min_id;
        if((offset >= 0) && (offset < range)) {
          lkup = lookup[offset];
          if(lkup > 0) {
            id[i] = lkup - 1;
          } else {
            id[i] = -1;         /* negative means no match */
          }
        } else
          id[i] = -1;
      }
    }
  }

  FreeP(lookup);
  return unique;

}

static const char *check_next_pdb_object(const char *p, int skip_to_next)
{
  const char *start = p;
  while(*p) {
    if(strstartswith(p, "HEADER")) {
      if(skip_to_next)
        return p;
      return start;
    } else if(strstartswith(p, "ATOM ") || strstartswith(p, "HETATM")) {
      return start;
    } else if(skip_to_next && strcmp("END", p) == 0) {
      /* if we pass over the END of the current PDB file, then reset start */
      start = p;
    }
    p = nextline(p);
  }
  return NULL;
}

static int get_multi_object_status(const char *p)
{                               /* expensive -- only call
                                   this if there is no
                                   other way to determine
                                   this information */
  int seen_header = 0;
  while(*p) {
    if(strstartswith(p, "HEADER")) {
      if(seen_header) {
        return 1;
      } else {
        seen_header = true;
      }
    }
    p = nextline(p);
  }
  return -1;
}

/**
 * If any atom in I->AtomInfo contains the wildcard character (from
 * "atom_name_wildcard" or "wildcard" setting), then set the object-level
 * "atom_name_wildcard" setting to " " (disables wildcard matching).
 */
int ObjectMoleculeAutoDisableAtomNameWildcard(ObjectMolecule * I)
{
  PyMOLGlobals *G = I->G;
  char wildcard = 0;
  int found_wildcard = false;

  {
    const char *tmp = SettingGet_s(G, NULL, I->Setting.get(), cSetting_atom_name_wildcard);
    if(tmp && tmp[0]) {
      wildcard = *tmp;
    } else {
      tmp = SettingGet_s(G, NULL, I->Setting.get(), cSetting_wildcard);
      if(tmp) {
        wildcard = *tmp;
      }
    }
    if(wildcard == 32)
      wildcard = 0;

  }

  if(wildcard) {
    int a;
    const char *p;
    char ch;
    const AtomInfoType *ai = I->AtomInfo;

    for(a = 0; a < I->NAtom; a++) {
      p = LexStr(G, ai->name);
      while((ch = *(p++))) {
        if(ch == wildcard) {
          found_wildcard = true;
          break;
        }
      }
      ai++;
    }
    if(found_wildcard) {
      ExecutiveSetObjSettingFromString(G, cSetting_atom_name_wildcard, " ",
                                       I, -1, true, true);
    }
  }
  return found_wildcard;
}


/*========================================================================*/
#define PDB_MAX_TAGS 64

static void ObjectMoleculePDBStr2CoordSetPASS1(PyMOLGlobals * G, int *ok,
    const char **restart_model, const char *p, int n_tags, const char* const* tag_start,
    int *nAtom, char cc[], int quiet, int *bogus_name_alignment,
    int *ssFlag, const char **next_pdb, PDBInfoRec *info, int only_read_one_model,
    int *ignore_conect, int *bondFlag, int *have_bond_order) {
  int seen_end_of_atoms = false;
  *restart_model = NULL;
  while(*ok && *p) {
    AddOrthoOutputIfMatchesTags(G, n_tags, *nAtom, tag_start, p, cc, quiet);
    if((strstartswith(p, "ATOM ") ||
          strstartswith(p, "HETATM")) && (!*restart_model)) {
      if(!seen_end_of_atoms)
        (*nAtom)++;
      if(*bogus_name_alignment) {
        ncopy(cc, nskip(p, 12), 4);   /* copy the atom field */
        if((cc[0] == 32) && (cc[1] != 32)) {  /* check to see if indentation was followed correctly, 32 - space */
          *bogus_name_alignment = false;
        }
      }
    } else if(strstartswith(p, "HELIX ")){
      *ssFlag = true;
    }else if(strstartswith(p, "SHEET ")){
      *ssFlag = true;
    }else if(strstartswith(p, "HEADER")) {
      if(*nAtom > 0) {         /* if we've already found atom records, then this must be a new pdb */
        (*next_pdb) = p;
        break;
      }
    } else if(strstartswith(p, "REMARK")) {
      //        char * start = p;  // USED TO SAVE REMARKS (TODO)
      do {
        if(info && strncmp("    GENERATED BY TRJCONV", p + 6, 24) == 0)
          info->ignore_header_names = true;
        p = nextline(p);
        AddOrthoOutputIfMatchesTags(G, n_tags, *nAtom, tag_start, p, cc, quiet);
      } while(strstartswith(p, "REMARK"));
      // REMARKS are string from start to p, but not saved currently (TODO)
      continue;
    } else if(strstartswith(p, "ENDMDL") && (!*restart_model)) {
      *restart_model = nextline(p);
      seen_end_of_atoms = true;
      if(only_read_one_model)
        break;
    } else if(strstartswith(p, "END")) {      /* stop parsing after END */
      ntrim(cc, p, 6);
      if(strcmp("END", cc) == 0) {    /* END */
        seen_end_of_atoms = true;
        if(next_pdb) {
          p = nextline(p);
          ncopy(cc, p, 6);
          if(strcmp("HEADER", cc) == 0) {
            (*next_pdb) = p;  /* found another PDB file after this one... */
          } else if(strcmp("ENDMDL", cc) == 0) {
            seen_end_of_atoms = false;
          }
        }
        break;
      }
    } else if(strstartswith(p, "CONECT")) {
      if(!*ignore_conect) 
        *bondFlag = true;
    } else if(strstartswith(p, "USER") && (!*restart_model)) {
    }
    p = nextline(p);
  }
}

/**
 * Datastructure for efficient array-based secondary structure lookup.
 */
struct SSHash {
  int n_ss;         // number of ss_list items
  int* ss[256];     // one array for each chain identifier
  SSEntry *ss_list; // VLA
};

static void sshash_free(SSHash *hash) {
  int a;
  if(!hash)
    return;
  for(a = 0; a <= 255; a++)
    FreeP(hash->ss[a]);
  VLAFreeP(hash->ss_list);
  FreeP(hash);
}

static SSHash * sshash_new() {
  SSHash *hash = pymol::calloc<SSHash>(1);
  ok_assert(1, hash);
  hash->n_ss = 1;
  hash->ss_list = VLAlloc(SSEntry, 50);
  ok_assert(1, hash->ss_list);
  return hash;
ok_except1:
  sshash_free(hash);
  return NULL;
}

/**
 * Insert a secondary structure record into the hash table.
 */
static int sshash_register_rec(SSHash * hash,
    unsigned char ss_chain1, int ss_resv1, char ss_inscode1,
    unsigned char ss_chain2, int ss_resv2, char ss_inscode2,
    char SSCode) {
  /* pretty confusing how this works... the following efficient (i.e. array-based)
     secondary structure lookup even when there are multiple insertion codes
     and where there may be multiple SS records for the residue using different
     insertion codes */

  unsigned char chain;
  int ss_found = false, ssi = 0, a, b, index;
  SSEntry *sst;

  // up to two iterations:
  // 1) assume chain1==chain2
  // 2) chains are not the same (undefined in PDB spec?)
  for (a = 0, chain = ss_chain1; a < 2; a++, chain = ss_chain2) {
    // allocate new array for chain if necc.
    if(!hash->ss[chain]) {
      ok_assert(1, hash->ss[chain] = pymol::calloc<int>(cResvMask + 1));
    }

    sst = NULL;
    // iterate over all residues indicated
    for(b = ss_resv1; b <= ss_resv2; b++) {
      index = b & cResvMask;

      if(hash->ss[chain][index]) {
        // make a unique copy in the event of multiple entries for one resv
        sst = NULL;
      }

      if(!sst) {
        VLACheck(hash->ss_list, SSEntry, hash->n_ss);
        ok_assert(1, hash->ss_list);
        ssi = (hash->n_ss)++;
        sst = hash->ss_list + ssi;
        sst->resv1 = ss_resv1;
        sst->resv2 = ss_resv2;
        sst->chain1 = ss_chain1;
        sst->chain2 = ss_chain2;
        sst->type = SSCode;
        sst->inscode1 = ss_inscode1;
        sst->inscode2 = ss_inscode2;
        ss_found = true;
      }
      sst->next = hash->ss[chain][index];
      hash->ss[chain][index] = ssi;
      if(sst->next)
        sst = NULL;           /* force another unique copy */
    }
  }
  return ss_found;
ok_except1:
  return false;
}

/**
 * Assign ai->ssType
 */
static void sshash_lookup(SSHash *hash, AtomInfoType *ai, unsigned char ss_chain1) {
  int index, ssi;
  SSEntry *sst = NULL;

  index = ai->resv & cResvMask;
  if(hash->ss[ss_chain1]) {
    ssi = hash->ss[ss_chain1][index];
    while(ssi) {
      sst = hash->ss_list + ssi;
      /* contains shared entry, or unique linked list for each residue */
      if(    ai->resv >= sst->resv1
          && ai->resv <= sst->resv2
          && (ai->resv != sst->resv1 || ai->inscode >= sst->inscode1)
          && (ai->resv != sst->resv2 || ai->inscode <= sst->inscode2))
      {
        ai->ssType[0] = sst->type;
        return;
      }
      ssi = sst->next;
    }
  }
}

/**
 * PQR atom line parsing
 *
 * Try to parse columns white space delimited (10 columns with optional
 * chain, 9 without).
 *
 * Where PQR files come from:
 *
 * pdb2pqr -> writes PDB-like fixed colums. APBS will fail to read those files
 * if columns are too wide.
 *
 * pdb2pqr --whitespace -> puts extra whitespace, starting at column 2, but
 * not between chain and resi! PyMOL <= 2.1 fails to read those files.
 *
 * APBS Tools Plugin by Michael Lerner adds extra whitespace before negative
 * coordinates (assuming -100.0 is the most likely source of error).
 *
 * @param[in,out] p     Pointer to parse from, points after the ATOM field.
 *                      Will move the pointer to the end of the line if
 *                      parsing was successful.
 * @param[out]    ai    Atom to populate with data
 * @param[out]    coord Coordinates to populate
 * @return true on success, false otherwise.
 */
static bool parse_pqr_atom_line(PyMOLGlobals * G,
    const char * &p,
    AtomInfoType * ai,
    float * coord)
{
  auto p_eol = nskip(p, 999);   // end of line pointer
  std::string cc(p, p_eol);     // line (starting after ATOM field)

  // whitespace splitting
  auto columns = strsplit(cc);

  // insert chain column if missing
  if (columns.size() == 9) {
    columns.insert(columns.begin() + 3, "");

    // check for concatenated chain + resi
    if (columns[4].size() > 4 && !isdigit(columns[4][0])) {
      columns[3] = columns[4].substr(0, 1);
      columns[4] = columns[4].substr(1);
    }
  }

  // for validation: if number parsing consumes the entire string, then dummy
  // will never be populated (sscanf(...) == 1, not 2)
  char dummy[2];

  // validate numeric fields and populate atom info and coordinates
  if (columns.size() == 10 &&
      sscanf(columns[0].c_str(), "%d%1s", &ai->id, dummy) == 1 &&
      sscanf(columns[5].c_str(), "%f%1s", coord + 0, dummy) == 1 &&
      sscanf(columns[6].c_str(), "%f%1s", coord + 1, dummy) == 1 &&
      sscanf(columns[7].c_str(), "%f%1s", coord + 2, dummy) == 1 &&
      sscanf(columns[8].c_str(), "%f%1s", &ai->partialCharge, dummy) == 1 &&
      sscanf(columns[9].c_str(), "%f%1s", &ai->elec_radius, dummy) == 1) {
    LexAssign(G, ai->name, columns[1].c_str());
    LexAssign(G, ai->resn, columns[2].c_str());
    LexAssign(G, ai->chain, columns[3].c_str());
    ai->setResi(columns[4].c_str());

    // move parser to next line
    p = p_eol;

    return true;
  }

  return false;
}

CoordSet *ObjectMoleculePDBStr2CoordSet(PyMOLGlobals * G,
                                        const char *buffer,
                                        AtomInfoType ** atInfoPtr,
                                        const char **restart_model,
                                        char *segi_override,
                                        char *pdb_name,
                                        const char **next_pdb,
                                        PDBInfoRec * info, int quiet, int *model_number)
{

  const char *p;
  int nAtom;
  int a;
  float *coord = NULL;
  CoordSet *cset = NULL;
  AtomInfoType *atInfo = NULL, *ai;
  int AFlag;
  char SSCode;
  int atomCount;
  int bondFlag = false;
  BondType *bond = NULL, *ii1, *ii2;
  int *idx;
  int nBond = 0;
  int b1, b2, nReal, maxAt;
  std::unique_ptr<CSymmetry> symmetry;
  int auto_show = RepGetAutoShowMask(G);
  int reformat_names = SettingGetGlobal_i(G, cSetting_pdb_reformat_names_mode);
  int truncate_resn = SettingGetGlobal_b(G, cSetting_pdb_truncate_residue_name);
  const char *tags_in = SettingGetGlobal_s(G, cSetting_pdb_echo_tags), *tag_start[PDB_MAX_TAGS];
  int n_tags = 0;
  int foundNextModelFlag = false;
  int ssFlag = false;
  int ss_resv1 = 0, ss_resv2 = 0;
  char ss_inscode1 = '\0', ss_inscode2 = '\0';
  unsigned char ss_chain1 = 0, ss_chain2 = 0;
  SSHash *ss_hash = NULL;
  char cc[MAXLINELEN], tags[MAXLINELEN];
  int ignore_pdb_segi = 0;
  int ss_valid, ss_found = false;
  int only_read_one_model = false;
  int ignore_conect = SettingGetGlobal_b(G, cSetting_pdb_ignore_conect);
  int have_bond_order = false;
  int seen_model, in_model = false;
  int is_end_of_object = false;
  int literal_names = SettingGetGlobal_b(G, cSetting_pdb_literal_names);
  int bogus_name_alignment = true;
  AtomName literal_name = "";
  int ok = true;
  lexidx_t segi_override_idx = LexIdx(G, segi_override);

  if(tags_in && (!quiet) && (!*restart_model)) {
    char *p = tags;
    strcpy(tags, tags_in);

    while(*p) {
      while(*p == ' ')          /* skip spaces */
        p++;
      if(*p) {
        tag_start[n_tags] = p;
        n_tags++;
        while(*p) {
          if(*p != ',') {
            if(*p == ' ')
              *p = 0;
            p++;
          } else
            break;
        }
        if(*p) {                /* terminate tag */
          *p = 0;
          p++;
        }
      }
    }
  }

  if(literal_names)
    reformat_names = 0;

  ignore_pdb_segi = SettingGetGlobal_b(G, cSetting_ignore_pdb_segi);

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

  if(!atInfo)
    ErrFatal(G, "PDBStr2CoordSet", "need atom information record!");    /* failsafe for old version.. */

  if(buffer == *restart_model)
    only_read_one_model = true;
  else if(info && (info->multiplex > 0)) {
    only_read_one_model = true;
    if(!info->multi_object_status) {    /* figure out if this is a multi-object (multi-HEADER) pdb file */
      info->multi_object_status = get_multi_object_status(p);
    }
  }
  /* PASS 1 */
  ObjectMoleculePDBStr2CoordSetPASS1(G, &ok, restart_model, p, n_tags,
      tag_start, &nAtom, cc, quiet, &bogus_name_alignment, &ssFlag, next_pdb,
      info, only_read_one_model, &ignore_conect, &bondFlag, &have_bond_order);
  /* INPUT USED:
     only_read_one_model - only reads one pdb model if set
     n_tags, tag_start - tags defined to report in log
     cc - just temporary char * buffer that is used, no input/output 
     OUTPUT USED:
     bogus_name_alignment - whether all ATOM/HETATM has correct names in PDB (12-16, starting with space
     ssFlag - if PDB has HELIX and/or SHEET records
     both INPUT/OUTPUT:
     nAtom - returns the number of atoms read in, also uses it to detect multiple PDBs
     ignore_conect - do not set bondFlag if set

     END PASS 1 */

  *restart_model = NULL;
  if (ok){
    coord = VLAlloc(float, 3 * nAtom);
    CHECKOK(ok, coord);
  }

  if(ok && atInfo){
    VLACheck(atInfo, AtomInfoType, nAtom);
    CHECKOK(ok, atInfo);
    if (ok)
      *atInfoPtr = atInfo;
  }
  if(ok && bondFlag) {
    nBond = 0;
    bond = VLACalloc(BondType, 6 * nAtom);
    CHECKOK(ok, bond);
  }
  p = buffer;
  PRINTFB(G, FB_ObjectMolecule, FB_Blather)
    " ObjectMoleculeReadPDB: Found %i atoms...\n", nAtom ENDFB(G);

  if(ok && ssFlag) {
    ss_hash = sshash_new();
  }

  a = 0;                        /* WATCHOUT */
  atomCount = 0;

  /* PASS 2 */
  seen_model = false;

  while(ok && *p) {
    /*      printf("%c%c%c%c%c%c\n",p[0],p[1],p[2],p[3],p[4],p[5]); */
    AFlag = false;
    SSCode = 0;
    if(strstartswith(p, "ATOM "))
      AFlag = 1;
    else if(strstartswith(p, "HETATM"))
      AFlag = 2;
    else if(strstartswith(p, "HEADER")) {

      if(pdb_name) {
        if(atomCount > 0) {
          /* have we already read atoms?  then this is probably a new PDB file */
          (*next_pdb) = p;      /* found another PDB file after this one... */
          break;
        } else if((!info) || (!info->ignore_header_names)) {
          /* if this isn't an MD trajectory... */
          const char *pp;
          pp = nskip(p, 62);    /* is there a four-letter PDB code? */
          pp = ntrim(pdb_name, pp, 4);
          if(pdb_name[0] == 0) {        /* if not, is there a plain name (for MERCK!) */
            p = nskip(p, 6);
            p = ntrim(cc, p, 44);
            UtilNCopy(pdb_name, cc, WordLength);
          } else {
            p = pp;
          }
        }
      }
    } else if(strstartswith(p, "MODEL ")) {
      if(model_number) {
        int tmp;
        p = nskip(p, 10);
        p = ncopy(cc, p, 5);
        if(sscanf(cc, "%d", &tmp) == 1)
          *model_number = tmp;
      }
      seen_model = true;
      in_model = true;
    } else if(strstartswith(p, "HELIX ")) {
      ss_valid = true;

      p = nskip(p, 19);
      ss_chain1 = (*p);
      p = nskip(p, 2);
      p = ncopy(cc, p, 4);
      if(!sscanf(cc, "%d", &ss_resv1))
        ss_valid = false;
      ss_inscode1 = makeInscode(*p);

      p = nskip(p, 6);
      ss_chain2 = (*p);
      p = nskip(p, 2);
      p = ncopy(cc, p, 4);

      if(!sscanf(cc, "%d", &ss_resv2))
        ss_valid = false;
      ss_inscode2 = makeInscode(*p);

      if(ss_valid) {
        PRINTFB(G, FB_ObjectMolecule, FB_Blather)
          " ObjectMolecule: read HELIX %c %d%.1s %c %d%.1s\n",
          ss_chain1, ss_resv1, &ss_inscode1, ss_chain2, ss_resv2, &ss_inscode2 ENDFB(G);
        SSCode = 'H';
      }

      if(ss_chain1 == ' ')
        ss_chain1 = 0;
      if(ss_chain2 == ' ')
        ss_chain2 = 0;

    } else if(strstartswith(p, "SHEET ")) {
      ss_valid = true;
      p = nskip(p, 21);
      ss_chain1 = (*p);
      p = nskip(p, 1);
      p = ncopy(cc, p, 4);
      if(!sscanf(cc, "%d", &ss_resv1))
        ss_valid = false;
      ss_inscode1 = makeInscode(*p);
      p = nskip(p, 6);
      ss_chain2 = (*p);
      p = nskip(p, 1);
      p = ncopy(cc, p, 4);
      if(!sscanf(cc, "%d", &ss_resv2))
        ss_valid = false;
      ss_inscode2 = makeInscode(*p);

      if(ss_valid) {
        PRINTFB(G, FB_ObjectMolecule, FB_Blather)
          " ObjectMolecule: read SHEET %c %d%.1s %c %d%.1s\n",
          ss_chain1, ss_resv1, &ss_inscode1, ss_chain2, ss_resv2, &ss_inscode2 ENDFB(G);
        SSCode = 'S';
      }

      if(ss_chain1 == ' ')
        ss_chain1 = 0;
      if(ss_chain2 == ' ')
        ss_chain2 = 0;

    } else if(strstartswith(p, "ENDMDL")) {
      if(*restart_model)
        in_model = false;
      else {
        *restart_model = nextline(p);
        in_model = false;
        if(only_read_one_model) {
          const char *pp;
          pp = nextline(p);
          if(strstartswith(pp, "END")) {   /* END we're going to be starting a new object... */
            (*next_pdb) = check_next_pdb_object(nextline(pp), true);
            if(info && (info->multiplex == 0)) {
              /* multiplex == 0:  FORCED multimodel behavior with concatenated PDB files */
              (*restart_model) = (*next_pdb);
              (*next_pdb) = NULL;
              foundNextModelFlag = true;
              info->multi_object_status = -1;
            } else {
              is_end_of_object = true;
            }
          } else if(strstartswith(pp, "MODEL")) {   /* not a new object...just a new state (model) */
            if(info && (info->multiplex > 0)) { /* end object if we're multiplexing */
              (*next_pdb) = check_next_pdb_object(pp, true);
              (*restart_model) = NULL;
            } else
              is_end_of_object = false;
          } else {
            if(pp[0] > 32)      /* more content follows... */
              (*next_pdb) = check_next_pdb_object(pp, true);
            else
              (*next_pdb) = NULL;       /* at end of file */
          }
          break;
        }
      }
    } else if(strstartswith(p, "END")) {
      ntrim(cc, p, 6);
      if((strcmp("END", cc) == 0) && (!in_model)) {     /* stop parsing here... */
        const char *pp;
        pp = nextline(p);       /* ...unless we're in MODEL or next line is itself ENDMDL */
        p = ncopy(cc, p, 6);
        if(!((cc[0] == 'E') && (cc[1] == 'N') && (cc[2] == 'D') && (cc[3] == 'M') && (cc[4] == 'D') && (cc[5] == 'L'))) {       /* NOTE: this test seems unnecessary given strcmp above... */
          if(!*next_pdb) {
            (*next_pdb) = check_next_pdb_object(pp, false);
          }
          if((*next_pdb) && info && (!info->multiplex) && !(*restart_model)) {
            /* multiplex == 0:  FORCED multimodel behavior with concatenated PDB files */
            (*restart_model) = (*next_pdb);
            (*next_pdb) = NULL;
            foundNextModelFlag = true;
            info->multi_object_status = -1;
            is_end_of_object = false;
            break;
          }
          if(*next_pdb) {       /* we've found another object... */
            if(*restart_model)
              is_end_of_object = false; /* however, if we're parsing multi-models, then we're not yet at the end */
            else
              is_end_of_object = true;
            break;
          } else if(!seen_model)
            break;
        }
      }
    } else if(strstartswith(p, "CRYST1") && (!*restart_model)) {
      if(!symmetry){
        symmetry.reset(new CSymmetry(G));
	CHECKOK(ok, symmetry);
      }
      if(symmetry) {
        int symFlag = true;
        PRINTFB(G, FB_ObjectMolecule, FB_Blather)
          " PDBStrToCoordSet: Attempting to read symmetry information\n" ENDFB(G);
        p = nskip(p, 6);
        symFlag = true;
        float cellparams[3];
        p = ncopy(cc, p, 9);
        if(sscanf(cc, "%f", cellparams + 0) != 1)
          symFlag = false;
        p = ncopy(cc, p, 9);
        if(sscanf(cc, "%f", cellparams + 1) != 1)
          symFlag = false;
        p = ncopy(cc, p, 9);
        if(sscanf(cc, "%f", cellparams + 2) != 1)
          symFlag = false;

        symmetry->Crystal.setDims(cellparams);

        p = ncopy(cc, p, 7);
        if(sscanf(cc, "%f", cellparams + 0) != 1)
          symFlag = false;
        p = ncopy(cc, p, 7);
        if(sscanf(cc, "%f", cellparams + 1) != 1)
          symFlag = false;
        p = ncopy(cc, p, 7);
        if(sscanf(cc, "%f", cellparams + 2) != 1)
          symFlag = false;

        symmetry->Crystal.setAngles(cellparams);

        p = nskip(p, 1);
        p = ncopy(cc, p, 11);
        UtilCleanStr(cc);
        symmetry->setSpaceGroup(cc);
        p = ncopy(cc, p, 3);
        if(sscanf(cc, "%d", &symmetry->PDBZValue) != 1)
          symmetry->PDBZValue = 1;
        if(!symFlag) {
          ErrMessage(G, "PDBStrToCoordSet", "Error reading CRYST1 record\n");
          symmetry.reset();
        }
      }
    } else if(strstartswith(p, "SCALE") && (!*restart_model) && info) { /* SCALEn */
      switch (p[5]) {
      case '1':
      case '2':
      case '3':
        {
          int flag = (p[5] - '1');
          int offset = flag * 4;
          int scale_flag = true;
          p = nskip(p, 10);
          p = ncopy(cc, p, 10);
          if(sscanf(cc, "%f", &info->scale.matrix[offset]) != 1)
            scale_flag = false;
          p = ncopy(cc, p, 10);
          if(sscanf(cc, "%f", &info->scale.matrix[offset + 1]) != 1)
            scale_flag = false;
          p = ncopy(cc, p, 10);
          if(sscanf(cc, "%f", &info->scale.matrix[offset + 2]) != 1)
            scale_flag = false;
          p = nskip(p, 5);
          p = ncopy(cc, p, 10);
          if(sscanf(cc, "%f", &info->scale.matrix[offset + 3]) != 1)
            scale_flag = false;
          if(scale_flag)
            info->scale.flag[flag] = true;
          PRINTFB(G, FB_ObjectMolecule, FB_Blather)
            " PDBStrToCoordSet: SCALE%d %8.4f %8.4f %8.4f %8.4f\n", flag + 1,
            info->scale.matrix[offset],
            info->scale.matrix[offset + 1],
            info->scale.matrix[offset + 2], info->scale.matrix[offset + 3]
            ENDFB(G);
        }
        break;
      }
    } else if(strstartswith(p, "CONECT") &&
              bondFlag && (!ignore_conect) && ((!*restart_model) || (!in_model))) {
      p = nskip(p, 6);
      p = ncopy(cc, p, 5);
      if(sscanf(cc, "%d", &b1) == 1)
        while(1) {
          p = ncopy(cc, p, 5);
          if(sscanf(cc, "%d", &b2) != 1)
            break;
          else {
            if((b1 >= 0) && (b2 >= 0) && (b1 != b2)) {  /* IDs must be positive and distinct */
              VLACheck(bond, BondType, nBond);
	      CHECKOK(ok, bond);
	      if (ok){
		if(b1 <= b2) {
		  bond[nBond].index[0] = b1;      /* temporarily store the atom indexes */
		  bond[nBond].index[1] = b2;
		  bond[nBond].order = 1;
		} else {
		  bond[nBond].index[0] = b2;
		  bond[nBond].index[1] = b1;
		  bond[nBond].order = 1;
		}
		nBond++;
	      }
            }
          }
        }
    } else if(strstartswith(p, "USER") && (!*restart_model)) {
    } else if(strstartswith(p, "ANISOU") && (!*restart_model) && (atomCount)) {
      ai = atInfo + atomCount - 1;

      /* TODO: check atom identifier match */

      {
        int dummy;
        p = nskip(p, 6);
        p = ncopy(cc, p, 5);
        if(!sscanf(cc, "%d", &dummy))
          dummy = 0;
        if(dummy == ai->id) {   /* ATOM ID must match */
            int dummy;
          float * anisou = ai->get_anisou();
          p = nskip(p, 17);
          for (int i = 0; i < 6; ++i) {
            p = ncopy(cc, p, 7);
            if(sscanf(cc, "%d", &dummy))
              anisou[i] = dummy / 10000.0F;
          }
        }
      }
    }

    /* END KEYWORDS */

    /* Secondary structure records */

    if(ok && SSCode) {
      ss_found = sshash_register_rec(ss_hash,
          ss_chain1, ss_resv1, ss_inscode1,
          ss_chain2, ss_resv2, ss_inscode2, SSCode);
    }
    /* Atom records */

    if(ok && AFlag && (!*restart_model)) {
      ai = atInfo + atomCount;
      p = nskip(p, 6);

      ai->rank = atomCount;

      if(info && info->is_pqr_file()) {
        if (parse_pqr_atom_line(G, p, ai, coord + a)) {
          goto pqr_done;
        }
      }

      p = ncopy(cc, p, 5);
      if(!sscanf(cc, "%d", &ai->id))
        ai->id = 0;

      p = nskip(p, 1);          /* to 12 */
      p = ncopy(literal_name, p, 4);
      if(literal_names) {
        LexAssign(G, ai->name, literal_name);
      } else {
        ParseNTrim(cc, literal_name, 4);
        LexAssign(G, ai->name, cc);
      }

      p = ncopy(cc, p, 1);
      if(*cc == 32)
        ai->alt[0] = 0;
      else {
        ai->alt[0] = *cc;
        ai->alt[1] = 0;
      }

      p = ntrim(cc, p, 4); /* now allowing for 4-letter residues */
      if (truncate_resn)        /* unless specifically disabled */
        cc[3] = 0;

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

      if(ai->name) {
        const char * ai_name = LexStr(G, ai->name);
        int name_len = strlen(ai_name);
        char name[5];
        switch (reformat_names) {
        case 1:                /* pdb compliant: HH12 becomes 2HH1, etc. */
          if(name_len > 3) {
            if((ai_name[0] >= 'A') && ((ai_name[0] <= 'Z')) &&
                isdigit(ai_name[3])) {
              if(!(((ai_name[1] >= 'a') && (ai_name[1] <= 'z')) ||
                   ((ai_name[0] == 'C') && (ai_name[1] == 'L')) ||    /* try to be smart about */
                   ((ai_name[0] == 'B') && (ai_name[1] == 'R')) ||    /* distinguishing common atoms */
                   ((ai_name[0] == 'C') && (ai_name[1] == 'A')) ||    /* in all-caps from typical */
                   ((ai_name[0] == 'F') && (ai_name[1] == 'E')) ||    /* nonatomic abbreviations */
                   ((ai_name[0] == 'C') && (ai_name[1] == 'U')) ||
                   ((ai_name[0] == 'N') && (ai_name[1] == 'A')) ||
                   ((ai_name[0] == 'N') && (ai_name[1] == 'I')) ||
                   ((ai_name[0] == 'M') && (ai_name[1] == 'G')) ||
                   ((ai_name[0] == 'M') && (ai_name[1] == 'N')) ||
                   ((ai_name[0] == 'H') && (ai_name[1] == 'G')) ||
                   ((ai_name[0] == 'S') && (ai_name[1] == 'E')) ||
                   ((ai_name[0] == 'S') && (ai_name[1] == 'I')) ||
                   ((ai_name[0] == 'Z') && (ai_name[1] == 'N'))
                 )) {
                strncpy(name + 1, ai_name, 3);
                name[0] = ai_name[3];
                name[4] = 0;
                LexAssign(G, ai->name, name);
              }
            }
          } else if(name_len == 3) {
            if((ai_name[0] == 'H') &&
               (ai_name[1] >= 'A') && ((ai_name[1] <= 'Z')) &&
               isdigit(ai_name[2])) {
              AtomInfoGetPDB3LetHydroName(G, LexStr(G, ai->resn), ai_name, name);
              LexAssign(G, ai->name, (name[0] == ' ') ? (name + 1) : name);
            }
          }
          break;
        case 2:                /* amber compliant: 2HH1 becomes HH12 */
        case 3:                /* pdb compliant, but use IUPAC within PyMOL */
          if(ai_name[0]) {
            if(isdigit(ai_name[0]) && ai_name[1] && (!isdigit(ai_name[1]))) {
              if (1 < name_len && name_len < 5) {
                strcpy(name, ai_name + 1);
                name[name_len - 1] = ai_name[0];
                name[name_len] = 0;
                LexAssign(G, ai->name, name);
              }
              break;
        default:               /* AS IS */
              break;
            }
          }
          break;
        case 4:                /* simply read trim and write back out with 3-letter names starting from the
                                   second column, and four-letter names starting in the first */
          ntrim(cc, ai_name, 4);
          LexAssign(G, ai->name, cc);
          break;
        }
      }

      p = ncopy(cc, p, 1);
      if (ai->chain){
        LexDec(G, ai->chain);
      }
      if(*cc == ' ') {
        ss_chain1 = 0;
        ai->chain = 0;
      } else {
        ss_chain1 = *cc;
        ai->chain = LexIdx(G, cc);
      }

      p = ncopy(cc, p, 4);
      if(!sscanf(cc, "%d", &ai->resv))
        ai->resv = 0;
      ai->setInscode(*p);
      p = nskip(p, 1);

      if(ssFlag) {              /* get secondary structure information (if avail) */
        sshash_lookup(ss_hash, ai, ss_chain1);
      } else {
        ai->cartoon = cCartoon_tube;
      }

      {
        p = nskip(p, 3);
        p = ncopy(cc, p, 8);
        sscanf(cc, "%f", coord + a);
        p = ncopy(cc, p, 8);
        sscanf(cc, "%f", coord + (a + 1));
        p = ncopy(cc, p, 8);
        sscanf(cc, "%f", coord + (a + 2));
      }

      if((!info) || (!info->is_pqr_file())) {     /* standard PDB file */
        p = ncopy(cc, p, 6);
        if(!sscanf(cc, "%f", &ai->q))
          ai->q = 1.0;

        p = ncopy(cc, p, 6);
        if(!sscanf(cc, "%f", &ai->b))
          ai->b = 0.0;

        if (info->variant == PDB_VARIANT_PDBQT) {
          ignore_pdb_segi = true;
          p = nskip(p, 4);
          p = ncopy(cc, p, 6);
          if(!sscanf(cc, "%f", &ai->partialCharge))
            ai->partialCharge = 0.0;

          // type is 78-79 in pdbqt, 77-78 in pdb
          p = nskip(p, 1);
        } else {
          p = nskip(p, 6);
          p = ncopy(cc, p, 4);
        }

        if(!ignore_pdb_segi) {
          if(!segi_override_idx) {
            if(cc[3] == '1' && atomCount && strncmp(p, "0000", 4) == 0) {
              /* atom ID overflow? (nonstandard use...)... */
              LexAssign(G, segi_override_idx, (ai - 1)->segi);
              LexAssign(G, ai->segi,          (ai - 1)->segi);
            } else {
              UtilCleanStr(cc);
              LexAssign(G, ai->segi, cc);
            }
          } else {
            LexAssign(G, ai->segi, segi_override_idx);
          }
        } else {
          LexAssign(G, ai->segi, 0);
        }

        p = ncopy(cc, p, 2);
        if(!sscanf(cc, "%s", ai->elem))
          ai->elem[0] = 0;
        else if(!((((ai->elem[0] >= 'a') && (ai->elem[0] <= 'z')) ||    /* don't get confused by PDB misuse */
                   ((ai->elem[0] >= 'A') && (ai->elem[0] <= 'Z'))) &&
                  (((ai->elem[1] == 0) ||
                    ((ai->elem[1] >= 'a') && (ai->elem[1] <= 'z')) ||
                    ((ai->elem[1] >= 'A') && (ai->elem[1] <= 'Z'))))))
          ai->elem[0] = 0;
        else if (info->variant == PDB_VARIANT_PDBQT) {
          if (strcmp(ai->elem, "A") == 0) {
            // aromatic carbon
            ai->elem[0] = 'C';
          } else if (isupper(ai->elem[1])) {
            // h-bond donor or acceptor
            ai->elem[1] = 0;
          }
        }

        if(!ai->elem[0]) {
          if(((literal_name[0] == ' ') || ((literal_name[0] >= '0') && (literal_name[0] <= '9'))) && (literal_name[1] >= 'A') && (literal_name[1] <= 'Z')) {    /* infer element from name column */
            ai->elem[0] = literal_name[1];
            ai->elem[1] = 0;
          } else if(((literal_name[0] >= 'A') && (literal_name[0] <= 'Z')) && (((literal_name[1] >= 'A') && (literal_name[1] <= 'Z')) || ((literal_name[1] >= 'a') && (literal_name[1] <= 'z')))) {     /* infer element from name column */
            ai->elem[0] = literal_name[0];
            ai->elem[2] = 0;
            if((literal_name[1] >= 'A') && (literal_name[1] <= 'Z')) {  /* second letter is capitalized */
              if(bogus_name_alignment) {
                /* if other atom names aren't properly aligned */
                ai->elem[1] = 0;        /* kill 2nd letter */
              } else if(literal_name[0] == 'H') {
                /* or if this is an ultra-bogus PDB with inconsistent 
                   indendentation, and this is likely a hydrogen */
                ai->elem[1] = 0;        /* kill 2nd letter */
              } else {
                ai->elem[1] = tolower(literal_name[1]);
              }
            } else
              ai->elem[1] = literal_name[1];
          }
        }

        p = ncopy(cc, p, 2);
        if((cc[1] == '-') || (cc[1] == '+')) {
          /* only read formal charge when sign is present */
          char ctmp = cc[0];
          cc[0] = cc[1];
          cc[1] = ctmp;
          if(!sscanf(cc, "%hhi", &ai->formalCharge))
            ai->formalCharge = 0;
        }

        /* end normal PDB */
      } else if(info && info->is_pqr_file()) {
        p = ParseWordNumberCopy(cc, p, MAXLINELEN - 1);
        if(!sscanf(cc, "%f", &ai->partialCharge))
          ai->partialCharge = 0.0F;

        p = ParseWordNumberCopy(cc, p, MAXLINELEN - 1);
        if(sscanf(cc, "%f", &ai->elec_radius) != 1)
          ai->elec_radius = 0.0F;
      }

pqr_done:

      ai->visRep = auto_show;

      if(AFlag == 1)
        ai->hetatm = 0;
      else {
        ai->hetatm = 1;
        ai->flags = cAtomFlag_ignore;
      }

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

      PRINTFD(G, FB_ObjectMolecule)
        "%s %s %d%c %s %8.3f %8.3f %8.3f %6.2f %6.2f %s\n",
        LexStr(G, ai->name), LexStr(G, ai->resn), ai->resv, ai->getInscode(true), LexStr(G, ai->chain),
        *(coord + a), *(coord + a + 1), *(coord + a + 2), ai->b, ai->q, LexStr(G, ai->segi) ENDFD;

      if(atomCount < nAtom) {     /* safety */
        a += 3;
        atomCount++;
      }
    }
    p = nextline(p);
  }

  /* END PASS 2 */

  if(ok && bondFlag) {
    UtilSortInPlace(G, bond, nBond, sizeof(BondType), (UtilOrderFn *) BondInOrder);
    if(nBond) {
      if(!have_bond_order) {    /* handle PDB bond-order kludge */
        ii1 = bond;
        ii2 = bond + 1;
        nReal = 1;
        ii1->order = 1;
        a = nBond - 1;
        while(a) {
          if((ii1->index[0] == ii2->index[0]) && (ii1->index[1] == ii2->index[1])) {
            ii1->order++;       /* count dup */
          } else {
            ii1++;              /* non-dup, make copy */
            ii1->index[0] = ii2->index[0];
            ii1->index[1] = ii2->index[1];
            ii1->order = ii2->order;
            nReal++;
          }
          ii2++;
          a--;
        }
        nBond = nReal;
      }
      /* now, find atoms we're looking for */

      /* determine ranges */
      maxAt = nAtom;
      ii1 = bond;
      for(a = 0; a < nBond; a++) {
        if(ii1->index[0] > maxAt)
          maxAt = ii1->index[0];
        if(ii1->index[1] > maxAt)
          maxAt = ii1->index[1];
        ii1++;
      }
      for(a = 0; a < nAtom; a++)
        if(maxAt < atInfo[a].id)
          maxAt = atInfo[a].id;
      /* build index */
      maxAt++;
      idx = pymol::malloc<int>(maxAt + 1);
      CHECKOK(ok, idx);
      if (ok){
	for(a = 0; a < maxAt; a++) {
	  idx[a] = -1;
	}
	for(a = 0; a < nAtom; a++)
	  idx[atInfo[a].id] = a;

	/* convert indices to bonds */
	ii1 = bond;
	ii2 = bond;
	nReal = 0;
      }
      if (ok) {
        int unbond_cations = SettingGetGlobal_i(G, cSetting_pdb_unbond_cations);
        int flag;

        for(a = 0; a < nBond; a++) {

          if((ii1->index[0] >= 0) && ((ii1->index[1]) >= 0)) {
            if((idx[ii1->index[0]] >= 0) && (idx[ii1->index[1]] >= 0)) {        /* in case PDB file has bad bonds */
              ii2->index[0] = idx[ii1->index[0]];
              ii2->index[1] = idx[ii1->index[1]];
              ii2->order = ii1->order;
              if((ii2->index[0] >= 0) && (ii2->index[1] >= 0)) {

                if(!have_bond_order) {  /* handle PDB bond order kludge */
                  if(ii1->order <= 2)
                    ii2->order = 1;
                  else if(ii1->order <= 4)
                    ii2->order = 2;
                  else if(ii1->order <= 6)
                    ii2->order = 3;
                  else
                    ii2->order = 4;
                }
                flag = true;
                if(unbond_cations) {
                  if(AtomInfoIsFreeCation(G, atInfo + ii2->index[0]))
                    flag = false;
                  else if(AtomInfoIsFreeCation(G, atInfo + ii2->index[1]))
                    flag = false;
                }
                if(flag) {
                  atInfo[ii2->index[0]].bonded = true;
                  atInfo[ii2->index[1]].bonded = true;
                  nReal++;
                  ii2++;
                }
              }
            }
          }
          ii1++;
        }
      }
      nBond = nReal;
      FreeP(idx);
    }
  }
  if(ss_found && !quiet) {
    PRINTFB(G, FB_ObjectMolecule, FB_Details)
      " ObjectMolecule: Read secondary structure assignments.\n" ENDFB(G);
  }
  if(symmetry && !quiet && (!only_read_one_model)) {
    PRINTFB(G, FB_ObjectMolecule, FB_Details)
      " ObjectMolecule: Read crystal symmetry information.\n" ENDFB(G);
  }
  PRINTFB(G, FB_ObjectMolecule, FB_Blather)
    " PDBStr2CoordSet: Read %d bonds from CONECT records (%p).\n", nBond,
    (void *) bond ENDFB(G);

  if (ok){
    cset = CoordSetNew(G);
    CHECKOK(ok, cset);

    cset->NIndex = nAtom;
    cset->Coord = pymol::vla_take_ownership(coord);
    cset->TmpBond = pymol::vla_take_ownership(bond);
    cset->NTmpBond = nBond;
    cset->Symmetry = std::move(symmetry);
    if(atInfoPtr)
      *atInfoPtr = atInfo;
    
    if((*restart_model) && (!foundNextModelFlag)) {
      /* if plan on need to reading another model into this object, 
	 make sure there is another model to read... */
      p = *restart_model;
      while(*p) {
	if(strstartswith(p, "HEADER")) {
	  /* seeing HEADER means we're off the end of the existing file */
	  break;
	} else if(strstartswith(p, "MODEL ") ||
	          strstartswith(p, "ENDMDL")) {
	  foundNextModelFlag = true;
	  break;
	}
	p = nextline(p);
      }
      if(!foundNextModelFlag) {
	*restart_model = NULL;
      }
    }
  }
  if (!ok){
    if (cset){
      delete cset;
      cset = NULL;
    } else {
      VLAFreeP(coord);
      VLAFreeP(bond);
    }
  }
  sshash_free(ss_hash);
  
  if (ok){
    if(!seen_model)
      *model_number = 1;
    
    if((*restart_model) && (*next_pdb)) { /* if we're mixing multistate objects and
					     trajectories, then enforce sanity by 
					     reading the models first... */
      if(is_end_of_object)
	(*restart_model) = NULL;
      else if((*next_pdb) < (*restart_model))
	(*next_pdb) = NULL;
    }
  }

  LexDec(G, segi_override_idx);
  return (cset);
}


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

void ObjectMoleculeInitHBondCriteria(PyMOLGlobals * G, HBondCriteria * hbc)
{
  hbc->maxAngle = SettingGet_f(G, NULL, NULL, cSetting_h_bond_max_angle);
  hbc->maxDistAtMaxAngle = SettingGet_f(G, NULL, NULL, cSetting_h_bond_cutoff_edge);
  hbc->maxDistAtZero = SettingGet_f(G, NULL, NULL, cSetting_h_bond_cutoff_center);
  hbc->power_a = SettingGet_f(G, NULL, NULL, cSetting_h_bond_power_a);
  hbc->power_b = SettingGet_f(G, NULL, NULL, cSetting_h_bond_power_b);
  hbc->cone_dangle =
    (float) cos(PI * 0.5 * SettingGet_f(G, NULL, NULL, cSetting_h_bond_cone) / 180.0F);
  if(hbc->maxDistAtMaxAngle != 0.0F) {
    hbc->factor_a = (float) (0.5 / pow(hbc->maxAngle, hbc->power_a));
    hbc->factor_b = (float) (0.5 / pow(hbc->maxAngle, hbc->power_b));
  }
}

static int ObjectMoleculeTestHBond(float *donToAcc, float *donToH, float *hToAcc,
                                   float *accPlane, HBondCriteria * hbc)
{
  float nDonToAcc[3], nDonToH[3], nAccPlane[3], nHToAcc[3];
  double angle;
  double cutoff;
  double curve;
  double dist;
  float dangle;

/* A ~~ H - D */

  normalize23f(donToAcc, nDonToAcc);
  normalize23f(hToAcc, nHToAcc);
  if(accPlane) {                /* remember, plane need not exist if it's water... */
    normalize23f(accPlane, nAccPlane);
    if(dot_product3f(nHToAcc, nAccPlane) > (-hbc->cone_dangle)) /* don't allow H behind Acceptor plane */
      return 0;
  }

  normalize23f(donToH, nDonToH);
  normalize23f(donToAcc, nDonToAcc);

  dangle = dot_product3f(nDonToH, nDonToAcc);
  if((dangle < 1.0F) && (dangle > 0.0F))
    angle = 180.0 * acos((double) dangle) / PI;
  else if(dangle > 0.0F)
    angle = 0.0;
  else
    angle = 90.0;

  if(angle > hbc->maxAngle)
    return 0;

  /* interpolate cutoff based on ADH angle */

  if(hbc->maxDistAtMaxAngle != 0.0F) {
    curve = (pow(angle, (double) hbc->power_a) * hbc->factor_a +
             pow(angle, (double) hbc->power_b) * hbc->factor_b);

    cutoff = (hbc->maxDistAtMaxAngle * curve) + (hbc->maxDistAtZero * (1.0 - curve));
  } else {
    cutoff = hbc->maxDistAtZero;
  }

  /*
     printf("angle %8.3f curve %8.3f %8.3f %8.3f %8.3f\n",angle,
     curve,cutoff,hbc->maxDistAtMaxAngle,hbc->maxDistAtZero);
   */

  dist = length3f(donToAcc);

  if(dist > cutoff)
    return 0;
  else
    return 1;

}


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

static int ObjectMoleculeFindBestDonorH(ObjectMolecule * I,
                                        int atom,
                                        int state, float *dir, float *best, 
                                        AtomInfoType **h_real)
{
  int result = 0;
  CoordSet *cs;
  float cand[3], cand_dir[3];
  float best_dot = 0.0F, cand_dot;

  if((state >= 0) && (state < I->NCSet) && (cs = I->CSet[state]) && (atom < I->NAtom)) {

    auto idx = cs->atmToIdx(atom);

    if(idx >= 0) {

      const float* orig = cs->coordPtr(idx);

      /*  do we need to add any new hydrogens? */

      auto const neighbors = AtomNeighbors(I, atom);
      int const nn = neighbors.size();

      if((nn < I->AtomInfo[atom].valence) || I->AtomInfo[atom].hb_donor) {      /* is there an implicit hydrogen? */
        if (CoordSetFindOpenValenceVector(cs, atom, best, dir)) {
          result = true;
          best_dot = dot_product3f(best, dir);
          add3f(orig, best, best);
          if(h_real)
            *h_real = NULL;
        }
      }
      /* iterate through real hydrogens looking for best match
         with desired direction */

      /* look for an attached non-hydrogen as a base */
      for (int i = 0; i < nn; ++i) {
        int const a1 = neighbors[i].atm;
        if(I->AtomInfo[a1].protons == 1) {      /* hydrogen */
          if(ObjectMoleculeGetAtomVertex(I, state, a1, cand)) { /* present */

            subtract3f(cand, orig, cand_dir);
            normalize3f(cand_dir);
            cand_dot = dot_product3f(cand_dir, dir);
            if(result) {        /* improved */
              if((best_dot < cand_dot) || (h_real && !*h_real)) {
                best_dot = cand_dot;
                copy3f(cand, best);
                if(h_real)
                  *h_real = I->AtomInfo + a1;
              }
            } else {            /* first */
              result = true;
              copy3f(cand, best);
              best_dot = cand_dot;
              if(h_real)
                *h_real = I->AtomInfo + a1;
            }
          }
        }
      }
    }
  }
  return result;
}


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

int ObjectMoleculeGetCheckHBond(AtomInfoType **h_real,
                                float *h_crd_ret,
                                ObjectMolecule * don_obj,
                                int don_atom,
                                int don_state,
                                ObjectMolecule * acc_obj,
                                int acc_atom, 
				int acc_state, 
				HBondCriteria * hbc)
{
  int result = 0;
  const CoordSet *csD, *csA;
  float donToAcc[3];
  float donToH[3];
  float bestH[3];
  float hToAcc[3];
  float accPlane[3], *vAccPlane = NULL;

  /* first, check for existence of coordinate sets */

  if((don_state >= 0) &&
     (don_state < don_obj->NCSet) &&
     (csD = don_obj->CSet[don_state]) &&
     (acc_state >= 0) &&
     (acc_state < acc_obj->NCSet) &&
     (csA = acc_obj->CSet[acc_state]) &&
     (don_atom < don_obj->NAtom) && (acc_atom < acc_obj->NAtom)) {

    /* now check for coordinates of these actual atoms */

    auto idxD = csD->atmToIdx(don_atom);
    auto idxA = csA->atmToIdx(acc_atom);

    if((idxA >= 0) && (idxD >= 0)) {

      /* now get local geometries, including 
         real or virtual hydrogen atom positions */

      const float* vDon = csD->coordPtr(idxD);
      const float* vAcc = csA->coordPtr(idxA);

      subtract3f(vAcc, vDon, donToAcc);

      if(ObjectMoleculeFindBestDonorH(don_obj,
                                      don_atom, don_state, donToAcc, bestH, h_real)) {

        subtract3f(bestH, vDon, donToH);
        subtract3f(vAcc, bestH, hToAcc);

        if(ObjectMoleculeGetAvgHBondVector(acc_obj, acc_atom,
                                           acc_state, accPlane, hToAcc) > 0.1) {
          vAccPlane = &accPlane[0];
        }
        result = ObjectMoleculeTestHBond(donToAcc, donToH, hToAcc, vAccPlane, hbc);
        if(result && h_crd_ret && h_real && *h_real)
          copy3f(bestH, h_crd_ret);
      }
    }
  }

  return (result);
}


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

float ObjectMoleculeGetMaxVDW(ObjectMolecule * I)
{
  float max_vdw = 0.0F;
  int a;
  const AtomInfoType *ai;
  if(I->NAtom) {
    ai = I->AtomInfo;
    for(a = 0; a < I->NAtom; a++) {
      if(max_vdw < ai->vdw)
        max_vdw = ai->vdw;
      ai++;
    }
  }
  return (max_vdw);
}


/*========================================================================*/
static PyObject *ObjectMoleculeCSetAsPyList(ObjectMolecule * I)
{
  PyObject *result = NULL;
  int a;
  result = PyList_New(I->NCSet);
  for(a = 0; a < I->NCSet; a++) {
    if(I->CSet[a]) {
      PyList_SetItem(result, a, CoordSetAsPyList(I->CSet[a]));
    } else {
      PyList_SetItem(result, a, PConvAutoNone(Py_None));
    }
  }
  return (PConvAutoNone(result));
}


/*static PyObject *ObjectMoleculeDiscreteCSetAsPyList(ObjectMolecule *I)
  {
  PyObject *result = NULL;
  return(PConvAutoNone(result));
  }*/
static int ObjectMoleculeCSetFromPyList(ObjectMolecule * I, PyObject * list)
{
  int ok = true;
  int a;
  if(ok)
    ok = PyList_Check(list);
  if(ok) {
    VLACheck(I->CSet, CoordSet *, I->NCSet);
    for(a = 0; a < I->NCSet; a++) {
      if(ok)
        ok = CoordSetFromPyList(I->G, PyList_GetItem(list, a), &I->CSet[a]);
      PRINTFB(I->G, FB_ObjectMolecule, FB_Debugging)
        " %s: ok %d after CoordSet %d\n", __func__, ok, a ENDFB(I->G);

      if(ok)
        if(I->CSet[a])          /* WLD 030205 */
          I->CSet[a]->Obj = I;
    }
  }
  return (ok);
}

static PyObject *ObjectMoleculeBondAsPyList(ObjectMolecule * I)
{
  PyObject *result = NULL;
  PyObject *bond_list;
  const BondType *bond;
  int a;

#ifndef PICKLETOOLS
  PyMOLGlobals *G = I->G;
  int pse_export_version = SettingGetGlobal_f(I->G, cSetting_pse_export_version) * 1000;

  if (SettingGetGlobal_b(G, cSetting_pse_binary_dump) && (!pse_export_version || pse_export_version >= 1765)){
    /* For the pse_binary_dump, save entire Bond array to a binary string array
     */

    // supported versions
    auto version = (!pse_export_version || pse_export_version >= 1810) ?
      181 : (pse_export_version >= 1770) ? 177 : 176;

    auto blob = Copy_To_BondType_Version(version, I->Bond.data(), I->NBond);
    auto blobsize = VLAGetByteSize(blob);

    result = PyList_New(2);
    PyList_SetItem(result, 0, PyInt_FromLong(version));
    PyList_SetItem(result, 1, PyBytes_FromStringAndSize(reinterpret_cast<const char*>(blob), blobsize));

    VLAFreeP(blob);

    return result;
  }
#endif
  result = PyList_New(I->NBond);
  bond = I->Bond;
  for(a = 0; a < I->NBond; a++) {
    size_t const list_size = bond->hasSymOp() ? 8 : 7;
    bond_list = PyList_New(list_size);
    PyList_SetItem(bond_list, 0, PyInt_FromLong(bond->index[0]));
    PyList_SetItem(bond_list, 1, PyInt_FromLong(bond->index[1]));
    PyList_SetItem(bond_list, 2, PyInt_FromLong(bond->order));
    PyList_SetItem(bond_list, 3, PyInt_FromLong(-1)); // id
    PyList_SetItem(bond_list, 4, PyInt_FromLong(0));  // stereo
    PyList_SetItem(bond_list, 5, PyInt_FromLong(bond->unique_id));
    PyList_SetItem(bond_list, 6, PyInt_FromLong(bond->has_setting));
    if (list_size > 7) {
      PyList_SetItem(bond_list, 7, PConvToPyObject(bond->symop_2));
    }
    PyList_SetItem(result, a, bond_list);
    bond++;
  }

  return (PConvAutoNone(result));
}

static int ObjectMoleculeBondFromPyList(ObjectMolecule * I, PyObject * list)
{
  PyMOLGlobals *G = I->G;
  int ok = true;
  int a;
  int stereo, ll = 0;
  PyObject *bond_list = NULL;
  BondType *bond;

  if(ok)
    ok = PyList_Check(list);
  if(ok)
    ll = PyList_Size(list);

  bool pse_binary_dump = false;

  if (ll >= 2) {
    // checking if from pse_binary_dump
    // pse_binary_dump saves 2 values: bondInfo_version, BondType binary
    CPythonVal *val1 = CPythonVal_PyList_GetItem(G, list, 1);
    pse_binary_dump = PyBytes_Check(val1);
    CPythonVal_Free(val1);
  }
  if (pse_binary_dump){
    CPythonVal *verobj = CPythonVal_PyList_GetItem(G, list, 0);
    int bondInfo_version;
    ok = PConvPyIntToInt(verobj, &bondInfo_version);

    CPythonVal *strobj = CPythonVal_PyList_GetItem(G, list, 1);
    auto strval = PyBytes_AsSomeString(strobj);

    if(ok)
      ok = bool((I->Bond = pymol::vla<BondType>(I->NBond)));

    Copy_Into_BondType_From_Version(strval.data(), bondInfo_version, I->Bond.data(), I->NBond);

    CPythonVal_Free(verobj);
    CPythonVal_Free(strobj);
  } else {
    if(ok)
      ok = bool((I->Bond = pymol::vla<BondType>(I->NBond)));
    bond = I->Bond.data();
  for(a = 0; a < I->NBond; a++) {
    bond_list = NULL;
    if(ok)
      bond_list = PyList_GetItem(list, a);
    if(ok)
      ok = PyList_Check(bond_list);
    if(ok)
      ll = PyList_Size(bond_list);
    if(ok)
      ok = PConvPyIntToInt(PyList_GetItem(bond_list, 0), &bond->index[0]);
    if(ok)
      ok = PConvPyIntToInt(PyList_GetItem(bond_list, 1), &bond->index[1]);
    if(ok)
      if((ok = CPythonVal_PConvPyIntToInt_From_List(I->G, bond_list, 2, &stereo)))
        bond->order = stereo;
    if(ok && (ll > 5)) {
      int has_setting;
      if(ok)
        ok = PConvPyIntToInt(PyList_GetItem(bond_list, 5), &bond->unique_id);
      if(ok)
        ok = PConvPyIntToInt(PyList_GetItem(bond_list, 6), &has_setting);
      if(ok)
        bond->has_setting = (short int) has_setting;
      if(ok && bond->unique_id) {       /* reserve existing IDs */
        bond->unique_id = SettingUniqueConvertOldSessionID(G, bond->unique_id);
      }
    }
      if (ll > 7) {
        PConvFromPyListItem(G, bond_list, 7, bond->symop_2);
      }
    bond++;
      CPythonVal_Free(bond_list);
  }
  }
  PRINTFB(G, FB_ObjectMolecule, FB_Debugging)
    " %s: ok %d after restore\n", __func__, ok ENDFB(G);

  return (ok);
}

#ifdef _PYMOL_IP_PROPERTIES
/**
 * Extract an atom property "column" as a Python list.
 *
 * As an optimization, trailing None values are removed, so the returned list
 * may be shorter than the atom array or even be empty.
 *
 * @param mol Object molecule which provides the atom array
 * @param func Function which extracts a property from an atom
 * @return List of properties
 */
static PyObject* AtomColumnAsPyList(const ObjectMolecule& mol,
    std::function<PyObject*(const AtomInfoType&)> func)
{
  auto list = PyList_New(mol.NAtom);
  PyObject* prev = Py_None;
  int pruned_size = 0;

  for (int i = 0; i < mol.NAtom; ++i) {
    PyObject* value = func(mol.AtomInfo[i]);

#ifndef PICKLETOOLS
    // Simple optimization: Repeated property lists will reference the same
    // Python object
    if (prev != value && PyObject_RichCompareBool(prev, value, Py_EQ)) {
      Py_INCREF(prev);
      Py_DECREF(value);
      value = prev;
    } else {
      prev = value;
    }

    if (value != Py_None) {
      pruned_size = i + 1;
    }
#endif

    PyList_SetItem(list, i, value);
  }

#ifndef PICKLETOOLS
  assert(pruned_size <= mol.NAtom);

  // Simple optimization: Prune "None" tail
  PyList_SetSlice(list, pruned_size, mol.NAtom, nullptr);

  assert(PyList_Size(list) == pruned_size);
#endif

  return list;
}
#endif

#ifdef _PYMOL_IP_PROPERTIES
/**
 * Restore an atom property "column" from a Python list.
 * @param mol Object molecule to update atoms in
 * @param list Property list (may be shorter than atom array)
 * @param func Function which sets a property for an atom
 */
static void AtomColumnFromPyList(ObjectMolecule& mol, PyObject* list,
    std::function<void(AtomInfoType&, PyObject*)> func)
{
  if (!list || !PyList_Check(list)) {
    return;
  }

  int size = PyList_Size(list);
  assert(size <= mol.NAtom);

  for (int i = 0; i < size; ++i) {
    func(mol.AtomInfo[i], PyList_GetItem(list, i));
  }
}
#endif

static PyObject *ObjectMoleculeAtomAsPyList(ObjectMolecule * I)
{
  PyMOLGlobals *G = I->G;
  PyObject *result = NULL;
  const AtomInfoType *ai;
  int a;
#ifndef PICKLETOOLS
  int pse_export_version = SettingGetGlobal_f(I->G, cSetting_pse_export_version) * 1000;

  if (SettingGetGlobal_b(G, cSetting_pse_binary_dump) && (!pse_export_version || pse_export_version >= 1765)){
    /* For the pse_binary_dump, record all strings in lex and
       write them into separate binary string
     */
    AtomInfoTypeConverter converter(G, I->NAtom);
    std::set<lexidx_t> lexIDs;
    int totalstlen = 0;
    ai = I->AtomInfo.data();
    for(a = 0; a < I->NAtom; a++) {
      if (ai->textType) lexIDs.insert(ai->textType);
      if (ai->chain) lexIDs.insert(ai->chain);
      if (ai->label) lexIDs.insert(ai->label);
      if (ai->custom) lexIDs.insert(ai->custom);
      if (ai->segi) lexIDs.insert(ai->segi);
      if (ai->resn) lexIDs.insert(ai->resn);
      if (ai->name) lexIDs.insert(ai->name);
      ++ai;
    }
    for (const auto& lexID : lexIDs) { // need to calculate totalstlen so we can allocate
      const char *lexstr = LexStr(G, lexID);
      int lexlen = strlen(lexstr);
      totalstlen += lexlen + 1;
    }
    int strinfolen = totalstlen + sizeof(int) * (lexIDs.size() + 1);
    void *strinfo = pymol::malloc<unsigned char>(strinfolen);
    int *strval = (int*)strinfo;
    *(strval++) = lexIDs.size(); // first write number of strings into binary data string
    char *strpl = (char*)((char*)strinfo + (1 + lexIDs.size()) * sizeof(int));
    /* write map of lex ids and strings into binary data string as an array of ids
       and null-terminated strings */
    for (const auto& lexID : lexIDs) {
      *(strval++) = converter.to_lexidx_int(lexID);
      const char *strptr = LexStr(G, lexID);
      strcpy(strpl, strptr);
      strpl += strlen(strptr) + 1;
    }

    auto version = AtomInfoVERSION;
    if (pse_export_version && pse_export_version < 1810) {
      if (pse_export_version < 1770) {
        version = 176;
      } else {
        version = 177;
      }
    }

    auto blob = converter.allocCopy(version, I->AtomInfo);
    auto blobsize = VLAGetByteSize(blob);

    // PyMOL versions up to 2.3.5 can only restore list size 3
    size_t result_size = 3;

    // Atom properties (not binary)
    PyObject* prop_list = nullptr;
#ifdef _PYMOL_IP_PROPERTIES
    if (pse_export_version > 2399) {
      prop_list = AtomColumnAsPyList(*I, [G](const AtomInfoType& atom) {
        return PConvAutoNone(
            atom.prop_id ? PropertyAsPyList(G, atom.prop_id, true) : nullptr);
      });

      result_size = 4;
    }
#endif

    result = PyList_New(result_size);
    PyList_SetItem(result, 0, PyInt_FromLong(version));
    PyList_SetItem(result, 1, PyBytes_FromStringAndSize(reinterpret_cast<const char*>(blob), blobsize));
    PyList_SetItem(result, 2, PyBytes_FromStringAndSize(reinterpret_cast<const char*>(strinfo), strinfolen));

    if (result_size > 3) {
      PyList_SetItem(result, 3, PConvAutoNone(prop_list));
    }

    VLAFreeP(blob);
    FreeP(strinfo);
    return result;
  }
#endif
  result = PyList_New(I->NAtom);
  ai = I->AtomInfo;
  for(a = 0; a < I->NAtom; a++) {
    PyList_SetItem(result, a, AtomInfoAsPyList(I->G, ai));
    ai++;
  }
  return (PConvAutoNone(result));
}

static int ObjectMoleculeAtomFromPyList(ObjectMolecule * I, PyObject * list)
{
  PyMOLGlobals *G = I->G;
  int ok = true;
  int a, ll = 0;
  AtomInfoType *ai;

  if(ok)
    ok = PyList_Check(list);
  if (ok)
    ll = PyList_Size(list);

  bool pse_binary_dump = false;

  if (ll >= 3) {
    // checking if from pse_binary_dump
    // pse_binary_dump saves 3 values: atomInfo_version, AtomInfo binary, and strings array
    CPythonVal *val1 = CPythonVal_PyList_GetItem(G, list, 1);
    CPythonVal *val2 = CPythonVal_PyList_GetItem(G, list, 2);
    pse_binary_dump = PyBytes_Check(val1) && PyBytes_Check(val2);
    CPythonVal_Free(val1);
    CPythonVal_Free(val2);
  }
  if (pse_binary_dump){
    CPythonVal *verobj = CPythonVal_PyList_GetItem(G, list, 0);
    int atomInfo_version;
    ok = PConvPyIntToInt(verobj, &atomInfo_version);

    CPythonVal *strlookupobj = CPythonVal_PyList_GetItem(G, list, 2);
    auto strval_1 = PyBytes_AsSomeString(strlookupobj);
    int *strval = (int*)strval_1.data();

    AtomInfoTypeConverter converter(G, I->NAtom);

    auto& oldIDtoLexID = converter.lexidxmap;
    int nstrings = *(strval++);
    char *strpl = (char*)(strval + nstrings);
    int strcnt = nstrings;
    int stlen;
    // populate oldIDtoLexID with nstrings from binary string data (3rd entry in list)
    while (strcnt){
      lexidx_t idx = LexIdx(G, strpl); // increments ref count, need to take into account
      int oldidx = *(strval++);
      oldIDtoLexID[oldidx] = idx;
      stlen = strlen(strpl);
      strpl += stlen + 1;
      strcnt--;
    }

    CPythonVal *strobj = CPythonVal_PyList_GetItem(G, list, 1);
    auto strval_2 = PyBytes_AsSomeString(strobj);

    VLACheck(I->AtomInfo, AtomInfoType, I->NAtom + 1);
    converter.copy(I->AtomInfo.data(), strval_2.data(), atomInfo_version);

    // go through AtomInfo array, swap new strings, convert colors, convert settings
    // (everything that AtomInfoFromPyList does except set properties, which are currently 
    //  not saved for pse_binary_dump) 
    AtomInfoType *ai = I->AtomInfo.data();
    for(a = 0; a < I->NAtom; ++a, ++ai) {
      ai->color = ColorConvertOldSessionIndex(G, ai->color);
      if (ai->unique_id){
        ai->unique_id = SettingUniqueConvertOldSessionID(G, ai->unique_id);
      }
    }
    // need to decrement since we call LexIdx() above on each
    for (auto it = oldIDtoLexID.begin(); it != oldIDtoLexID.end(); ++it){
      LexDec(G, it->second);
    }
    CPythonVal_Free(verobj);
    CPythonVal_Free(strobj);
    CPythonVal_Free(strlookupobj);

#ifdef _PYMOL_IP_PROPERTIES
    if (ll > 3) {
      // Restore atom properties
      AtomColumnFromPyList(*I, PyList_GetItem(list, 3), //
          [G](AtomInfoType& atom, PyObject* value) {
            assert(atom.prop_id == 0);
            atom.prop_id = PropertyFromPyList(G, value);
          });
    }
#endif

  } else {
    // The old slow way of loading in AtomInfo, using python lists
    if (ok)
      VLACheck(I->AtomInfo, AtomInfoType, I->NAtom + 1);
    CHECKOK(ok, I->AtomInfo);
    ai = I->AtomInfo.data();
    for(a = 0; ok && a < I->NAtom; a++) {
      PyObject *val = PyList_GetItem(list, a);
      ok &= AtomInfoFromPyList(I->G, ai, val);
      ai++;
    }
  }
  PRINTFB(I->G, FB_ObjectMolecule, FB_Debugging)
    " %s: ok %d \n", __func__, ok ENDFB(I->G);
  return (ok);
}

int ObjectMoleculeNewFromPyList(PyMOLGlobals * G, PyObject * list,
                                ObjectMolecule ** result)
{
  int ok = true;
  ObjectMolecule *I = NULL;
  int discrete_flag = 0;
  (*result) = NULL;

  if(ok)
    ok = PyList_Check(list);
  /* TO SUPPORT BACKWARDS COMPATIBILITY...
     Always check ll when adding new PyList_GetItem's */
  if(ok)
    ok = PConvPyIntToInt(PyList_GetItem(list, 8), &discrete_flag);

  if (ok)
    I = new ObjectMolecule(G, discrete_flag);
  CHECKOK(ok, I);

  if(ok){
    PyObject *val = PyList_GetItem(list, 0);
    ok = ObjectFromPyList(G, val, I);
  }
  if(ok)
    ok = PConvPyIntToInt(PyList_GetItem(list, 1), &I->NCSet);
  if(ok)
    ok = PConvPyIntToInt(PyList_GetItem(list, 2), &I->NBond);
  if(ok)
    ok = PConvPyIntToInt(PyList_GetItem(list, 3), &I->NAtom);
  if(ok)
    ok = ObjectMoleculeCSetFromPyList(I, PyList_GetItem(list, 4));
  if(ok){
    ok = CoordSetFromPyList(G, PyList_GetItem(list, 5), &I->CSTmpl);

    if(I->CSTmpl)
      I->CSTmpl->Obj = I;
  }
  if(ok){
    CPythonVal *val = CPythonVal_PyList_GetItem(G, list, 6);
    ok = ObjectMoleculeBondFromPyList(I, val);
    CPythonVal_Free(val);
  }
  if (!ok && I)
    I->NBond = 0;
  if(ok){
    CPythonVal *val = CPythonVal_PyList_GetItem(G, list, 7);
    ok = ObjectMoleculeAtomFromPyList(I, val);
    CPythonVal_Free(val);
  }
  if (!ok && I)
    I->NAtom = 0;
  if(ok){
    CPythonVal *val = CPythonVal_PyList_GetItem(G, list, 10);
    I->Symmetry.reset(SymmetryNewFromPyList(G, val));
    CPythonVal_Free(val);
  }
  /* 11 was CurCSet */
  /* 12 was BondCounter */
  if(ok)
    ok = PConvPyIntToInt(PyList_GetItem(list, 13), &I->AtomCounter);

  I->updateAtmToIdx();

  if (ok)
    I->invalidate(cRepAll, cRepInvAll, -1);
  if(ok)
    (*result) = I;
  else {
    /* cleanup */
    if (I)
        DeleteP(I);
    (*result) = NULL;
  }
  return (ok);
}


/*========================================================================*/
PyObject *ObjectMoleculeAsPyList(ObjectMolecule * I)
{
  PyObject *result = NULL;

  /* first, dump the atoms */

  result = PyList_New(16);
  PyList_SetItem(result, 0, ObjectAsPyList(I));
  PyList_SetItem(result, 1, PyInt_FromLong(I->NCSet));
  PyList_SetItem(result, 2, PyInt_FromLong(I->NBond));
  PyList_SetItem(result, 3, PyInt_FromLong(I->NAtom));
  PyList_SetItem(result, 4, ObjectMoleculeCSetAsPyList(I));
  PyList_SetItem(result, 5, CoordSetAsPyList(I->CSTmpl));
  PyList_SetItem(result, 6, ObjectMoleculeBondAsPyList(I));
  PyList_SetItem(result, 7, ObjectMoleculeAtomAsPyList(I));
  PyList_SetItem(result, 8, PyInt_FromLong(I->DiscreteFlag));
  PyList_SetItem(result, 9, PyInt_FromLong(I->DiscreteFlag ? I->NAtom : 0 /* NDiscrete */));
  PyList_SetItem(result, 10, SymmetryAsPyList(I->Symmetry.get()));
  PyList_SetItem(result, 11, PyInt_FromLong(0 /* CurCSet */));
  PyList_SetItem(result, 12, PyInt_FromLong(-1 /* BondCounter */));
  PyList_SetItem(result, 13, PyInt_FromLong(I->AtomCounter));

  float pse_export_version = SettingGetGlobal_f(I->G, cSetting_pse_export_version);

  if(I->DiscreteFlag
      && (pse_export_version || !SettingGetGlobal_b(I->G, cSetting_pse_binary_dump))
      && pse_export_version < 1.7699) {
    int *dcs;
    int a;
    CoordSet *cs;

    /* get coordinate set indices */

    for(a = 0; a < I->NCSet; a++) {
      cs = I->CSet[a];
      if(cs) {
        cs->tmp_index = a;
      }
    }

    dcs = pymol::malloc<int>(I->NAtom);

    for(a = 0; a < I->NAtom; a++) {
      cs = I->DiscreteCSet[a];
      if(cs)
        dcs[a] = cs->tmp_index;
      else
        dcs[a] = -1;
    }

    PyList_SetItem(result, 14, PConvIntArrayToPyList(I->DiscreteAtmToIdx, I->NAtom));
    PyList_SetItem(result, 15, PConvIntArrayToPyList(dcs, I->NAtom));
    FreeP(dcs);
  } else {
    PyList_SetItem(result, 14, PConvAutoNone(NULL));
    PyList_SetItem(result, 15, PConvAutoNone(NULL));
  }

  return (PConvAutoNone(result));
}

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

static
float connect_cutoff_adjustment(
    const AtomInfoType * ai1,
    const AtomInfoType * ai2)
{
  if (ai1->isHydrogen() || ai2->isHydrogen())
    return -0.2f;

  if (ai1->protons == cAN_S || ai2->protons == cAN_S)
    return 0.2f;

  return 0.f;
}

/**
 * True if two atoms should be bonded
 */
static
bool is_distance_bonded(
    PyMOLGlobals * G,
    const CoordSet * cs,
    const AtomInfoType * ai1,
    const AtomInfoType * ai2,
    const float * v1,
    const float * v2,
    float cutoff,
    int connect_mode,
    int discrete_chains,
    bool connect_bonded,
    bool unbond_cations)
{
  auto dst = (float) diff3f(v1, v2);

  if (dst < R_SMALL4)
    return false;

  dst -= (ai1->vdw + ai2->vdw) / 2;

  cutoff += connect_cutoff_adjustment(ai1, ai2);

  if (dst > cutoff)
    return false;

  if (ai1->isHydrogen() && ai2->isHydrogen())
    return false;

  if (discrete_chains > 0 && ai1->chain != ai2->chain)
    return false;

  if (!connect_bonded && ai1->bonded && ai2->bonded)
    return false;

  bool water_flag = (
      AtomInfoKnownWaterResName(G, LexStr(G, ai1->resn)) ||
      AtomInfoKnownWaterResName(G, LexStr(G, ai2->resn)));

  if (connect_mode != 3 &&
      cs->TmpBond && /* connectivity information present in file */
      ai1->hetatm &&
      ai2->hetatm &&
      !water_flag &&
      !(AtomInfoKnownPolymerResName(LexStr(G, ai1->resn)) &&
        AtomInfoKnownPolymerResName(LexStr(G, ai2->resn))))
    return false;

  // don't connect water atoms in different residues
  if (water_flag && !AtomInfoSameResidue(G, ai1, ai2))
    return false;

  // don't connect atoms with different, non-NULL alternate conformations
  if (ai1->alt[0] != ai2->alt[0] && ai1->alt[0] && ai2->alt[0])
    return false;

  // if either is a cation, unbond is user wants
  if (unbond_cations &&
      (AtomInfoIsFreeCation(G, ai1) ||
       AtomInfoIsFreeCation(G, ai2)))
    return false;

  return true;
}

/**
 * Do bonding of atoms in `I`, using distances and/or temporary bonds in `cs`.
 *
 * Incorporates `cs->TmpBond` unless `connect_mode` is 2.
 * Incorporates `cs->TmpLinkBond`.
 *
 * @param I Molecule to modify
 * @param cs Coordinates and temporary bonds to consider
 * @param bondSearchMode If false and `connect_mode` != 2, do not search for new
 * bonds (only use TmpBond/TmpLinkBond).
 * @param connectModeOverride Overrides `connect_mode` setting if not -1
 * @param pbc Use periodic boundary conditions (find symop bonds)
 *
 * `connect_mode` options:
 * 0 = distance-based (excluding HETATM to HETATM) and CONECT records (default)
 * 1 = CONECT records
 * 2 = distance-based, ignores CONECT records
 * 3 = distance-based (including HETATM to HETATM) and CONECT records
 * 4 = same as `connect_mode` = 0 (special meaning during mmCIF loading)
 */
bool ObjectMoleculeConnect(ObjectMolecule* I, CoordSet* cs, bool bondSearchMode,
    int connectModeOverride, bool pbc)
{
  return ObjectMoleculeConnect(
      I, I->NBond, I->Bond, cs, bondSearchMode, connectModeOverride, pbc);
}

/*========================================================================*/
bool ObjectMoleculeConnect(ObjectMolecule* I, int& nBond, pymol::vla<BondType>& bondvla,
                          struct CoordSet *cs, int bondSearchMode,
                          int connectModeOverride,
                          bool pbc)
{
  PyMOLGlobals *G = I->G;
  AtomInfoType* const ai = I->AtomInfo.data();
  auto discrete_chains = SettingGet<int>(G, cSetting_pdb_discrete_chains);
  auto const connect_bonded = SettingGet<bool>(G, cSetting_connect_bonded);
  auto const unbond_cations = SettingGet<int>(G, cSetting_pdb_unbond_cations);
  auto const cutoff_v = SettingGet<float>(G, cSetting_connect_cutoff);
  auto const max_cutoff = cutoff_v + 0.2F; ///< Sulfur cutoff
  auto connect_mode = (connectModeOverride >= 0)
                          ? connectModeOverride
                          : SettingGet<int>(G, cSetting_connect_mode);

  if (connect_mode == 2) {
    // Force use of distance-based connectivity, ignoring that
    // provided with file.
    bondSearchMode = true;
    cs->NTmpBond = 0;
    VLAFreeP(cs->TmpBond);
  } else if (connect_mode == 4) {
    // mmCIF specific, fall back to default to get any bonds for PDB, XYZ, etc.
    connect_mode = 0;
  }

  nBond = 0;
  auto const maxBond = cs->NIndex * 8;
  // Number of bonds is typically close to number of atoms
  bondvla.reserve(cs->NIndex * 1.2);
  p_return_val_if_fail(bondvla, false); // memory error

  bool repeat = false;
  switch (connect_mode) {
  case 0: /* distance-based and explicit (not HETATM to HETATM) */
  case 2: /* distance-based only */
  case 3: /* distance-based and explicit (even HETATM to HETATM) */
    repeat = bondSearchMode && cs->NIndex > 0;
  }

  // Distance-based bond location
  while (repeat) {
    repeat = false;
    nBond = 0;

    // For monitoring excessive numbers of bonds
    int violations = 0;
    auto const max_violations = cs->NIndex >> 3; // 12%
    auto const cnt = pymol::make_unique<signed char[]>(size_t(cs->NIndex));
    p_return_val_if_fail(cnt, false); // memory error

    /* initialize each atom's (max) expected valence */
    for (unsigned i = 0; i < cs->NIndex; ++i) {
      auto valcnt = AtomInfoGetExpectedValence(G, ai + cs->IdxToAtm[i]);
      cnt[i] = (valcnt < 0) ? 6 : valcnt;
    }

    // Search for symop bonds (periodic boundary conditions)?
    int offset_begin = 0, offset_end = 1, symmat_end = 1;
    if (pbc && cs->getSymmetry() && //
        !cs->getSymmetry()->Crystal.isSuspicious()) {
      offset_begin = -1;
      offset_end = 2;
      symmat_end = cs->getSymmetry()->getNSymMat();
      assert(symmat_end > 0);
    }

    /* make a map of the local neighborhood in space */
    auto const map = std::unique_ptr<MapType>(
        MapNew(G, (max_cutoff + MAX_VDW) * (offset_begin ? -1 : 1), //
            cs->Coord, cs->NIndex));
    p_return_val_if_fail(map, false); // memory error
    MapSetupExpress(map.get()); // Don't let MapEIter call this in omp parallel

    /// Return false on error
    auto const find_bonds_for_atom = [&](unsigned i, float const* v1,
                                         pymol::SymOp const& symop) -> bool {
      auto const a1 = cs->IdxToAtm[i];
      auto* const ai1 = ai + a1;

      for (const auto j : MapEIter(*map, v1)) {
        if (i <= j && !symop)
          continue;

        /* position in space for atom 2 */
        auto const* const v2 = cs->coordPtr(j);
        auto const a2 = cs->IdxToAtm[j];
        auto* const ai2 = ai + a2;

        if (!is_distance_bonded(G, cs, ai1, ai2, v1, v2, cutoff_v, connect_mode,
                discrete_chains, connect_bonded, unbond_cations))
          continue;

        /* we have a bond, now process it */

        int order = 1;
        if (!ai1->hetatm || ai1->resn == G->lex_const.MSE) {
          if (AtomInfoSameResidue(I->G, ai1, ai2)) {
            /* hookup standard disconnected PDB residue */
            assign_pdb_known_residue(G, ai1, ai2, &order);
          }
        }

#ifdef PYMOL_OPENMP
#pragma omp critical
#endif
        {
          auto const bnd = bondvla.check(nBond++);
          BondTypeInit2(
              bnd, a2, a1, -order /* store tentative valence as negative */);
          bnd->symop_2 = symop;

          /* if we allow bonds between chains and it screws up
           * the bonding, disallow inter-chain bonds */
          if (discrete_chains < 0) {
            /* decrement free valences, since we have a bond */
            if (--cnt[i] == -2)
              violations++;
            if (--cnt[j] == -2)
              violations++;

            if (violations > max_violations) {
              PRINTFB(G, FB_ObjectMolecule, FB_Blather)
              " %s: Assuming chains are discrete...\n", __func__ ENDFB(G);

              discrete_chains = 1;
              repeat = 1;
            }
          }
        }

        if (repeat) {
          return false;
        }
      }

      return true;
    };

    bool break_all = false;

    // Do bond search in parallel for every atom
#ifdef PYMOL_OPENMP
#pragma omp parallel for
#endif
    for (int i = 0; i < cs->NIndex; ++i) {
      float _v1_buf[3];
      pymol::SymOp symop{};
      for (symop.x = offset_begin; symop.x < offset_end; ++symop.x) {
        for (symop.y = offset_begin; symop.y < offset_end; ++symop.y) {
          for (symop.z = offset_begin; symop.z < offset_end; ++symop.z) {
            for (symop.index = 0; symop.index != symmat_end; ++symop.index) {
              auto const* const v1 = cs->coordPtrSym(i, symop, _v1_buf);
              assert(v1);

              if (break_all || !find_bonds_for_atom(i, v1, symop) ||
                  nBond > maxBond) {
                goto break_all_find_bonds_for_atom;
              }
            }
          }
        }
      }

      continue;
    break_all_find_bonds_for_atom:
      // can't "break" inside an omp parallel block
      break_all = true;
    }

    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      " %s: Found %d bonds.\n", __func__, nBond ENDFB(G);
  }

  /* if we have explicit connectivity, determine if we need to set check_conect_all */
  if (cs->NTmpBond && cs->TmpBond) {
    bool check_conect_all = false;
    bool pdb_conect_all = false;

    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      " %s: incorporating explicit bonds. %d %d\n", __func__,
      nBond, cs->NTmpBond ENDFB(G);
    if((nBond == 0) && (cs->NTmpBond > 0) &&
       bondSearchMode && (connect_mode == 0) && cs->NIndex) {
      /* if no bonds were found, and we have explicit connectivity,
       * try to determine if we need to set pdb_conect_mode */
      for (unsigned i = 0; i < cs->NIndex; ++i) {
        auto const& ai1 = ai[cs->IdxToAtm[i]];
        if (ai1.bonded && !ai1.hetatm) {
          /* apparent PDB ATOM record with explicit bonding... */
          check_conect_all = true;
          break;
        }
      }
    }

    bondvla.check(nBond + cs->NTmpBond);
    p_return_val_if_fail(bondvla, false); // memory error

    auto* ii1 = bondvla.data() + nBond;
    auto* ii2 = cs->TmpBond.data();
    int n_atom = I->NAtom;
    for (unsigned a = 0; a < cs->NTmpBond; ++a) {
      auto const a1 = cs->IdxToAtm[ii2->index[0]];
      auto const a2 = cs->IdxToAtm[ii2->index[1]];
      if((a1 >= 0) && (a2 >= 0) && (a1 < n_atom) && (a2 < n_atom)) {
          if(check_conect_all) {
            if((!ai[a1].hetatm) && (!ai[a2].hetatm)) {
              /* found bond between non-HETATMs -- so tell PyMOL to CONECT all ATOMs
               * when writing out a PDB file */
              pdb_conect_all = true;
            }
          }
          ai[a1].bonded = true;
          ai[a2].bonded = true;
          *ii1 = std::move(*ii2);
          ii1->index[0] = a1;
          ii1->index[1] = a2;
          ii1++;
          ii2++;
          nBond++;
      }
    }

    VLAFreeP(cs->TmpBond);
    cs->NTmpBond = 0;

    if(pdb_conect_all) {
	int dummy;
	if(!SettingGetIfDefined_b(G, I->Setting.get(), cSetting_pdb_conect_all, &dummy)) {
          {
            auto handle = I->getSettingHandle(-1);
            if(handle) {
	      SettingCheckHandle(G, *handle);
	      SettingSet_b(handle->get(), cSetting_pdb_conect_all, true);
	    }
	  }
	}
    }
  }
  
  /* Link b/t ligand and residue? */
  if (cs->NTmpLinkBond && cs->TmpLinkBond) {
    PRINTFB(G, FB_ObjectMolecule, FB_Blather)
      "%s: incorporating linkage bonds. %d %d\n", __func__,
      nBond, cs->NTmpLinkBond ENDFB(G);

    bondvla.check(nBond + cs->NTmpLinkBond);
    p_return_val_if_fail(bondvla, false); // memory error

    auto* ii1 = bondvla.data() + nBond;
    auto* ii2 = cs->TmpLinkBond.data();
    for (unsigned a = 0; a < cs->NTmpLinkBond; ++a) {
      auto const a1 = ii2->index[0];               /* first atom is in object */
      auto const a2 = cs->IdxToAtm[ii2->index[1]]; /* second is in the cset */
      ai[a1].bonded = true;
      ai[a2].bonded = true;
      (*ii1) = std::move(*ii2);
      ii1->index[0] = a1;
      ii1->index[1] = a2;
      ii1++;
      ii2++;
    }
    nBond += cs->NTmpLinkBond;
    VLAFreeP(cs->TmpLinkBond);
    cs->NTmpLinkBond = 0;
  }

  // Eliminate duplicates
  // TODO do we expect any?
  // TODO should we also check with swapped indices?
  // TODO why not for discrete objects?
  if (nBond > 1 && !I->DiscreteFlag) {
    PRINTFD(G, FB_ObjectMolecule)
      " %s: elminating duplicates with %d bonds...\n", __func__, nBond ENDFD;

    UtilSortInPlace(
        G, bondvla.data(), nBond, sizeof(BondType), (UtilOrderFn*) BondInOrder);
    auto* ii1 = bondvla.data();
    auto* ii2 = bondvla.data() + 1;
    for (int a = nBond; --a;) {
      if (BondCompare(ii2, ii1) != 0) {
        if (++ii1 != ii2) {
          *ii1 = std::move(*ii2);
        }
      } else if (ii2->order > 0 && ii1->order < 0) {
        // use most certain valence
        ii1->order = ii2->order;
      }
      ii2++;
    }
    nBond = ii1 - bondvla.data() + 1;
  }

  bondvla.resize(nBond);

  // restore bond order positivity
  for (auto bnd = bondvla.begin(), bnd_end = bnd + nBond; bnd != bnd_end;
       ++bnd) {
    if (bnd->order < 0)
      bnd->order = -bnd->order;
  }

  PRINTFD(G, FB_ObjectMolecule)
    " %s: leaving with %d bonds...\n", __func__, nBond ENDFD;
  return true;
}

void ObjectMoleculeConnectDiscrete(ObjectMolecule* I, int searchFlag,
    int connectModeOverride, bool pbc)
{
  for (int i = 0; i < I->NCSet; i++) {
    if (!I->CSet[i]) {
      continue;
    }

    int nbond = 0;
    pymol::vla<BondType> bond;

    ObjectMoleculeConnect(I, nbond, bond, I->CSet[i], searchFlag, connectModeOverride, pbc);

    if (!bond) {
      continue;
    }

    if (!I->Bond) {
      I->Bond = std::move(bond);
    } else {
      I->Bond.check(I->NBond + nbond - 1);
      std::copy_n(bond.data(), nbond, I->Bond.data() + I->NBond);
    }

    I->NBond += nbond;
  }
}

/*========================================================================*/
/**
 * Sort the ObjectMolecule::AtomInfo and ObjectMolecule::Bond arrays and adjust
 * IdxToAtm/AtmToIdx in all coordiante sets.
 *
 * This function has no effect on discrete objects.
 */
int ObjectMoleculeSort(ObjectMolecule * I)
{                               /* sorts atoms and bonds */
  int *index;
  int *outdex = NULL;
  int a, b;
  int ok = true;
  if(!I->DiscreteFlag) {        /* currently, discrete objects are never sorted */
    int already_in_order = true;
    int i_NAtom = I->NAtom;
    index = AtomInfoGetSortedIndex(I->G, I, I->AtomInfo, i_NAtom, &outdex);
    CHECKOK(ok, index);
    if (ok){
      for(a = 0; a < i_NAtom; a++) {
	if(index[a] != a) {
	  already_in_order = false;
	  break;
	}
      }
    }
    if(ok && !already_in_order) {     /* if we aren't already in perfect order */

      for(a = 0; a < I->NBond; a++) {   /* bonds */
        I->Bond[a].index[0] = outdex[I->Bond[a].index[0]];
        I->Bond[a].index[1] = outdex[I->Bond[a].index[1]];
      }

      for(a = -1; a < I->NCSet; a++) {  /* coordinate set mapping */
        auto* cs = (a < 0) ? I->CSTmpl : I->CSet[a];

        if(cs) {
          int cs_NIndex = cs->NIndex;
          int *cs_IdxToAtm = cs->IdxToAtm.data();
          for(b = 0; b < cs_NIndex; b++)
            cs_IdxToAtm[b] = outdex[cs_IdxToAtm[b]];
        }
      }

      I->updateAtmToIdx();

      ExecutiveUniqueIDAtomDictInvalidate(I->G);

      pymol::vla<AtomInfoType> atInfo(i_NAtom);
      CHECKOK(ok, atInfo);
      if (ok){
	for(a = 0; a < i_NAtom; a++)
	  atInfo[a] = std::move(I->AtomInfo[index[a]]);
        I->AtomInfo = std::move(atInfo);
      }
    }
    AtomInfoFreeSortedIndexes(I->G, &index, &outdex);
    if (ok){
      UtilSortInPlace(I->G, I->Bond.data(), I->NBond, sizeof(BondType),
		      (UtilOrderFn *) BondInOrder);
      /* sort...important! */
      I->invalidate(cRepAll, cRepInvAtoms, -1);     /* important */
    }
  }
  return ok;
}

int ObjectMoleculeSetGeometry(
    PyMOLGlobals* G, ObjectMolecule* I, int sele, int geom, int valence)
{
  int count = 0;
  for (int a = 0; a < I->NAtom; ++a) {
    auto s = I->AtomInfo[a].selEntry;

    if(SelectorIsMember(G, s, sele)) {
      auto& ai = I->AtomInfo[a];
      ai.geom = geom;
      ai.valence = valence;
      ai.chemFlag = true;
      count++;
      break;
    }
  }
  return count;
}

