
/* 
A* -------------------------------------------------------------------
B* This file contains source code for the PyMOL computer program
C* copyright 1998-2006 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 <unordered_set>

#include"os_python.h"

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

#include"ObjectAlignment.h"
#include"Base.h"
#include"MemoryDebug.h"
#include"CGO.h"
#include"Scene.h"
#include"Setting.h"
#include"PConv.h"
#include"main.h"
#include"Color.h"
#include"Executive.h"
#include"OVContext.h"
#include"Util.h"
#include"Selector.h"
#include"Seq.h"
#include"Seeker.h"
#include"ShaderMgr.h"
#include "Lex.h"

/* 
   just so you don't forget...

   alignment vlas are zero-separated lists of groups of unique atom identifiers 

*/

static int GroupOrderKnown(PyMOLGlobals * G,
                           int *curVLA,
                           const int *newVLA,
                           int cur_start,
                           int new_start, ObjectMolecule * guide, int *action)
{
  int order_known = false;
  if(guide) {
    int c, id;
    int cur_offset = -1;
    int new_offset = -1;

    /* find lowest offset within the cur group */
    c = cur_start;
    while((id = curVLA[c++])) {
      auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
      if (eoo && eoo->obj == guide) {
        if((cur_offset < 0) || (eoo->atm < cur_offset))
          cur_offset = eoo->atm;
      }
    }

    /* find lowest offset within the new group */
    c = new_start;
    while((id = newVLA[c++])) {
      auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
      if (eoo && eoo->obj == guide) {
        if((new_offset < 0) || (eoo->atm < new_offset))
          new_offset = eoo->atm;
      }
    }

    if((new_offset >= 0) && (cur_offset >= 0)) {
      if(new_offset < cur_offset) {
        order_known = true;
        *action = -1;
      } else if(new_offset > cur_offset) {
        order_known = true;
        *action = 1;
      }
    }
  }
  return order_known;
}

static int AlignmentFindTag(PyMOLGlobals * G, AtomInfoType * ai, int sele,
                            int n_more_plus_one)
{
  int result = 0;      /* default -- no tag */
  AtomInfoType *ai0 = ai;
  while(1) {
    int tag = SelectorIsMember(G, ai0->selEntry, sele);
    if(tag && (ai0->flags & cAtomFlag_guide))   /* use guide atom if present */
      return tag;
    if(result < tag) {
      if(!result)
        result = tag;
      else if(ai0->flags & cAtomFlag_guide)     /* residue based and on guide atom */
        result = tag;
    }
    n_more_plus_one--;
    if(n_more_plus_one > 0) {
      ai0++;
      if(!AtomInfoSameResidueP(G, ai, ai0))
        break;
    } else
      break;
  }
  return result;
}

/**
 * Get single letter abbreviation for CLUSTAL output.
 *
 * See also:
 * pymol.exporting._resn_to_aa
 * AtomInfoKnownNucleicResName()
 * AtomInfoKnownProteinResName()
 * SeekerGetAbbr()
 */
static char get_abbr(PyMOLGlobals * G, const AtomInfoType * ai) {
  const char * resn = LexStr(G, ai->resn);
  const char unknown = ((ai->flags & cAtomFlag_polymer)) ? '?' : 0;

  if ((ai->flags & cAtomFlag_nucleic)) {
    if (resn[0] == 'D') {
      ++resn;
    }

    if (strlen(resn) != 1) {
      return unknown;
    }

    return resn[0];
  }

  return SeekerGetAbbr(G, resn, 0, unknown);
}

