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


/* meaning of defines 

_PYMOL_MONOLITHIC: means that we're building PyMOL and its Python C
dependencies as one C library.  That means we need to explicitly call
the initialization functions for these libraries on startup.

*/
#include"os_python.h"

#include"os_predef.h"
#include"os_std.h"
#include"Base.h"


/* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
#include"os_proprietary.h"
#include<process.h>
#endif

/* END PROPRIETARY CODE SEGMENT */

#ifdef _PYMOL_MINGW
#define putenv _putenv
#endif

#include"os_time.h"
#include"os_unix.h"

#include"MemoryDebug.h"
#include"Base.h"
#include"Err.h"
#include"P.h"
#include"PConv.h"
#include"Ortho.h"
#include"Cmd.h"
#include"main.h"
#include"AtomInfo.h"
#include"CoordSet.h"
#include"Util.h"
#include"Executive.h"
#include"PyMOLOptions.h"
#include"PyMOL.h"
#include "Lex.h"
#include "Seeker.h"
#include "Feedback.h"

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

#include <memory>

/**
 * Use with functions which return a PyObject - if they return nullptr, then an
 * unexpected exception has occured.
 */
#define ASSERT_PYOBJECT_NOT_NULL(obj, return_value)                            \
  if (!obj) {                                                                  \
    PyErr_Print();                                                             \
    return return_value;                                                       \
  }

static int label_copy_text(char *dst, const char *src, int len, int max)
{
  dst += len;
  while(len < max) {
    if(!*src)
      break;
    *(dst++) = *(src++);
    len++;
  }
  *dst = 0;
  return len;
}

static int label_next_token(WordType dst, const char **expr)
{
  const char *p = *expr;
  char *q = dst;
  char ch;
  int tok_len = 0;
  int tok_max = sizeof(WordType) - 1;

  /* skip leading whitespace (if any) */

  while((ch = *p)) {
    if(ch > 33)
      break;
    p++;
  }

  /* copy the token */

  while((ch = *p)) {
    if(((ch >= 'a') && (ch <= 'z')) ||
       ((ch >= 'A') && (ch <= 'Z')) || ((ch >= '0') && (ch <= '9')) || ((ch == '_'))) {
      if(tok_len < tok_max) {
        *(q++) = ch;
        tok_len++;
      }
    } else {
      break;
    }
    p++;
  }
  *q = 0;
  if(p != *expr)
    *expr = p;
  else if(*p)
    *expr = p + 1;              /* always advance input by at least one character */

  /* let caller know whether we read anything */

  return (q != dst);
}

int PLabelExprUsesVariable(PyMOLGlobals * G, const char *expr, const char *var)
{
  char ch, quote = 0;
  int escaped = false;
  while((ch = *(expr++))) {
    if(!quote) {
      if(ch == '\'') {
        quote = ch;
      } else if(ch == '"') {
        quote = ch;
      } else if((ch < 33) || (ch == '+') || (ch == '(') || (ch == ')')) {
        /* nop */
      } else if(ch > 32) {
        WordType tok;
        expr--;
        if(label_next_token(tok, &expr)) {
          if(!strcmp(tok, var)) {
	    return 1;
	  }
	}
      }
    } else {
      if(ch == quote) {
        quote = 0;
      } else if(ch == '\\') {
        if(!escaped) {
          escaped = true;
        } else {
          escaped = false;
        }
      }
    }
  }
  return 0;
}

int PLabelAtomAlt(PyMOLGlobals * G, AtomInfoType * at, const char *model, const char *expr, int index)
{
  /* alternate C implementation which bypasses Python expressions -- works
     only for simple label formats "..."+property+... */

  int result = false;
  OrthoLineType label;
  int label_len = 0;
  int label_max = sizeof(OrthoLineType);
  OrthoLineType buffer;
  char ch, quote = 0;
  int escaped = false;
  const char *origexpr = expr;

  label[0] = 0;
  while((ch = *(expr++))) {
    if(!quote) {
      if(ch == '\'') {
        quote = ch;
      } else if(ch == '"') {
        quote = ch;
      } else if((ch < 33) || (ch == '+') || (ch == '(') || (ch == ')')) {
        /* nop */
      } else if(ch > 32) {
        WordType tok;
	int tokresult = true;
        expr--;
        if(label_next_token(tok, &expr)) {
          /* brain-dead linear string matching */
          buffer[0] = 0;
          if(!strcmp(tok, "model")) {
            label_len = label_copy_text(label, model, label_len, label_max);
          } else if(!strcmp(tok, "index")) {
            sprintf(buffer, "%d", index + 1);
          } else if(!strcmp(tok, "type")) {
            if(at->hetatm)
              label_len = label_copy_text(label, "HETATM", label_len, label_max);
            else
              label_len = label_copy_text(label, "ATOM", label_len, label_max);
          } else if(!strcmp(tok, "name")) {
            label_len = label_copy_text(label, LexStr(G, at->name), label_len, label_max);
          } else if(!strcmp(tok, "resn")) {
            label_len = label_copy_text(label, LexStr(G, at->resn), label_len, label_max);
          } else if(!strcmp(tok, "resi")) {
            sprintf(buffer, "%d%c", at->resv, at->inscode);
          } else if(!strcmp(tok, "resv")) {
            sprintf(buffer, "%d", at->resv);
          } else if(!strcmp(tok, "chain")) {
            label_len = label_copy_text(label, LexStr(G, at->chain), label_len, label_max);
          } else if(!strcmp(tok, "alt")) {
            label_len = label_copy_text(label, at->alt, label_len, label_max);
          } else if(!strcmp(tok, "segi")) {
            label_len = label_copy_text(label, LexStr(G, at->segi), label_len, label_max);
          } else if(!strcmp(tok, "ss")) {
            label_len = label_copy_text(label, at->ssType, label_len, label_max);
          } else if(!strcmp(tok, "vdw")) {
            sprintf(buffer, "%1.2f", at->vdw);
          } else if(!strcmp(tok, "elec_radius")) {
            sprintf(buffer, "%1.2f", at->elec_radius);
          } else if(!strcmp(tok, "text_type")) {
            const char *st = LexStr(G, at->textType);
            label_len = label_copy_text(label, st, label_len, label_max);
          } else if(!strcmp(tok, "custom")) {
            const char *st = LexStr(G, at->custom);
            label_len = label_copy_text(label, st, label_len, label_max);
          } else if(!strcmp(tok, "elem")) {
            label_len = label_copy_text(label, at->elem, label_len, label_max);
          } else if(!strcmp(tok, "geom")) {
            sprintf(buffer, "%d", at->geom);
          } else if(!strcmp(tok, "valence")) {
            sprintf(buffer, "%d", at->valence);
          } else if(!strcmp(tok, "rank")) {
            sprintf(buffer, "%d", at->rank);
          } else if(!strcmp(tok, "flags")) {
            if(at->flags) {
              sprintf(buffer, "%X", at->flags);
            } else {
              strcpy(buffer, "0");
            }
          } else if(!strcmp(tok, "q")) {
            sprintf(buffer, "%1.2f", at->q);
          } else if(!strcmp(tok, "b")) {
            sprintf(buffer, "%1.2f", at->b);
          } else if(!strcmp(tok, "numeric_type")) {
            if(at->customType != cAtomInfoNoType)
              sprintf(buffer, "%d", at->customType);
            else {
              strcpy(buffer, "?");
            }
          } else if(!strcmp(tok, "partial_charge")) {
            sprintf(buffer, "%1.3f", at->partialCharge);
          } else if(!strcmp(tok, "formal_charge")) {
            sprintf(buffer, "%d", at->formalCharge);
          } else if(!strcmp(tok, "stereo")) {
            strcpy(buffer, AtomInfoGetStereoAsStr(at));
          } else if(!strcmp(tok, "color")) {
            sprintf(buffer, "%d", at->color);
          } else if(!strcmp(tok, "cartoon")) {
            sprintf(buffer, "%d", at->cartoon);
          } else if(!strcmp(tok, "ID")) {
            sprintf(buffer, "%d", at->id);
          } else if(!strcmp(tok, "str")) {
            /* nop */
          } else {
	    tokresult = false;
	  }
          if(buffer[0]) {
            label_len = label_copy_text(label, buffer, label_len, label_max);
          }
        } else {
	  if (tok[0]){
	    label_len = label_copy_text(label, "?", label_len, label_max);
	    label_len = label_copy_text(label, tok, label_len, label_max);
	  } else {
	    tokresult = false;
	  }
	}
	result |= tokresult;
      } else {
        if(label_len < label_max) {
          label[label_len] = '?';
          label_len++;
	  result = true;
        }
      }
    } else {
      if(ch == quote) {
        quote = 0;
	result = true;
      } else if(ch == '\\') {
        if(!escaped) {
          escaped = true;
        } else {
          if(label_len < label_max) {
            label[label_len] = ch;
            label_len++;
          }
          escaped = false;
        }
      } else {
        if(label_len < label_max) {
          label[label_len] = ch;
          label_len++;
          label[label_len] = 0;
        }
      }
    }
  }

  if (!result && !label[0]){
    // if label is not set, just use expression as a string for label
    strncpy(label, origexpr, OrthoLineLength);
    result = true;
  }

  LexDec(G, at->label);
  at->label = result ? LexIdx(G, label) : 0;

  return (result);
}

#ifndef _PYMOL_NOPY


/* all of the following Python objects must be invariant & global for the application */


/* these are module / module properties -- global and static for a given interpreter */


/* local to this C code module */

static PyObject *P_pymol = nullptr;
static PyObject *P_pymol_dict = nullptr;   /* must be refomed into globals and instance properties */
static PyObject *P_cmd = nullptr;

static PyObject *P_povray = nullptr;
static PyObject *P_traceback = nullptr;
static PyObject *P_parser = nullptr;

static PyObject *P_vfont = nullptr;

/* module import helper */

static
PyObject * PImportModuleOrFatal(const char * name) {
  PyObject * mod = PyImport_ImportModule(name);
  if(!mod) {
    fprintf(stderr, "PyMOL-Error: can't find '%s'\n", name);
    exit(EXIT_FAILURE);
  }
  return mod;
}

static
PyObject * PGetAttrOrFatal(PyObject * o, const char * name) {
  PyObject * attr = PyObject_GetAttrString(o, name);
  if(!attr) {
    fprintf(stderr, "PyMOL-Error: can't find '%s'\n", name);
    exit(EXIT_FAILURE);
  }
  return attr;
}

