#include "SceneMouse.h"
#include "ButMode.h"
#include "Editor.h"
#include "Executive.h"
#include "Matrix.h"
#include "Menu.h"
#include "ObjectDist.h"
#include "ObjectGadget.h"
#include "ObjectMolecule.h"
#include "ObjectSlice.h"
#include "P.h"
#include "PyMOLGlobals.h"
#include "PyMOLObject.h"
#include "SceneDef.h"
#include "ScenePicking.h"
#include "Selector.h"
#include "Seq.h"
#include "Wizard.h"
#include "Feedback.h"

#include <glm/vec3.hpp>
#include <glm/mat4x4.hpp>
#include <glm/gtc/type_ptr.hpp>

#define cDoubleTime 0.35

NamedPicking::NamedPicking(const Picking& pick)
  : src(pick.src)
{
  if (pick.context.object != nullptr) {
    context.name = pick.context.object->Name;
  }
  context.state = pick.context.state;
}

static void SceneNoteMouseInteraction(PyMOLGlobals* G)
{
  SceneAbortAnimation(G);
  if (SettingGet_b(G, nullptr, nullptr, cSetting_mouse_restart_movie_delay)) {
    SceneRestartFrameTimer(G);
  }
}

static void SceneLoopClick(Block* block, int button, int x, int y, int mod)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  I->LoopRect.left = x;
  I->LoopRect.top = y;
  I->LoopRect.right = x;
  I->LoopRect.bottom = y;
  I->LoopFlag = true;
  I->LoopMod = mod;
  OrthoSetLoopRect(G, true, &I->LoopRect);
  OrthoGrab(G, block);
}

static void SceneLoopDrag(Block* block, int x, int y, int mod)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  I->LoopRect.right = x;
  I->LoopRect.bottom = y;
  OrthoSetLoopRect(G, true, &I->LoopRect);
}

static void SceneLoopRelease(Block* block, int button, int x, int y, int mod)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  int tmp;
  int mode;
  mode = ButModeTranslate(G, button, I->LoopMod);

  if (I->LoopRect.top < I->LoopRect.bottom) {
    tmp = I->LoopRect.top;
    I->LoopRect.top = I->LoopRect.bottom;
    I->LoopRect.bottom = tmp;
  }
  if (I->LoopRect.right < I->LoopRect.left) {
    tmp = I->LoopRect.right;
    I->LoopRect.right = I->LoopRect.left;
    I->LoopRect.left = tmp;
  }
  OrthoSetLoopRect(G, false, &I->LoopRect);
  ExecutiveSelectRect(G, &I->LoopRect, mode);
  I->LoopFlag = false;
  OrthoUngrab(G);
  OrthoDirty(G);
}

void SceneClickButtonAddTo(PyMOLGlobals* G, pymol::CObject* obj,
    pymol::zstring_view selName, pymol::zstring_view buffer,
    pymol::zstring_view sel_mode_kw)
{
  CScene* I = G->Scene;
  if (SelectorIndexByName(G, selName.c_str()) >= 0) {
    auto buf2 = pymol::string_format(
        "(((%s) or %s(%s)) and not ((%s(%s)) and %s(%s)))", selName,
        sel_mode_kw, buffer, sel_mode_kw, buffer, sel_mode_kw, selName);
    SelectorCreate(G, selName.c_str(), buf2.c_str(), nullptr, false, nullptr);
    if (obj->type == cObjectMolecule) {
      if (SettingGetGlobal_i(G, cSetting_logging)) {
        auto objMol = (ObjectMolecule*) obj;
        auto atomSele = ObjectMoleculeGetAtomSeleLog(
            objMol, I->LastPicked.src.index, false);
        auto select = pymol::string_format(
            "(((%s) or %s(%s)) and not ((%s(%s)) and %s(%s)))", selName,
            sel_mode_kw, atomSele, sel_mode_kw, atomSele, sel_mode_kw, selName);
        auto pLogBuffer =
            pymol::string_format("cmd.select('%s',\"%s(%s)\",enable=1)",
                selName, sel_mode_kw, select);
        PLog(G, pLogBuffer, cPLog_pym);
      }
    }
  } else {
    auto buf2 = pymol::string_format("%s(%s)", sel_mode_kw, buffer);
    SelectorCreate(G, selName.c_str(), buf2.c_str(), nullptr, false, nullptr);
    if (obj->type == cObjectMolecule) {
      if (SettingGet<int>(G, cSetting_logging)) {
        auto objMol = (ObjectMolecule*) obj;
        auto atomSele = ObjectMoleculeGetAtomSeleLog(
            objMol, I->LastPicked.src.index, false);
        auto select = pymol::string_format(
            "cmd.select('%s',\"%s(%s)\")", selName, sel_mode_kw, atomSele);
        PLog(G, select, cPLog_pym);
      }
    }
  }
  if (SettingGet<bool>(G, cSetting_auto_hide_selections))
    ExecutiveHideSelections(G);
  if (SettingGet<bool>(G, cSetting_auto_show_selections))
    ExecutiveSetObjVisib(G, selName, 1, false);
  WizardDoSelect(G, selName.c_str(), I->LastPicked.context.state);
}

/**
 * Check for Double click
 * @param x cursor X position
 * @param Y cursor Y position
 * @param mod modifier key (Shift, ctrl, etc...)
 * @param when Time which this click happened
 * @return new button
 */
static int SceneClickCheckDoubleClick(
    PyMOLGlobals* G, int button, int x, int y, int mod, double when)
{
  auto I = G->Scene;
  // check for double click (within 0.35s and 10sq. pixels
  if (((ButModeCheckPossibleSingleClick(G, button, mod) || (!mod)) &&
          ((when - I->LastClickTime) < cDoubleTime))) {
    int dx = abs(I->LastWinX - x);
    int dy = abs(I->LastWinY - y);
    if ((dx < 10) && (dy < 10) && (I->LastButton == button)) {
      switch (button) {
      case P_GLUT_LEFT_BUTTON:
        button = P_GLUT_DOUBLE_LEFT;
        break;
      case P_GLUT_MIDDLE_BUTTON:
        button = P_GLUT_DOUBLE_MIDDLE;
        break;
      case P_GLUT_RIGHT_BUTTON:
        button = P_GLUT_DOUBLE_RIGHT;
        break;
      }
    }
  }
  return button;
}

/**
 * @param button which button pressed
 * @param x Cursor X position
 * @param Y Cursor Y position
 * @param mod modifier key (Shift, ctrl, etc...)
 * @return true if click was handled (resolved with action)
 */
static int SceneClickSceneButton(
    PyMOLGlobals* G, int button, int x, int y, int mod)
{
  auto I = G->Scene;
  for (std::size_t i = 0u; i < I->SceneVec.size(); i++) {
    auto& elem = I->SceneVec[i];
    if (elem.drawn && elem.rect.contains(x, y)) {
      switch (button) {
      case P_GLUT_LEFT_BUTTON: /* normal activate (with interpolation) */
        I->Pressed = i;
        I->Over = i;
        I->PressMode = 1;
        SceneDirty(G);
        return true;
      case P_GLUT_MIDDLE_BUTTON: /* rapid browse mode */
        I->Pressed = i;
        I->PressMode = 2;
        I->Over = i;

        {
          const char* cur_name =
              SettingGetGlobal_s(G, cSetting_scene_current_name);
          int animate = -1;
          if (mod & cOrthoCTRL)
            animate = 0;
          if (cur_name && elem.name != cur_name) {
            auto buffer = pymol::string_format(
                "cmd.scene('''%s''',animate=%d)", elem.name, animate);
            PParse(G, buffer);
            PFlush(G);
            PLog(G, buffer, cPLog_pym);
          }
        }
        return true;
      case P_GLUT_RIGHT_BUTTON: /* drag or menu... */
        I->Pressed = i;
        I->PressMode = 3;
        I->Over = i;
        return true;
      }
      break;
    }
  }
  return false;
}

/**
 * Handles object-related click events
 * @param obj clicked object
 * @param LastPicked last picked info
 * @param mode mouse button mode
 * @param sel_mode_kw Current selection mode operator for the mouse
 *
 */