int ObjectAlignmentAsStrVLA(PyMOLGlobals * G, ObjectAlignment * I, int state, int format,
                            char **str_vla)
{
  int ok = true;
  ov_size len = 0;
  char *vla = VLAlloc(char, 1000);
  int force_update = false;
  int active_only = false;
  int max_name_len = 12;        /* default indentation */

  if(state < 0)
    state = I->getCurrentState();
  if(state < 0)
    state = SceneGetState(G);
  if(state >= 0 && state < I->getNFrame()) {
    ObjectAlignmentState *oas = I->State.data() + state;
    if(oas->alignVLA) {
      if(state != I->SelectionState) {  /* get us a selection for the current state */
        I->ForceState = state;
        force_update = true;
        I->update();
      }

      switch (format) {
      case 0:                  /* aln */
        UtilConcatVLA(&vla, &len, "CLUSTAL\n\n");
        break;
      }

      {
        int align_sele = SelectorIndexByName(G, I->Name);
        if(align_sele >= 0) {
          int nRow = 0;
          ov_size nCol = 0;
          CSeqRow *row_vla = NULL, *row;
          char *cons_str = NULL;
          void *hidden = NULL;

          ObjectMolecule *obj;

          {
            row_vla = VLACalloc(CSeqRow, 10);

            /* first, find out which objects are included in the
               alignment and count the name length */

            while(ExecutiveIterateObjectMolecule(G, &obj, &hidden)) {
              if((obj->Enabled || !active_only) && (obj->Name[0] != '_')) {
                int a;
                const AtomInfoType *ai = obj->AtomInfo.data();
                for(a = 0; a < obj->NAtom; a++) {
                  if(SelectorIsMember(G, ai->selEntry, align_sele)) {
                    int name_len = strlen(obj->Name);
                    if(max_name_len < name_len)
                      max_name_len = name_len;
                    VLACheck(row_vla, CSeqRow, nRow);
                    row = row_vla + nRow;
                    row->obj = obj;
                    row->nCol = obj->NAtom;
                    nRow++;
                    break;
                  }
                  ai++;
                }
              }
            }

            /* next, figure out how many total columns exist */

            {
              int done = false;
              while(!done) {
                int a;
                int min_tag = -1;
                int untagged_col = false;
                done = true;
                for(a = 0; a < nRow; a++) {
                  row = row_vla + a;
                  while(row->cCol < row->nCol) {        /* advance to next tag in each row & find lowest */
                    AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                    done = false;
                    if(AtomInfoSameResidueP(G, row->last_ai, ai)) {
                      row->cCol++;
                    } else if(!get_abbr(G, ai)) {      /* not a polymer residue */
                      row->cCol++;
                    } else {
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(tag) { /* we're at a tagged atom... */
                        if(min_tag > tag)
                          min_tag = tag;
                        else if(min_tag < 0)
                          min_tag = tag;
                        break;
                      } else {
                        untagged_col = true;
                        break;
                      }
                    }
                  }
                  if(untagged_col)
                    break;
                }
                if(untagged_col) {
                  nCol++;
                  /* increment all untagged atoms */
                  for(a = 0; a < nRow; a++) {
                    row = row_vla + a;
                    if(row->cCol < row->nCol) {
                      AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(!tag) {
                        row->last_ai = ai;
                        row->cCol++;
                      }
                    }
                  }
                } else if(min_tag >= 0) {
                  /* increment all matching tagged atoms */
                  nCol++;
                  for(a = 0; a < nRow; a++) {
                    row = row_vla + a;
                    if(row->cCol < row->nCol) {
                      AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(tag == min_tag) {      /* advance past this tag */
                        row->cCol++;
                        row->last_ai = ai;
                      }
                    }
                  }
                }
              }
            }
            /* allocate storage for the sequence alignment */

            cons_str = pymol::calloc<char>(nCol + 1);  /* conservation string */

            {
              int a;
              for(a = 0; a < nRow; a++) {
                row = row_vla + a;
                row->txt = pymol::vla<char>(nCol + 1);
                row->len = 0;
                row->last_ai = NULL;
                row->cCol = 0;
              }
            }

            nCol = 0;

            {
              int done = false;
              while(!done) {
                int a;
                int min_tag = -1;
                int untagged_col = false;
                done = true;
                for(a = 0; a < nRow; a++) {
                  row = row_vla + a;
                  while(row->cCol < row->nCol) {        /* advance to next tag in each row & find lowest */
                    AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                    done = false;
                    if(AtomInfoSameResidueP(G, row->last_ai, ai)) {
                      row->cCol++;
                    } else if(!get_abbr(G, ai)) {      /* not a polymer residue */
                      row->cCol++;
                    } else {
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(tag) { /* we're at a tagged atom... */
                        if(min_tag > tag)
                          min_tag = tag;
                        else if(min_tag < 0)
                          min_tag = tag;
                        break;
                      } else {
                        untagged_col = true;
                        break;
                      }
                    }
                  }
                }
                if(untagged_col) {
                  /* increment all untagged atoms */
                  for(a = 0; a < nRow; a++) {
                    row = row_vla + a;
                    if(row->cCol < row->nCol) {
                      AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(!tag) {
                        if(!AtomInfoSameResidueP(G, row->last_ai, ai)) {
                          row->last_ai = ai;
                          row->txt[row->len] = get_abbr(G, ai);
                        } else {
                          row->txt[row->len] = '-';
                        }
                        row->len++;
                        row->cCol++;
                      } else {
                        row->txt[row->len] = '-';
                        row->len++;
                      }
                    } else {
                      row->txt[row->len] = '-';
                      row->len++;
                    }
                  }
                  cons_str[nCol] = ' ';
                  nCol++;
                } else if(min_tag >= 0) {
                  char cons_abbr = ' ';
                  int abbr_cnt = 0;
                  /* increment all matching tagged atoms */
                  for(a = 0; a < nRow; a++) {
                    row = row_vla + a;
                    if(row->cCol < row->nCol) {
                      AtomInfoType *ai = row->obj->AtomInfo + row->cCol;
                      int tag =
                        AlignmentFindTag(G, ai, align_sele, row->nCol - row->cCol);
                      if(tag == min_tag) {      /* advance past this tag */
                        char abbr;
                        if(!AtomInfoSameResidueP(G, row->last_ai, ai)) {
                          row->last_ai = ai;
                          abbr = get_abbr(G, ai);
                          if(cons_abbr == ' ')
                            cons_abbr = abbr;
                          else if(cons_abbr != abbr)
                            cons_abbr = 0;
                          abbr_cnt++;
                        } else {
                          abbr = '-';
                          cons_abbr = 0;
                        }
                        row->txt[row->len] = abbr;
                        row->len++;
                        row->cCol++;
                      } else {
                        cons_abbr = 0;
                        row->txt[row->len] = '-';
                        row->len++;
                      }
                    } else {
                      cons_abbr = 0;
                      row->txt[row->len] = '-';
                      row->len++;
                    }
                  }
                  if(abbr_cnt > 1) {
                    if(cons_abbr)
                      cons_str[nCol] = '*';     /* aligned and identical */
                    else
                      cons_str[nCol] = '.';     /* aligned but not identical */
                  } else
                    cons_str[nCol] = ' ';
                  nCol++;
                }
              }
            }

            {
              int block_width = 76 - (max_name_len + 1);
              int done = false;
              ov_size seq_len = 0;
              int a;
              while(!done) {
                done = true;
                for(a = 0; a < nRow; a++) {
                  row = row_vla + a;
                  UtilNPadVLA(&vla, &len, row->obj->Name, max_name_len + 1);
                  if(seq_len < row->len) {
                    UtilNPadVLA(&vla, &len, row->txt + seq_len, block_width);
                  }
                  UtilConcatVLA(&vla, &len, "\n");
                }
                if(seq_len < nCol) {
                  UtilNPadVLA(&vla, &len, "", max_name_len + 1);
                  UtilNPadVLA(&vla, &len, cons_str + seq_len, block_width);
                  UtilConcatVLA(&vla, &len, "\n");
                }
                seq_len += block_width;
                for(a = 0; a < nRow; a++) {
                  row = row_vla + a;
                  if(seq_len < row->len) {
                    done = false;
                    break;
                  }
                }
                UtilConcatVLA(&vla, &len, "\n");
              }
            }
          }

          /* free up resources */
          if(row_vla) {
            int a;
            for(a = 0; a < nRow; a++) {
              row = row_vla + a;
              row->txt.freeP();
            }
          }
          FreeP(cons_str);
          VLAFreeP(row_vla);
        }
      }
    }
  }

  if(force_update) {
    I->update();
  }

  VLASize(vla, char, len + 1);
  vla[len] = 0;
  *str_vla = vla;
  return ok;
}