/* used elsewhere */

PyObject *P_menu = nullptr;        /* menu definitions are currently global */
PyObject *P_xray = nullptr;        /* okay as global */
PyObject *P_chempy = nullptr;      /* okay as global */
PyObject *P_models = nullptr;      /* okay as global */
PyObject *P_setting = nullptr;     /* okay as global -- just used for names */
PyObject *P_CmdException = nullptr;
PyObject *P_QuietException = nullptr;
PyObject *P_IncentiveOnlyException = nullptr;

static PyMappingMethods wrapperMappingMethods, settingMappingMethods;
static PyTypeObject Wrapper_Type = {
  PyVarObject_HEAD_INIT(NULL, 0)
  "wrapper.Wrapper",            /* tp_name */
  0,                            /* tp_basicsize */
};
static PyTypeObject settingWrapper_Type = {
  PyVarObject_HEAD_INIT(NULL, 0)
  "wrapper.SettingWrapper",     /* tp_name */
  0,                            /* tp_basicsize */
};

#ifdef _PYMOL_IP_PROPERTIES
#endif

/**
 * If `wob` is not in a valid state (outside iterate-family context), raise
 * an error and return false.
 */
static bool check_wrapper_scope(WrapperObject * wobj) {
  if (wobj && wobj->obj)
    return true;

  PyErr_SetString(PyExc_RuntimeError,
      "wrappers cannot be used outside the iterate-family commands");

  return false;
}

/**
 * key: Python int (setting index) or str (setting name)
 *
 * Return the setting index or -1 for unknown `key`
 *
 * Raise LookupError if `key` doesn't name a known setting
 */
static int get_and_check_setting_index(PyMOLGlobals * G, PyObject * key) {
  int setting_id;

  if(PyInt_Check(key)) {
    setting_id = PyInt_AS_LONG(key);
  } else {
    key = PyObject_Str(key);
    setting_id = SettingGetIndex(G, PyString_AS_STRING(key));
    Py_DECREF(key);
  }

  if (setting_id < 0 || setting_id >= cSetting_INIT) {
    PyErr_SetString(PyExc_LookupError, "unknown setting");
    return -1;
  }

  return setting_id;
}

/**
 * Access a setting with iterate et. al.
 *
 * s[key]
 *
 * obj: `s` object in iterate-family namespace
 *
 * Raise LookupError if `key` doesn't name a known setting
 */
static
PyObject *SettingWrapperObjectSubScript(PyObject *obj, PyObject *key){
  auto& wobj = reinterpret_cast<SettingPropertyWrapperObject*>(obj)->wobj;
  int setting_id;
  PyObject *ret = nullptr;

  if (!check_wrapper_scope(wobj)) {
    return nullptr;
  }

  auto G = wobj->G;

  if ((setting_id = get_and_check_setting_index(G, key)) == -1) {
    return nullptr;
  }

  if (wobj->idx >= 0){
    // atom-state level
    ret = SettingGetIfDefinedPyObject(G, wobj->cs, wobj->idx, setting_id);
  }

  if (!ret){
    // atom level
    ret = SettingGetIfDefinedPyObject(G, wobj->atomInfo, setting_id);

    if (!ret) {
      // object-state, object, or global
      ret = SettingGetPyObject(G,
          wobj->cs ? wobj->cs->Setting.get() : nullptr,
          wobj->obj->Setting.get(), setting_id);
    }
  }
  return PConvAutoNone(ret);
}

/**
 * Set an atom or atom-state level setting with alter or alter_state.
 *
 * s[key] = val
 *
 * obj: `s` object in cmd.alter/cmd.alter_state namespace
 *
 * Return 0 on success or -1 on failure.
 *
 * Raise TypeError if setting not modifiable in the current context, and
 * LookupError if `key` doesn't name a known setting
 */
static
int SettingWrapperObjectAssignSubScript(PyObject *obj, PyObject *key, PyObject *val){
  auto& wobj = reinterpret_cast<SettingPropertyWrapperObject*>(obj)->wobj;

  if (!check_wrapper_scope(wobj)) {
    return -1;
  }

  int setting_id;
  auto G = wobj->G;

  if (wobj->read_only){
    PyErr_SetString(PyExc_TypeError, "Use alter/alter_state to modify settings");
    return -1;
  }

  if ((setting_id = get_and_check_setting_index(G, key)) == -1) {
    return -1;
  }

  if (wobj->idx >= 0) {
    // atom-state level
    if(!SettingLevelCheck(G, setting_id, cSettingLevel_astate)) {
      PyErr_SetString(PyExc_TypeError,
          "only atom-state level settings can be set in alter_state function");
      return -1; // failure
    } else if (CoordSetSetSettingFromPyObject(G, wobj->cs, wobj->idx, setting_id, val)) {
    }
  } else {
    // atom level
    if(!SettingLevelCheck(G, setting_id, cSettingLevel_atom)) {
      PyErr_SetString(PyExc_TypeError,
          "only atom-level settings can be set in alter function");
      return -1; // failure
    } else if (AtomInfoSetSettingFromPyObject(G, wobj->atomInfo, setting_id, val)) {
      AtomInfoSettingGenerateSideEffects(G, wobj->obj, setting_id, wobj->atm);
    }
  }

  return 0; // success
}

#ifdef _PYMOL_IP_PROPERTIES
#endif

/**
 * Python iterator over atom or atom-state setting indices
 */
static PyObject* SettingWrapperObjectIter(PyObject *self)
{
  auto& wobj = reinterpret_cast<SettingPropertyWrapperObject*>(self)->wobj;

  if (!check_wrapper_scope(wobj)) {
    return nullptr;
  }

  int unique_id = wobj->atomInfo->unique_id;

  if (wobj->idx >= 0) {
    unique_id =
      wobj->cs->atom_state_setting_id ?
      wobj->cs->atom_state_setting_id[wobj->idx] : 0;
  }

  PyObject * items = SettingUniqueGetIndicesAsPyList(wobj->G, unique_id);
  PyObject * iter = PyObject_GetIter(items);
  Py_XDECREF(items);

  return iter;
}

#ifdef _PYMOL_IP_PROPERTIES
#endif

/**
 * Allows attribute-like syntax for item lookups
 *
 * o.key -> o[key] if `key` is not an attribute of `o`
 */
static PyObject* PyObject_GenericGetAttrOrItem(PyObject *o, PyObject *key) {
  PyObject *ret = PyObject_GenericGetAttr(o, key);
  if (!PyErr_Occurred())
    return ret;
  PyErr_Clear();
  return PyObject_GetItem(o, key);
}

/**
 * Allows attribute-like syntax for item assignment
 *
 * `o.key = value` -> `o[key] = value`
 */
static
int PyObject_GenericSetAttrAsItem(PyObject *o, PyObject *key, PyObject *value) {
  return PyObject_SetItem(o, key, value);
}

/**
 * Generic getter for member variable pointer at struct byte offset
 */
template <typename T, typename S>
static T* get_member_pointer(S* instance, size_t offset)
{
  return reinterpret_cast<T*>(reinterpret_cast<char*>(instance) + offset);
}
template <typename T, typename S>
static T const* get_member_pointer(S const* instance, size_t offset)
{
  return reinterpret_cast<T const*>(
      reinterpret_cast<char const*>(instance) + offset);
}

/**
 * Explicit valence of an atom, defined as the sum of bond orders.
 *
 * Delocalized/aromatic bonds count as order=1.5 (heuristic).
 *
 * Should be equivalent to:
 *   OBAtom::GetExplicitValence() [Open Babel 3.0]
 */
static int getExplicitValence(ObjectMolecule const* obj, size_t atm)
{
  int value = 0;

  for (auto const& item : AtomNeighbors(obj, atm)) {
    int const order = obj->Bond[item.bond].order;

    if (order == cBondOrderDeloc) {
      // simple rule which gets all aromatic C atoms right, but can
      // be wrong for example for neutral aromatic N atoms with degree 3 or
      // for neutral carboxy O atoms which PyMOL also assigns bond oder 4.
      value += 3;
    } else {
      value += 2 * order;
    }
  }

  return value / 2;
}

/**
 * Explicit degree of an Atom, defined as the number of directly-bonded
 * neighbors in the graph.
 *
 * Should be equivalent to:
 *   RDKit::Atom::getDegree()
 *   OBAtom::GetExplicitDegree() [Open Babel 3.0]
 */
static unsigned getExplicitDegree(ObjectMolecule const* obj, size_t atm)
{
  return AtomNeighbors(obj, atm).size();
}

/**
 * iterate-family namespace implementation: lookup
 *
 * Raise NameError if state attributes are accessed outside of iterate_state
 */
