// Copyright 2017-2023, Nicholas Sharp and the Polyscope contributors. https://polyscope.run

#include "polyscope/polyscope.h"

#include <chrono>
#include <fstream>
#include <iostream>
#include <thread>

#include "ImGuizmo.h"
#include "imgui.h"
#include "implot.h"

#include "polyscope/imgui_config.h"
#include "polyscope/options.h"
#include "polyscope/pick.h"
#include "polyscope/render/engine.h"
#include "polyscope/utilities.h"
#include "polyscope/view.h"

#include "stb_image.h"

#include "nlohmann/json.hpp"
using json = nlohmann::json;

#include "IconFontCppHeaders/IconsLucide.h"

namespace polyscope {

// Note: Storage for global members lives in state.cpp and options.cpp

// Helpers
namespace {

// === Implement the context stack

// The context stack should _always_ have at least one context in it. The lowest context is the one created at
// initialization.
struct ContextEntry {
  ImGuiContext* context;
  ImPlotContext* plotContext;
  std ::function<void()> callback;
  bool drawDefaultUI;
};
std::vector<ContextEntry> contextStack;
int frameTickStack = 0;

bool redrawNextFrame = true;
bool unshowRequested = false;

// Some state about imgui windows to stack them
constexpr float INITIAL_LEFT_WINDOWS_WIDTH = 305;

auto prevMainLoopTime = std::chrono::steady_clock::now();
float rollingMainLoopDurationMicrosec = 0.;
float rollingMainLoopEMA = 0.05;
float lastMainLoopDurationMicrosec = 0.;

const std::string prefsFilename = ".polyscope.ini";

void readPrefsFile() {

  try {

    std::ifstream inStream(prefsFilename);
    if (inStream) {

      json prefsJSON;
      inStream >> prefsJSON;

      // Set values
      // Do some basic validation on the sizes first to work around bugs with bogus values getting written to init file
      if (view::windowWidth == -1 && prefsJSON.count("windowWidth") > 0) { // only load if not already set
        int val = prefsJSON["windowWidth"];
        if (val >= 64 && val < 10000) view::windowWidth = val;
      }
      if (view::windowHeight == -1 && prefsJSON.count("windowHeight") > 0) { // only load if not already set
        int val = prefsJSON["windowHeight"];
        if (val >= 64 && val < 10000) view::windowHeight = val;
      }
      if (prefsJSON.count("windowPosX") > 0) {
        int val = prefsJSON["windowPosX"];
        if (val >= 0 && val < 10000) view::initWindowPosX = val;
      }
      if (prefsJSON.count("windowPosY") > 0) {
        int val = prefsJSON["windowPosY"];
        if (val >= 0 && val < 10000) view::initWindowPosY = val;
      }
      if (prefsJSON.count("uiScale") > 0) {
        float val = prefsJSON["uiScale"];
        if (val >= 0.25 && val <= 4.0) options::uiScale = val;
      }
    }

  }
  // We never really care if something goes wrong while loading preferences, so eat all exceptions
  catch (...) {
    polyscope::warning("Parsing of prefs file .polyscope.ini failed");
  }
}

void writePrefsFile() {

  // Update values as needed
  int posX, posY;
  std::tie(posX, posY) = render::engine->getWindowPos();
  int windowWidth = view::windowWidth;
  int windowHeight = view::windowHeight;
  float uiScale = options::uiScale;

  // Validate values. Don't write the prefs file if any of these values are obviously bogus (this seems to happen at
  // least on Windows when the application is minimzed)
  bool valuesValid = true;
  valuesValid &= posX >= 0 && posX < 10000;
  valuesValid &= posY >= 0 && posY < 10000;
  valuesValid &= windowWidth >= 64 && windowWidth < 10000;
  valuesValid &= windowHeight >= 64 && windowHeight < 10000;
  valuesValid &= uiScale >= 0.25 && uiScale <= 4.;
  if (!valuesValid) return;

  // Build json object
  // clang-format off
  json prefsJSON = {
      {"windowWidth", windowWidth}, 
      {"windowHeight", windowHeight}, 
      {"windowPosX", posX},
      {"windowPosY", posY},         
      {"uiScale", uiScale},
  };
  // clang-format on

  // Write out json object
  std::ofstream o(prefsFilename);
  o << std::setw(4) << prefsJSON << std::endl;
}

void setInitialWindowWidths() {
  internal::leftWindowsWidth = INITIAL_LEFT_WINDOWS_WIDTH * options::uiScale;
  internal::rightWindowsWidth = options::rightGuiPaneWidth * options::uiScale;
}

void ensureWindowWidthsSet() {
  if (internal::leftWindowsWidth <= 0. || internal::rightWindowsWidth <= 0.) {
    setInitialWindowWidths();
  }
  internal::rightWindowsWidth = options::rightGuiPaneWidth * options::uiScale;
}

// Helper to get a structure map

std::map<std::string, std::unique_ptr<Structure>>& getStructureMapCreateIfNeeded(std::string typeName) {
  if (state::structures.find(typeName) == state::structures.end()) {
    state::structures[typeName] = std::map<std::string, std::unique_ptr<Structure>>();
  }
  return state::structures[typeName];
}

void sleepForFramerate() {
  // If needed, block the program execution to hit the intended framerate
  // (if not used, the render loop may busy-run maxed out at 1000+ fps and waste resources)

  // WARNING:  similar logic duplicated in this function and in shouldSkipFrameForFramerate()

  if (options::maxFPS != -1) {
    auto currTime = std::chrono::steady_clock::now();
    long microsecPerLoop = 1000000 / options::maxFPS;
    microsecPerLoop = (95 * microsecPerLoop) / 100; // give a little slack so we actually hit target fps
    while (std::chrono::duration_cast<std::chrono::microseconds>(currTime - prevMainLoopTime).count() <
           microsecPerLoop) {
      std::this_thread::yield();
      currTime = std::chrono::steady_clock::now();
    }
  }
}

bool shouldSkipFrameForFramerate() {
  // Returns true if the current frame should be skipped to maintain the framerate

  // NOTE: right now this logic is pretty simplistic, it just allows the frame to happen if 95% of the frametime has
  // already passed. This might lead to choppiness in application loops which run at e.g. 150% of the target FPS, or
  // miss opporunities for better timing if frameTick() is called in extremely tight loops.
  //
  // In the future we could write a fancier version of this function implementing smarter policies using
  // rollingMainLoopDurationMicrosec

  // WARNING: similar logic duplicated in this function and in sleepForFramerate()

  if (options::maxFPS <= 0) return false;

  auto currTime = std::chrono::steady_clock::now();
  float microsecPerLoop = 1000000 / options::maxFPS;
  microsecPerLoop = (95 * microsecPerLoop) / 100; // give a little slack so we actually hit target fps
  // NOTE: we could incorporate rollingMainLoopDurationMicrosec here, but since the loop time is recorded at the
  // beginning of each frame, the previous frame's time is _already_ incorporated into the timing.
  auto nextLoopStartTimeToHitTarget =
      prevMainLoopTime + std::chrono::microseconds(static_cast<int64_t>(std::round(microsecPerLoop)));

  return currTime < nextLoopStartTimeToHitTarget;
}

void markLastFrameTime() {
  auto currTime = std::chrono::steady_clock::now();

  // update the prev & rolling average frame time
  lastMainLoopDurationMicrosec =
      std::chrono::duration_cast<std::chrono::microseconds>(currTime - prevMainLoopTime).count();
  rollingMainLoopDurationMicrosec =
      (1. - rollingMainLoopEMA) * rollingMainLoopDurationMicrosec + rollingMainLoopEMA * lastMainLoopDurationMicrosec;

  // mark the time of this frame
  prevMainLoopTime = currTime;
}

} // namespace

// === Core global functions

void init(std::string backend) {
  if (isInitialized()) {
    if (backend != state::backend) {
      exception("re-initializing with different backend is not supported");
    }
    // otherwise silently allow multiple-init
    return;
  }

  info(5, "Initializing Polyscope");

  state::backend = backend;

  if (options::usePrefsFile) {
    readPrefsFile();
  }
  if (view::windowWidth == -1) view::windowWidth = view::defaultWindowWidth;
  if (view::windowHeight == -1) view::windowHeight = view::defaultWindowHeight;

  // Initialize the rendering engine
  render::initializeRenderEngine(backend);

  // Initialie ImGUI
  IMGUI_CHECKVERSION();
  render::engine->createNewImGuiContext();

  // Create an initial context based context. Note that calling show() never actually uses this context, because it
  // pushes a new one each time. But using frameTick() may use this context.
  contextStack.push_back(ContextEntry{ImGui::GetCurrentContext(), ImPlot::GetCurrentContext(), nullptr, true});
  internal::contextStackSize++;

  view::invalidateView();

  state::initialized = true;
  state::doDefaultMouseInteraction = true;
}

void checkInitialized() {
  if (!state::initialized) {
    exception("Polyscope has not been initialized");
  }
}

bool isInitialized() { return state::initialized; }

void pushContext(std::function<void()> callbackFunction, bool drawDefaultUI) {

  // WARNING: code duplicated here and in screenshot.cpp


  // Here, we create a new ImGui context sharing the same window and render
  // with it, al in the midst of an existing ImGui frame. This is a fairly hacky
  // thing to do within ImGui that only barely works, especially with v1.92 changes
  // around contexts, dynamic fonts, and the GLFW ImGui backend. Currently, it requires
  // some slight customization of the backend to even be possible.
  //
  // Useful resources:
  //  -  https://github.com/ocornut/imgui/issues/8680 (and linked PR)

  // Create a new context and push it on to the stack
  ImGuiIO& oldIO = ImGui::GetIO(); // used to GLFW + OpenGL data to the new IO object
  ImGuiContext* oldContext = ImGui::GetCurrentContext();
#ifdef IMGUI_HAS_DOCK
  // WARNING this code may not currently work, recent versions of imgui have changed this functionality,
  // and we do not regularly test with the docking branch.
  ImGuiPlatformIO& oldPlatformIO = ImGui::GetPlatformIO();
#endif
  render::engine->createNewImGuiContext();
  ImGuiContext* newContext = ImGui::GetCurrentContext();
  ImGuiIO& newIO = ImGui::GetIO();
  ImPlotContext* newPlotContext = ImPlot::GetCurrentContext();
  render::engine->updateImGuiContext(oldContext, &oldIO, newContext, &newIO);
#ifdef IMGUI_HAS_DOCK
  // see warning above
  ImGui::GetMainViewport()->PlatformHandle = oldPlatformIO.Viewports[0]->PlatformHandle;
#endif
  ImGuizmo::PushContext();

  contextStack.push_back(ContextEntry{newContext, newPlotContext, callbackFunction, drawDefaultUI});
  internal::contextStackSize++;

  if (contextStack.size() > 50) {
    // Catch bugs with nested show()
    exception("Uh oh, polyscope::show() was recusively MANY times (depth > 50), this is probably a bug. Perhaps "
              "you are accidentally calling show() every time polyscope::userCallback executes?");
  };

  // Make sure the window is visible
  render::engine->showWindow();

  // Re-enter main loop until the context has been popped
  size_t currentContextStackSize = contextStack.size();

  // Helper lambda to perform cleanup of the context
  auto cleanupContext = [&]() {
    // Restore the previous context, if there was one
    if (!contextStack.empty()) {
      ImGui::SetCurrentContext(contextStack.back().context);
      ImPlot::SetCurrentContext(contextStack.back().plotContext);
      render::engine->updateImGuiContext(newContext, &newIO, oldContext, &oldIO);
      ImGuizmo::PopContext();
    }

    // WARNING: code duplicated here and in screenshot.cpp
    // Workaround overzealous ImGui assertion before destroying any inner context
    // https://github.com/ocornut/imgui/pull/7175
    newIO.BackendPlatformUserData = nullptr;
    newIO.BackendRendererUserData = nullptr;
    ImPlot::DestroyContext(newPlotContext);
    ImGui::DestroyContext(newContext);
  };

  try {
    while (contextStack.size() >= currentContextStackSize) {

      sleepForFramerate();
      mainLoopIteration();

      // auto-exit if the window is closed
      if (render::engine->windowRequestsClose()) {
        popContext();
      }
    }
  } catch (...) {
    // Ensure cleanup happens even if an exception is thrown
    // NOTE: we don't really guarantee exception safety in general, it's likely that things are broken if an excpetion
    // was thrown. But this handles a few of the worst and most confusing cases.
    popContext();
    cleanupContext();
    throw; // re-throw the exception after cleanup
  }

  cleanupContext();
}


void popContext() {
  if (contextStack.empty()) {
    exception("Called popContext() too many times");
    return;
  }
  contextStack.pop_back();
  internal::contextStackSize--;
}

ImGuiContext* getCurrentContext() { return contextStack.empty() ? nullptr : contextStack.back().context; }

void frameTick() {
  checkInitialized();

  // Do some sanity-checking around control flow and use of frameTick() / show()
  if (contextStack.size() > 1) {
    exception("Do not call frameTick() while show() is already looping the main loop.");
  }
  if (frameTickStack > 0) {
    exception("You called frameTick() while a previous call was in the midst of executing. Do not re-enter frameTick() "
              "or call it recursively.");
  }


  // == Logic for frame tick FPS limits
  bool savedVsyncValue = false;         // see below
  bool needToRestoreVSyncValue = false; // need to save this, the setting could change during mainLoopIteration()
  switch (options::frameTickLimitFPSMode) {
  case LimitFPSMode::IgnoreLimits:
    // Ugly workaround to preserve the API:
    // We want vsync to be disabled if we're ignoring fps limits (otherwise the platform will potentially block on
    // render swap to match the displays refresh rate). But it's currently a bool read in the render call and I don't
    // want to change that API. So we temporarily set it to false and restore the value after. ONEDAY: when we have a
    // major version update, change the API on the vsync setting to make this unecessary
    savedVsyncValue = options::enableVSync;
    options::enableVSync = false;
    needToRestoreVSyncValue = true;
    break;
  case LimitFPSMode::BlockToHitTarget:
    sleepForFramerate();
    break;
  case LimitFPSMode::SkipFramesToHitTarget:
    if (shouldSkipFrameForFramerate()) {
      return;
    }
    break;
  }


  frameTickStack++;
  render::engine->showWindow();

  mainLoopIteration();

  if (needToRestoreVSyncValue) {
    // restore saved value, see note above
    options::enableVSync = savedVsyncValue;
  }

  frameTickStack--;
}

void requestRedraw() { redrawNextFrame = true; }
bool redrawRequested() { return redrawNextFrame; }

void drawStructures() {

  // Draw all off the structures registered with polyscope

  for (auto& catMap : state::structures) {
    for (auto& s : catMap.second) {
      s.second->draw();
    }
  }

  // Also render any slice plane geometry
  for (std::unique_ptr<SlicePlane>& s : state::slicePlanes) {
    s->drawGeometry();
  }
}

void drawStructuresDelayed() {
  // "delayed" drawing allows structures to render things which should be rendered after most of the scene has been
  // drawn
  for (auto& catMap : state::structures) {
    for (auto& s : catMap.second) {
      s.second->drawDelayed();
    }
  }
}

namespace {

float dragDistSinceLastRelease = 0.0;

// State for delayed pick selection
bool pendingPickActive = false;
PickResult pendingPickResult;
float pendingPickTime = 0.0f;

void processInputEvents() {
  ImGuiIO& io = ImGui::GetIO();

  // RECALL: in ImGUI language, on MacOS "ctrl" == "cmd", so all the options
  // below referring to ctrl really mean cmd on MacOS.

  // If any mouse button is pressed, trigger a redraw
  if (ImGui::IsAnyMouseDown()) {
    requestRedraw();
  }

  bool widgetCapturedMouse = false;

  // Handle scroll events for 3D view
  if (state::doDefaultMouseInteraction) {

    for (WeakHandle<Widget> wHandle : state::widgets) {
      if (wHandle.isValid()) {
        Widget& w = wHandle.get();
        widgetCapturedMouse = w.interact();
        if (widgetCapturedMouse) {
          break;
        }
      }
    }

    // === Mouse inputs
    if (!io.WantCaptureMouse && !widgetCapturedMouse) {

      { // Process scroll via "mouse wheel" (which might be a touchpad)
        float xoffset = io.MouseWheelH;
        float yoffset = io.MouseWheel;
        float scrollOffset = yoffset;
        float clipPlaneOffset = std::abs(xoffset) > std::abs(yoffset) ? xoffset : yoffset;

        // NOTE: here we used to scroll according the to the larger of the two offsets (x or y)
        // (there was a comment about 'shift swaps scroll on some platforms'). However, on many
        // common machines (e.g. macs with touchpads),  two finger scrolling produces both x and y
        // offsets simultaneously, leading to jumpy zooms when an x was greater than a y.
        // So now we just use the y offset always for zooming. (But still use either for clip plane shifting,
        // which might involve intentionally holding shift)

        bool scrollClipPlane = io.KeyShift && !io.KeyCtrl;
        if (scrollClipPlane && clipPlaneOffset != 0.0f) {
          view::processClipPlaneShift(clipPlaneOffset);
          requestRedraw();
        }
        if (!scrollClipPlane && scrollOffset != 0.0f) {
          view::processZoom(0.5 * scrollOffset);
          requestRedraw();
        }
      }


      { // Process drags
        bool dragLeft = ImGui::IsMouseDragging(0);
        bool dragRight = !dragLeft && ImGui::IsMouseDragging(1); // left takes priority, so only one can be true
        if (dragLeft || dragRight) {

          glm::vec2 dragDelta{io.MouseDelta.x / view::windowWidth, -io.MouseDelta.y / view::windowHeight};
          dragDistSinceLastRelease += std::abs(dragDelta.x);
          dragDistSinceLastRelease += std::abs(dragDelta.y);

          // exactly one of these will be true
          bool isRotate = dragLeft && !io.KeyShift && !io.KeyCtrl;
          bool isTranslate = (dragLeft && io.KeyShift && !io.KeyCtrl) || dragRight;
          bool isDragZoom = dragLeft && io.KeyShift && io.KeyCtrl;

          if (isDragZoom) {
            view::processZoom(dragDelta.y * 5);
          }
          if (isRotate) {
            glm::vec2 currPos{io.MousePos.x / view::windowWidth,
                              (view::windowHeight - io.MousePos.y) / view::windowHeight};
            currPos = (currPos * 2.0f) - glm::vec2{1.0, 1.0};
            if (std::abs(currPos.x) <= 1.0 && std::abs(currPos.y) <= 1.0) {
              view::processRotate(currPos - 2.0f * dragDelta, currPos);
            }
          }
          if (isTranslate) {
            view::processTranslate(dragDelta);
          }
        }
      }

      { // Click picks to select content onscreen
        float dragIgnoreThreshold = 0.01;
        bool anyModifierHeld = io.KeyShift || io.KeyCtrl || io.KeyAlt;
        bool ctrlShiftHeld = io.KeyShift && io.KeyCtrl;

        // NOTE: there is some extra 'pending' logic here, because we use double clicks to recenter the view, but we
        // don't want a pick to trigger after the first click of a double click.  To handle this, we delay applying
        // picks until enough time has passed that a double click would have been registered.

        // Check if enough time has passed for a pending pick to be applied
        if (pendingPickActive) {
          float elapsedSec = ImGui::GetTime() - pendingPickTime;
          float pickDelaySec = ImGui::GetIO().MouseDoubleClickTime + 0.05f; // 50ms margin for safety
          if (haveSelection() || elapsedSec >= pickDelaySec) {
            // enough time has passed, apply the pending pick
            setSelection(pendingPickResult);
            pendingPickActive = false;
          }
        }

        if (!anyModifierHeld && (io.MouseReleased[0] && io.MouseClickedLastCount[0] == 1)) {

          // don't pick at the end of a long drag
          if (dragDistSinceLastRelease < dragIgnoreThreshold) {
            glm::vec2 screenCoords{io.MousePos.x, io.MousePos.y};
            PickResult pickResult = pickAtScreenCoords(screenCoords);
            // queue the pick for delayed application
            pendingPickResult = pickResult;
            pendingPickTime = ImGui::GetTime();
            pendingPickActive = true;
          }
        }

        // Clear pick
        if (!anyModifierHeld && io.MouseReleased[1]) {
          if (dragDistSinceLastRelease < dragIgnoreThreshold) {
            resetSelection();
          }
          dragDistSinceLastRelease = 0.0;
          pendingPickActive = false;
        }

        // Double-click or Ctrl-shift left-click to set new center
        if ((io.MouseReleased[0] && io.MouseClickedLastCount[0] == 2) || (io.MouseReleased[0] && ctrlShiftHeld)) {
          if (dragDistSinceLastRelease < dragIgnoreThreshold) {
            glm::vec2 screenCoords{io.MousePos.x, io.MousePos.y};
            view::processSetCenter(screenCoords);
            pendingPickActive = false; // cancel any pending pick from the first click
          }
        }
      }
    }
  }

  // Reset the drag distance after any release
  if (io.MouseReleased[0]) {
    dragDistSinceLastRelease = 0.0;
  }

  // === Key-press inputs
  if (!io.WantCaptureKeyboard) {
    view::processKeyboardNavigation(io);
  }
}


void renderSlicePlanes() {
  for (std::unique_ptr<SlicePlane>& s : state::slicePlanes) {
    s->draw();
  }
}

void renderScene() {

  render::engine->applyTransparencySettings();

  render::engine->sceneBuffer->clearColor = {0., 0., 0.};
  render::engine->sceneBuffer->clearAlpha = 0.;
  render::engine->sceneBuffer->clear();

  if (!render::engine->bindSceneBuffer()) return;

  // If a view has never been set, this will set it to the home view
  view::ensureViewValid();

  if (!options::renderScene) return;

  if (render::engine->getTransparencyMode() == TransparencyMode::Pretty) {
    // Special depth peeling case: multiple render passes
    // We will perform several "peeled" rounds of rendering in to the usual scene buffer. After each, we will manually
    // composite in to the final scene buffer.


    // Clear the final buffer explicitly since we will gradually composite in to it rather than just blitting directly
    // as in normal rendering.
    render::engine->sceneBufferFinal->clearColor = glm::vec3{0., 0., 0.};
    render::engine->sceneBufferFinal->clearAlpha = 0;
    render::engine->sceneBufferFinal->clear();

    render::engine->setDepthMode(DepthMode::Less); // we need depth to be enabled for the clear below to do anything
    render::engine->sceneDepthMinFrame->clear();


    for (int iPass = 0; iPass < options::transparencyRenderPasses; iPass++) {
      bool isRedraw = iPass > 0;
      internal::renderPassIsRedraw = isRedraw;

      render::engine->bindSceneBuffer();
      render::engine->clearSceneBuffer();

      render::engine->applyTransparencySettings();
      drawStructures();

      // Draw ground plane, slicers, etc
      render::engine->groundPlane.draw(isRedraw);
      if (!isRedraw) {
        // Only on first pass (kinda weird, but works out, and doesn't really matter)
        renderSlicePlanes();
      }

      render::engine->applyTransparencySettings();
      drawStructuresDelayed();

      // Composite the result of this pass in to the result buffer
      render::engine->sceneBufferFinal->bind();
      render::engine->setDepthMode(DepthMode::Disable);
      render::engine->setBlendMode(BlendMode::AlphaUnder);
      render::engine->compositePeel->draw();

      // Update the minimum depth texture
      render::engine->updateMinDepthTexture();
    }
    internal::renderPassIsRedraw = false; // reset to usual value


  } else {
    // Normal case: single render pass
    internal::renderPassIsRedraw = false;

    render::engine->applyTransparencySettings();
    drawStructures();

    render::engine->groundPlane.draw();
    renderSlicePlanes();

    render::engine->applyTransparencySettings();
    drawStructuresDelayed();

    render::engine->sceneBuffer->blitTo(render::engine->sceneBufferFinal.get());
  }
}

void renderSceneToScreen() {
  render::engine->bindDisplay();
  if (options::debugDrawPickBuffer) {
    // special debug draw
    pick::evaluatePickQuery(-1, -1); // populate the buffer
    render::engine->pickFramebuffer->blitTo(render::engine->displayBuffer.get());
  } else {
    render::engine->applyLightingTransform(render::engine->sceneColorFinal);
  }
}

void purgeWidgets() {
  // remove any widget objects which are no longer defined
  state::widgets.erase(std::remove_if(state::widgets.begin(), state::widgets.end(),
                                      [](const WeakHandle<Widget>& w) { return !w.isValid(); }),
                       state::widgets.end());
}

void userGuiBegin() {
  ensureWindowWidthsSet();

  ImVec2 userGuiLoc;
  if (options::userGuiIsOnRightSide) {
    // right side
    userGuiLoc = ImVec2(view::windowWidth - (internal::rightWindowsWidth + internal::imguiStackMargin),
                        internal::imguiStackMargin);

    ImGui::SetNextWindowSize(ImVec2(internal::rightWindowsWidth, 0.));
  } else {
    // left side
    if (options::buildDefaultGuiPanels) {
      userGuiLoc = ImVec2(internal::leftWindowsWidth + 2 * internal::imguiStackMargin, internal::imguiStackMargin);
    } else {
      userGuiLoc = ImVec2(internal::imguiStackMargin, internal::imguiStackMargin);
    }
  }


  ImGui::PushID("user_callback");
  ImGui::SetNextWindowPos(userGuiLoc);

  ImGui::Begin("##Command UI", nullptr);
}

void userGuiEnd() {

  if (options::userGuiIsOnRightSide) {
    internal::lastWindowHeightUser =
        internal::imguiStackMargin + ImGui::GetWindowHeight(); // TODO using deprecated function
    internal::lastRightSideFreeX = view::windowWidth - internal::imguiStackMargin;
    internal::lastRightSideFreeY = internal::lastWindowHeightUser;
  } else {
    internal::lastWindowHeightUser = 0;
    internal::lastRightSideFreeX = view::windowWidth - internal::imguiStackMargin;
    internal::lastRightSideFreeY = 0;
  }
  ImGui::End();
  ImGui::PopID();
}

} // namespace

void buildPolyscopeGui() {
  ensureWindowWidthsSet();

  // Create window
  ImGui::SetNextWindowPos(ImVec2(internal::imguiStackMargin, internal::imguiStackMargin));
  ImGui::SetNextWindowSize(ImVec2(internal::leftWindowsWidth, 0.));

  ImGui::Begin("Polyscope ", nullptr);

  if (ImGui::Button(ICON_LC_HOUSE " Reset View")) {
    view::flyToHomeView();
  }
  ImGui::SameLine();
  ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(1.0f, 0.0f));
  if (ImGui::Button("Screenshot")) {
    ScreenshotOptions options;
    options.transparentBackground = options::screenshotTransparency;
    options.includeUI = options::screenshotWithImGuiUI;
    screenshot(options);
  }
  ImGui::SameLine();
  if (ImGui::ArrowButton("##Option", ImGuiDir_Down)) {
    ImGui::OpenPopup("ScreenshotOptionsPopup");
  }
  ImGui::PopStyleVar();
  if (ImGui::BeginPopup("ScreenshotOptionsPopup")) {

    ImGui::Checkbox("with transparency", &options::screenshotTransparency);
    // ImGui::Checkbox("with UI", &options::screenshotWithImGuiUI); // temporarily disabled while broken

    if (ImGui::BeginMenu("file format")) {
      if (ImGui::MenuItem(".png", NULL, options::screenshotExtension == ".png")) options::screenshotExtension = ".png";
      if (ImGui::MenuItem(".jpg", NULL, options::screenshotExtension == ".jpg")) options::screenshotExtension = ".jpg";
      ImGui::EndMenu();
    }

    ImGui::EndPopup();
  }