static int *AlignmentMerge(PyMOLGlobals * G, int *curVLA, const int *newVLA,
                           ObjectMolecule * guide, ObjectMolecule * flush)
{
  /* curVLA and newVLA must be properly sized and zero terminated... */
  int *result = NULL;
  int n_result = 0;

  {
    {

      int n_cur = VLAGetSize(curVLA);
      int n_new = VLAGetSize(newVLA);

      // get the set of non-guide objects in the new alignment, they need
      // to be flushed (removed) from the current alignment
      std::unordered_set<const ObjectMolecule*> flushobjects;
      if (flush) {
        flushobjects.insert(flush);
      } else {
        for (const int *it = newVLA, *it_end = it + n_new; it != it_end; ++it) {
          if (*it) {
            auto eoo = ExecutiveUniqueIDAtomDictGet(G, *it);
            if (eoo && eoo->obj != guide) {
              flushobjects.insert(eoo->obj);
            }
          }
        }
      }

      /* first, go through and eliminate old matching atoms between guide and flush (if any) */
      {
        int cur_start = 0;
        while(cur_start < n_cur) {

          while((cur_start < n_cur) && !curVLA[cur_start]) {
            cur_start++;
          }

          {
            int other_seen = 0;
            int flush_seen = false;
            ObjectMolecule *obj;

            {
              int cur = cur_start;
              int id;
              while((id = curVLA[cur])) {
                auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                if (eoo) {
                  obj = eoo->obj;
                  if(flushobjects.count(obj)) {
                    flush_seen = true;
                  } else {
                    other_seen++;
                  }
                }
                cur++;
              }
            }

            if(flush_seen) {    /* eliminate flush atoms */
              int cur = cur_start;
              int id;
              while((id = curVLA[cur])) {
                auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                if (eoo) {
                  obj = eoo->obj;
                  if(flushobjects.count(obj)) {
                    int tmp = cur;
                    while(curVLA[tmp]) {
                      curVLA[tmp] = curVLA[tmp + 1];
                      tmp++;
                    }
                  }
                }
                cur++;
              }
            }

            if(other_seen < 2) {     /* eliminate orphaned atoms */
              int cur = cur_start;
              while(curVLA[cur])
                curVLA[cur++] = 0;
            }

            while(curVLA[cur_start])
              cur_start++;
            while((cur_start < n_cur) && !curVLA[cur_start]) {
              cur_start++;
            }
          }
        }
      }

      /* now combine the alignments */

      {
        std::unordered_set<int> used;
        std::unordered_set<int> active;
        int cur_start = 0;
        int new_start = 0;

        result = VLAlloc(int, ((n_cur < n_new) ? n_new : n_cur));

        while((cur_start < n_cur) || (new_start < n_new)) {

          int action;           /* -1 = insert new, 0 = merge, 1 = insert cur */

          /* make sure both lists are queued up on the next identifier */

          while((cur_start < n_cur) && !curVLA[cur_start])
            cur_start++;
          while((new_start < n_new) && !newVLA[new_start])
            new_start++;

          if(newVLA[new_start]) {       /* default is to insert new first... */
            action = -1;
          } else {
            action = 1;
          }

          if((cur_start < n_cur) && (new_start < n_new) &&
             curVLA[cur_start] && newVLA[new_start]) {
            /* both lists active */

            int c, id;
            int overlapping = false;

            active.clear();
            c = cur_start;
            while((id = curVLA[c++])) { /* record active atoms */
              active.insert(id);
            }

            c = new_start;
            while((id = newVLA[c++])) { /* see if there are any matches */
              if (active.find(id) != active.end()) {
                overlapping = true;
                break;
              }
            }

            if(overlapping) {
              /* overlapping, so merge */
              action = 0;

            } else {
              /* non-overlapping, so we need to figure out which goes first... */
              if(!GroupOrderKnown(G, curVLA, newVLA,
                                  cur_start, new_start, guide, &action)) {
                int c, id;
                ObjectMolecule *obj, *last_obj = NULL;
                c = cur_start;
                while((id = curVLA[c++])) {
                  auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                  if (eoo) {
                    obj = eoo->obj;
                    if(obj != last_obj) {
                      if(GroupOrderKnown(G, curVLA, newVLA,
                                         cur_start, new_start, obj, &action))
                        break;
                      else
                        last_obj = obj;
                    }
                  }
                }

                /* if order isn't set by now, then it doesn't matter...
                   so group new will go in first */
              }
            }
          }

          /* check assumptions */

          if((action < 1) && !(new_start < n_new))
            action = 1;
          else if((action > (-1)) && !(cur_start < n_cur))
            action = -1;

          /* take action */

          {
            int id;

            switch (action) {
            case -1:           /* insert new */
              if(new_start < n_new) {
                while((id = newVLA[new_start])) {
                  if (used.find(id) == used.end()) {
                    used.insert(id);
                    VLACheck(result, int, n_result);
                    result[n_result] = id;
                    n_result++;
                  }
                  new_start++;
                }
                while((new_start < n_new) && (!newVLA[new_start]))
                  new_start++;
              }
              VLACheck(result, int, n_result);
              result[n_result] = 0;
              n_result++;
              break;
            case 0:            /* merge, with cur going first */
              if(new_start < n_new) {
                while((id = newVLA[new_start])) {
                  if (used.find(id) == used.end()) {
                    used.insert(id);
                    VLACheck(result, int, n_result);
                    result[n_result] = id;
                    n_result++;
                  }
                  new_start++;
                }
                while((new_start < n_new) && (!newVLA[new_start]))
                  new_start++;
              }
              if(cur_start < n_cur) {
                while((id = curVLA[cur_start])) {
                  if (used.find(id) == used.end()) {
                    used.insert(id);
                    VLACheck(result, int, n_result);
                    result[n_result] = id;
                    n_result++;
                  }
                  cur_start++;
                }
                while((cur_start < n_cur) && (!curVLA[cur_start]))
                  cur_start++;
              }
              VLACheck(result, int, n_result);
              result[n_result] = 0;
              n_result++;
              break;
            case 1:            /* insert cur */
              if(cur_start < n_cur) {
                while((id = curVLA[cur_start])) {
                  if (used.find(id) == used.end()) {
                    used.insert(id);
                    VLACheck(result, int, n_result);
                    result[n_result] = id;
                    n_result++;
                  }
                  cur_start++;
                }
                while((cur_start < n_cur) && (!curVLA[cur_start]))
                  cur_start++;
                VLACheck(result, int, n_result);
                result[n_result] = 0;
                n_result++;
              }
              break;
            }
          }
        }
      }
    }
  }
  if(result && n_result && (!result[n_result - 1])) {
    VLACheck(result, int, n_result);
    result[n_result] = 0;
    n_result++;
  }
  VLASize(result, int, n_result);
  return result;
}