void SceneClickObject(PyMOLGlobals* G, pymol::CObject* obj, const NamedPicking& LastPicked,
    int mode, pymol::zstring_view sel_mode_kw)
{
  std::string selName;
  switch (obj->type) {
  case cObjectMolecule: {
    if (Feedback(G, FB_Scene, FB_Results)) {
      auto buffer = obj->describeElement(LastPicked.src.index);
      PRINTF " You clicked %s", buffer.c_str() ENDF(G);
      OrthoRestorePrompt(G);
    }
    auto buffer =
        pymol::string_format("%s`%d", obj->Name, LastPicked.src.index + 1);
    switch (mode) { // Populate SelName/buffers
    case cButModeLB:
    case cButModeAddToLB:
      selName = "lb";
      break;
    case cButModeMB:
    case cButModeAddToMB:
      selName = "mb";
      break;
    case cButModeRB:
    case cButModeAddToRB:
      selName = "rb";
      break;
    case cButModeSeleSet:
    case cButModeSeleToggle:
      ExecutiveGetActiveSeleName(
          G, selName, true, SettingGet<int>(G, cSetting_logging));
      break;
    case cButModeDragMol: {
      auto objMol = (ObjectMolecule*) obj;
      auto atomSele =
          ObjectMoleculeGetAtomSeleLog(objMol, LastPicked.src.index, false);
      auto pLogBuffer =
          pymol::string_format("cmd.drag(\"bymol (%s)\")", atomSele);
      PParse(G, pLogBuffer);
      PLog(G, buffer, cPLog_pym);
    } break;
    case cButModeDragObj: {
      auto objMol = (ObjectMolecule*) obj;
      auto atomSele =
          ObjectMoleculeGetAtomSeleLog(objMol, LastPicked.src.index, false);
      auto pLogBuffer =
          pymol::string_format("cmd.drag(\"byobject (%s)\")", atomSele);
      PParse(G, pLogBuffer);
      PLog(G, pLogBuffer, cPLog_pym);
    } break;
    case cButModeOrigAt:
      SceneNoteMouseInteraction(G);
      {
        float v1[3];

        if (ObjectMoleculeGetAtomTxfVertex((ObjectMolecule*) obj,
                LastPicked.context.state, LastPicked.src.index, v1)) {
          EditorFavorOrigin(G, v1);
          ExecutiveOrigin(G, nullptr, true, nullptr, v1, 0);
        }
      }
      if (obj->type == cObjectMolecule) {
        if (SettingGetGlobal_i(G, cSetting_logging)) {
          auto objMol = (ObjectMolecule*) obj;
          auto atomSele =
              ObjectMoleculeGetAtomSeleLog(objMol, LastPicked.src.index, false);
          auto pLogBuffer =
              pymol::string_format("cmd.origin(\"%s\")", atomSele);
          PLog(G, pLogBuffer, cPLog_pym);
        }
        if (Feedback(G, FB_Scene, FB_Results)) {
          auto buffer = obj->describeElement(LastPicked.src.index);
          PRINTF " You clicked %s", buffer.c_str() ENDF(G);
          OrthoRestorePrompt(G);
        }
      }
      PRINTFB(G, FB_Scene, FB_Actions)
      " Scene: Origin set.\n" ENDFB(G);
      break;
    case cButModeCent:
      SceneNoteMouseInteraction(G);
      {
        float v1[3];

        if (ObjectMoleculeGetAtomTxfVertex((ObjectMolecule*) obj,
                LastPicked.context.state, LastPicked.src.index, v1)) {
          ExecutiveCenter(G, nullptr, 0, true, -1, v1, true);
        }
      }

      if (SettingGet<int>(G, cSetting_logging)) {
        auto objMol = (ObjectMolecule*) obj;
        auto atomSele =
            ObjectMoleculeGetAtomSeleLog(objMol, LastPicked.src.index, false);
        auto pLogBuffer = pymol::string_format("cmd.center(\"%s\",state=%d)",
            atomSele, LastPicked.context.state + 1);
        PLog(G, pLogBuffer, cPLog_pym);
      }
      break;
    }
    switch (mode) {
    case cButModeSimpleClick:
      break;
    case cButModeLB:
    case cButModeMB:
    case cButModeRB:
    case cButModeSeleSet: {
      auto buf2 = pymol::string_format("(%s(%s))", sel_mode_kw, buffer);
      SelectorCreate(G, selName.c_str(), buf2.c_str(), nullptr, false, nullptr);
    }
      if (SettingGet<bool>(G, cSetting_auto_hide_selections))
        ExecutiveHideSelections(G);
      if (SettingGet<bool>(G, cSetting_auto_show_selections))
        ExecutiveSetObjVisib(G, selName, 1, false);
      if (obj->type == cObjectMolecule) {
        if (SettingGet<int>(G, cSetting_logging)) {
          auto objMol = (ObjectMolecule*) obj;
          auto buf1 =
              ObjectMoleculeGetAtomSeleLog(objMol, LastPicked.src.index, false);
          auto pLogBuffer =
              pymol::string_format("cmd.select('%s',\"%s(%s)\",enable=1)",
                  selName, sel_mode_kw, buf1);
          PLog(G, pLogBuffer, cPLog_pym);
        }
      }
      WizardDoSelect(G, selName.c_str(), LastPicked.context.state);
      break;
    case cButModeAddToLB:
    case cButModeAddToMB:
    case cButModeAddToRB:
    case cButModeSeleToggle:
      SceneClickButtonAddTo(G, obj, selName, buffer.c_str(), sel_mode_kw);
      break;
    }
  }
  case cObjectGadget:
    break;
  default:
    EditorInactivate(G);
    break;
  }
}

void SceneClickTransformObject(
    PyMOLGlobals* G, pymol::CObject* obj, const NamedPicking& LastPicked, int mode, bool is_single_click)
{
  auto I = G->Scene;
  switch (obj->type) {
  case cObjectMolecule:
    switch (mode) {
    case cButModeMenu: {
      ObjectMolecule* objMol = (ObjectMolecule*) obj;
      int active_sele = ExecutiveGetActiveSele(G);
      if (active_sele && SelectorIsMember(G,
                             objMol->AtomInfo[LastPicked.src.index].selEntry,
                             active_sele)) {
        /* user clicked on a selected atom */
        ObjectNameType name;
        ExecutiveGetActiveSeleName(
            G, name, false, SettingGetGlobal_i(G, cSetting_logging));
        MenuActivate2Arg(G, I->LastWinX, I->LastWinY + 20, /* selection menu */
            I->LastWinX, I->LastWinY, is_single_click, "pick_sele", name, name);
      } else {
        /* user clicked on an atom not in a selection */
        auto buffer = obj->describeElement(LastPicked.src.index);
        auto atomSele = ObjectMoleculeGetAtomSeleLog(
            (ObjectMolecule*) obj, LastPicked.src.index, false);
        MenuActivate2Arg(G, I->LastWinX, I->LastWinY + 20, I->LastWinX,
            I->LastWinY, is_single_click, "pick_menu", buffer.c_str(),
            atomSele.c_str());
      }
    } break;
    case cButModePickAtom1:
      if (obj && obj->type == cObjectMolecule) {
        if (Feedback(G, FB_Scene, FB_Results)) {
          auto buffer = obj->describeElement(LastPicked.src.index);
          PRINTF " You clicked %s -> (%s)\n", buffer.c_str(), cEditorSele1 ENDF(G);
        }
        if (SettingGet<int>(G, cSetting_logging)) {
          auto objMol = (ObjectMolecule*) obj;
          auto atomSele = ObjectMoleculeGetAtomSeleLog(
              objMol, LastPicked.src.index, false);
          auto pLogBuffer =
              pymol::string_format("cmd.edit(\"%s\",pkresi=1)", atomSele);
          PLog(G, pLogBuffer, cPLog_pym);
        }
        OrthoRestorePrompt(G);
        auto buffer = pymol::string_format(
            "%s`%d", obj->Name, LastPicked.src.index + 1);
        EditorInactivate(G);
        SelectorCreate(G, cEditorSele1, buffer.c_str(), nullptr, true, nullptr);
        EditorActivate(G, SettingGet<int>(G, cSetting_state) - 1, false);
        if (EditorActive(G)) {
          EditorDefineExtraPks(G);
        }
        WizardDoPick(G, 0, LastPicked.context.state);
      }
      break;
    case cButModePickAtom:
      if (obj && obj->type == cObjectMolecule) {
        auto buffer = obj->describeElement(LastPicked.src.index);
        if (EditorIsBondMode(G)
            /* &&!(EditorIsAnActiveObject(G,(ObjectMolecule*)obj)) */
        ) {
          EditorInactivate(G);
          EditorLogState(G, false);
        }
        if ((!EditorIsBondMode(G)) &&
            EditorDeselectIfSelected(
                G, (ObjectMolecule*) obj, LastPicked.src.index, true)) {
          PRINTF " You unpicked %s.", buffer.c_str() ENDF(G);
          if (EditorActive(G))
            EditorDefineExtraPks(G);
          EditorLogState(G, false);
        } else {
          if (EditorIsBondMode(G) &&
              EditorDeselectIfSelected(
                  G, (ObjectMolecule*) obj, LastPicked.src.index, false)) {
            EditorInactivate(G);
          }
          WordType name;
          EditorGetNextMultiatom(G, name);

          PRINTFB(G, FB_Scene, FB_Results)
          " You clicked %s -> (%s)\n", buffer.c_str(), name ENDFB(G);
          /* TODO: logging */

          auto buffer = pymol::string_format(
              "%s`%d", obj->Name, LastPicked.src.index + 1);
          ExecutiveDelete(G, name);
          SelectorCreate(G, name, buffer.c_str(), nullptr, true, nullptr);
          EditorActivate(G, SettingGet<int>(G, cSetting_state) - 1, false);
          if (EditorActive(G)) {
            EditorDefineExtraPks(G);
          }
          EditorLogState(G, false);
          WizardDoPick(G, 0, LastPicked.context.state);
        }
      }
      break;
    }
    break;
  case cObjectGadget:
    break;
  default:
    EditorInactivate(G);
    break;
  }
}