static
PyObject * WrapperObjectSubScript(PyObject *obj, PyObject *key){

  static PyObject * pystr_HETATM        = PyString_InternFromString("HETATM");
  static PyObject * pystr_ATOM          = PyString_InternFromString("ATOM");
  static PyObject * pystr_QuestionMark  = PyString_InternFromString("?");

  auto wobj = static_cast<WrapperObject*>(obj);

  if (!check_wrapper_scope(wobj))
    return nullptr;

  PyMOLGlobals* G = wobj->G;
  PyObject* ret = nullptr;

  auto const keyobj = unique_PyObject_ptr(PyObject_Str(key));
  auto const aprop = PyString_AS_STRING(keyobj.get());
  auto const ap = PyMOL_GetAtomPropertyInfo(G->PyMOL, aprop);

  if (ap) {
#ifdef _PYMOL_IP_EXTRAS
    switch (ap->id) {
    case ATOM_PROP_STEREO:
      if (ObjectMoleculeUpdateMMStereoInfoForState(G, wobj->obj, wobj->state - 1) < 0) {
        PyErr_SetString(P_CmdException,
            "please install rdkit or set SCHRODINGER variable");
        return nullptr;
      }
      break;
    case ATOM_PROP_TEXT_TYPE:
#ifndef NO_MMLIBS
      ObjectMoleculeUpdateAtomTypeInfoForState(G, wobj->obj, wobj->state - 1, 1, 0);
#endif
      break;
    }
#endif

    switch (ap->Ptype){
    case cPType_string:
      ret = PyUnicode_FromString(
          get_member_pointer<char>(wobj->atomInfo, ap->offset));
      break;
    case cPType_schar:
      ret = PyLong_FromLong(
          *get_member_pointer<signed char>(wobj->atomInfo, ap->offset));
      break;
    case cPType_int:
      ret =
          PyLong_FromLong(*get_member_pointer<int>(wobj->atomInfo, ap->offset));
      break;
    case cPType_uint32:
      ret = PyLong_FromUnsignedLong(
          *get_member_pointer<uint32_t>(wobj->atomInfo, ap->offset));
      break;
    case cPType_int_as_string:
      ret = PyUnicode_FromString(LexStr(wobj->G,
          *get_member_pointer<lexborrow_t>(wobj->atomInfo, ap->offset)));
      break;
    case cPType_float:
      ret = PyFloat_FromDouble(
          *get_member_pointer<float>(wobj->atomInfo, ap->offset));
      break;
    case cPType_char_as_type:
      ret = PIncRef(wobj->atomInfo->hetatm ? pystr_HETATM : pystr_ATOM);
      break;
    case cPType_model:
      ret = PyUnicode_FromString(wobj->obj->Name);
      break;
    case cPType_index:
      ret = PyLong_FromLong(wobj->atm + 1);
      break;
    case cPType_int_custom_type: {
      auto val = *get_member_pointer<int>(wobj->atomInfo, ap->offset);
      if (val != cAtomInfoNoType) {
        ret = PyLong_FromLong(val);
      } else {
        ret = PIncRef(pystr_QuestionMark);
      }
    } break;
    case cPType_xyz_float:
      if (wobj->idx < 0) {
        PyErr_SetString(PyExc_NameError,
            "x/y/z only available in iterate_state and alter_state");
      } else {
        ret = PyFloat_FromDouble(wobj->cs->coordPtr(wobj->idx)[ap->offset]);
      }
      break;
    case cPType_settings:
      if (!wobj->settingWrapperObject) {
        wobj->settingWrapperObject = static_cast<SettingPropertyWrapperObject*>(
            PyType_GenericNew(&settingWrapper_Type, Py_None, Py_None));
        wobj->settingWrapperObject->wobj = wobj;
      }
      ret = PIncRef(wobj->settingWrapperObject);
      break;
    case cPType_properties:
#ifndef _PYMOL_IP_PROPERTIES
      PyErr_SetString(P_IncentiveOnlyException,
          "'properties/p' not supported in Open-Source PyMOL");
#else
      static_assert(false, "");
#endif
      break;
    case cPType_state:
      ret = PyLong_FromLong(wobj->state);
      break;
    default:
      switch (ap->id) {
      case ATOM_PROP_RESI: {
        char resi[8];
        AtomResiFromResv(resi, sizeof(resi), wobj->atomInfo);
        ret = PyUnicode_FromString(resi);
      } break;
      case ATOM_PROP_STEREO: {
        auto mmstereotype = AtomInfoGetStereoAsStr(wobj->atomInfo);
        ret = PyUnicode_FromString(mmstereotype);
      } break;
      case ATOM_PROP_ONELETTER: {
        const char* st = LexStr(G, wobj->atomInfo->resn);
        char abbr[2] = {SeekerGetAbbr(G, st, 'O', 'X'), 0};
        ret = PyUnicode_FromString(abbr);
      } break;
      case ATOM_PROP_EXPLICIT_DEGREE: {
        ret = PyLong_FromLong(getExplicitDegree(wobj->obj, wobj->atm));
      } break;
      case ATOM_PROP_EXPLICIT_VALENCE: {
        ret = PyLong_FromLong(getExplicitValence(wobj->obj, wobj->atm));
      } break;
      default:
        PyErr_SetString(PyExc_SystemError, "unhandled atom property type");
      }
    }
  } else {
    /* if not an atom property, check if local variable in dict */
    if (wobj->dict) {
      ret = PyDict_GetItem(wobj->dict, key); // Borrowed reference
    }
    if (ret) {
      Py_INCREF(ret);
    } else {
      PyErr_SetObject(PyExc_KeyError, key);
    }
  }

  return ret;
}

/**
 * Make IPython happy, which may call f_locals.get("__tracebackhide__", 0) and
 * crash if there is no get() method (the wrapper object is f_locals).
 */
static PyObject* WrapperObject_get(PyObject* self, PyObject* args)
{
  auto nargs = PyTuple_Size(args);
  assert(0 < nargs && nargs < 3);

  // Could call WrapperObjectSubScript here, but we don't really need that. To
  // fix the IPython issue, it's sufficient to return default or None.

  if (nargs == 2) {
    return PIncRef(PyTuple_GET_ITEM(args, 1));
  }

  Py_RETURN_NONE;
}

static PyMethodDef wrapperMethods[] = {
    {"get", WrapperObject_get, METH_VARARGS, nullptr},
    {nullptr},
};

/**
 * iterate-family namespace implementation: assignment
 *
 * Raise TypeError for read-only variables
 */
static
int WrapperObjectAssignSubScript(PyObject *obj, PyObject *key, PyObject *val){
  auto wobj = static_cast<WrapperObject*>(obj);

  if (!check_wrapper_scope(wobj)) {
    return -1;
  }

  auto G = wobj->G;

  const auto keyobj = unique_PyObject_ptr(PyObject_Str(key));
  const char* const aprop = PyString_AS_STRING(keyobj.get());
  const AtomPropertyInfo* ap = PyMOL_GetAtomPropertyInfo(G->PyMOL, aprop);

  if (ap) {
    if (wobj->read_only) {
      PyErr_SetString(
          PyExc_TypeError, "Use alter/alter_state to modify values");
      return -1;
    }

#ifdef _PYMOL_IP_EXTRAS
    if (wobj->cs) {
      switch (ap->id) {
      case ATOM_PROP_STEREO:
        wobj->cs->validMMStereo = MMPYMOLX_PROP_STATE_USER;
        break;
      case ATOM_PROP_TEXT_TYPE:
        wobj->cs->validTextType = MMPYMOLX_PROP_STATE_USER;
        break;
      }
    }
#endif

    bool changed = false;

    switch (ap->Ptype) {
    case cPType_string: {
      PyObject* valobj = PyObject_Str(val);
      const char* valstr = PyString_AS_STRING(valobj);
      char* dest = get_member_pointer<char>(wobj->atomInfo, ap->offset);
      if (strlen(valstr) > ap->maxlen) {
        strncpy(dest, valstr, ap->maxlen);
      } else {
        strcpy(dest, valstr);
      }
      Py_DECREF(valobj);
      changed = true;
    } break;
    case cPType_schar: {
      int valint = PyInt_AsLong(val);
      if (valint == -1 && PyErr_Occurred())
        return -1;
      *get_member_pointer<signed char>(wobj->atomInfo, ap->offset) = valint;
      changed = true;
    } break;
    case cPType_int: {
      int valint = PyInt_AsLong(val);
      if (valint == -1 && PyErr_Occurred())
        return -1;
      *get_member_pointer<int>(wobj->atomInfo, ap->offset) = valint;
      changed = true;
    } break;
    case cPType_uint32: {
      auto valint = PyLong_AsUnsignedLong(val);
      if (valint == -1 && PyErr_Occurred())
        return -1;
      *get_member_pointer<uint32_t>(wobj->atomInfo, ap->offset) = valint;
      changed = true;
    } break;
    case cPType_int_as_string: {
      auto dest = get_member_pointer<lexidx_t>(wobj->atomInfo, ap->offset);
      const auto valobj = unique_PyObject_ptr(PyObject_Str(val));
      const char* valstr = PyString_AS_STRING(valobj.get());
      LexAssign(G, *dest, valstr);
      changed = true;
    } break;
    case cPType_float:
      if (!PConvPyObjectToFloat(
              val, get_member_pointer<float>(wobj->atomInfo, ap->offset))) {
        return -1;
      }
      changed = true;
      break;
    case cPType_char_as_type: {
      const auto valobj = unique_PyObject_ptr(PyObject_Str(val));
      const char* valstr = PyString_AS_STRING(valobj.get());
      wobj->atomInfo->hetatm = ((valstr[0] == 'h') || (valstr[0] == 'H'));
      changed = true;
    } break;
    case cPType_int_custom_type: {
      const auto valobj = unique_PyObject_ptr(PyObject_Str(val));
      const char* valstr = PyString_AS_STRING(valobj.get());
      auto* dest = get_member_pointer<int>(wobj->atomInfo, ap->offset);
      if (valstr[0] == '?') {
        *dest = cAtomInfoNoType;
      } else {
        int valint = PyInt_AS_LONG(val);
        *dest = valint;
      }
      changed = true;
    } break;
    case cPType_xyz_float:
      if (wobj->idx < 0) {
        PyErr_SetString(PyExc_NameError, "x/y/z only available in alter_state");
        return -1;
      } else {
        float* v = wobj->cs->coordPtr(wobj->idx) + ap->offset;
        if (!PConvPyObjectToFloat(val, v)) {
          return -1;
        }
      }
      break;
    default:
      switch (ap->id) {
      case ATOM_PROP_RESI:
        if (PConvPyIntToInt(val, &wobj->atomInfo->resv)) {
          wobj->atomInfo->inscode = '\0';
        } else {
          const auto valobj = unique_PyObject_ptr(PyObject_Str(val));
          wobj->atomInfo->setResi(PyString_AS_STRING(valobj.get()));
        }
        break;
      case ATOM_PROP_STEREO: {
        const auto valobj = unique_PyObject_ptr(PyObject_Str(val));
        const char* valstr = PyString_AS_STRING(valobj.get());
        AtomInfoSetStereo(wobj->atomInfo, valstr);
      } break;
      default:
        PyErr_Format(PyExc_TypeError, "'%s' is read-only", aprop);
        return -1;
      }
    }

    if (changed) {
      switch (ap->id) {
      case ATOM_PROP_ELEM:
        wobj->atomInfo->protons = 0;
        wobj->atomInfo->vdw = 0;
        AtomInfoAssignParameters(wobj->G, wobj->atomInfo);
        break;
      case ATOM_PROP_RESV:
        wobj->atomInfo->inscode = '\0';
        break;
      case ATOM_PROP_SS:
        wobj->atomInfo->ssType[0] = toupper(wobj->atomInfo->ssType[0]);
        break;
      case ATOM_PROP_FORMAL_CHARGE:
        wobj->atomInfo->chemFlag = false;
        break;
      }
    }
  } else {
    /* if not an atom property, then its a local variable, store it */
    if (!wobj->dict) {
      wobj->dict = PyDict_New();
    }
    PyDict_SetItem(wobj->dict, key, val);
  }

  return 0; /* 0 success, -1 failure */
}

/* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
static PyObject *P_time = nullptr;
static PyObject *P_sleep = nullptr;
#endif

/* END PROPRIETARY CODE SEGMENT */

static void PUnlockAPIWhileBlocked(PyMOLGlobals * G);
static void PLockAPIWhileBlocked(PyMOLGlobals * G);

#define P_log_file_str "_log_file"

/**
 * Acquire the status lock (blocking)
 * @pre GIL
 */
void PLockStatus(PyMOLGlobals * G)
{                               /* assumes we have the GIL */
  assert(PyGILState_Check());

  PXDecRef(PYOBJECT_CALLMETHOD(G->P_inst->lock_api_status, "acquire", nullptr));
}

/**
 * Acquire the status lock (non-blocking)
 * @return True if lock could be acquired
 * @pre GIL
 */
int PLockStatusAttempt(PyMOLGlobals * G)
{                               /* assumes we have the GIL */
  assert(PyGILState_Check());

  unique_PyObject_ptr got_lock(
      PYOBJECT_CALLMETHOD(G->P_inst->lock_api_status, "acquire", "i", 0));

  ASSERT_PYOBJECT_NOT_NULL(got_lock, true);

  return PyObject_IsTrue(got_lock.get());
}

/**
 * Release the status lock
 * @pre GIL
 */
void PUnlockStatus(PyMOLGlobals * G)
{                               /* assumes we have the GIL */
  assert(PyGILState_Check());

  PXDecRef(PYOBJECT_CALLMETHOD(G->P_inst->lock_api_status, "release", nullptr));
}

/**
 * Acquire GLUT lock
 * @pre GIL
 */
static void PLockGLUT(PyMOLGlobals * G)
{                               /* assumes we have the GIL */
  PXDecRef(PYOBJECT_CALLMETHOD(G->P_inst->lock_api_glut, "acquire", nullptr));
}

/**
 * Release GLUT lock
 * @pre GIL
 */
static void PUnlockGLUT(PyMOLGlobals * G)
{                               /* assumes we have the GIL */
  PXDecRef(PYOBJECT_CALLMETHOD(G->P_inst->lock_api_glut, "release", nullptr));
}

static long P_glut_thread_id = -1;


/* enables us to keep glut out if by chance it grabs the API
 * in the middle of a nested API based operation */

static
void PCatchInit(void);


/*
  PyObject *GetBondsDict(PyMOLGlobals *G)
  {
  PyObject *result = nullptr;
  result = PyObject_GetAttrString(P_chempy,"bonds");
  if(!result) ErrMessage(G,"PyMOL","can't find 'chempy.bonds.bonds'");
  return(result);
  }
*/

PyObject *PGetFontDict(PyMOLGlobals * G, float size, int face, int style)
{                               /* assumes we have a valid interpreter lock */
  assert(PyGILState_Check());

  PyObject *result = nullptr;

  if(!P_vfont) {
    P_vfont = PyImport_ImportModule("pymol.vfont");
  }
  if(!P_vfont) {
    PRINTFB(G, FB_Python, FB_Errors)
      " PyMOL-Error: can't find module 'vfont'" ENDFB(G);
  } else {
    result = PYOBJECT_CALLMETHOD(P_vfont, "get_font", "fii", size, face, style);
  }
  return (PConvAutoNone(result));
}

int PComplete(PyMOLGlobals * G, char *str, int buf_size)
{
  assert(!PyGILState_Check());

  int ret = false;
  PyObject *result;
  const char *st2;
  PBlockAndUnlockAPI(G);
  if(G->P_inst->complete) {
    result = PYOBJECT_CALLFUNCTION(G->P_inst->complete, "s", str);
    if(result) {
      if(PyString_Check(result)) {
        st2 = PyString_AsString(result);
        UtilNCopy(str, st2, buf_size);
        ret = true;
      }
      Py_DECREF(result);
    }
  }
  PLockAPIAndUnblock(G);
  return (ret);
}

int PTruthCallStr0(PyObject * object, const char *method)
{
  assert(PyGILState_Check());

  int result = false;
  PyObject *tmp;
  tmp = PYOBJECT_CALLMETHOD(object, method, "");
  if(tmp) {
    if(PyObject_IsTrue(tmp))
      result = 1;
    Py_DECREF(tmp);
  }
  return (result);
}

int PTruthCallStr(PyObject * object, const char *method, const char *argument)
{
  assert(PyGILState_Check());

  int result = false;
  PyObject *tmp;
  tmp = PYOBJECT_CALLMETHOD(object, method, "s", argument);
  if(tmp) {
    if(PyObject_IsTrue(tmp))
      result = 1;
    Py_DECREF(tmp);
  }
  return (result);
}

int PTruthCallStr1i(PyObject * object, const char *method, int argument)
{
  assert(PyGILState_Check());

  int result = false;
  PyObject *tmp;
  tmp = PYOBJECT_CALLMETHOD(object, method, "i", argument);
  if(tmp) {
    if(PyObject_IsTrue(tmp))
      result = 1;
    Py_DECREF(tmp);
  }
  return (result);
}

int PTruthCallStr1s(PyObject * object, const char *method, const char *argument)
{
  assert(PyGILState_Check());

  int result = false;
  PyObject *tmp;
  tmp = PYOBJECT_CALLMETHOD(object, method, "s", argument);
  if(tmp) {
    if(PyObject_IsTrue(tmp))
      result = 1;
    Py_DECREF(tmp);
  }
  return (result);
}

int PTruthCallStr4i(PyObject * object, const char *method, int a1, int a2, int a3, int a4)
{
  assert(PyGILState_Check());

  int result = false;
  PyObject *tmp;
  tmp = PYOBJECT_CALLMETHOD(object, method, "iiii", a1, a2, a3, a4);
  if(tmp) {
    if(PyObject_IsTrue(tmp))
      result = 1;
    Py_DECREF(tmp);
  }
  return (result);
}

PyObject *PXIncRef(PyObject * obj)
{
  assert(PyGILState_Check());

  if(!obj)
    obj = Py_None;
  Py_XINCREF(obj);
  return obj;
}

void PXDecRef(PyObject * obj)
{
  assert(PyGILState_Check());

  Py_XDECREF(obj);
}

static ov_status CacheCreateEntry(PyObject ** result, PyObject * input)
{
  assert(PyGILState_Check());

  ov_status status = OV_STATUS_FAILURE;
  if(input && PyTuple_Check(input)) {
    ov_size tuple_size = PyTuple_Size(input);
    ov_size tot_size = tuple_size;
    PyObject *hash_code = PyTuple_New(tuple_size);
    PyObject *entry = PyList_New(6);
    if(hash_code && entry) {
      /* compute hash codes & total input size */
      ov_size i;
      status = OV_STATUS_SUCCESS;
      for(i = 0; i < tuple_size; i++) {
        PyObject *item = PyTuple_GetItem(input, i);
        long hash_long;
        if(item != Py_None) {
          /* here we are assuming that different Python versions will
           * hash tuples of ints & floats in exactly the same way (at least to 31 bits of significance) */
          hash_long = 0x7FFFFFFF & PyObject_Hash(item); /* pos 32 bit # to preserve 32-bit/64-bit compat */
        } else {
          hash_long = 0;        /* None doesn't hash consistently from Python version to version */
        }
        PyTuple_SetItem(hash_code, i, PyInt_FromLong(hash_long));
        if(PyTuple_Check(item)) {
          tot_size += PyTuple_Size(item);
        }
      }
      PyList_SetItem(entry, 0, PyInt_FromLong(tot_size));
      PyList_SetItem(entry, 1, hash_code);
      PyList_SetItem(entry, 2, PXIncRef(input));
      PyList_SetItem(entry, 3, PXIncRef(nullptr));
      PyList_SetItem(entry, 4, PyInt_FromLong(0));      /* access count */
      PyList_SetItem(entry, 5, PyFloat_FromDouble(0.0));        /* timestamp */
    }
    if(!OV_OK(status)) {
      PXDecRef(hash_code);
      PXDecRef(entry);
    } else {
      *result = entry;
    }
  }
  if(PyErr_Occurred())
    PyErr_Print();
  return status;
}

ov_status PCacheSet(PyMOLGlobals * G, PyObject * entry, PyObject * output)
{
  assert(PyGILState_Check());

  ov_status status = OV_STATUS_FAILURE;
  if(G->P_inst->cache && output) {
    ov_size tuple_size = PyTuple_Size(output);
    ov_size tot_size = tuple_size + PyInt_AsLong(PyList_GetItem(entry, 0));
    status = OV_STATUS_SUCCESS;
    {
      ov_size i;
      for(i = 0; i < tuple_size; i++) {
        PyObject *item = PyTuple_GetItem(output, i);
        if(PyTuple_Check(item)) {
          tot_size += PyTuple_Size(item);
        }
      }
    }
    PyList_SetItem(entry, 0, PyInt_FromLong(tot_size)); /* update total size */
    PyList_SetItem(entry, 3, PXIncRef(output));
    PXDecRef(PYOBJECT_CALLMETHOD(G->P_inst->cmd, "_cache_set",
                                 "OiO", entry, SettingGetGlobal_i(G, cSetting_cache_max),
                                 G->P_inst->cmd));
    /* compute the hash codes */
  }
  if(PyErr_Occurred())
    PyErr_Print();
  return status;
}

ov_status PCacheGet(PyMOLGlobals * G,
                    PyObject ** result_output, PyObject ** result_entry, PyObject * input)
{
  assert(PyGILState_Check());

  ov_status status = OV_STATUS_NO;
  if(G->P_inst->cache) {
    PyObject *entry = nullptr;
    PyObject *output = nullptr;

    if(OV_OK(CacheCreateEntry(&entry, input))) {
      output = PYOBJECT_CALLMETHOD(G->P_inst->cmd, "_cache_get",
                                   "OOO", entry, Py_None, G->P_inst->cmd);
      if(output == Py_None) {
        Py_DECREF(output);
        output = nullptr;
      } else {
        status = OV_STATUS_YES;
      }
    }
    /* compute the hash codes */
    if(OV_OK(status)) {
      *result_entry = entry;
      *result_output = output;
    } else {
      PXDecRef(entry);
      PXDecRef(output);
    }
  }
  if(PyErr_Occurred())
    PyErr_Print();
  return status;
}