  ImGui::SameLine();
  if (ImGui::Button(ICON_LC_CIRCLE_QUESTION_MARK)) {
    // do nothing, just want hover state
  }
  if (ImGui::IsItemHovered()) {

    ImGui::SetNextWindowPos(
        ImVec2(2 * internal::imguiStackMargin + internal::leftWindowsWidth, internal::imguiStackMargin));
    ImGui::SetNextWindowSize(ImVec2(0., 0.));

    // clang-format off
		ImGui::Begin("Controls", NULL, ImGuiWindowFlags_NoTitleBar);
    ImGui::TextUnformatted("View Navigation:");
			ImGui::TextUnformatted("   Rotate: [left click drag]");
			ImGui::TextUnformatted("   Translate: [shift] + [left click drag] OR [right click drag]");
			ImGui::TextUnformatted("   Zoom: [scroll] OR [ctrl/cmd] + [shift] + [left click drag]");
			ImGui::TextUnformatted("   To set the view orbit center, double-click OR hold");
			ImGui::TextUnformatted("     [ctrl/cmd] + [shift] and [left click] in the scene.");
			ImGui::TextUnformatted("   Save and restore camera poses via the system clipboard with");
			ImGui::TextUnformatted("     [ctrl/cmd-c] and [ctrl/cmd-v].");
      ImGui::TextUnformatted("\nMenu Navigation:");
			ImGui::TextUnformatted("   Menu headers with a '>' can be clicked to collapse and expand.");
			ImGui::TextUnformatted("   Use [ctrl/cmd] + [left click] to manually enter any numeric value");
			ImGui::TextUnformatted("     via the keyboard.");
			ImGui::TextUnformatted("   Press [space] to dismiss popup dialogs.");
		ImGui::TextUnformatted("\nSelection:");
			ImGui::TextUnformatted("   Select elements of a structure with [left click]. Data from");
			ImGui::TextUnformatted("     that element will be shown on the right. Use [right click]");
			ImGui::TextUnformatted("     to clear the selection.");
		ImGui::End();
    // clang-format on
  }