static PyObject *ObjectAlignmentStateAsPyList(ObjectAlignmentState * I)
{
  PyObject *result = NULL;

  result = PyList_New(2);
  if(I->alignVLA) {
    PyList_SetItem(result, 0, PConvIntVLAToPyList(I->alignVLA));
  } else {
    PyList_SetItem(result, 0, PConvAutoNone(NULL));
  }
  PyList_SetItem(result, 1, PyString_FromString(I->guide));
  return (PConvAutoNone(result));
}

static PyObject *ObjectAlignmentAllStatesAsPyList(ObjectAlignment * I)
{

  PyObject *result = NULL;
  int a;
  result = PyList_New(I->getNFrame());
  for(a = 0; a < I->getNFrame(); a++) {
    PyList_SetItem(result, a, ObjectAlignmentStateAsPyList(I->State.data() + a));
  }
  return (PConvAutoNone(result));

}

static int ObjectAlignmentStateFromPyList(PyMOLGlobals * G, ObjectAlignmentState * I,
                                          PyObject * list, int version)
{
  int ok = true;
  int ll = 0;
  if(ok)
    ok = (list != NULL);
  if(ok)
    ok = PyList_Check(list);
  if(ok)
    ll = PyList_Size(list);
  /* TO SUPPORT BACKWARDS COMPATIBILITY...
     Always check ll when adding new PyList_GetItem's */
  if(ok && (ll > 1)) {
    PConvPyListToIntVLA(PyList_GetItem(list, 0), &I->alignVLA);
    strcpy(I->guide, PyString_AsString(PyList_GetItem(list, 1)));

    for (auto& align : I->alignVLA) {
      if (align) {
        align = SettingUniqueConvertOldSessionID(G, align);
      }
    }
  }
  return (ok);
}