void PSleepWhileBusy(PyMOLGlobals * G, int usec)
{
  assert(!PyGILState_Check());

#ifndef WIN32
  struct timeval tv;
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: napping.\n" ENDFD;
  tv.tv_sec = 0;
  tv.tv_usec = usec;
  select(0, nullptr, nullptr, nullptr, &tv);
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: nap over.\n" ENDFD;
#else
  /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(P_sleep, "f", usec / 1000000.0));
  PUnblock(G);
  /* END PROPRIETARY CODE SEGMENT */
#endif
}

void PSleepUnlocked(PyMOLGlobals * G, int usec)
{                               /* can only be called by the glut process */
  assert(!PyGILState_Check());

#ifndef WIN32
  struct timeval tv;
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: napping.\n" ENDFD;
  tv.tv_sec = 0;
  tv.tv_usec = usec;
  select(0, nullptr, nullptr, nullptr, &tv);
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: nap over.\n" ENDFD;
#else
  /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(P_sleep, "f", usec / 1000000.0));
  PUnblock(G);
  /* END PROPRIETARY CODE SEGMENT */
#endif
}

void PSleep(PyMOLGlobals * G, int usec)
{                               /* can only be called by the glut process */
  assert(!PyGILState_Check());

#ifndef WIN32
  struct timeval tv;
  PUnlockAPIAsGlut(G);
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: napping.\n" ENDFD;
  tv.tv_sec = 0;
  tv.tv_usec = usec;
  select(0, nullptr, nullptr, nullptr, &tv);
  PRINTFD(G, FB_Threads)
    " PSleep-DEBUG: nap over.\n" ENDFD;
  PLockAPIAsGlut(G, true);
#else
  /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
  PBlockAndUnlockAPI(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(P_sleep, "f", usec / 1000000.0));
  PLockAPIAndUnblock(G);
  /* END PROPRIETARY CODE SEGMENT */
#endif

}

static PyObject *PCatchWrite(PyObject * self, PyObject * args);
static PyObject *PCatch_install(PyObject * self, PyObject * args);

static
void my_interrupt(int a)
{
  exit(EXIT_FAILURE);
}

void PDumpTraceback(PyObject * err)
{
  assert(PyGILState_Check());

  PYOBJECT_CALLMETHOD(P_traceback, "print_tb", "O", err);
}

void PDumpException()
{
  assert(PyGILState_Check());

  PYOBJECT_CALLMETHOD(P_traceback, "print_exc", "");
}

static
WrapperObject * WrapperObjectNew() {
  auto wobj = static_cast<WrapperObject*>(
      PyType_GenericNew(&Wrapper_Type, Py_None, Py_None));
  wobj->dict = nullptr;
  wobj->settingWrapperObject = nullptr;
#ifdef _PYMOL_IP_PROPERTIES
  wobj->propertyWrapperObject = nullptr;
#endif
  return wobj;
}

/**
 * @pre GIL
 */
int PAlterAtomState(PyMOLGlobals * G, PyObject *expr_co, int read_only,
                    ObjectMolecule *obj, CoordSet *cs, int atm, int idx,
                    int state, PyObject * space)
{
  assert(PyGILState_Check());

  int result = true;

  auto wobj = WrapperObjectNew();
  wobj->G = G;
  wobj->obj = obj;
  wobj->cs = cs;
  wobj->atomInfo = obj->AtomInfo + atm;
  wobj->atm = atm;
  wobj->idx = idx;
  wobj->read_only = read_only;
  wobj->state = state + 1;

  PXDecRef(PyEval_EvalCode((PyObject*) expr_co, space, (PyObject*) wobj));
  Py_DECREF(wobj);

  if(PyErr_Occurred()) {
    result = false;
  }
  return result;
}

int PAlterAtom(PyMOLGlobals* G, ObjectMolecule* obj, CoordSet* cs,
    PyObject* expr_co, int read_only, int atm, PyObject* space)
{
  int state = (obj->DiscreteFlag ? obj->AtomInfo[atm].discrete_state : 0) - 1;
  return PAlterAtomState(G, expr_co, read_only, obj, cs, atm, /* idx */ -1, state, space);
}

/**
 * String conversion which takes "label_digits" setting into account.
 */
static
int PLabelPyObjectToStrMaxLen(PyMOLGlobals * G, PyObject * obj, char *buffer, int maxlen)
{
  assert(PyGILState_Check());

  if (obj && PyFloat_Check(obj)) {
    snprintf(buffer, maxlen + 1, "%.*f",
        SettingGetGlobal_i(G, cSetting_label_digits),
        PyFloat_AsDouble(obj));
    return true;
  }
  return PConvPyObjectToStrMaxLen(obj, buffer, maxlen);
}

int PLabelAtom(PyMOLGlobals* G, ObjectMolecule* obj, CoordSet* cs,
    PyObject* expr_co, int atm)
{
  assert(PyGILState_Check());

  PyObject *P_inst_dict = G->P_inst->dict;
  OrthoLineType label;
  AtomInfoType * ai = obj->AtomInfo + atm;

  if (!expr_co){
    // unsetting label
    LexAssign(G, ai->label, 0);
    return true;
  }

  auto wobj = WrapperObjectNew();
  wobj->G = G;
  wobj->obj = obj;
  wobj->cs = cs;
  wobj->atomInfo = ai;
  wobj->atm = atm;
  wobj->idx = -1;
  wobj->read_only = true;

  if (obj->DiscreteFlag) {
    wobj->state = obj->AtomInfo[atm].discrete_state;
  } else {
    wobj->state = 0;
  }

  auto resultPyObject =
      unique_PyObject_ptr(PyEval_EvalCode(expr_co, P_inst_dict, wobj));

  if (PyErr_Occurred()) {
    return false;
  }

  if (!PLabelPyObjectToStrMaxLen(
          G, resultPyObject.get(), label, sizeof(OrthoLineType) - 1)) {
    if (!PyErr_Occurred()) {
      ErrMessage(G, "Label", "Aborting on error. Labels may be incomplete.");
    }

    return false;
  }

  LexAssign(G, ai->label, label);
  return true;
}

/**
 * - Release API lock with flushing
 * - Pop valid context
 * - Release GLUT lock
 * @pre no GIL
 */
void PUnlockAPIAsGlut(PyMOLGlobals * G)
{                               /* must call with unblocked interpreter */
  assert(!PyGILState_Check());

  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", 0, G->P_inst->cmd));  /* NOTE this may flush the command buffer! */
  PLockStatus(G);
  PyMOL_PopValidContext(G->PyMOL);
  PUnlockStatus(G);
  PUnlockGLUT(G);
  PUnblock(G);
}

/**
 * - Release API lock without flushing
 * - Pop valid context
 * - Release GLUT lock
 * @pre no GIL
 */
void PUnlockAPIAsGlutNoFlush(PyMOLGlobals * G)
{                               /* must call with unblocked interpreter */
  assert(!PyGILState_Check());

  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", -1, G->P_inst->cmd)); /* prevents flushing of the buffer */
  PLockStatus(G);
  PyMOL_PopValidContext(G->PyMOL);
  PUnlockStatus(G);
  PUnlockGLUT(G);
  PUnblock(G);
}

/**
 * Acquire API lock
 * @param block_if_busy If false and PyMOL is busy, do not block
 * @return False if API lock could not be acquired
 * @pre GIL
 */
static int get_api_lock(PyMOLGlobals * G, int block_if_busy)
{
  assert(PyGILState_Check());

  if (!block_if_busy) {
    unique_PyObject_ptr got_lock(
        PYOBJECT_CALLFUNCTION(G->P_inst->lock_attempt, "O", G->P_inst->cmd));

    ASSERT_PYOBJECT_NOT_NULL(got_lock, false);

    if (PyObject_IsTrue(got_lock.get())) {
      return true;
    }

    PLockStatus(G);
    bool busy = PyMOL_GetBusy(G->PyMOL, false);
    PUnlockStatus(G);

    if (busy) {
      return false;
    }
  }

  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->lock, "O", G->P_inst->cmd));
  return true;
}

/**
 * - Acquire GLUT lock
 * - Push valid context
 * - Acquire API lock
 * - Wait for glut_thread_keep_out == 0
 *
 * @param block_if_busy If false and PyMOL is busy, API lock is non-blocking
 * @return False if locks could not be acquired
 * @pre no GIL
 */
int PLockAPIAsGlut(PyMOLGlobals * G, int block_if_busy)
{
  assert(!PyGILState_Check());

  PBlock(G);

  PLockGLUT(G);

  PLockStatus(G);
  PyMOL_PushValidContext(G->PyMOL);
  PUnlockStatus(G);

  while (true) {
    if(!get_api_lock(G, block_if_busy)) {
      PLockStatus(G);
      PyMOL_PopValidContext(G->PyMOL);
      PUnlockStatus(G);
      PUnlockGLUT(G);
      PUnblock(G);
      return false;               /* busy -- so allow main to update busy status display (if any) */
    }

    if (G->P_inst->glut_thread_keep_out == 0) {
      break;
    }

    /* IMPORTANT: keeps the glut thread out of an API operation... */
    /* NOTE: the keep_out variable can only be changed or read by the thread
       holding the API lock, therefore it is safe even through increment
       isn't atomic. */

    PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", -1, G->P_inst->cmd));       /* prevent buffer flushing */
#ifndef WIN32
    {
      struct timeval tv;

      PUnblock(G);
      tv.tv_sec = 0;
      tv.tv_usec = 50000;
      select(0, nullptr, nullptr, nullptr, &tv);
      PBlock(G);
    }
#else
    /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
    PXDecRef(PYOBJECT_CALLFUNCTION(P_sleep, "f", 0.050));
    /* END PROPRIETARY CODE SEGMENT */
#endif
  }

  PUnblock(G);                  /* API is now locked, so we can free up Python... */

  return true;
}


/* THESE CALLS ARE REQUIRED FOR MONOLITHIC COMPILATION TO SUCCEED UNDER WINDOWS. */
#ifndef _PYMOL_EMBEDDED

#ifdef __cplusplus
extern "C" {
#endif

/* 
 *  void        initExtensionClass(void);
 *   void        initsglite(void);
 */
void init_champ(void);

#ifdef __cplusplus
}
#endif
#endif

#ifdef _PYMOL_MONOLITHIC
#ifndef _PYMOL_EMBEDDED

/* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
#ifdef _PYMOL_NUMPY_INIT
void init_numpy();
void initmultiarray();
void initarrayfns();
void initlapack_lite();
void initumath();
void initranlib();
#endif
#endif

/* END PROPRIETARY CODE SEGMENT */
#endif
#endif