  // View options tree
  view::buildViewGui();

  // Appearance options tree
  render::engine->buildEngineGui();

  // Render options tree
  ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver);
  if (ImGui::TreeNode("Render")) {

    // fps display
    ImGui::Text("Rolling: %.1f ms/frame (%.1f fps)", 1e-3f * rollingMainLoopDurationMicrosec,
                1.e6f / rollingMainLoopDurationMicrosec);
    ImGui::Text("Last: %.1f ms/frame (%.1f fps)", 1e-3f * lastMainLoopDurationMicrosec,
                1.e6f / lastMainLoopDurationMicrosec);

    bool isFrameTickShow = frameTickStack > 0;
    if (isFrameTickShow) {
      // build a little combo box to pick fps mode
      constexpr std::array<LimitFPSMode, 3> limitFPSModeVals = {
          LimitFPSMode::IgnoreLimits, LimitFPSMode::BlockToHitTarget, LimitFPSMode::SkipFramesToHitTarget};
      auto to_string = [](LimitFPSMode x) -> std::string {
        switch (x) {
        case LimitFPSMode::IgnoreLimits:
          return "ignore limits";
        case LimitFPSMode::BlockToHitTarget:
          return "block to hit target";
        case LimitFPSMode::SkipFramesToHitTarget:
          return "skip frames to hit target";
        }
        return ""; // unreachable
      };
      if (ImGui::BeginCombo("fps mode##frame tick limit fps mode", to_string(options::frameTickLimitFPSMode).c_str())) {
        for (LimitFPSMode x : limitFPSModeVals) {
          if (ImGui::Selectable(to_string(x).c_str(), options::frameTickLimitFPSMode == x)) {
            options::frameTickLimitFPSMode = x;
            ImGui::SetItemDefaultFocus();
          }
        }
        ImGui::EndCombo();
      }
    }

    ImGui::BeginDisabled(isFrameTickShow && options::frameTickLimitFPSMode == LimitFPSMode::IgnoreLimits);

    ImGui::PushItemWidth(40 * options::uiScale);

    if (ImGui::InputInt("max fps", &options::maxFPS, 0)) {
      if (options::maxFPS < 1 && options::maxFPS != -1) {
        options::maxFPS = -1;
      }
    }

    ImGui::PopItemWidth();
    ImGui::SameLine();
    ImGui::Checkbox("vsync", &options::enableVSync);

    ImGui::EndDisabled();

    ImGui::TreePop();
  }

  ImGui::SetNextItemOpen(false, ImGuiCond_FirstUseEver);
  if (ImGui::TreeNode("Debug")) {

    if (ImGui::Button("Force refresh")) {
      refresh();
    }
    ImGui::Checkbox("Show pick buffer", &options::debugDrawPickBuffer);
    ImGui::Checkbox("Always redraw", &options::alwaysRedraw);

    static bool showDebugTextures = false;
    ImGui::Checkbox("Show debug textures", &showDebugTextures);
    if (showDebugTextures) {
      render::engine->showTextureInImGuiWindow("Scene", render::engine->sceneColor.get());
      render::engine->showTextureInImGuiWindow("Scene Final", render::engine->sceneColorFinal.get());
    }
    ImGui::TreePop();
  }


  internal::lastWindowHeightPolyscope = ImGui::GetWindowHeight();
  internal::leftWindowsWidth = ImGui::GetWindowWidth();

  ImGui::End();
}