/**
 * Handle logic for objects with pickable bonds
 * @param x cursor X
 * @param y cursor Y
 * @param mode mouse button mode
 * @param lastPicked last picked info
 */

void SceneClickPickBond(PyMOLGlobals* G, int x, int y, int mode, const NamedPicking& LastPicked)
{
  auto I = G->Scene;
  auto obj = ExecutiveFindObject<ObjectMolecule>(G, LastPicked.context.name);
  EditorInactivate(G);
  if (!obj) {
    return;
  }
  if (Feedback(G, FB_Scene, FB_Results)) {
    auto buffer = obj->describeElement(I->LastPicked.src.index);
    PRINTF " You clicked %s -> (%s)", buffer.c_str(), cEditorSele1 ENDF(G);
    OrthoRestorePrompt(G);
  }

  /*        ObjectMoleculeChooseBondDir(objMol,I->LastPicked.bond,
     &I->LastPicked.src.index,&atIndex); */

  {
    auto buffer = pymol::string_format("%s`%d", obj->Name, I->LastPicked.src.index + 1);
    SelectorCreate(G, cEditorSele1, buffer.c_str(), nullptr, true, nullptr);
  }

  if (LastPicked.src.bond >= 0) {
    int atIndex = obj->Bond[LastPicked.src.bond].index[0];
    if (atIndex == LastPicked.src.index)
      atIndex = obj->Bond[LastPicked.src.bond].index[1];
    if (Feedback(G, FB_Scene, FB_Results)) {
      auto buffer = obj->describeElement(atIndex);
      PRINTF " You clicked %s -> (%s)", buffer.c_str(), cEditorSele2 ENDF(G);
      OrthoRestorePrompt(G);
    }

    if (SettingGetGlobal_i(G, cSetting_logging)) {
      auto buf1 = ObjectMoleculeGetAtomSeleLog(
          obj, LastPicked.src.index, false);
      auto buf2 = ObjectMoleculeGetAtomSeleLog(obj, atIndex, false);
      auto buffer = pymol::string_format("cmd.edit(\"%s\",\"%s\")", buf1, buf2);
      PLog(G, buffer, cPLog_pym);
    }
    auto buffer = pymol::string_format("%s`%d", obj->Name, atIndex + 1);
    SelectorCreate(G, cEditorSele2, buffer.c_str(), nullptr, true, nullptr);
    EditorActivate(G, SettingGetGlobal_i(G, cSetting_state) - 1, true);

    if (mode == cButModePkTorBnd) {
      /* get ready to drag */
      SceneDontCopyNext(G);
      EditorPrepareDrag(G, obj, -1, LastPicked.src.index,
          SettingGetGlobal_i(G, cSetting_state) - 1, mode);
      I->SculptingFlag = 1;
      I->SculptingSave =
          obj->AtomInfo[LastPicked.src.index].protekted;
      obj->AtomInfo[LastPicked.src.index].protekted = cAtomProtected_drag;
    }
    WizardDoPick(G, 1, LastPicked.context.state);
  } else {
    WizardDoPick(G, 0, LastPicked.context.state);
  }
  if (SettingGetGlobal_b(G, cSetting_auto_hide_selections))
    ExecutiveHideSelections(G);
}

/**
 * Handle logic for picking nothing
 * @param button which button pressed
 * @param mod modifier key (Shift, ctrl, etc...)
 * @param mode mouse button mode
 */

void SceneClickPickNothing(PyMOLGlobals* G, int button, int mod, int mode)
{
  auto I = G->Scene;
  switch (mode) {
  case cButModeSeleSet: {
    ObjectNameType name;
    if (ExecutiveGetActiveSeleName(
            G, name, false, SettingGet<int>(G, cSetting_logging))) {
      SelectorCreate(G, name, "none", nullptr, true, nullptr);
      if (SettingGet<int>(G, cSetting_logging)) {
        auto buf2 = pymol::string_format("cmd.select('%s','none')\n", name);
        PLog(G, buf2, cPLog_no_flush);
      }
      SeqDirty(G);
    }
  }
  case cButModeSeleToggle: {
    ObjectNameType name;

    if (ExecutiveGetActiveSeleName(
            G, name, false, SettingGet<int>(G, cSetting_logging))) {
      ExecutiveSetObjVisib(G, name, 0, false);
      if (SettingGet<int>(G, cSetting_logging)) {
        auto buf2 = pymol::string_format("cmd.disable('%s')\n", name);
        PLog(G, buf2, cPLog_no_flush);
      }
    }
  } break;
  case cButModeSimpleClick:
    PyMOL_SetClickReady(G->PyMOL, "", -1, button, mod, I->LastWinX,
        I->Height - (I->LastWinY + 1), nullptr, 0);
    break;
  }
  PRINTFB(G, FB_Scene, FB_Blather)
  " %s: no atom found nearby.\n", __func__ ENDFB(G);
  /* this here to prevent display weirdness after
     an unsuccessful picking pass... not sure it helps though */
  SceneInvalidate(G);
  OrthoRestorePrompt(G);
}

static int get_stereo_x(int x, int* last_x, int width, ClickSide* click_side)
{
  int width_2 = width / 2;
  int width_3 = width / 3;
  if (!last_x) {
    if (x > width_2) {
      x -= width_2;
      if (click_side)
        *click_side = ClickSide::Right;
    } else {
      if (click_side)
        *click_side = ClickSide::Left;
    }
  } else {
    if ((x - (*last_x)) > width_3) {
      x -= width_2;
      if (click_side)
        *click_side = ClickSide::Right;
    } else if (((*last_x) - x) > width_3) {
      x += width_2;
      if (click_side)
        *click_side = ClickSide::Right;
    } else {
      if (click_side)
        *click_side = ClickSide::Left;
    }
  }
  return x;
}