static int ObjectAlignmentAllStatesFromPyList(ObjectAlignment * I, PyObject * list,
                                              int version)
{
  int ok = true;
  int a;
  if(ok)
    ok = PyList_Check(list);
  if(ok) {
    int nstates = PyList_Size(list);
    I->State.resize(nstates);
    for(a = 0; a < nstates; a++) {
      auto *val = PyList_GetItem(list, a);
      ok =
        ObjectAlignmentStateFromPyList(I->G, I->State.data() + a, val,
                                       version);
      if(!ok)
        break;
    }
  }
  return (ok);
}

int ObjectAlignmentNewFromPyList(PyMOLGlobals * G, PyObject * list,
                                 ObjectAlignment ** result, int version)
{
  int ok = true;
  ObjectAlignment *I = NULL;
  (*result) = NULL;
  if(ok)
    ok = (list != NULL);
  if(ok)
    ok = PyList_Check(list);

  I = new ObjectAlignment(G);
  if(ok)
    ok = (I != NULL);

  if(ok){
    auto *val = PyList_GetItem(list, 0);
    ok = ObjectFromPyList(G, val, I);
  }
  if(ok)
    ok = ObjectAlignmentAllStatesFromPyList(I, PyList_GetItem(list, 2), version);
  if(ok) {
    (*result) = I;
    ObjectAlignmentRecomputeExtent(I);
  } else {
    /* cleanup? */
  }
  return (ok);
}