void buildStructureGui() {
  ensureWindowWidthsSet();

  // Create window
  ImGui::SetNextWindowPos(
      ImVec2(internal::imguiStackMargin, internal::lastWindowHeightPolyscope + 2 * internal::imguiStackMargin));
  ImGui::SetNextWindowSize(ImVec2(internal::leftWindowsWidth, view::windowHeight - internal::lastWindowHeightPolyscope -
                                                                  3 * internal::imguiStackMargin));
  ImGui::Begin("Structures", nullptr);

  // only show groups if there are any
  if (state::groups.size() > 0) {
    if (ImGui::CollapsingHeader("Groups", ImGuiTreeNodeFlags_DefaultOpen)) {
      for (auto& x : state::groups) {
        if (x.second->isRootGroup()) {
          x.second->buildUI();
        }
      }
    }
  }

  // groups have an option to hide structures from this list; assemble a list of structures to skip
  std::unordered_set<Structure*> structuresToSkip;
  for (auto& x : state::groups) {
    x.second->appendStructuresToSkip(structuresToSkip);
  }


  for (auto& catMapEntry : state::structures) {
    std::string catName = catMapEntry.first;

    std::map<std::string, std::unique_ptr<Structure>>& structureMap = catMapEntry.second;

    ImGui::PushID(catName.c_str()); // ensure there are no conflicts with
                                    // identically-named labels

    // Build the structure's UI
    ImGui::SetNextItemOpen(structureMap.size() > 0, ImGuiCond_FirstUseEver);
    if (ImGui::CollapsingHeader((catName + " (" + std::to_string(structureMap.size()) + ")").c_str())) {
      // Draw shared GUI elements for all instances of the structure
      if (structureMap.size() > 0) {
        structureMap.begin()->second->buildSharedStructureUI();
      }

      int32_t skipCount = 0;
      for (auto& x : structureMap) {
        ImGui::SetNextItemOpen(structureMap.size() <= 8,
                               ImGuiCond_FirstUseEver); // closed by default if more than 8

        if (structuresToSkip.find(x.second.get()) != structuresToSkip.end()) {
          skipCount++;
          continue;
        }

        x.second->buildUI();
      }

      if (skipCount > 0) {
        ImGui::Text("  (skipped %d hidden structures)", skipCount);
      }
    }

    ImGui::PopID();
  }

  internal::leftWindowsWidth = ImGui::GetWindowWidth();

  ImGui::End();
}