/*========================================================================*/
void SceneClick(Block* block, int button, int x, int y, int mod, double when)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  const char* sel_mode_kw = "";
  int is_single_click =
      ((button == P_GLUT_SINGLE_LEFT) || (button == P_GLUT_SINGLE_MIDDLE) ||
          (button == P_GLUT_SINGLE_RIGHT));

  if (!is_single_click) {
    int click_handled = false;

    if (I->ButtonsShown) {
      /* check & handle a click on the scrollbar */
      if (I->ScrollBarActive) {
        if ((x - I->rect.left) < (SceneScrollBarWidth + SceneScrollBarMargin)) {
          click_handled = true;
          I->m_ScrollBar.click(button, x, y, mod);
        }
      }
      if (!click_handled) {
        for (const auto& elem : I->SceneVec) {
          if (elem.drawn && elem.rect.contains(x, y)) {
            click_handled = true;
            break;
          }
        }
      }
    }

    if (!click_handled) {
      button = SceneClickCheckDoubleClick(G, button, x, y, mod, when);
    } // end not click handled

    if (ButModeCheckPossibleSingleClick(G, button, mod) || (!mod)) {
      I->PossibleSingleClick = 1;
    } else {
      const char* but_mode_name =
          SettingGetGlobal_s(G, cSetting_button_mode_name);
      if (but_mode_name && but_mode_name[0] == '1') {
        I->PossibleSingleClick = 1;
      } else {
        I->PossibleSingleClick = 0;
      }
    }
  } // end not single-click

  int click_handled = false;
  auto click_side = ClickSide::None;

  I->LastWinX = x;
  I->LastWinY = y;
  I->LastClickTime = when;
  I->LastButton = button;
  I->LastMod = mod;
  I->Threshold = 0;

  SceneGetCenter(G, I->LastClickVertex);
  {
    float vScale = SceneGetExactScreenVertexScale(G, I->LastClickVertex);
    float v[3];
    v[0] = -(I->Width / 2 - (x - I->rect.left)) * vScale;
    v[1] = -(I->Height / 2 - (y - I->rect.bottom)) * vScale;
    v[2] = 0;
    MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v, v);
    add3f(v, I->LastClickVertex, I->LastClickVertex);
  }

  if (I->ButtonsShown) {

    if (I->ScrollBarActive) {
      if ((x - I->rect.left) < (SceneScrollBarWidth + SceneScrollBarMargin)) {
        click_handled = true;
        I->m_ScrollBar.click(button, x, y, mod);
      }
    }
    if (!click_handled) {
      click_handled = SceneClickSceneButton(G, button, x, y, mod);
    }
  }
  if (!click_handled) {
    int mode = ButModeTranslate(
        G, button, mod); /* trying to work around something... */

    I->Button = button;
    I->SculptingSave = cAtomProtected_off;
    switch (mode) {
    case cButModeScaleSlabExpand:
      SceneNoteMouseInteraction(G);
      SceneClip(G, SceneClipMode::Scaling,
          1.0F + (0.2 * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale)),
          nullptr, 0);
      break;
    case cButModeScaleSlabShrink:
      SceneNoteMouseInteraction(G);
      SceneClip(G, SceneClipMode::Scaling,
          1.0F - (0.2 * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale)),
          nullptr, 0);
      break;
    case cButModeMoveSlabForward:
      SceneNoteMouseInteraction(G);
      {
        float old_front = I->m_view.m_clip().m_front;
        float old_back = I->m_view.m_clip().m_back;
        float old_origin = -I->m_view.pos().z;
        SceneClip(G, SceneClipMode::Proportional,
            0.1F * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale), nullptr, 0);
        SceneDoRoving(G, old_front, old_back, old_origin, true, false);
      }
      break;
    case cButModeMoveSlabBackward:
      SceneNoteMouseInteraction(G);
      {
        float old_front = I->m_view.m_clip().m_front;
        float old_back = I->m_view.m_clip().m_back;
        float old_origin = -I->m_view.pos().z;

        SceneClip(G, SceneClipMode::Proportional,
            -0.1F * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale), nullptr, 0);
        SceneDoRoving(G, old_front, old_back, old_origin, true, false);
      }
      break;
    case cButModeZoomForward:
      SceneNoteMouseInteraction(G);
      {
        float factor =
            -((I->m_view.m_clipSafe().m_front + I->m_view.m_clipSafe().m_back) /
                2) *
            0.1 * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale);
        if (factor <= 0.0F) {
          I->m_view.translate(0.0f, 0.0f, factor);
          I->m_view.m_clip().m_front -= factor;
          I->m_view.m_clip().m_back -= factor;
          UpdateFrontBackSafe(I);
        }
      }
      break;
    case cButModeZoomBackward:
      SceneNoteMouseInteraction(G);
      {
        float factor =
            ((I->m_view.m_clipSafe().m_front + I->m_view.m_clipSafe().m_back) / 2) *
            0.1F * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale);
        if (factor >= 0.0F) {
          I->m_view.translate(0.0f, 0.0f, factor);
          I->m_view.m_clip().m_front -= factor;
          I->m_view.m_clip().m_back -= factor;
          UpdateFrontBackSafe(I);
        }
      }
      break;
    case cButModeMoveSlabAndZoomForward:
      SceneNoteMouseInteraction(G);
      {
        float old_front = I->m_view.m_clip().m_front;
        float old_back = I->m_view.m_clip().m_back;
        float old_origin = -I->m_view.pos().z;
        SceneClip(G, SceneClipMode::Proportional,
            0.1F * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale), nullptr, 0);
        SceneDoRoving(G, old_front, old_back, old_origin, true, true);
      }
      break;
    case cButModeMoveSlabAndZoomBackward:
      SceneNoteMouseInteraction(G);
      {
        float old_front = I->m_view.m_clip().m_front;
        float old_back = I->m_view.m_clip().m_back;
        float old_origin = -I->m_view.pos().z;
        SceneClip(G, SceneClipMode::Proportional,
            -0.1F * SettingGetGlobal_f(G, cSetting_mouse_wheel_scale), nullptr, 0);
        SceneDoRoving(G, old_front, old_back, old_origin, true, true);
      }
      break;
    case cButModeRectAdd: /* deprecated */
    case cButModeRectSub: /* deprecated */
    case cButModeRect:    /* deprecated */
    case cButModeSeleAddBox:
    case cButModeSeleSetBox:
    case cButModeSeleSubBox:
      SceneLoopClick(block, button, x, y, mod);
      return;
      break;
    case cButModeRotDrag:
    case cButModeMovDrag:
    case cButModeMovDragZ:
      SceneNoteMouseInteraction(G);
      SceneDontCopyNext(G);

      y = y - I->margin.bottom;
      x = x - I->margin.left;

      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, nullptr);

      I->LastX = x;
      I->LastY = y;
      EditorReadyDrag(G, SettingGetGlobal_i(G, cSetting_state) - 1);

      if (EditorDraggingObjectMatrix(G)) {
        auto obj = EditorDragObject(G);
        if (obj) {
          if (SettingGetGlobal_b(G, cSetting_movie_auto_store)) {
            ObjectTranslateTTT(obj, nullptr, true);
            I->MotionGrabbedObj = obj;
            obj->Grabbed = true;
            if (SettingGetGlobal_i(G, cSetting_movie_auto_interpolate)) {
              I->ReinterpolateFlag = true;
              I->ReinterpolateObj = obj;
            }
          } else {
            ObjectTranslateTTT(obj, nullptr, false);
          }
        }
      }
      break;
    case cButModeRotXYZ:
    case cButModeTransXY:
    case cButModeTransZ:
    case cButModeClipNF:
    case cButModeClipN:
    case cButModeClipF:
    case cButModeRotZ:
    case cButModeInvRotZ:
    case cButModeRotL:
    case cButModeMovL:
    case cButModeMvzL:
      SceneNoteMouseInteraction(G);
      SceneDontCopyNext(G);

      y = y - I->margin.bottom;
      x = x - I->margin.left;

      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, nullptr);

      I->LastX = x;
      I->LastY = y;
      break;
    case cButModePickAtom1:
    case cButModePickAtom:
    case cButModeMenu:
      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, nullptr);

      if (SceneDoXYPick(G, x, y, click_side)) {
        auto obj = I->LastPicked.context.object;
        y = y - I->margin.bottom;
        x = x - I->margin.left;
        I->LastX = x;
        I->LastY = y;
        SceneClickTransformObject(G, obj, I->LastPicked, mode, is_single_click);
      } else { /* no atom picked */
        switch (mode) {
        case cButModeMenu:

          MenuActivate3fv(G, I->LastWinX, I->LastWinY, I->LastWinX, I->LastWinY,
              is_single_click, "main_menu", I->LastClickVertex);
          break;
        default:
          EditorInactivate(G);
          if (SettingGetGlobal_i(G, cSetting_logging)) {
            PLog(G, "cmd.edit()", cPLog_pym);
          }
          break;
        }
      }
      SceneDirty(G);
      break;
    case cButModePickBond:
    case cButModePkTorBnd:
      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, &click_side);

      if (SceneDoXYPick(G, x, y, click_side)) {
        y = y - I->margin.bottom;
        x = x - I->margin.left;
        I->LastX = x;
        I->LastY = y;

        if (mode == cButModePkTorBnd) {
          I->Threshold = 3;
          I->ThresholdX = x;
          I->ThresholdY = y;
        }
        SceneClickPickBond(G, x, y, mode, I->LastPicked);
      } else {
        EditorInactivate(G);
        EditorLogState(G, false);
      }
      SceneInvalidate(G);
      break;
    case cButModeRotObj:
    case cButModeMovObj:
    case cButModeMovObjZ:
    case cButModeRotView:
    case cButModeMovView:
    case cButModeMovViewZ:
    case cButModeRotFrag:
    case cButModeMovFrag:
    case cButModeMovFragZ:
    case cButModeTorFrag:
    case cButModeMoveAtom:
    case cButModeMoveAtomZ:
      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, &click_side);

      if (SceneDoXYPick(G, x, y, click_side)) {
        auto obj = I->LastPicked.context.object;
        y = y - I->margin.bottom;
        x = x - I->margin.left;
        I->LastX = x;
        I->LastY = y;
        switch (obj->type) {
        case cObjectMolecule:

          if (I->LastPicked.src.bond == cPickableLabel) {
            /* if user picks a label with move object/move fragment,
               then move the object/fragment, not the label */

            switch (mode) {
            case cButModeRotObj:
            case cButModeMovObj:
            case cButModeMovObjZ:
            case cButModeRotFrag:
            case cButModeMovFrag:
            case cButModeMovFragZ:
            case cButModeMovViewZ:
            case cButModeRotView:
            case cButModeMovView:
              I->LastPicked.src.bond = cPickableAtom;
              break;
            }
          }

          switch (mode) {
          case cButModeMovViewZ:
          case cButModeRotView:
          case cButModeMovView: {
            if (SettingGetGlobal_b(G, cSetting_movie_auto_store)) {
              ObjectTranslateTTT(obj, nullptr, true);
              I->MotionGrabbedObj = obj;
              obj->Grabbed = true;
              if (SettingGetGlobal_i(G, cSetting_movie_auto_interpolate)) {
                I->ReinterpolateFlag = true;
                I->ReinterpolateObj = obj;
              }
            } else {
              ObjectTranslateTTT(obj, nullptr, false);
            }
          } break;
          }

          if (I->LastPicked.src.bond >= cPickableAtom) {
            if (Feedback(G, FB_Scene, FB_Results)) {
              auto buffer = obj->describeElement(I->LastPicked.src.index);
              PRINTF " You clicked %s", buffer.c_str() ENDF(G);
              OrthoRestorePrompt(G);
            }
          }
          {
            auto objMol = (ObjectMolecule*) obj;
            EditorPrepareDrag(G, obj, -1, I->LastPicked.src.index,
                SettingGetGlobal_i(G, cSetting_state) - 1, mode);

            if (I->LastPicked.src.bond >= cPickableAtom) {
              I->SculptingFlag = 1;
              I->SculptingSave =
                  objMol->AtomInfo[I->LastPicked.src.index].protekted;
              objMol->AtomInfo[I->LastPicked.src.index].protekted = cAtomProtected_drag;
            }
          }
          break;
        case cObjectSlice:
          if (ObjectSliceGetVertex((ObjectSlice*) obj, I->LastPicked.src.index,
                  I->LastPicked.src.bond, I->LastPickVertex)) {
            I->LastPickVertexFlag = true;
          }
          break;
        case cObjectMeasurement:
          break;
        case cObjectGadget:
          break;
        default:
          EditorInactivate(G);
          break;
        }
      }
      break;

    case cButModeSeleSet:
    case cButModeSeleToggle:
      sel_mode_kw = SceneGetSeleModeKeyword(G);

      /* intentional pass through */

    case cButModeLB:
    case cButModeMB:
    case cButModeRB:
    case cButModeAddToLB:
    case cButModeAddToMB:
    case cButModeAddToRB:
    case cButModeSimpleClick:
    case cButModeOrigAt:
    case cButModeCent:
    case cButModeDragMol:
    case cButModeDragObj:
      if (stereo_via_adjacent_array(I->StereoMode))
        x = get_stereo_x(x, nullptr, I->Width, &click_side);

      if (SceneDoXYPick(G, x, y, click_side)) {
        auto obj = I->LastPicked.context.object;

        assert(I->LastPicked.src.bond != cPickableNoPick);

        if (mode == cButModeSimpleClick) {
          float pos_store[3];
          const float* pos = nullptr;
          int const index = I->LastPicked.src.index; /* 1-based */
          int const state = I->LastPicked.context.state;
          auto objmol = dynamic_cast<ObjectMolecule const*>(obj);

          if (objmol &&
              ObjectMoleculeGetAtomTxfVertex(objmol, state, index, pos_store))
            pos = pos_store;

          PyMOL_SetClickReady(G->PyMOL, obj->Name, index, button, mod,
              I->LastWinX, I->Height - (I->LastWinY + 1), pos,
              state + 1 /* 1-based */, I->LastPicked.src.bond);
        }
        SceneClickObject(G, obj, I->LastPicked, mode, sel_mode_kw);
      } else { // Nothing picked
        SceneClickPickNothing(G, button, mod, mode);
      }
    }

    I->StartX = I->LastX;
    I->StartY = I->LastY;
  }
}