PyObject *ObjectAlignmentAsPyList(ObjectAlignment * I)
{
  PyObject *result = NULL;

  result = PyList_New(3);
  PyList_SetItem(result, 0, ObjectAsPyList(I));
  PyList_SetItem(result, 1, PyInt_FromLong(I->getNFrame()));
  PyList_SetItem(result, 2, ObjectAlignmentAllStatesAsPyList(I));

  return (PConvAutoNone(result));
}


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

void ObjectAlignmentRecomputeExtent(ObjectAlignment * I)
{
  float mx[3], mn[3];
  int extent_flag = false;
  int a;
  for(a = 0; a < I->getNFrame(); a++)
    if(I->State[a].primitiveCGO) {
      if(CGOGetExtent(I->State[a].primitiveCGO.get(), mn, mx)) {
        if(!extent_flag) {
          extent_flag = true;
          copy3f(mx, I->ExtentMax);
          copy3f(mn, I->ExtentMin);
        } else {
          max3f(mx, I->ExtentMax, I->ExtentMax);
          min3f(mn, I->ExtentMin, I->ExtentMin);
        }
      }
    }
  I->ExtentFlag = extent_flag;
}


/*========================================================================*/
void ObjectAlignment::update()
{
  auto I = this;
  int update_needed = false;
  {
    int a;
    for(a = 0; a < getNFrame(); a++) {
      ObjectAlignmentState *oas = I->State.data() + a;
      if(!oas->valid){
        update_needed = true;
      }
    }
  }
  if(update_needed) {
    {
      int a;
      for(a = 0; a < getNFrame(); a++) {
        ObjectAlignmentState *oas = I->State.data() + a;
	if(!oas->valid){
          ObjectMolecule *guide_obj = NULL;
          if(oas->guide[0]) {
            guide_obj = ExecutiveFindObjectMoleculeByName(G, oas->guide);
          }
          if(I->SelectionState == a)
            I->SelectionState = -1;

          oas->primitiveCGO.reset();

          oas->id2tag.clear();

          {
            CGO *cgo = CGONew(G);

            if(oas->alignVLA) {
              int id, b = 0, c;
              auto& vla = oas->alignVLA;
              int n_id = vla.size();
              float mean[3], vert[3], gvert[3];
              int n_coord = 0;
              int tag = SELECTOR_BASE_TAG + 1;
              auto& id2tag = oas->id2tag;

              CGOBegin(cgo, GL_LINES);

              while(b < n_id) {

                int gvert_valid;
                while((b < n_id) && (!vla[b]))
                  b++;

                if(!(b < n_id))
                  break;

                c = b;
                n_coord = 0;
                gvert_valid = false;
                zero3f(mean);
                while((id = vla[c++])) {
                  auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                  if (eoo) {
                    if(ObjectMoleculeGetAtomVertex(eoo->obj, a,
                                                   eoo->atm, vert)) {
                      n_coord++;
                      add3f(vert, mean, mean);
                      if(eoo->obj == guide_obj) {
                        copy3f(vert, gvert);
                        gvert_valid = true;
                      }
                    }
                  }
                }

                if(n_coord > 2) {       /* >2 points, then draw to mean or guide vertex */
                  float scale = 1.0F / n_coord;

                  scale3f(mean, scale, mean);

                  c = b;
                  while((id = vla[c++])) {
                    auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                    if (eoo) {
                      if(ObjectMoleculeGetAtomVertex(eoo->obj, a,
                                                     eoo->atm, vert)) {
                        if(gvert_valid) {
                          if(eoo->obj != guide_obj) {
                            cgo->add<cgo::draw::line>(gvert, vert);
                          }
                        } else {
                          cgo->add<cgo::draw::line>(mean, vert);
                        }
                      }
                    }
                  }
                } else if(n_coord) {    /* if 2 points, then simply draw a line */
                  float first[3];
                  int first_flag = true;
                  c = b;
                  while((id = vla[c++])) {
                    auto eoo = ExecutiveUniqueIDAtomDictGet(G, id);
                    if (eoo) {
                      if(ObjectMoleculeGetAtomVertex(eoo->obj, a,
                                                     eoo->atm, vert)) {
                        if(first_flag) {
                          copy3f(vert, first);
                          first_flag = false;
                        } else {
                          cgo->add<cgo::draw::line>(first, vert);
                        }
                      }
                    }
                  }
                }
                /* update the it2tag dictionary */

                tag++;

                while((b < n_id) && vla[b]) {
                  id2tag[vla[b]] = tag;
                  b++;
                }
              }
              CGOEnd(cgo);
            }

            CGOStop(cgo);
            oas->primitiveCGO.reset(cgo);
            if (!CGOHasOperationsOfType(oas->primitiveCGO.get(), cgo::draw::line::op_code)){
              oas->primitiveCGO.reset();
            }
          }
          oas->valid = true;
        }
      }
    }
  }
  if(I->SelectionState < 0) {
    int state = -1;
    if(I->ForceState >= 0) {
      state = I->ForceState;
      I->ForceState = 0;
    } else {
      state = I->getCurrentState();
    }
    // TODO do these fallbacks make any sense?
    if(state < 0)
      state = SceneGetState(G);
    if(state >= I->getNFrame())
      state = I->getNFrame() - 1;
    if(state < 0)
      state = 0;
    if(state < I->getNFrame()) {
      ObjectAlignmentState *oas = I->State.data() + state;
      if(!oas->id2tag.empty()) {
        SelectorDelete(G, I->Name);
        SelectorCreateFromTagDict(G, I->Name, oas->id2tag, false);
        I->SelectionState = state;
      }
    }
  }
  SceneInvalidate(I->G);
}


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