void buildPickGui() {
  ensureWindowWidthsSet();

  if (haveSelection()) {

    ImGui::SetNextWindowPos(ImVec2(view::windowWidth - (internal::rightWindowsWidth + internal::imguiStackMargin),
                                   internal::lastRightSideFreeY + internal::imguiStackMargin));
    ImGui::SetNextWindowSize(ImVec2(internal::rightWindowsWidth, 0.));

    ImGui::Begin("Selection", nullptr);
    PickResult selection = getSelection();


    ImGui::Text("screen coordinates: (%.2f,%.2f)  depth: %g", selection.screenCoords.x, selection.screenCoords.y,
                selection.depth);
    ImGui::Text("world position: <%g, %g, %g>", selection.position.x, selection.position.y, selection.position.z);
    ImGui::NewLine();

    ImGui::TextUnformatted((selection.structureType + ": " + selection.structureName).c_str());
    if (selection.quantityName != "") {
      ImGui::TextUnformatted(("Quantity: " + selection.quantityName).c_str());
    }
    ImGui::Separator();

    if (selection.structureHandle.isValid()) {
      selection.structureHandle.get().buildPickUI(selection);
    } else {
      // this is a paranoid check, it _should_ never happen since we
      // clear the selection when a structure is deleted
      // NOTE: it might be possible to hit this due to the way we set delayed picks, if the structure is deleted during
      // the delay
      ImGui::TextUnformatted("ERROR: INVALID STRUCTURE");
      resetSelection();
    }

    internal::rightWindowsWidth = ImGui::GetWindowWidth();
    internal::lastRightSideFreeY += internal::imguiStackMargin + ImGui::GetWindowHeight();
    ImGui::End();
  }
}