/*========================================================================*/
void SceneRelease(Block* block, int button, int x, int y, int mod, double when)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  int release_handled = false;
  if (I->ButtonsShown && I->PressMode) {
    if (I->ScrollBarActive) {
      if ((x - I->rect.left) < (SceneScrollBarWidth + SceneScrollBarMargin)) {
        I->m_ScrollBar.release(button, x, y, mod);
        release_handled = true;
      }
    }
    if (!release_handled) {
      int ungrab = true;
      if (I->PressMode) {
        I->Over = -1;
        auto elem = &I->SceneVec.front();
        for (std::size_t i = 0u; i < I->SceneVec.size(); i++) {
          auto& elem_ = I->SceneVec[i];
          if (elem_.drawn && elem_.rect.contains(x, y)) {
            elem = &elem_;
            I->Over = i;
            break;
          }
        }
        if (I->Over >= 0) {
          release_handled = true;
          switch (I->PressMode) {
          case 1:
            if (I->Over == I->Pressed) {
              auto buffer = pymol::string_format("cmd.scene('''%s''')", elem->name);
              PParse(G, buffer);
              PFlush(G);
              PLog(G, buffer, cPLog_pym);
            }
            break;
          case 2: {
            const char* cur_name =
                SettingGetGlobal_s(G, cSetting_scene_current_name);
            if (cur_name && elem->name != cur_name) {
              auto buffer = pymol::string_format("cmd.scene('''%s''')", elem->name);
              PParse(G, buffer);
              PFlush(G);
              PLog(G, buffer, cPLog_pym);
            }
          } break;
          case 3:
            if (I->Pressed == I->Over) {
              Block* block = MenuActivate1Arg(G, I->LastWinX,
                  I->LastWinY + 20, /* scene menu */
                  I->LastWinX, I->LastWinY, true, "scene_menu", elem->name.c_str());
              if (block)
                block->drag(x, y, mod);
              ungrab = false;
            }
            break;
          }
        }
      }
      I->LastPickVertexFlag = false;
      I->Pressed = -1;
      I->Over = -1;
      I->PressMode = 0;
      if (ungrab)
        OrthoUngrab(G);
    }
  }
  if (!release_handled) {
    ObjectMolecule* obj;
    I->LastReleaseTime = when;
    if (I->PossibleSingleClick == 1) {
      double slowest_single_click = 0.25F;
      double diff = when - I->LastClickTime;

      slowest_single_click += I->ApproxRenderTime;

      if ((diff < 0.0) || (diff > slowest_single_click))
        I->PossibleSingleClick = 0;
      else {
        int but = -1;
        I->PossibleSingleClick = 2;
        I->SingleClickDelay = 0.15;

        switch (I->LastButton) {
        case P_GLUT_LEFT_BUTTON:
          but = P_GLUT_DOUBLE_LEFT;
          break;
        case P_GLUT_MIDDLE_BUTTON:
          but = P_GLUT_DOUBLE_MIDDLE;
          break;
        case P_GLUT_RIGHT_BUTTON:
          but = P_GLUT_DOUBLE_RIGHT;
          break;
        }
        if (but > 0) {
          int mode = ButModeTranslate(G, but, mod);
          if (mode == cButModeNone)
            I->SingleClickDelay =
                0.0; /* no double-click set? force immediate single click */
        }
      }
    }
    if (I->LoopFlag) {
      I->PossibleSingleClick = 0;
      SceneLoopRelease(block, button, x, y, mod);
      return;
    }
    OrthoUngrab(G);
    I->LoopFlag = false;
    if (I->SculptingFlag) {
      /* SettingSet(G,cSetting_sculpting,1); */
      obj = (ObjectMolecule*) I->LastPicked.context.object;
      if (obj) {
        obj->AtomInfo[I->LastPicked.src.index].protekted = I->SculptingSave;
      }
      I->SculptingFlag = 0;
    }
  }
  if (I->ReinterpolateFlag && I->ReinterpolateObj) {
    if (ExecutiveValidateObjectPtr(G, I->ReinterpolateObj, 0)) {
      ObjectMotionReinterpolate(I->ReinterpolateObj);
    }
    I->ReinterpolateFlag = true;
    I->ReinterpolateObj = nullptr;
  }
  if (I->MotionGrabbedObj) {
    if (ExecutiveValidateObjectPtr(G, I->MotionGrabbedObj, 0)) {
      I->MotionGrabbedObj->Grabbed = false;
      I->MotionGrabbedObj = nullptr;
    }
  }
}