#ifdef _PYMOL_MONOLITHIC
#ifndef _PYMOL_EMBEDDED
#ifdef __cplusplus
extern "C" {
#endif

/*
 * void        initExtensionClass(void);
 * void        initsglite(void);
 */
void init_champ(void);
#ifdef _PYMOL_PYOMM
void init_pyomm(void);
#endif

#ifdef __cplusplus
}
#endif
#endif
#endif


void PConvertOptions(CPyMOLOptions * rec, PyObject * options)
{
  assert(PyGILState_Check());

  const char *load_str;

  rec->pmgui = !PyInt_AsLong(PyObject_GetAttrString(options, "no_gui"));
  rec->internal_gui = PyInt_AsLong(PyObject_GetAttrString(options, "internal_gui"));
  rec->internal_feedback =
    PyInt_AsLong(PyObject_GetAttrString(options, "internal_feedback"));
  rec->show_splash = PyInt_AsLong(PyObject_GetAttrString(options, "show_splash"));
  rec->security = PyInt_AsLong(PyObject_GetAttrString(options, "security"));
  rec->game_mode = PyInt_AsLong(PyObject_GetAttrString(options, "game_mode"));
  rec->force_stereo = PyInt_AsLong(PyObject_GetAttrString(options, "force_stereo"));
  rec->winX = PyInt_AsLong(PyObject_GetAttrString(options, "win_x"));
  rec->winY = PyInt_AsLong(PyObject_GetAttrString(options, "win_y"));
  rec->winPX = PyInt_AsLong(PyObject_GetAttrString(options, "win_px"));
  rec->winPY = PyInt_AsLong(PyObject_GetAttrString(options, "win_py"));
  rec->blue_line = PyInt_AsLong(PyObject_GetAttrString(options, "blue_line"));
  rec->external_gui = PyInt_AsLong(PyObject_GetAttrString(options, "external_gui"));
  rec->siginthand = PyInt_AsLong(PyObject_GetAttrString(options, "sigint_handler"));
  rec->reuse_helper = PyInt_AsLong(PyObject_GetAttrString(options, "reuse_helper"));
  rec->auto_reinitialize =
    PyInt_AsLong(PyObject_GetAttrString(options, "auto_reinitialize"));
  rec->keep_thread_alive =
    PyInt_AsLong(PyObject_GetAttrString(options, "keep_thread_alive"));
  rec->quiet = PyInt_AsLong(PyObject_GetAttrString(options, "quiet"));
#ifdef _PYMOL_IP_EXTRAS
  rec->incentive_product = true;
  PyObject_SetAttrString(options, "incentive_product", PyInt_FromLong(1));
#else
  rec->incentive_product =
    PyInt_AsLong(PyObject_GetAttrString(options, "incentive_product"));
#endif
  rec->multisample = PyInt_AsLong(PyObject_GetAttrString(options, "multisample"));
  rec->window_visible = PyInt_AsLong(PyObject_GetAttrString(options, "window_visible"));
  rec->read_stdin = PyInt_AsLong(PyObject_GetAttrString(options, "read_stdin"));
  rec->presentation = PyInt_AsLong(PyObject_GetAttrString(options, "presentation"));
  rec->defer_builds_mode =
    PyInt_AsLong(PyObject_GetAttrString(options, "defer_builds_mode"));
  rec->full_screen = PyInt_AsLong(PyObject_GetAttrString(options, "full_screen"));
  load_str = PyString_AsString(PyObject_GetAttrString(options, "after_load_script"));
  rec->sphere_mode = PyInt_AsLong(PyObject_GetAttrString(options, "sphere_mode"));
  rec->stereo_capable = PyInt_AsLong(PyObject_GetAttrString(options, "stereo_capable"));
  rec->stereo_mode = PyInt_AsLong(PyObject_GetAttrString(options, "stereo_mode"));
  rec->zoom_mode = PyInt_AsLong(PyObject_GetAttrString(options, "zoom_mode"));
  rec->no_quit = PyInt_AsLong(PyObject_GetAttrString(options, "no_quit"));
  rec->launch_status = PyInt_AsLong(PyObject_GetAttrString(options, "launch_status"));
  rec->gldebug = PyInt_AsLong(PyObject_GetAttrString(options, "gldebug"));
  rec->openvr_stub = PyInt_AsLong(PyObject_GetAttrString(options, "openvr_stub"));

  if(load_str) {
    if(load_str[0]) {
      UtilNCopy(rec->after_load_script, load_str, PYMOL_MAX_OPT_STR);
    }
  }
  if(PyErr_Occurred()) {
    PyErr_Print();
  }
}

void PGetOptions(CPyMOLOptions * rec)
{
  assert(PyGILState_Check());

  PyObject *pymol, *invocation, *options;

  pymol = PImportModuleOrFatal("pymol");
  invocation = PGetAttrOrFatal(pymol, "invocation");     /* get a handle to the invocation module */
  options = PGetAttrOrFatal(invocation, "options");

  PConvertOptions(rec, options);
  Py_XDECREF(invocation);
  Py_XDECREF(options);
  Py_XDECREF(pymol);
}

/**
 * Runs a string in the namespace of the pymol global module
 * @pre GIL
 */
void PRunStringModule(PyMOLGlobals * G, const char *str)
{
  assert(PyGILState_Check());

  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->exec, "Os", P_pymol, str));
}

/**
 * Runs a string in the namespace of the pymol instance
 * @pre GIL
 */
void PRunStringInstance(PyMOLGlobals * G, const char *str)
{
  assert(PyGILState_Check());

  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->exec, "Os", G->P_inst->obj, str));
}

static void WrapperObjectDealloc(PyObject* self)
{
  auto wo = static_cast<WrapperObject*>(self);
  Py_XDECREF(wo->settingWrapperObject);
#ifdef _PYMOL_IP_PROPERTIES
  Py_XDECREF(wo->propertyWrapperObject);
#endif
  Py_XDECREF(wo->dict);
  self->ob_type->tp_free(self);
}

void PInit(PyMOLGlobals * G, int global_instance)
{
  assert(PyGILState_Check());

  /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
#ifdef _PYMOL_MONOLITHIC
#ifndef _PYMOL_EMBEDDED
#define _PYMOL_INIT_MODULES
#endif
#endif
#endif
  /* END PROPRIETARY CODE SEGMENT */

#ifdef _PYMOL_INIT_MODULES
  /* Win32 module build: includes pyopengl, numpy, and sglite */
  /* sglite 
   * initExtensionClass();
   * initsglite();
   */
  init_champ();
#ifdef _PYMOL_PYOMM
  init_pyomm();
#endif

  /* initialize numeric python */
#ifdef _PYMOL_NUMPY_INIT
  init_numpy();
  initmultiarray();
  initarrayfns();
  initlapack_lite();
  initumath();
  initranlib();
#endif
  /* initialize PyOpenGL */
#endif

  if(true /* global_instance */) {
    PCatchInit();               /* setup standard-output catch routine */
  }

  P_pymol = PImportModuleOrFatal("pymol");
  P_pymol_dict = PyModule_GetDict(P_pymol);
  Py_XINCREF(P_pymol_dict);
  if(!P_pymol_dict)
    ErrFatal(G, "PyMOL", "can't find globals for 'pymol'");

  if(global_instance) {         /* if global singleton PyMOL... */
    G->P_inst = pymol::calloc<CP_inst>(1);
    G->P_inst->obj = P_pymol;
    G->P_inst->dict = P_pymol_dict;
    {
      int a;
      SavedThreadRec *str = G->P_inst->savedThread;
      for(a = 0; a < MAX_SAVED_THREAD; a++) {
        (str++)->id = -1;
      }
    }
  }

  {
    G->P_inst->exec = PGetAttrOrFatal(P_pymol, "exec_str");

    if(global_instance) {
      PCatch_install(NULL, nullptr);
    }

    P_traceback = PImportModuleOrFatal("traceback");
    P_cmd = PImportModuleOrFatal("pymol.cmd");

    if(global_instance) {
      assert(SingletonPyMOLGlobals);
      /* implies global singleton pymol, so set up the global handle */
      PyObject_SetAttrString(P_cmd, "_COb",
          PyCapsule_New(&SingletonPyMOLGlobals, nullptr, nullptr));

      /* cmd module is itself the api for the global PyMOL instance */
      G->P_inst->cmd = P_cmd;
    }

    /* right now, all locks are global -- eventually some of these may
       become instance-specific in order to improve concurrency */

    G->P_inst->lock = PGetAttrOrFatal(G->P_inst->cmd, "lock");
    G->P_inst->lock_attempt = PGetAttrOrFatal(G->P_inst->cmd, "lock_attempt");
    G->P_inst->unlock = PGetAttrOrFatal(G->P_inst->cmd, "unlock");
    G->P_inst->lock_api_status = PGetAttrOrFatal(G->P_inst->cmd, "lock_api_status");
    G->P_inst->lock_api_glut = PGetAttrOrFatal(G->P_inst->cmd, "lock_api_glut");

    /* 'do' command */

    G->P_inst->cmd_do = PGetAttrOrFatal(G->P_inst->cmd, "do");

    /* cache */
    G->P_inst->cache = PyObject_GetAttrString(G->P_inst->obj, "_cache");

    /* invariant stuff */

    P_menu = PImportModuleOrFatal("pymol.menu");
    P_setting = PImportModuleOrFatal("pymol.setting");
    P_povray = PImportModuleOrFatal("pymol.povray");

#ifdef _PYMOL_XRAY
    P_xray = PImportModuleOrFatal("pymol.xray");
#endif

    /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
    P_time = PImportModuleOrFatal("time");
    P_sleep = PGetAttrOrFatal(P_time, "sleep");
#endif
    /* END PROPRIETARY CODE SEGMENT */

    P_parser = PImportModuleOrFatal("pymol.parser");

    {
      PyObject *fn_closure = PyObject_GetAttrString(P_parser, "new_parse_closure");
      G->P_inst->parse = PYOBJECT_CALLFUNCTION(fn_closure, "O", G->P_inst->cmd);
      PXDecRef(fn_closure);
      if(!G->P_inst->parse)
        ErrFatal(G, "PyMOL", "can't create 'parse' function closure");
    }

    {
      PyObject *fn_closure = PyObject_GetAttrString(P_parser, "new_complete_closure");
      G->P_inst->complete = PYOBJECT_CALLFUNCTION(fn_closure, "O", G->P_inst->cmd);
      PXDecRef(fn_closure);
      if(!G->P_inst->complete)
        ErrFatal(G, "PyMOL", "can't create 'complete' function closure");
    }

    {
      PyObject *fn_closure = PGetAttrOrFatal(P_pymol, "_colortype");
      G->P_inst->colortype = PYOBJECT_CALLFUNCTION(fn_closure, "O", G->P_inst->cmd);
      PXDecRef(fn_closure);
    }

    P_chempy = PImportModuleOrFatal("chempy");
    P_models = PImportModuleOrFatal("chempy.models");
    P_CmdException = PGetAttrOrFatal(P_pymol, "CmdException");
    P_QuietException = PGetAttrOrFatal(P_cmd, "QuietException");
    P_IncentiveOnlyException = PGetAttrOrFatal(P_pymol, "IncentiveOnlyException");

    /* backwards compatibility */

    PRunStringModule(G, "glutThread = thread.get_ident()");

    P_glut_thread_id = PyThread_get_thread_ident();

#ifndef WIN32
    if(G->Option->siginthand) {
      signal(SIGINT, my_interrupt);
    }
#endif

  if (!Wrapper_Type.tp_basicsize) {
    Wrapper_Type.tp_basicsize = sizeof(WrapperObject);
    Wrapper_Type.tp_dealloc = &WrapperObjectDealloc;
    Wrapper_Type.tp_flags = Py_TPFLAGS_DEFAULT;
    wrapperMappingMethods.mp_length = nullptr;
    wrapperMappingMethods.mp_subscript = &WrapperObjectSubScript;
    wrapperMappingMethods.mp_ass_subscript = &WrapperObjectAssignSubScript;
    Wrapper_Type.tp_as_mapping = &wrapperMappingMethods;
    Wrapper_Type.tp_methods = wrapperMethods;
    
    settingWrapper_Type.tp_basicsize = sizeof(SettingPropertyWrapperObject);
    settingWrapper_Type.tp_flags = Py_TPFLAGS_DEFAULT;
    settingWrapper_Type.tp_iter = &SettingWrapperObjectIter;
    settingMappingMethods.mp_length = nullptr;
    settingMappingMethods.mp_subscript = &SettingWrapperObjectSubScript;
    settingMappingMethods.mp_ass_subscript = &SettingWrapperObjectAssignSubScript;
    settingWrapper_Type.tp_as_mapping = &settingMappingMethods;
    settingWrapper_Type.tp_getattro = PyObject_GenericGetAttrOrItem;
    settingWrapper_Type.tp_setattro = PyObject_GenericSetAttrAsItem;
    
#ifdef _PYMOL_IP_PROPERTIES
#endif

    if (PyType_Ready(&Wrapper_Type) < 0
        || PyType_Ready(&settingWrapper_Type) < 0
        ){
      PRINTFB(G, FB_Python, FB_Errors)
	" PInit: Wrapper_Type, settingWrapper_Type, propertyWrapper_Type not ready\n" ENDFB(G);
      return;
    }
    Py_INCREF(&Wrapper_Type);
    Py_INCREF(&settingWrapper_Type);
  }
  }

#ifdef _PYMOL_NO_MSGPACKC
  // fallback MMTF support
  PyRun_SimpleString(
      "import pymol.importing;"
      "pymol.importing.loadable.mmtf = None;"
      "pymol.importing.loadfunctions.setdefault('mmtf',"
      "pymol.importing.load_mmtf)");
#endif
}