void buildUserGuiAndInvokeCallback() {

  if (!options::invokeUserCallbackForNestedShow && (contextStack.size() + frameTickStack) > 2) {
    return;
  }

  if (state::userCallback) {

    bool beganUserGUI = false;
    if (options::buildGui && options::openImGuiWindowForUserCallback) {
      userGuiBegin();
      beganUserGUI = true;
    }

    state::userCallback();

    if (beganUserGUI) {
      userGuiEnd();
    } else {
      internal::lastWindowHeightUser = internal::imguiStackMargin;
      internal::lastRightSideFreeX = view::windowWidth - internal::imguiStackMargin;
      internal::lastRightSideFreeY = internal::imguiStackMargin;
    }

  } else {
    internal::lastWindowHeightUser = internal::imguiStackMargin;
    internal::lastRightSideFreeX = view::windowWidth - internal::imguiStackMargin;
    internal::lastRightSideFreeY = internal::imguiStackMargin;
  }
}

void draw(bool withUI, bool withContextCallback) {
  processLazyProperties();

  // Update buffer and context
  render::engine->makeContextCurrent();
  render::engine->bindDisplay();
  render::engine->setBackgroundColor({0., 0., 0.});
  render::engine->setBackgroundAlpha(0);
  render::engine->clearDisplay();

  if (withUI) {
    render::engine->ImGuiNewFrame();

    processInputEvents();
    view::updateFlight();
    showDelayedWarnings();
  }

  // Build the GUI components
  if (withUI) {
    if (contextStack.back().drawDefaultUI) {

      // Note: It is important to build the user GUI first, because it is likely that callbacks there will modify
      // polyscope data. If we do these modifications happen later in the render cycle, they might invalidate data which
      // is necessary when ImGui::Render() happens below.
      buildUserGuiAndInvokeCallback();

      if (options::buildGui) {
        if (options::buildDefaultGuiPanels) {
          buildPolyscopeGui();
          buildStructureGui();
          buildPickGui();
        }

        for (WeakHandle<Widget> wHandle : state::widgets) {
          if (wHandle.isValid()) {
            Widget& w = wHandle.get();
            w.buildUI();
          }
        }
      }
    }
  }

  // Execute the context callback, if there is one.
  // This callback is a Polyscope implementation detail, which is distinct from the userCallback (which gets called
  // above)
  if (withContextCallback && contextStack.back().callback) {
    (contextStack.back().callback)();
  }

  processLazyProperties();

  // Draw structures in the scene
  if (redrawNextFrame || options::alwaysRedraw) {
    renderScene();
    redrawNextFrame = false;
  }
  renderSceneToScreen();

  // Draw the GUI
  if (withUI) {
    // render widgets
    render::engine->bindDisplay();
    for (WeakHandle<Widget> wHandle : state::widgets) {
      if (wHandle.isValid()) {
        Widget& w = wHandle.get();
        w.draw();
      }
    }

    render::engine->bindDisplay();
    render::engine->ImGuiRender();
  }
}


void mainLoopIteration() {
  markLastFrameTime();

  processLazyProperties();
  processLazyPropertiesOutsideOfImGui();

  render::engine->makeContextCurrent();
  render::engine->updateWindowSize();

  // Process UI events
  render::engine->pollEvents();

  // Housekeeping
  purgeWidgets();

  // Rendering
  draw();
  render::engine->swapDisplayBuffers();
}