/*========================================================================*/
void SceneDrag(Block* block, int x, int y, int mod, double when)
{
  PyMOLGlobals* G = block->m_G;
  CScene* I = G->Scene;
  float scale, vScale;
  float v1[3], v2[3], n1[3], n2[3], r1, r2, cp[3], v3[3];
  float dx, dy, dt;
  float axis[3], axis2[3], theta, omega;
  float old_front, old_back, old_origin;
  int mode;
  int eff_width;
  int moved_flag;
  int adjust_flag;
  int drag_handled = false;
  int virtual_trackball;
  pymol::CObject* obj;

  if (I->PossibleSingleClick) {
    double slowest_single_click_drag = 0.15;
    if ((when - I->LastClickTime) > slowest_single_click_drag) {
      I->PossibleSingleClick = 0;
    }
  }

  if (I->LoopFlag) {
    SceneLoopDrag(block, x, y, mod);
    return;
  }
  if (I->ButtonsShown && I->PressMode) {
    if (I->ButtonsValid) {
      drag_handled = true;
      I->Over = -1;
      auto elem = &I->SceneVec.front();
      for (std::size_t i = 0u; i < I->SceneVec.size(); i++) {
        auto& elem_ = I->SceneVec[i];
        if (elem_.drawn && elem_.rect.contains(x, y)) {
          elem = &elem_;
          I->Over = i;
          OrthoDirty(G);
          break;
        }
      }
      switch (I->PressMode) {
      case 2:
        if (I->Over >= 0) {
          if (I->Pressed != I->Over) {
            const char* cur_name =
                SettingGetGlobal_s(G, cSetting_scene_current_name);
            if (cur_name && elem->name != cur_name) {
              int animate = -1;
              if (mod & cOrthoCTRL)
                animate = 0;
              auto buffer = pymol::string_format("cmd.scene('''%s''',animate=%d)", elem->name,
                  animate);
              PParse(G, buffer);
              PFlush(G);
              PLog(G, buffer, cPLog_pym);
            }
            I->Pressed = I->Over;
          }
        } else {
          I->Pressed = -1;
        }
      case 3:
        if ((I->Over >= 0) && (I->Pressed != I->Over))
          I->PressMode = 4; /* activate dragging */
        break;
      }

      if (I->PressMode == 4) { /* dragging */
        if ((I->Over >= 0) && (I->Pressed != I->Over) && (I->Pressed >= 0)) {

          auto pressed = &I->SceneVec[I->Pressed];
          std::string buffer;

          if (I->Over > 0) { /* not over the first scene in list */
            SceneElem* first = elem - 1;
            SceneElem* second = pressed;
            if (first >= pressed) {
              first = elem;
              second = pressed;
            }
            buffer = pymol::string_format("cmd.scene_order(['''%s''','''%s'''])", first->name,
                second->name);
          } else {
            buffer = pymol::string_format("cmd.scene_order(['''%s'''],location='top')",
                pressed->name);
          }
          PParse(G, buffer);
          PFlush(G);
          PLog(G, buffer, cPLog_pym);
          I->Pressed = I->Over;
          I->ButtonsValid = false;
          if (SettingGetGlobal_b(G, cSetting_scene_buttons)) {
            OrthoInvalidateDoDraw(G);
          }
        }
      }
    }
  }

  if (!drag_handled) {

    mode = ButModeTranslate(G, I->Button, mod);

    y = y - I->margin.bottom;

    scale = (float) I->Height;
    if (scale > I->Width)
      scale = (float) I->Width;
    scale = 0.45F * scale;
    SceneInvalidateCopy(G, false);
    SceneDontCopyNext(G);
    switch (mode) {
    case cButModePickAtom:
      obj = I->LastPicked.context.object;
      if (obj)
        switch (obj->type) {
        case cObjectGadget: {
          auto gad = static_cast<ObjectGadget*>(obj);

          ObjectGadgetGetVertex(
              gad, I->LastPicked.src.index, I->LastPicked.src.bond, v1);

          vScale = SceneGetExactScreenVertexScale(G, v1);
          if (stereo_via_adjacent_array(I->StereoMode)) {
            x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
          }

          /* transform into model coodinate space */
          switch (obj->getRenderContext()) {
          case pymol::RenderContext::UnitWindow: {
            float divisor;
            divisor = (float) I->Width;
            if (I->Height < I->Width)
              divisor = (float) I->Height;
            v2[0] = (x - I->LastX) / divisor;
            v2[1] = (y - I->LastY) / divisor;
            v2[2] = 0;
          } break;
          default:
          case pymol::RenderContext::Camera:
            v2[0] = (x - I->LastX) * vScale;
            v2[1] = (y - I->LastY) * vScale;
            v2[2] = 0;
            MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
            break;
          }
          add3f(v1, v2, v2);
          ObjectGadgetSetVertex(
              gad, I->LastPicked.src.index, I->LastPicked.src.bond, v2);
          if (I->LastPicked.src.index) { // pick id on gadget is 1 for band (to
                                         // change values), 0 for everything
                                         // else (to move it around)
            SceneChanged(G); // changing values, need to update gadget text
          } else {
            PyMOL_NeedRedisplay(G->PyMOL); // moving gadget, just re-draw
          }
        } break;
        }
      I->LastX = x;
      I->LastY = y;
      break;
    case cButModeRotDrag:
      eff_width = I->Width;
      if (stereo_via_adjacent_array(I->StereoMode)) {
        eff_width = I->Width / 2;
        x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
      }

      virtual_trackball =
          SettingGet_i(G, nullptr, nullptr, cSetting_virtual_trackball);

      if (virtual_trackball == 2 &&
          (!I->prev_no_z_rotation1 || !I->prev_no_z_rotation2)) {
        /* when virtual_trackball=2 and twisting, need to set v1,v2 relative to
         * orig */
        v2[0] = (float) (eff_width / 2) - I->orig_x_rotation;
        v2[1] = (float) (I->Height / 2) - I->orig_y_rotation;

        v1[0] = v2[0] - (float) (x - I->LastX);
        v1[1] = v2[1] - (float) (y - I->LastY);

      } else if (virtual_trackball == 1) {
        v1[0] = (float) (eff_width / 2) - x;
        v1[1] = (float) (I->Height / 2) - y;

        v2[0] = (float) (eff_width / 2) - I->LastX;
        v2[1] = (float) (I->Height / 2) - I->LastY;

      } else {
        v1[0] = (float) (I->LastX) - x;
        v1[1] = (float) (I->LastY) - y;

        v2[0] = 0;
        v2[1] = 0;
      }

      r1 = (float) sqrt1f(v1[0] * v1[0] + v1[1] * v1[1]);
      r2 = (float) sqrt1f(v2[0] * v2[0] + v2[1] * v2[1]);

      {
        short r1lt, r2lt;
        if (virtual_trackball == 2) {
          r1lt = I->prev_no_z_rotation1;
          r2lt = I->prev_no_z_rotation2;
        } else {
          r1lt = r1 < scale;
          r2lt = r2 < scale;
          I->prev_no_z_rotation1 = r1 < scale;
          I->prev_no_z_rotation2 = r2 < scale;
          I->orig_x_rotation = x;
          I->orig_y_rotation = y;
        }
        if (r1lt) {
          v1[2] = (float) sqrt1f(scale * scale - r1 * r1);
        } else {
          v1[2] = 0.0;
        }
        if (r2lt) {
          v2[2] = (float) sqrt1f(scale * scale - r2 * r2);
        } else {
          v2[2] = 0.0;
        }
      }
      normalize23f(v1, n1);
      normalize23f(v2, n2);
      cross_product3f(n1, n2, cp);
      theta =
          (float) (SettingGet_f(G, nullptr, nullptr, cSetting_mouse_scale) * 2 * 180 *
                   asin(sqrt1f(cp[0] * cp[0] + cp[1] * cp[1] + cp[2] * cp[2])) /
                   cPI);
      dx = (v1[0] - v2[0]);
      dy = (v1[1] - v2[1]);
      dt = (float) (SettingGet_f(G, nullptr, nullptr, cSetting_mouse_limit) *
                    sqrt1f(dx * dx + dy * dy) / scale);

      if (theta > dt)
        theta = dt;

      normalize23f(cp, axis);

      axis[2] = -axis[2];

      theta = theta / (1.0F + (float) fabs(axis[2]));

      /* transform into model coodinate space */
      MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), axis, v2);
      v1[0] = (float) (cPI * theta / 180.0);
      EditorDrag(G, nullptr, -1, mode, SettingGetGlobal_i(G, cSetting_state) - 1,
          v1, v2, v3);
      I->LastX = x;
      I->LastY = y;
      break;
    case cButModeMovDrag:
    case cButModeMovDragZ:
      if (I->Threshold) {
        if ((abs(x - I->ThresholdX) > I->Threshold) ||
            (abs(y - I->ThresholdY) > I->Threshold)) {
          I->Threshold = 0;
        }
      }
      if (!I->Threshold) {

        copy3f(glm::value_ptr(I->m_view.origin()), v1);
        vScale = SceneGetExactScreenVertexScale(G, v1);
        if (stereo_via_adjacent_array(I->StereoMode)) {
          x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
        }

        if (mode == cButModeMovDragZ) {
          v2[0] = 0;
          v2[1] = 0;
          v2[2] = -(y - I->LastY) * vScale;
        } else {
          v2[0] = (x - I->LastX) * vScale;
          v2[1] = (y - I->LastY) * vScale;
          v2[2] = 0;
        }

        v3[0] = 0.0F;
        v3[1] = 0.0F;
        v3[2] = 1.0F;

        /* transform into model coodinate space */
        MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
        MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v3, v3);

        EditorDrag(G, nullptr, -1, mode, SettingGetGlobal_i(G, cSetting_state) - 1,
            v1, v2, v3);
      }
      I->LastX = x;
      I->LastY = y;
      break;
    case cButModeRotObj:
    case cButModeMovObj:
    case cButModeMovObjZ:
    case cButModeRotView:
    case cButModeMovView:
    case cButModeMovViewZ:
    case cButModeRotFrag:
    case cButModeMovFrag:
    case cButModeMovFragZ:
    case cButModeTorFrag:
    case cButModeMoveAtom:
    case cButModeMoveAtomZ:
    case cButModePkTorBnd:
      obj = I->LastPicked.context.object;
      if (obj) {
        if (I->Threshold) {
          if ((abs(x - I->ThresholdX) > I->Threshold) ||
              (abs(y - I->ThresholdY) > I->Threshold)) {
            I->Threshold = 0;
          }
        }
        if (!I->Threshold)
          switch (obj->type) {
          case cObjectGadget: /* note repeated above */
          {
            auto gad = static_cast<ObjectGadget*>(obj);

            ObjectGadgetGetVertex(
                gad, I->LastPicked.src.index, I->LastPicked.src.bond, v1);

            vScale = SceneGetExactScreenVertexScale(G, v1);
            if (stereo_via_adjacent_array(I->StereoMode)) {
              x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
            }

            /* transform into model coodinate space */
            switch (obj->getRenderContext()) {
            case pymol::RenderContext::UnitWindow: {
              float divisor;
              divisor = (float) I->Width;
              if (I->Height < I->Width)
                divisor = (float) I->Height;
              v2[0] = (x - I->LastX) / divisor;
              v2[1] = (y - I->LastY) / divisor;
              v2[2] = 0;
            } break;
            default:
            case pymol::RenderContext::Camera:
              v2[0] = (x - I->LastX) * vScale;
              v2[1] = (y - I->LastY) * vScale;
              v2[2] = 0;
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
              break;
            }
            add3f(v1, v2, v2);
            ObjectGadgetSetVertex(
                gad, I->LastPicked.src.index, I->LastPicked.src.bond, v2);
            if (I->LastPicked.src
                    .index) {  // pick id on gadget is 1 for band (to change
                               // values), 0 for everything else (to move it
                               // around)
              SceneChanged(G); // changing values, need to update gadget text
            } else {
              PyMOL_NeedRedisplay(G->PyMOL); // moving gadget, just re-draw
            }
          } break;
          case cObjectMolecule:
            if (ObjectMoleculeGetAtomTxfVertex((ObjectMolecule*) obj,
                    I->LastPicked.context.state, I->LastPicked.src.index, v1)) {
              /* scale properly given the current projection matrix */
              vScale = SceneGetExactScreenVertexScale(G, v1);

              if (stereo_via_adjacent_array(I->StereoMode)) {
                x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
              }

              switch (mode) {
              case cButModeMovFragZ:
              case cButModeMovObjZ:
              case cButModeMovViewZ:
              case cButModeMoveAtomZ:
                v2[0] = 0;
                v2[1] = 0;
                v2[2] = -(y - I->LastY) * vScale;
                break;
              default:
                v2[0] = (x - I->LastX) * vScale;
                v2[1] = (y - I->LastY) * vScale;
                v2[2] = 0;
                break;
              }

              v3[0] = 0.0F;
              v3[1] = 0.0F;
              v3[2] = 1.0F;

              /* transform into model coodinate space */
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v3, v3);

              if (I->LastPicked.src.bond >= cPickableAtom) {
                if ((mode != cButModeMoveAtom) && (mode != cButModeMoveAtomZ)) {
                  EditorDrag(G, obj, I->LastPicked.src.index, mode,
                      SettingGetGlobal_i(G, cSetting_state) - 1, v1, v2, v3);
                  switch (mode) {
                  case cButModeMovViewZ:
                  case cButModeRotView:
                  case cButModeMovView:
                    if (SettingGetGlobal_i(G, cSetting_movie_auto_store) &&
                        SettingGetGlobal_i(
                            G, cSetting_movie_auto_interpolate)) {
                      I->ReinterpolateFlag = true;
                      I->ReinterpolateObj = obj;
                    }
                    break;
                  }
                } else {
                  int log_trans =
                      SettingGetGlobal_b(G, cSetting_log_conformations);
                  ObjectMolecule* objOM = (ObjectMolecule*) obj;
                  ObjectMoleculeMoveAtom(objOM, I->LastPicked.context.state,
                      I->LastPicked.src.index, v2, 1, log_trans);
                  /* -- JV - if this object knows about distances, then move
                   * them if necessary */
                  /* check the dynamic_measures setting and make sure the object
                   * has a distance measure, first  */
                  /* obviated by new method
                  if (SettingGetGlobal_i(G, cSetting_dynamic_measures))
                    ObjectMoleculeMoveDist( (ObjectMolecule *) obj,
                  SettingGetGlobal_i(G, cSetting_state)-1,
                  I->LastPicked.src.index, v2, 1, log_trans);
                  */
                  SceneInvalidate(G);
                }
              } else {
                int log_trans =
                    SettingGetGlobal_b(G, cSetting_log_conformations);
                ObjectMolecule* objOM = (ObjectMolecule*) obj;
                float diffInPixels[2];
                diffInPixels[0] = (x - I->LastX);
                diffInPixels[1] = (y - I->LastY);
                ObjectMoleculeMoveAtomLabel(objOM, I->LastPicked.context.state,
                    I->LastPicked.src.index, v2, log_trans, diffInPixels);
                SceneInvalidate(G);
              }
            }
            break;
          case cObjectSlice: {
            ObjectSlice* slice = (ObjectSlice*) obj;

            if (I->LastPickVertexFlag) {
              auto state = SceneGetState(G);

              copy3f(I->LastPickVertex, v1);

              vScale = SceneGetExactScreenVertexScale(G, v1);

              if (stereo_via_adjacent_array(I->StereoMode)) {
                x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
              }

              v2[0] = (x - I->LastX) * vScale;
              v2[1] = (y - I->LastY) * vScale;
              v2[2] = 0;

              v3[0] = 0.0F;
              v3[1] = 0.0F;
              v3[2] = 1.0F;

              /* transform into model coodinate space */
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v3, v3);

              ObjectSliceDrag(slice, state, mode, v1, v2, v3);
            }
          } break;
          case cObjectMeasurement:
            if (ObjectDistGetLabelTxfVertex((ObjectDist*) obj,
                    I->LastPicked.context.state, I->LastPicked.src.index, v1)) {
              /* scale properly given the current projection matrix */
              vScale = SceneGetExactScreenVertexScale(G, v1);
              if (stereo_via_adjacent_array(I->StereoMode)) {
                x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
              }

              switch (mode) {
              case cButModeMovFragZ:
              case cButModeMovObjZ:
              case cButModeMovViewZ:
              case cButModeMoveAtomZ:
                v2[0] = 0;
                v2[1] = 0;
                v2[2] = -(y - I->LastY) * vScale;
                break;
              default:
                v2[0] = (x - I->LastX) * vScale;
                v2[1] = (y - I->LastY) * vScale;
                v2[2] = 0;
                break;
              }

              v3[0] = 0.0F;
              v3[1] = 0.0F;
              v3[2] = 1.0F;

              /* transform into model coodinate space */
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v2, v2);
              MatrixInvTransformC44fAs33f3f(glm::value_ptr(I->m_view.rotMatrix()), v3, v3);

              if (I->LastPicked.src.bond == cPickableLabel) {
                int log_trans =
                    SettingGetGlobal_b(G, cSetting_log_conformations);
                ObjectDistMoveLabel((ObjectDist*) obj,
                    I->LastPicked.context.state, I->LastPicked.src.index, v2, 1,
                    log_trans);
                SceneInvalidate(G);
              }
            }
            break;
          default:
            break;
          }
      }
      I->LastX = x;
      I->LastY = y;
      break;
    case cButModeTransXY:

      SceneNoteMouseInteraction(G);

      vScale = SceneGetExactScreenVertexScale(G, glm::value_ptr(I->m_view.origin()));
      if (stereo_via_adjacent_array(I->StereoMode)) {

        x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
      }

      v2[0] = (x - I->LastX) * vScale;
      v2[1] = (y - I->LastY) * vScale;
      v2[2] = 0.0F;

      moved_flag = false;
      if (I->LastX != x) {
        I->m_view.translate(v2[0], 0.0f, 0.0f);
        I->LastX = x;
        SceneInvalidate(G);
        moved_flag = true;
      }
      if (I->LastY != y) {
        I->m_view.translate(0.0f, v2[1], 0.0f);
        I->LastY = y;
        SceneInvalidate(G);
        moved_flag = true;
      }

      EditorFavorOrigin(G, nullptr);
      if (moved_flag && SettingGetGlobal_b(G, cSetting_roving_origin)) {
        SceneGetCenter(G, v2); /* gets position of center of screen */
        SceneOriginSet(G, v2, true);
      }
      if (moved_flag && SettingGetGlobal_b(G, cSetting_roving_detail)) {
        SceneRovingDirty(G);
      }
      break;
    case cButModeRotXYZ:
    case cButModeRotZ:
    case cButModeInvRotZ:
    case cButModeTransZ:
    case cButModeClipNF:
    case cButModeClipN:
    case cButModeClipF:
    case cButModeRotL:
    case cButModeMovL:
    case cButModeMvzL:

      SceneNoteMouseInteraction(G);
      virtual_trackball =
          SettingGet_i(G, nullptr, nullptr, cSetting_virtual_trackball);
      eff_width = I->Width;
      if (stereo_via_adjacent_array(I->StereoMode)) {
        eff_width = I->Width / 2;
        x = get_stereo_x(x, &I->LastX, I->Width, nullptr);
      }
      if (virtual_trackball == 2 &&
          (!I->prev_no_z_rotation1 || !I->prev_no_z_rotation2)) {
        /* when virtual_trackball=2 and twisting, need to set v1,v2 relative to
         * orig */
        v2[0] = (float) (eff_width / 2) - I->orig_x_rotation;
        v2[1] = (float) (I->Height / 2) - I->orig_y_rotation;

        v1[0] = v2[0] - (float) (x - I->LastX);
        v1[1] = v2[1] - (float) (y - I->LastY);

      } else if (virtual_trackball == 1) {
        v1[0] = (float) (eff_width / 2) - x;
        v1[1] = (float) (I->Height / 2) - y;

        v2[0] = (float) (eff_width / 2) - I->LastX;
        v2[1] = (float) (I->Height / 2) - I->LastY;

      } else {
        v1[0] = (float) (I->LastX) - x;
        v1[1] = (float) (I->LastY) - y;

        v2[0] = 0;
        v2[1] = 0;
      }

      r1 = (float) sqrt1f(v1[0] * v1[0] + v1[1] * v1[1]);
      r2 = (float) sqrt1f(v2[0] * v2[0] + v2[1] * v2[1]);

      {
        short r1lt, r2lt;
        if (SettingGet_i(G, nullptr, nullptr, cSetting_virtual_trackball) == 2) {
          r1lt = I->prev_no_z_rotation1;
          r2lt = I->prev_no_z_rotation2;
        } else {
          r1lt = r1 < scale;
          r2lt = r2 < scale;
          I->prev_no_z_rotation1 = r1 < scale;
          I->prev_no_z_rotation2 = r2 < scale;
          I->orig_x_rotation = x;
          I->orig_y_rotation = y;
        }
        if (r1lt) {
          float val = scale * scale - r1 * r1;
          short isNeg = val < 0.f;
          v1[2] = (float) sqrt(fabs(val)) * (isNeg ? -1.f : 1.f);
        } else {
          v1[2] = 0.0;
        }
        if (r2lt) {
          float val = scale * scale - r2 * r2;
          short isNeg = val < 0.f;
          v2[2] = (float) sqrt(fabs(val)) * (isNeg ? -1.f : 1.f);
        } else {
          v2[2] = 0.0;
        }
      }
      normalize23f(v1, n1);
      normalize23f(v2, n2);
      cross_product3f(n1, n2, cp);
      theta =
          (float) (SettingGet_f(G, nullptr, nullptr, cSetting_mouse_scale) * 2 * 180 *
                   asin(sqrt1f(cp[0] * cp[0] + cp[1] * cp[1] + cp[2] * cp[2])) /
                   cPI);

      dx = (v1[0] - v2[0]);
      dy = (v1[1] - v2[1]);
      dt = (float) (SettingGet_f(G, nullptr, nullptr, cSetting_mouse_limit) *
                    sqrt1f(dx * dx + dy * dy) / scale);
      if (theta > dt)
        theta = dt;

      normalize23f(cp, axis);

      theta = theta / (1.0F + (float) fabs(axis[2]));

      v1[2] = 0.0;
      v2[2] = 0.0;
      normalize23f(v1, n1);
      normalize23f(v2, n2);
      cross_product3f(n1, n2, cp);
      omega =
          (float) (2 * 180 *
                   asin(sqrt1f(cp[0] * cp[0] + cp[1] * cp[1] + cp[2] * cp[2])) /
                   cPI);
      normalize23f(cp, axis2);

      old_front = I->m_view.m_clip().m_front;
      old_back = I->m_view.m_clip().m_back;
      old_origin = -I->m_view.pos().z;

      moved_flag = false;
      adjust_flag = false;
      switch (mode) {
      case cButModeRotXYZ:
        if (I->LastX != x) {
          SceneRotate(G, theta, axis[0], axis[1], -axis[2]);
          I->LastX = x;
          adjust_flag = true;
        }
        if (I->LastY != y) {
          SceneRotate(G, theta, axis[0], axis[1], -axis[2]);
          I->LastY = y;
          adjust_flag = true;
        }
        break;
      case cButModeRotZ:
        if (I->LastX != x) {
          SceneRotate(G, omega, axis2[0], axis2[1], -axis2[2]);
          I->LastX = x;
          adjust_flag = true;
        }
        if (I->LastY != y) {
          SceneRotate(G, omega, axis2[0], axis2[1], -axis2[2]);
          I->LastY = y;
          adjust_flag = true;
        }
        break;
      case cButModeInvRotZ:
        if (I->LastX != x) {
          SceneRotate(G, (I->LastX - x) / 2.0F, 0.0F, 0.0F, 1.0F);
          I->LastX = x;
          adjust_flag = true;
        }
        break;
      case cButModeTransZ:
        // zoom
        if (I->LastY != y) {
          {
            // the 5.0 min depth below is an empirical value
            float factor = SettingGetGlobal_f(G, cSetting_mouse_z_scale) *
                           (y - I->LastY) / 400.0 *
                           std::max(5.f, -I->m_view.pos().z);
            if (!SettingGetGlobal_b(G, cSetting_legacy_mouse_zoom))
              factor = -factor;
            I->m_view.translate(0.0f, 0.0f, factor);
            I->m_view.m_clip().m_front -= factor;
            I->m_view.m_clip().m_back -= factor;
            UpdateFrontBackSafe(I);
          }
          I->LastY = y;
          SceneInvalidate(G);
          adjust_flag = true;
        }
        break;
      case cButModeClipNF:
        if (I->LastX != x) {
          I->m_view.m_clip().m_back -= (((float) x) - I->LastX) / 10;
          I->LastX = x;
          moved_flag = true;
        }
        if (I->LastY != y) {
          I->m_view.m_clip().m_front -= (((float) y) - I->LastY) / 10;
          I->LastY = y;
          moved_flag = true;
        }
        if (moved_flag) {
          SceneClipSet(G, I->m_view.m_clip().m_front, I->m_view.m_clip().m_back);
        }
        break;
      case cButModeClipN:
        if (I->LastX != x || I->LastY != y) {
          I->m_view.m_clip().m_front -= (x - I->LastX + y - I->LastY) / 10.f;
          I->LastX = x;
          I->LastY = y;
          SceneClipSet(G, I->m_view.m_clip().m_front, I->m_view.m_clip().m_back);
          moved_flag = true;
        }
        break;
      case cButModeClipF:
        if (I->LastX != x || I->LastY != y) {
          I->m_view.m_clip().m_back -= (x - I->LastX + y - I->LastY) / 10.f;
          I->LastX = x;
          I->LastY = y;
          SceneClipSet(G, I->m_view.m_clip().m_front, I->m_view.m_clip().m_back);
          moved_flag = true;
        }
        break;
      case cButModeRotL:
      case cButModeMovL: {
        /* when light_count == 1, there is an ambient light;
         * when light_count == 2, there are two lights, the ambient and
         * a directional, called "light". When there are three, it's the
         * ambient, light and the third is light2, and so on.
         *
         * Should we use an off-by-one here to make this easier for the
         * user to understand? light1 is ambient, light2 is first directional
         *
         */
        float pos[3];
        int which_light;
        float ms = 0.01;

        which_light = light_setting_indices
            [glm::clamp(SettingGetGlobal_i(G, cSetting_edit_light), 1, 9) - 1];

        copy3f(SettingGet<const float*>(G, which_light), pos);

        pos[0] += (float) dx * ms;
        pos[1] += (float) dy * ms;

        SettingSet_3fv(G->Setting, which_light, pos);
        SettingGenerateSideEffects(G, which_light, nullptr, 0, 1);
        I->LastX = x;
        I->LastY = y;
      } break;
      case cButModeMvzL: {
        float pos[3];
        int which_light;
        float ms = 0.01;
        float factor = 0.f;

        /* when light_count == 1, there is an ambient light;
         * when light_count == 2, there are two lights, the ambient and
         * a directional, called "light". When there are three, it's the
         * ambient, light and the third is light2, and so on.
         */

        which_light = light_setting_indices
            [glm::clamp(SettingGetGlobal_i(G, cSetting_edit_light), 1, 9) - 1];

        if (I->LastY != y) {
          factor =
              400 /
              ((I->m_view.m_clipSafe().m_front + I->m_view.m_clipSafe().m_back) /
                  2);
          if (factor >= 0.0F) {
            factor = SettingGetGlobal_f(G, cSetting_mouse_z_scale) *
                     (((float) y) - I->LastY) / factor;
            if (!SettingGetGlobal_b(G, cSetting_legacy_mouse_zoom))
              factor = -factor;
          }
        }

        copy3f(SettingGet<const float*>(G, which_light), pos);
        SettingGenerateSideEffects(G, which_light, nullptr, 0, 1);

        pos[2] -= factor * ms;

        SettingSet_3fv(G->Setting, which_light, pos);
        I->LastX = x;
        I->LastY = y;
      } break;
      }
      if (moved_flag)
        SceneDoRoving(G, old_front, old_back, old_origin, adjust_flag, false);
    }
  }
  if (I->PossibleSingleClick) {
    int max_single_click_drag = 4;
    int dx = abs(I->StartX - I->LastX);
    int dy = abs(I->StartY - I->LastY);
    if ((dx > max_single_click_drag) || (dy > max_single_click_drag)) {
      I->PossibleSingleClick = false;
    }
  }
}