int ObjectAlignment::getNFrame() const
{
  return State.size();
}


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

void ObjectAlignment::render(RenderInfo * info)
{
  auto I = this;
  int state = info->state;
  CRay *ray = info->ray;
  auto pick = info->pick;
  const RenderPass pass = info->pass;
  ObjectAlignmentState *sobj = NULL;
  const float *color;

  ObjectPrepareContext(I, info);

  color = ColorGet(G, I->Color);

  if (pick)
    return;

  if(pass == RenderPass::Opaque || ray) {
    if((I->visRep & cRepCGOBit)) {

      for(StateIterator iter(G, I->Setting.get(), state, I->getNFrame()); iter.next();) {
        sobj = I->State.data() + iter.state;

        if (!sobj->primitiveCGO)
          continue;

	if(ray) {
	    CGORenderRay(sobj->primitiveCGO.get(), ray, info, color, NULL, I->Setting.get(), NULL);
	} else if(G->HaveGUI && G->ValidContext) {
#ifndef PURE_OPENGL_ES_2
	  if(!info->line_lighting)
	    glDisable(GL_LIGHTING);
#endif
	  SceneResetNormal(G, true);
          bool use_shader = SettingGetGlobal_b(G, cSetting_use_shaders);

          CGO * cgo = NULL;

          if (use_shader) {
            bool as_cylinders =
              SettingGetGlobal_b(G, cSetting_alignment_as_cylinders) &&
              SettingGetGlobal_b(G, cSetting_render_as_cylinders);

            bool trilines = !as_cylinders && SettingGetGlobal_b(G, cSetting_trilines);

            if (sobj->renderCGO && (
                  (as_cylinders ^ sobj->renderCGO_has_cylinders) ||
                  (trilines ^ sobj->renderCGO_has_trilines))){
              sobj->renderCGO.reset();
            }

            if (!sobj->renderCGO) {
              int shader =
                as_cylinders    ? GL_CYLINDER_SHADER :
                trilines        ? GL_TRILINES_SHADER : GL_LINE_SHADER;

              CGO *tmpCGO = CGONew(G), *tmp2CGO = NULL;
              CGOEnable(tmpCGO, shader);
              CGOSpecial(tmpCGO, SET_ALIGNMENT_UNIFORMS_ATTRIBS);

              if (as_cylinders) {
                tmp2CGO = CGOConvertLinesToCylinderShader(sobj->primitiveCGO.get(), tmpCGO, false);
              } else if (trilines) {
                tmp2CGO = CGOConvertToTrilinesShader(sobj->primitiveCGO.get(), tmpCGO, false);
              } else {
                tmp2CGO = CGOConvertToLinesShader(sobj->primitiveCGO.get(), tmpCGO, false);
              }

              tmpCGO->free_append(tmp2CGO);

              CGODisable(tmpCGO, shader);

              sobj->renderCGO.reset(tmpCGO);
              sobj->renderCGO_has_cylinders = as_cylinders;
              sobj->renderCGO_has_trilines = trilines;
            }

            cgo = sobj->renderCGO.get();
          } else {
            cgo = sobj->primitiveCGO.get();
          }

          if (cgo) {
            CGORender(cgo, color, I->Setting.get(), NULL, info, NULL);
          }

#ifndef PURE_OPENGL_ES_2
	  glEnable(GL_LIGHTING);
#endif
	}
      }
    }
  }
}