void show(size_t forFrames) {

  checkInitialized();

  if (isHeadless() && forFrames == 0) {
    info("You called show() while in headless mode. In headless mode there is no display to create windows on. By "
         "default, the show() call will block indefinitely. If you did not mean to run in headless mode, check the "
         "initialization settings. Otherwise, be sure to set a callback to make something happen while polyscope is "
         "showing the UI, or use functions like screenshot() to render directly without calling show().");
  }

  unshowRequested = false;

  // the popContext() doesn't quit until _after_ the last frame, so we need to decrement by 1 to get the count right
  if (forFrames > 0) forFrames--;

  auto checkFrames = [&]() {
    if (forFrames == 0 || unshowRequested) {
      popContext();
    } else {
      forFrames--;
    }
  };

  if (options::giveFocusOnShow) {
    render::engine->focusWindow();
  }

  pushContext(checkFrames);

  if (options::usePrefsFile) {
    writePrefsFile();
  }

  // if this was the outermost show(), hide the window afterward
  if (contextStack.size() == 1) {
    if (options::hideWindowAfterShow) {
      render::engine->hideWindow();
    }
  }
}

void unshow() { unshowRequested = true; }

bool windowRequestsClose() {
  if (render::engine && render::engine->windowRequestsClose()) {
    return true;
  }

  return false;
}

bool isHeadless() {
  if (!isInitialized()) {
    exception("must initialize Polyscope with init() before calling isHeadless().");
  }
  if (render::engine) {
    return render::engine->isHeadless();
  }
  return false;
}

void removeEverything() {
  removeAllStructures();
  removeAllGroups();
  removeAllSlicePlanes();
  removeAllTransformationGizmos();
  clearMessages();
  state::userCallback = nullptr;
  state::filesDroppedCallback = nullptr;
  options::configureImGuiStyleCallback = configureImGuiStyle; // restore defaults
  options::prepareImGuiFontsCallback = loadBaseFonts;
}

void shutdown(bool allowMidFrameShutdown) {
  checkInitialized();

  if (!allowMidFrameShutdown && contextStack.size() > 1) {
    terminatingError("shutdown() was called mid-frame (e.g. in a per-frame callback, or UI element). This is not "
                     "permitted, shutdown() may only be called when the main loop is not executing.");
  }

  if (options::usePrefsFile) {
    writePrefsFile();
  }

  removeEverything();

  // Shut down the render engine
  render::engine->shutdown();
  delete render::engine;
  contextStack.clear();
  internal::contextStackSize = 0;
  render::engine = nullptr;
  state::backend = "";
  state::initialized = false;
}

bool registerStructure(Structure* s, bool replaceIfPresent) {

  std::string typeName = s->typeName();
  std::map<std::string, std::unique_ptr<Structure>>& sMap = getStructureMapCreateIfNeeded(typeName);

  // Check if the structure name is in use
  bool inUse = sMap.find(s->name) != sMap.end();
  if (inUse) {
    if (replaceIfPresent) {
      removeStructure(s->name);
    } else {
      exception("Attempted to register structure with name " + s->name +
                ", but a structure with that name already exists");
      return false;
    }
  }

  // Center/scale if desired
  if (options::autocenterStructures) {
    s->centerBoundingBox();
  }
  if (options::autoscaleStructures) {
    s->rescaleToUnit();
  }

  // Add the new structure
  sMap[s->name] = std::unique_ptr<Structure>(s); // take ownership with a unique pointer
  updateStructureExtents();
  requestRedraw();

  return true;
}

Structure* getStructure(std::string type, std::string name) {

  // If there are no structures of that type it is an automatic fail
  if (state::structures.find(type) == state::structures.end()) {
    exception("No structures of type " + type + " registered");
    return nullptr;
  }
  std::map<std::string, std::unique_ptr<Structure>>& sMap = state::structures[type];

  // Special automatic case, return any
  if (name == "") {
    if (sMap.size() != 1) {
      exception("Cannot use automatic structure get with empty name unless there is exactly one structure of that type "
                "registered");
      return nullptr;
    }
    return sMap.begin()->second.get();
  }

  // General case
  if (sMap.find(name) == sMap.end()) {
    exception("No structure of type " + type + " with name " + name + " registered");
    return nullptr;
  }
  return sMap[name].get();
}

bool hasStructure(std::string type, std::string name) {
  // If there are no structures of that type it is an automatic fail
  if (state::structures.find(type) == state::structures.end()) {
    return false;
  }
  std::map<std::string, std::unique_ptr<Structure>>& sMap = state::structures[type];

  // Special automatic case, return any
  if (name == "") {
    if (sMap.size() != 1) {
      exception(
          "Cannot use automatic has-structure test with empty name unless there is exactly one structure of that type "
          "registered");
    }
    return true;
  }
  return sMap.find(name) != sMap.end();
}

void removeStructure(std::string type, std::string name, bool errorIfAbsent) {

  // If there are no structures of that type it is an automatic fail
  if (state::structures.find(type) == state::structures.end()) {
    if (errorIfAbsent) {
      exception("No structures of type " + type + " registered");
    }
    return;
  }
  std::map<std::string, std::unique_ptr<Structure>>& sMap = state::structures[type];

  // Check if structure exists
  if (sMap.find(name) == sMap.end()) {
    if (errorIfAbsent) {
      exception("No structure of type " + type + " and name " + name + " registered");
    }
    return;
  }

  // Structure exists, remove it
  Structure* s = sMap[name].get();
  if (static_cast<void*>(s) == static_cast<void*>(internal::globalFloatingQuantityStructure)) {
    internal::globalFloatingQuantityStructure = nullptr;
  }
  // remove it from all existing groups
  for (auto& g : state::groups) {
    g.second->removeChildStructure(*s);
  }
  resetSelectionIfStructure(s);
  sMap.erase(s->name);
  updateStructureExtents();
  return;
}

void removeStructure(Structure* structure, bool errorIfAbsent) {
  removeStructure(structure->typeName(), structure->name, errorIfAbsent);
}

void removeStructure(std::string name, bool errorIfAbsent) {

  // Check if we can find exactly one structure matching the name
  Structure* targetStruct = nullptr;
  for (auto& typeMap : state::structures) {
    for (auto& entry : typeMap.second) {

      // Found a matching structure
      if (entry.first == name) {
        if (targetStruct == nullptr) {
          targetStruct = entry.second.get();
        } else {
          exception(
              "Cannot use automatic structure remove with empty name unless there is exactly one structure of that "
              "type registered. Found two structures of different types with that name: " +
              targetStruct->typeName() + " and " + typeMap.first + ".");
          return;
        }
      }
    }
  }

  // Error if none found.
  if (targetStruct == nullptr) {
    if (errorIfAbsent) {
      exception("No structure named: " + name + " to remove.");
    }
    return;
  }

  removeStructure(targetStruct->typeName(), targetStruct->name, errorIfAbsent);
  requestRedraw();
}

void removeAllStructures() {

  for (auto& typeMap : state::structures) {

    // dodge iterator invalidation
    std::vector<std::string> names;
    for (auto& entry : typeMap.second) {
      names.push_back(entry.first);
    }

    // remove all
    for (std::string name : names) {
      removeStructure(typeMap.first, name);
    }
  }

  requestRedraw();
  resetSelection();
}