int PPovrayRender(PyMOLGlobals * G, const char *header, const char *inp, const char *file, int width,
                  int height, int antialias)
{
  assert(!PyGILState_Check());

  PyObject *result;
  int ok;
  PBlock(G);
  result =
    PYOBJECT_CALLMETHOD(P_povray, "render_from_string", "sssiii", header, inp, file,
                        width, height, antialias);
  ok = PyObject_IsTrue(result);
  Py_DECREF(result);
  PUnblock(G);
  return (ok);
}

void PFree(PyMOLGlobals * G)
{
  assert(PyGILState_Check());

  PXDecRef(G->P_inst->parse);
  PXDecRef(G->P_inst->complete);
  PXDecRef(G->P_inst->colortype);
}

/**
 * Terminates the application with a call to `exit(code)`.
 */
void PExit(PyMOLGlobals * G, int code)
{
  assert(!PyGILState_Check());

  ExecutiveDelete(G, "all");
  PBlock(G);

  PyMOL_PushValidContext(G->PyMOL);
  PyMOL_Stop(G->PyMOL);
  PyMOL_PopValidContext(G->PyMOL);

#ifndef _PYMOL_NO_MAIN
  if(G->Main) {
    MainFree();
  }
#endif

  PyMOL_Free(G->PyMOL);

#if 1
  /* we're having trouble with threading errors after calling Py_Exit,
     so for the time being, let's just take the process down at this
     point, instead of allowing PyExit to be called. */

  exit(code);
#else

  Py_Exit(code);
#endif
}

/**
 * Add `str` to the command queue
 */
void PParse(PyMOLGlobals * G, pymol::zstring_view str_view)
{
  OrthoCommandIn(G, str_view.c_str());
}

/**
 * Call `cmd.do(str)`
 *
 * Effectively calls `PParse(str.splitlines())` with logging and echo.
 */
void PDo(PyMOLGlobals * G, const char *str)
{                               /* assumes we already hold the re-entrant API lock */
  int blocked;
  PyObject *ret ;
  blocked = PAutoBlock(G);
  ret = PYOBJECT_CALLFUNCTION(G->P_inst->cmd_do, "s", str);
  Py_XDECREF(ret);
  PAutoUnblock(G, blocked);
}

/**
 * Write `str` to the log file (if one is open).
 *
 * str: command or expression to log
 * format: cPLog_pml (`str` is PyMOL command)
 *         cPLog_pym (`str` is Python expression)
 *         cPLog_no_flush (write `str` as is)
 *         cPLog_pml_lf (unused TODO remove?)
 *
 * See also equivalent Python impelemtation: cmd.log()
 */
void PLog(PyMOLGlobals * G, pymol::zstring_view str_view, int format)
{
  int mode;
  int a = sizeof(OrthoLineType) - 15;
  int blocked;
  PyObject *log;
  auto str = str_view.c_str();
  OrthoLineType buffer = "";
  mode = SettingGetGlobal_i(G, cSetting_logging);
  if(mode) {
    blocked = PAutoBlock(G);
    log = PyDict_GetItemString(P_pymol_dict, P_log_file_str);
    if(log && (log != Py_None)) {
      if(format == cPLog_no_flush) {
        PYOBJECT_CALLMETHOD(log, "write", "s", str);    /* maximize responsiveness (for real-time) */
      } else {
        switch (mode) {
        case cPLog_pml:        /* .pml file */
          switch (format) {
          case cPLog_pml_lf:
            strcpy(buffer, str);
            break;
          case cPLog_pml:
          case cPLog_pym:
            strcpy(buffer, str);
            strcat(buffer, "\n");
            break;
          }
          break;
        case cPLog_pym:        /* .pym file */
          if((str[0] == '_') && (str[1]) == ' ')
            str += 2;
          switch (format) {
          case cPLog_pml_lf:
            a = strlen(str);
            while(a && str[a - 1] < 32) a--; /* trim CR/LF etc. */
          case cPLog_pml:
            if (str[0] == '/') {
              strncat(buffer, str + 1, a - 1);
              strcat(buffer, "\n");
            } else {
              strcpy(buffer, "cmd.do('''");
              char * b = buffer + strlen(buffer);
              for (; a && *str; --a) {
                if (*str == '\\' || *str == '\'') {
                  *(b++) = '\\';
                }
                *(b++) = *(str++);
              }
              strcpy(b, "''')\n");
            }
            break;
          case cPLog_pym:
            strcpy(buffer, str);
            strcat(buffer, "\n");
            break;
          }
        }
        PYOBJECT_CALLMETHOD(log, "write", "s", buffer);
        PYOBJECT_CALLMETHOD(log, "flush", "");
      }
    }
    PAutoUnblock(G, blocked);
  }
}

/**
 * Call flush() on the open log file handle
 */
void PLogFlush(PyMOLGlobals * G)
{
  int mode;
  PyObject *log;
  int blocked;
  mode = SettingGetGlobal_i(G, cSetting_logging);
  if(mode) {
    blocked = PAutoBlock(G);
    log = PyDict_GetItemString(P_pymol_dict, P_log_file_str);
    if(log && (log != Py_None)) {
      PYOBJECT_CALLMETHOD(log, "flush", "");
    }
    PAutoUnblock(G, blocked);
  }
}

#define PFLUSH_REPORT_UNCAUGHT_EXCEPTIONS(G)                                   \
  if (PyErr_Occurred()) {                                                      \
    PyErr_Print();                                                             \
    PRINTFB(G, FB_Python, FB_Errors)                                           \
    " %s: Uncaught exception.  PyMOL may have a bug.\n", __func__ ENDFB(G);    \
  }

/**
 * Execute commands from the command queue.
 * If the current thread holds the GIL, do nothing. Otherwise the queue will
 * be empty after this call.
 * @return False if the queue was empty
 */
int PFlush(PyMOLGlobals * G)
{
  /* NOTE: ASSUMES unblocked Python threads and a locked API */
  if(!OrthoCommandWaiting(G))
    return false;

  if (PAutoBlock(G)) {
    if(!(PIsGlutThread() && G->P_inst->glut_thread_keep_out)) {
      /* don't run if we're currently banned */
      auto ortho = G->Ortho;
      while(!OrthoCommandIsEmpty(*ortho)){
        auto buffer = OrthoCommandOut(*ortho);
        OrthoCommandSetBusy(G, true);
        OrthoCommandNest(G, 1);

        // TODO if we release here, another thread might acquire the API lock
        // and block us when we try to lock again. (see PYMOL-3249)
        // Example: set_key F1, ray async=1
        //          => hitting F1 will block GUI updates, even though ray
        //             is running async
        // PUnlockAPIWhileBlocked(G);

        PFLUSH_REPORT_UNCAUGHT_EXCEPTIONS(G);
        PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->parse, "si", buffer.c_str(), 0));
        PFLUSH_REPORT_UNCAUGHT_EXCEPTIONS(G);

        // TODO see above
        // PLockAPIWhileBlocked(G);

        OrthoCommandSetBusy(G, false);
        /* make sure no commands left at this level */
        while(OrthoCommandWaiting(G))
          PFlushFast(G);
        OrthoCommandNest(G, -1);
      }
    }
    PUnblock(G);
  }
  return true;
}