void ObjectAlignment::invalidate(cRep_t rep, cRepInv_t level, int state)
{
  if((rep == cRepAll) || (rep == cRepCGO)) {
    for(StateIterator iter(G, Setting.get(), state, getNFrame()); iter.next();) {
      ObjectAlignmentState& sobj = State[iter.state];
      sobj.valid = false;
      sobj.renderCGO.reset();
    }
  }
}


/*========================================================================*/
ObjectAlignment::ObjectAlignment(PyMOLGlobals * G) : pymol::CObject(G)
{
  type = cObjectAlignment;
}


/*========================================================================*/
ObjectAlignment *ObjectAlignmentDefine(PyMOLGlobals * G,
                                       ObjectAlignment * obj,
                                       const pymol::vla<int>& align_vla,
                                       int state,
                                       int merge,
                                       ObjectMolecule * guide, ObjectMolecule * flush)
{
  ObjectAlignment *I = NULL;

  if(obj) {
    if(obj->type != cObjectAlignment)       /* TODO: handle this */
      obj = NULL;
  }
  if(!obj) {
    I = new ObjectAlignment(G);
  } else {
    I = obj;
    I->invalidate(cRepAll, cRepInvRep, state);
  }
  if(state < 0)
    state = I->getNFrame();

  VecCheck(I->State, state);

  {
    ObjectAlignmentState *oas = I->State.data() + state;
    oas->valid = false;
    if(guide) {
      strcpy(oas->guide, guide->Name);
    }
    if(align_vla.data()) {
      if(merge && oas->alignVLA) {
        int *new_vla = AlignmentMerge(G, oas->alignVLA.data(), align_vla.data(), guide, flush);
        if(new_vla) {
          oas->alignVLA = pymol::vla_take_ownership(new_vla);
        }
      } else {
        oas->alignVLA = align_vla;
      }
    } else {
      VLAFreeP(oas->alignVLA);
    }
  }
  if(I) {
    ObjectAlignmentRecomputeExtent(I);
  }
  SceneChanged(G);
  SceneCountFrames(G);
  return (I);
}

pymol::CObject* ObjectAlignment::clone() const
{
  return new ObjectAlignment(*this);
}