Group* createGroup(std::string name) {
  checkInitialized();

  // check if group already exists
  bool inUse = state::groups.find(name) != state::groups.end();
  if (inUse) {
    exception("Attempted to register group with name " + name + ", but a group with that name already exists");
    return nullptr;
  }

  // add to the group map
  state::groups[name] = std::unique_ptr<Group>(new Group(name));

  return state::groups[name].get();
}

Group* getGroup(std::string groupName) {
  // check if group exists
  bool groupExists = state::groups.find(groupName) != state::groups.end();
  if (!groupExists) {
    exception("No group with name " + groupName + " exists");
    return nullptr;
  }

  return state::groups.find(groupName)->second.get();
}

void removeGroup(std::string name, bool errorIfAbsent) {
  // Check if group exists
  if (state::groups.find(name) == state::groups.end()) {
    if (errorIfAbsent) {
      exception("No group with name " + name + " registered");
    }
    return;
  }

  // Group exists, remove it
  state::groups.erase(name);
  return;
}

void removeGroup(Group* group, bool errorIfAbsent) { removeGroup(group->name, errorIfAbsent); }

void removeAllGroups() { state::groups.clear(); }

void refresh() {

  // reset the ground plane
  render::engine->groundPlane.prepare();

  // reset all of the structures
  for (auto& cat : state::structures) {
    for (auto& x : cat.second) {
      x.second->refresh();
    }
  }

  requestRedraw();
}

void processLazyProperties() {

  // Note: This function essentially represents lazy software design, and it's an ugly and error-prone part of the
  // system. The reason for it that some settings require action on a change (e..g re-drawing the scene), but we want to
  // allow variable-set syntax like `polyscope::setting = newVal;` rather than getters and setters like
  // `polyscope::setSetting(newVal);`. It might have been better to simple set options with setter from the start, but
  // that ship has sailed.
  //
  // This function is a workaround which polls for changes to options settings, and performs any necessary additional
  // work.
  //
  // There is a second function processLazyPropertiesOutsideOfImGui() which handles a few more that can only be set
  // at limited times when an ImGui frame is not active.

  // projection mode
  if (internal::lazy::projectionMode != view::projectionMode) {
    internal::lazy::projectionMode = view::projectionMode;
    view::setProjectionMode(view::projectionMode);
  }

  // transparency mode
  if (internal::lazy::transparencyMode != options::transparencyMode) {
    internal::lazy::transparencyMode = options::transparencyMode;
    render::engine->setTransparencyMode(options::transparencyMode);
  }

  // transparency render passes
  if (internal::lazy::transparencyRenderPasses != options::transparencyRenderPasses) {
    internal::lazy::transparencyRenderPasses = options::transparencyRenderPasses;
    requestRedraw();
  }

  // ssaa
  if (internal::lazy::ssaaFactor != options::ssaaFactor) {
    internal::lazy::ssaaFactor = options::ssaaFactor;
    render::engine->setSSAAFactor(options::ssaaFactor);
  }

  // ground plane
  if (internal::lazy::groundPlaneEnabled != options::groundPlaneEnabled ||
      internal::lazy::groundPlaneMode != options::groundPlaneMode) {
    internal::lazy::groundPlaneEnabled = options::groundPlaneEnabled;
    if (!options::groundPlaneEnabled) {
      // if the (depecated) groundPlaneEnabled = false, set mode to None, so we only have one variable to check
      options::groundPlaneMode = GroundPlaneMode::None;
    }
    internal::lazy::groundPlaneMode = options::groundPlaneMode;
    requestRedraw();
  }
  if (internal::lazy::groundPlaneHeightFactor.asAbsolute() != options::groundPlaneHeightFactor.asAbsolute() ||
      internal::lazy::groundPlaneHeightFactor.isRelative() != options::groundPlaneHeightFactor.isRelative()) {
    internal::lazy::groundPlaneHeightFactor = options::groundPlaneHeightFactor;
    requestRedraw();
  }
  if (internal::lazy::shadowBlurIters != options::shadowBlurIters) {
    internal::lazy::shadowBlurIters = options::shadowBlurIters;
    requestRedraw();
  }
  if (internal::lazy::shadowDarkness != options::shadowDarkness) {
    internal::lazy::shadowDarkness = options::shadowDarkness;
    requestRedraw();
  }
};

void processLazyPropertiesOutsideOfImGui() {
  // Like processLazyProperties, but this one handles properties which cannot be changed mid-ImGui frame

  // uiScale
  if (internal::lazy::uiScale != options::uiScale) {
    internal::lazy::uiScale = options::uiScale;
    render::engine->configureImGui();
    setInitialWindowWidths();
  }
}

void updateStructureExtents() {

  if (!options::automaticallyComputeSceneExtents) {
    return;
  }

  // Note: the cost multiple calls to this function scales only with the number of structures, not the size of the data
  // in those structures, because structures internally cache the extents of their data.

  // Compute length scale and bbox as the max of all structures
  state::lengthScale = 0.0;
  glm::vec3 minBbox = glm::vec3{1, 1, 1} * std::numeric_limits<float>::infinity();
  glm::vec3 maxBbox = -glm::vec3{1, 1, 1} * std::numeric_limits<float>::infinity();

  for (auto& cat : state::structures) {
    for (auto& x : cat.second) {
      if (!x.second->hasExtents()) {
        continue;
      }
      state::lengthScale = std::max(state::lengthScale, x.second->lengthScale());
      auto bbox = x.second->boundingBox();
      minBbox = componentwiseMin(minBbox, std::get<0>(bbox));
      maxBbox = componentwiseMax(maxBbox, std::get<1>(bbox));
    }
  }

  // If we got a non-finite bounding box, fix it
  if (!isFinite(minBbox) || !isFinite(maxBbox)) {
    minBbox = -glm::vec3{1, 1, 1};
    maxBbox = glm::vec3{1, 1, 1};
  }

  // If we got a degenerate bounding box, perturb it slightly
  if (minBbox == maxBbox) {
    double offsetScale = (state::lengthScale == 0) ? 1e-3 : state::lengthScale * 1e-3;
    glm::vec3 offset{offsetScale, offsetScale, offsetScale};
    minBbox = minBbox - offset / 2.f;
    maxBbox = maxBbox + offset / 2.f;
  }

  std::get<0>(state::boundingBox) = minBbox;
  std::get<1>(state::boundingBox) = maxBbox;

  // If we got a bounding box but not a length scale we can use the size of the
  // box as a scale. If we got neither, we'll end up with a constant near 1 due
  // to the above correction
  if (state::lengthScale == 0) {
    state::lengthScale = glm::length(maxBbox - minBbox);
  }

  requestRedraw();
}

namespace state {
glm::vec3 center() { return 0.5f * (std::get<0>(state::boundingBox) + std::get<1>(state::boundingBox)); }
} // namespace state

} // namespace polyscope