/**
 * Execute commands from the command queue.
 * @return False if the queue was empty
 * @pre GIL
 * @post queue is empty
 */
int PFlushFast(PyMOLGlobals * G)
{
  assert(PyGILState_Check());

  /* NOTE: ASSUMES we currently have blocked Python threads and an unlocked API */
  int did_work = false;
  auto ortho = G->Ortho;
  while(!OrthoCommandIsEmpty(*ortho)){
    auto buffer = OrthoCommandOut(*ortho);
    OrthoCommandSetBusy(G, true);
    OrthoCommandNest(G, 1);
    did_work = true;

    PFLUSH_REPORT_UNCAUGHT_EXCEPTIONS(G);
    PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->parse, "si", buffer.c_str(), 0));
    PFLUSH_REPORT_UNCAUGHT_EXCEPTIONS(G);

    OrthoCommandSetBusy(G, false);
    /* make sure no commands left at this level */
    while(OrthoCommandWaiting(G))
      PFlushFast(G);
    OrthoCommandNest(G, -1);
  }
  return did_work;
}

/**
 * Acquire the GIL.
 * @pre no GIL
 * @post GIL
 */
void PBlock(PyMOLGlobals * G)
{
  assert(!PyGILState_Check());

  if(!PAutoBlock(G)) {
    ErrFatal(G, "PBlock", "Threading error detected.  Terminating...");
  }

  assert(PyGILState_Check());
}

/**
 * Acquire the GIL.
 * Return false if the current thread already holds the GIL.
 * @post GIL
 */
int PAutoBlock(PyMOLGlobals * G)
{
#ifndef _PYMOL_EMBEDDED
  SavedThreadRec *SavedThread = G->P_inst->savedThread;

  auto id = PyThread_get_thread_ident();

  for (auto a = MAX_SAVED_THREAD - 1; a; --a) {
    if (SavedThread[a].id == id) {
      assert(!PyGILState_Check());

      // Aquire GIL
      PyEval_RestoreThread(SavedThread[a].state);

      SavedThread[a].id = -1;

      assert(PyGILState_Check());
      return 1;
    }
  }

  assert(PyGILState_Check());
  return 0;
#else
  return 1;
#endif
}

/**
 * Can be called anytime, no lock conditions
 * @return True if the current thread is the GUI thread
 */
int PIsGlutThread(void)
{
  return (PyThread_get_thread_ident() == P_glut_thread_id);
}

/**
 * Release GIL
 * @pre GIL
 * @post no GIL
 */
void PUnblock(PyMOLGlobals * G)
{
#ifndef _PYMOL_EMBEDDED
  assert(PyGILState_Check());

  SavedThreadRec *SavedThread = G->P_inst->savedThread;
  auto a = MAX_SAVED_THREAD - 1;
  /* NOTE: ASSUMES a locked API */

  /* reserve a space while we have a lock */
  for (; a; --a) {
    if (SavedThread[a].id == -1) {
      SavedThread[a].id = PyThread_get_thread_ident();
      break;
    }
  }

  // Release GIL
  SavedThread[a].state = PyEval_SaveThread();

  assert(!PyGILState_Check());
#endif
}

/**
 * Release GIL if flag=true
 */
void PAutoUnblock(PyMOLGlobals * G, int flag)
{
  if(flag)
    PUnblock(G);
}

/**
 * Acquire GIL, release API lock and flush
 * @pre no GIL
 * @post GIL
 * @post no API lock
 */
void PBlockAndUnlockAPI(PyMOLGlobals * G)
{
  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", 0, G->P_inst->cmd));
}

/**
 * Acquire API lock
 * @param block_if_busy If false and PyMOL is busy, do not block
 * @return False if API lock could not be acquired
 * @pre no GIL
 */
int PLockAPI(PyMOLGlobals * G, int block_if_busy)
{
  int result = true;
  PBlock(G);
  if(block_if_busy) {
    PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->lock, "O", G->P_inst->cmd));
  } else {                      /* not blocking if PyMOL is busy */

    PyObject *got_lock =
      PYOBJECT_CALLFUNCTION(G->P_inst->lock_attempt, "O", G->P_inst->cmd);

    if(got_lock) {
      result = PyInt_AsLong(got_lock);
      Py_DECREF(got_lock);
    }
  }
  PUnblock(G);
  return result;
}

/**
 * Release API lock with flushing
 * @pre no GIL
 * @pre API lock
 */
void PUnlockAPI(PyMOLGlobals * G)
{
  PBlock(G);
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", 0, G->P_inst->cmd));
  PUnblock(G);
}

/**
 * Release API lock without flushing
 * @pre GIL
 * @pre API lock
 * @post no API lock
 */
static void PUnlockAPIWhileBlocked(PyMOLGlobals * G)
{
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->unlock, "iO", -1, G->P_inst->cmd));
}

/**
 * Acquire API lock
 * @pre GIL
 * @post GIL
 * @post API lock
 */
static void PLockAPIWhileBlocked(PyMOLGlobals * G)
{
  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->lock, "O", G->P_inst->cmd));
}

/**
 * Try to acquire API lock (non-blocking if busy) and release GIL
 * @return False if API lock could not be acquired
 * @pre GIL
 * @post no GIL if API lock could be acquired
 */
int PTryLockAPIAndUnblock(PyMOLGlobals * G)
{
  int result = get_api_lock(G, false);
  if(result) {
    PUnblock(G);
  }
  return result;
}

/**
 * Acquire API lock and release the GIL
 * @pre GIL
 * @post no GIL
 * @post API Lock
 */
void PLockAPIAndUnblock(PyMOLGlobals * G)
{
  assert(PyGILState_Check());

  PXDecRef(PYOBJECT_CALLFUNCTION(G->P_inst->lock, "O", G->P_inst->cmd));
  PUnblock(G);
}

/**
 * Assign a variable in the pymol namespace, like
 * @verbatim
   import pymol
   setattr(pymol, name, value)
   @endverbatim
 * @pre no GIL
 */
void PDefineFloat(PyMOLGlobals * G, const char *name, float value)
{
  assert(!PyGILState_Check());

  char buffer[OrthoLineLength];
  sprintf(buffer, "%s = %f\n", name, value);
  PBlock(G);
  PRunStringModule(G, buffer);
  PUnblock(G);
}

/**
 * If the error indicator is set (means: If a Python exception was raised),
 * then clear it and print the traceback.
 *
 * Special case is CmdException, which will be printed without a traceback.
 */
void PErrPrintIfOccurred(PyMOLGlobals* G)
{
  assert(PyGILState_Check());

  PyObject *type = nullptr, *value = nullptr, *traceback = nullptr;
  PyErr_Fetch(&type, &value, &traceback);

  if (!type) {
    return;
  }

  if (!value || !PyErr_GivenExceptionMatches(type, P_CmdException)) {
    PyErr_Restore(type, value, traceback);
    PyErr_Print();
    return;
  }

  Py_XDECREF(traceback);

  if (PyObject* strobj = PyObject_Str(value)) {
    const char* str = PyUnicode_AsUTF8(strobj);
    assert(str);

    G->Feedback->addColored(str, FB_Errors);
    G->Feedback->add("\n");

    Py_DECREF(strobj);
  } else {
    assert(PyErr_Occurred());
    PyErr_Print();
  }

  Py_DECREF(type);
  Py_DECREF(value);
}

/* A static module */

static PyObject *PCatchWrite(PyObject * self, PyObject * args)
{
  char *str;
  PyArg_ParseTuple(args, "s", &str);
  if(str[0]) {
    if(SingletonPyMOLGlobals) {
      if(Feedback(SingletonPyMOLGlobals, FB_Python, FB_Output)) {
        OrthoAddOutput(SingletonPyMOLGlobals, str);
      }
    }
  }
  return PConvAutoNone(Py_None);
}

static PyObject *PCatchWritelines(PyObject * self, PyObject * args)
{
  PyObject *seq;
  int len;
  PyArg_ParseTuple(args, "O", &seq);
  if(seq && PySequence_Check(seq)) {
    if((len = PySequence_Size(seq)) > 0) {
      int i;
      for(i = 0; i < len; i++) {
        PyObject *obj = PySequence_GetItem(seq, i);
        if(obj && PyString_Check(obj)) {
          const char *str = PyString_AsString(obj);
          if(SingletonPyMOLGlobals) {
            if(Feedback(SingletonPyMOLGlobals, FB_Python, FB_Output)) {
              OrthoAddOutput(SingletonPyMOLGlobals, str);
            }
          }
        }
        Py_XDECREF(obj);
      }
    }
  }
  return PConvAutoNone(Py_None);
}

static PyObject *PCatchFlush(PyObject * self, PyObject * args)
{
  fflush(stdout);
  fflush(stderr);
  return PConvAutoNone(Py_None);
}

static PyObject *PCatchIsAtty(PyObject * self, PyObject * args)
{
  Py_RETURN_FALSE;
}

static PyObject *PCatch_install(PyObject * self, PyObject * args)
{
  PyRun_SimpleString(
      "import sys, pcatch\n"
      "if sys.stdout is not pcatch:"
      "pcatch.closed = False;"
      "pcatch.encoding = 'UTF-8';"
      "sys.stderr = sys.stdout = pcatch");
  return PConvAutoNone(Py_None);
}

static PyMethodDef PCatch_methods[] = {
  {"writelines", PCatchWritelines, METH_VARARGS},
  {"write", PCatchWrite, METH_VARARGS},
  {"flush", PCatchFlush, METH_VARARGS},
  {"isatty", PCatchIsAtty, METH_VARARGS}, // called by pip.main(["install", "..."])
  {"_install", PCatch_install, METH_VARARGS},
  {NULL, nullptr}                  /* sentinel */
};

void PCatchInit(void)
{
  assert(PyGILState_Check());

  static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT,
    "pcatch", nullptr, -1, PCatch_methods };
  PyObject * pcatch = PyModule_Create(&moduledef);
  if (pcatch) {
    PyDict_SetItemString(PyImport_GetModuleDict(), "pcatch", pcatch);
    Py_DECREF(pcatch);
  }
}

namespace pymol
{
GIL_Ensure::GIL_Ensure() {
  state = PyGILState_Ensure();
}
GIL_Ensure::~GIL_Ensure()
{
  PyGILState_Release(state);
}
} // namespace pymol

#endif
