# Copyright (C) 2010-2022 The ESPResSo project
#
# This file is part of ESPResSo.
#
# ESPResSo is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ESPResSo is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import ctypes
import math
import os
import sys
import time
import itertools
import threading
import numpy as np
import matplotlib.pyplot

import OpenGL.GL
import OpenGL.GLE
import OpenGL.GLU
import OpenGL.GLUT

import espressomd
import espressomd.particle_data


class openGLLive():
    """
    This class provides live visualization using pyOpenGL.
    Use the update method to push your current simulation state after
    integrating. Modify the appearance with a list of keywords.
    Timed callbacks can be registered via the :meth:`register_callback` method
    and keyboard callbacks via :meth:`keyboard_manager.register_button()
    <espressomd.visualization.KeyboardManager.register_button>`.

    Parameters
    ----------

    system : :class:`espressomd.system.System`
    window_size : (2,) array_like of :obj:`int`, optional
        Size of the visualizer window in pixels.
    name : :obj:`str`, optional
        The name of the visualizer window.
    background_color : (3,) array_like of :obj:`float`, optional
        RGB of the background.
    periodic_images : (3,) array_like of :obj:`int`, optional
        Periodic repetitions on both sides of the box in xyz-direction.
    draw_box : :obj:`bool`, optional
        Draw wireframe boundaries.
    draw_axis : :obj:`bool`, optional
        Draw xyz system axes.
    draw_nodes : :obj:`bool`, optional
        Draw node boxes.
    draw_cells : :obj:`bool`, optional
        Draw cell boxes.
    quality_particles : :obj:`int`, optional
        The number of subdivisions for particle spheres.
    quality_bonds : :obj:`int`, optional
        The number of subdivisions for cylindrical bonds.
    quality_arrows : :obj:`int`, optional
        The number of subdivisions for external force arrows.
    quality_constraints : :obj:`int`, optional
        The number of subdivisions for primitive constraints.
    close_cut_distance : :obj:`float`, optional
        The distance from the viewer to the near clipping plane.
    far_cut_distance : :obj:`float`, optional
        The distance from the viewer to the far clipping plane.
    camera_position : :obj:`str` or (3,) array_like of :obj:`float`, optional
        Initial camera position. Use ``'auto'`` (default) for shifted position in z-direction.
    camera_target : :obj:`str` or (3,) array_like of :obj:`float`, optional
        Initial camera target. Use ``'auto'`` (default) to look towards the system center.
    camera_right : (3,) array_like of :obj:`float`, optional
        Camera right vector in system coordinates. Default is ``[1, 0, 0]``
    particle_sizes : :obj:`str` or array_like of :obj:`float` or callable, optional
        * ``'auto'`` (default): The Lennard-Jones sigma value of the
          self-interaction is used for the particle diameter.
        * callable: A lambda function with one argument. Internally,
          the numerical particle type is passed to the lambda
          function to determine the particle radius.
        * list: A list of particle radii, indexed by the particle type.
    particle_coloring : :obj:`str`, optional
        * ``'auto'`` (default): Colors of charged particles are
          specified by ``particle_charge_colors``, neutral particles
          by ``particle_type_colors``.
        * ``'charge'``: Minimum and maximum charge of all particles is determined by the
          visualizer. All particles are colored by a linear
          interpolation of the two colors given by
          ``particle_charge_colors`` according to their charge.
        * ``'type'``: Particle colors are specified by ``particle_type_colors``,
          indexed by their numerical particle type.
        * ``'node'``: Color according to the node the particle is on.
    particle_type_colors : array_like of :obj:`float`, optional
        Colors for particle types.
    particle_type_materials : array_like of :obj:`str`, optional
        Materials of the particle types.
    particle_charge_colors : (2,) array_like of :obj:`float`, optional
        Two colors for min/max charged particles.
    draw_constraints : :obj:`bool`, optional
        Enables constraint visualization. For simple constraints
        (planes, spheres and cylinders), OpenGL primitives are
        used. Otherwise, visualization by rasterization is used.
    rasterize_pointsize : :obj:`float`, optional
        Point size for the rasterization dots.
    rasterize_resolution : :obj:`float`, optional
        Accuracy of the rasterization.
    quality_constraints : :obj:`int`, optional
        The number of subdivisions for primitive constraints.
    constraint_type_colors : array_like of :obj:`float`, optional
        Colors of the constraints by type.
    constraint_type_materials : array_like of :obj:`str`, optional
        Materials of the constraints by type.
    draw_bonds : :obj:`bool`, optional
        Enables bond visualization.
    bond_type_radius : array_like of :obj:`float`, optional
        Radii of bonds by type.
    bond_type_colors : array_like of :obj:`float`, optional
        Color of bonds by type.
    bond_type_materials : array_like of :obj:`str`, optional
        Materials of bonds by type.
    ext_force_arrows : :obj:`bool`, optional
        Enables external force visualization.
    ext_force_arrows_type_scale : array_like of :obj:`float`, optional
        List of scale factors of external force arrows for different particle types.
    ext_force_arrows_type_colors : array_like of :obj:`float`, optional
        Colors of ext_force arrows for different particle types.
    ext_force_arrows_type_materials : array_like of :obj:`str`, optional
        Materials of ext_force arrows for different particle types.
    ext_force_arrows_type_radii : array_like of :obj:`float`, optional
        List of arrow radii for different particle types.
    force_arrows : :obj:`bool`, optional
        Enables particle force visualization.
    force_arrows_type_scale : array_like of :obj:`float`, optional
        List of scale factors of particle force arrows for different particle types.
    force_arrows_type_colors : array_like of :obj:`float`, optional
        Colors of particle force arrows for different particle types.
    force_arrows_type_materials : array_like of :obj:`str`, optional
        Materials of particle force arrows for different particle types.
    force_arrows_type_radii : array_like of :obj:`float`, optional
        List of arrow radii for different particle types.
    velocity_arrows : :obj:`bool`, optional
        Enables particle velocity visualization.
    velocity_arrows_type_scale : array_like of :obj:`float`, optional
        List of scale factors of particle velocity arrows for different particle types.
    velocity_arrows_type_colors : array_like of :obj:`float`, optional
        Colors of particle velocity arrows for different particle types.
    velocity_arrows_type_materials : array_like of :obj:`str`, optional
        Materials of particle velocity arrows for different particle types.
    velocity_arrows_type_radii : array_like of :obj:`float`, optional
        List of arrow radii for different particle types.
    director_arrows : :obj:`bool`, optional
        Enables particle director visualization.
    director_arrows_type_scale : :obj:`float`, optional
        Scale factor of particle director arrows for different particle types.
    director_arrows_type_colors : array_like of :obj:`float`, optional
        Colors of particle director arrows for different particle types.
    director_arrows_type_materials : array_like of :obj:`str`, optional
        Materials of particle director arrows for different particle types.
    director_arrows_type_radii : array_like of :obj:`float`, optional
        List of arrow radii for different particle types.
    drag_enabled : :obj:`bool`, optional
        Enables mouse-controlled particles dragging (Default: False)
    drag_force : :obj:`bool`, optional
        Factor for particle dragging
    LB_draw_nodes : :obj:`bool`, optional
        Draws a lattice representation of the LB nodes that are no boundaries.
    LB_draw_node_boundaries : :obj:`bool`, optional
        Draws a lattice representation of the LB nodes that are boundaries.
    LB_draw_boundaries : :obj:`bool`, optional
        Draws the LB shapes.
    LB_draw_velocity_plane : :obj:`bool`, optional
        Draws LB node velocity arrows in a plane perpendicular to the axis in
        ``LB_plane_axis``, at a distance ``LB_plane_dist`` from the origin,
        every ``LB_plane_ngrid`` grid points.
    LB_plane_axis : :obj:`int`, optional
        LB node velocity arrows are drawn in a plane perpendicular to the
        x, y, or z axes, which are encoded by values 0, 1, or 2 respectively.
    LB_plane_dist : :obj:`float`, optional
        LB node velocity arrows are drawn in a plane perpendicular to the
        x, y, or z axes, at a distance ``LB_plane_dist`` from the origin.
    LB_plane_ngrid : :obj:`int`, optional
        LB node velocity arrows are drawn in a plane perpendicular to the
        x, y, or z axes, every ``LB_plane_ngrid`` grid points.
    LB_vel_scale : :obj:`float`, optional
        Rescale LB node velocity arrow length.
    LB_vel_radius_scale : :obj:`float`, optional
        Rescale LB node velocity arrow radii.
    LB_arrow_color : (3,) array_like of :obj:`float`, optional
        RGB of the LB velocity arrows.
    LB_arrow_material : :obj:`str`, optional
        Material of LB arrows.
    quality_constraints : :obj:`int`, optional
        The number of subdivisions for LB arrows.
    light_pos : (3,) array_like of :obj:`float`, optional
        If auto (default) is used, the light is placed dynamically in
        the particle barycenter of the system. Otherwise, a fixed
        coordinate can be set.
    light_colors : array_like of :obj:`float`, optional
        Three lists to specify ambient, diffuse and specular light colors.
    light_brightness : :obj:`float`, optional
        Brightness (inverse constant attenuation) of the light.
    light_size : :obj:`float`, optional
        Size (inverse linear attenuation) of the light. If auto
        (default) is used, the light size will be set to a reasonable
        value according to the box size at start.
    spotlight_enabled : :obj:`bool`, optional
        If set to True (default), it enables a spotlight on the
        camera position pointing in look direction.
    spotlight_colors : array_like of :obj:`float`, optional
        Three lists to specify ambient, diffuse and specular spotlight colors.
    spotlight_angle : :obj:`float`, optional
        The spread angle of the spotlight in degrees (from 0 to 90).
    spotlight_brightness : :obj:`float`, optional
        Brightness (inverse constant attenuation) of the spotlight.
    spotlight_focus : :obj:`float`, optional
        Focus (spot exponent) for the spotlight from 0 (uniform) to 128.

    Notes
    -----
    The visualization of some constraints is either improved by or even relies
    on the presence of an installed OpenGL Extrusion library on your system.

    """

    materials = {
        'bright': [0.9, 1.0, 0.8, 0.4, 1.0],
        'medium': [0.6, 0.8, 0.2, 0.4, 1.0],
        'dark': [0.4, 0.5, 0.1, 0.4, 1.0],
        'transparent1': [0.6, 0.8, 0.2, 0.5, 0.8],
        'transparent2': [0.6, 0.8, 0.2, 0.5, 0.4],
        'transparent3': [0.6, 0.8, 0.2, 0.5, 0.2],
        'rubber': [0, 0.4, 0.7, 0.078125, 1.0],
        'chrome': [0.25, 0.4, 0.774597, 0.6, 1.0],
        'plastic': [0, 0.55, 0.7, 0.25, 1.0],
        'steel': [0.25, 0.38, 0, 0.32, 1.0]
    }

    def __init__(self, system, **kwargs):
        # default properties
        self.specs = {
            'window_size': [800, 800],
            'name': 'ESPResSo Visualization',

            'background_color': [0, 0, 0],

            'draw_fps': False,
            'draw_box': True,
            'draw_axis': True,
            'draw_nodes': False,
            'draw_cells': False,

            'update_fps': 30,

            'periodic_images': [0, 0, 0],

            'quality_particles': 20,
            'quality_bonds': 16,
            'quality_arrows': 16,
            'quality_constraints': 32,

            'close_cut_distance': 0.1,
            'far_cut_distance': 5,

            'camera_position': 'auto',
            'camera_target': 'auto',
            'camera_right': [1.0, 0.0, 0.0],

            'particle_coloring': 'auto',
            'particle_sizes': 'auto',
            'particle_type_colors':
                [[1, 1, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 1], [1, 0.5, 0], [0.5, 0, 1]],
            'particle_type_materials': ['medium'],
            'particle_charge_colors': [[1, 0, 0], [0, 1, 0]],

            'draw_constraints': True,
            'rasterize_pointsize': 10,
            'rasterize_resolution': 75.0,
            'constraint_type_colors':
                [[0.5, 0.5, 0.5], [0, 0.5, 0.5], [0.5, 0, 0.5],
                    [0.5, 0.5, 0], [0, 0, 0.5], [0.5, 0, 0]],
            'constraint_type_materials': ['transparent1'],

            'draw_bonds': True,
            'bond_type_radius': [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5],
            'bond_type_colors':
                [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 0], [1, 0.5, 0], [0.5, 0, 1]],
            'bond_type_materials': ['medium'],

            'ext_force_arrows': False,
            'ext_force_arrows_type_scale': [1.0],
            'ext_force_arrows_type_colors':
                [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 0], [1, 0.5, 0], [0.5, 0, 1]],
            'ext_force_arrows_type_materials': ['transparent2'],
            'ext_force_arrows_type_radii': [0.2],

            'velocity_arrows': False,
            'velocity_arrows_type_scale': [1.0],
            'velocity_arrows_type_colors':
                [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 0], [1, 0.5, 0], [0.5, 0, 1]],
            'velocity_arrows_type_materials': ['transparent2'],
            'velocity_arrows_type_radii': [0.2],

            'force_arrows': False,
            'force_arrows_type_scale': [1.0],
            'force_arrows_type_colors':
                [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 0], [1, 0.5, 0], [0.5, 0, 1]],
            'force_arrows_type_materials': ['transparent2'],
            'force_arrows_type_radii': [0.2],

            'director_arrows': False,
            'director_arrows_type_scale': [1.0],
            'director_arrows_type_colors':
                [[1, 1, 1], [1, 0, 1], [0, 0, 1], [0, 1, 1],
                    [1, 1, 0], [1, 0.5, 0], [0.5, 0, 1]],
            'director_arrows_type_materials': ['transparent2'],
            'director_arrows_type_radii': [0.2],

            'LB_draw_nodes': False,
            'LB_draw_node_boundaries': False,
            'LB_draw_boundaries': False,

            'LB_draw_velocity_plane': False,
            'LB_plane_axis': 2,
            'LB_plane_dist': 0,
            'LB_plane_ngrid': 5,
            'LB_vel_scale': 1.0,
            'LB_vel_radius_scale': 0.005,
            'LB_arrow_color': [1.0, 1.0, 1.0],
            'LB_arrow_material': 'transparent1',
            'LB_arrow_quality': 16,

            'light_pos': 'auto',
            'light_colors':
                [[0.1, 0.1, 0.2], [0.9, 0.9, 0.9], [1.0, 1.0, 1.0]],
            'light_brightness': 1.0,
            'light_size': 'auto',

            'spotlight_enabled': False,
            'spotlight_colors':
                [[0.2, 0.2, 0.3], [0.5, 0.5, 0.5], [1.0, 1.0, 1.0]],
            'spotlight_angle': 45,
            'spotlight_focus': 1,
            'spotlight_brightness': 0.6,

            'drag_enabled': False,
            'drag_force': 3.0
        }

        # overwrite with user properties
        for key in kwargs:
            if key not in self.specs:
                raise ValueError(f'{key} is no valid visualization property')
            else:
                self.specs[key] = kwargs[key]

        # dependencies
        if not espressomd.has_features('EXTERNAL_FORCES'):
            self.specs['drag_enabled'] = False
            self.specs['ext_force_arrows'] = False

        if not espressomd.has_features('ROTATION'):
            self.specs['director_arrows'] = False

        if not espressomd.has_features('LB_BOUNDARIES') and \
                not espressomd.has_features('LB_BOUNDARIES_GPU'):
            self.specs['LB_draw_boundaries'] = False
            self.specs['LB_draw_node_boundaries'] = False

        # ESPResSo-related inits that are known only when running the
        # integration loop are called once in the update loop
        # (constraints, node boxes, cell boxes, charge range, bonds)

        # ESPResSo-related inits that are known on instantiation go here:

        # content of particle data
        self.has_particle_data = {
            'velocity': self.specs['velocity_arrows'],
            'force': self.specs['force_arrows'],
            'ext_force': self.specs['ext_force_arrows'] or
            self.specs['drag_enabled']}

        if espressomd.has_features('ELECTROSTATICS'):
            self.has_particle_data['charge'] = \
                self.specs['particle_coloring'] == 'auto' or \
                self.specs['particle_coloring'] == 'charge'
        else:
            self.has_particle_data['charge'] = False
        self.has_particle_data['director'] = self.specs['director_arrows']
        self.has_particle_data['node'] = self.specs['particle_coloring'] == 'node'

        # particle info of highlighted particle: collect particle attributes
        self.highlighted_particle = {}
        self.particle_attributes = []
        for d in dir(espressomd.particle_data.ParticleHandle):
            if isinstance(getattr(espressomd.particle_data.ParticleHandle, d),
                          type(espressomd.particle_data.ParticleHandle.pos)):
                if d not in ["pos_folded"]:
                    self.particle_attributes.append(d)
        self.max_len_attr = max([len(a) for a in self.particle_attributes])

        # fixed colors from inverse background color for good contrast
        self.inverse_bg_color = \
            np.array([1 - self.specs['background_color'][0],
                      1 - self.specs['background_color'][1],
                      1 - self.specs['background_color'][2], 1.0])

        self.node_box_color = np.copy(self.inverse_bg_color)
        self.node_box_color[0] += 0.5 * (0.5 - self.node_box_color[0])

        self.cell_box_color = np.copy(self.inverse_bg_color)
        self.cell_box_color[1] += 0.5 * (0.5 - self.cell_box_color[1])

        self.lb_box_color = np.copy(self.inverse_bg_color)
        self.lb_box_color[2] = 0.5

        self.lb_box_color_boundary = np.copy(self.inverse_bg_color)
        self.lb_box_color_boundary[1] = 0.5

        self.text_color = np.copy(self.inverse_bg_color)

        # increase line thickness if node/cell box is enabled
        self.line_width_fac = 1.0
        if self.specs['draw_nodes']:
            self.line_width_fac += 0.5
        if self.specs['draw_cells']:
            self.line_width_fac += 0.5

        # has periodic images
        self.has_images = any(i != 0 for i in self.specs['periodic_images'])
        self.image_vectors = [
            list(range(-i, i + 1)) for i in self.specs['periodic_images']]

        # various inits
        self.system = system
        self.system_info = {}

        self.last_box_l = self.system.box_l
        self.fps_last = 0
        self.fps = 0
        self.fps_count = 1

        self.glut_main_loop_started = False
        self.screenshot_initialized = False
        self.hasParticleData = False
        self.quit_safely = False
        self.paused = False
        self.take_screenshot = False
        self.screenshot_captured = False

        self.keyboard_manager = KeyboardManager()
        self.mouse_manager = MouseManager()
        self.camera = self._init_camera()

        self.timers = []
        self.particles = {}

        # times of last call of update(), redraw_on_idle()
        self.last_update = time.time()
        self.last_draw = time.time()

        self.trigger_set_particle_drag = False
        self.trigger_reset_particle_drag = False
        self.drag_id = -1

        # list of [(x, y), string] for user-defined text
        self.user_texts = []

    def update_system_info(self):
        """Update the information stored in dict self.system_info.

        The dictionary is used for showing system information, such as particle
        or constraint information, in the visualizer window.

        """
        # collect system information
        self.system_info = {
            'Actors': [], 'Non-bonded interactions': [],
            'Bonded interactions': [b for b in self.system.bonded_inter],
            'Constraints': [], 'Thermostat': self.system.thermostat.get_state()
        }

        if len(self.system_info['Bonded interactions']) == 0:
            self.system_info['Bonded interactions'].append('None')

        # collect all actors
        for a in self.system.actors:
            self.system_info['Actors'].append(str(a))
        if len(self.system_info['Actors']) == 0:
            self.system_info['Actors'].append('None')

        # collect all types from particles and constraints
        all_types = set(self.system.part.all().type)
        constraint_types = []
        for c in self.system.constraints[:]:
            constraint_types.append(c.get_parameter('particle_type'))
        all_types.update(constraint_types)

        # collect all active non-bonded interactions
        all_non_bonded_inters = [
            x for x in dir(self.system.non_bonded_inter[0, 0])
            if not x.startswith('__') and x != 'type1' and x != 'type2']
        for t1 in all_types:
            for t2 in all_types:
                for check_nb in all_non_bonded_inters:
                    nb = getattr(
                        self.system.non_bonded_inter[t1, t2], check_nb)
                    if nb is not None and nb.is_active():
                        self.system_info['Non-bonded interactions'].append(
                            [t1, t2, nb.type_name(), nb.get_params()])
        if len(self.system_info['Non-bonded interactions']) == 0:
            self.system_info['Non-bonded interactions'].append('None')

        # collect constraints
        for c in self.system.constraints[:]:
            co = c.get_params()
            co_s = c.get_parameter('shape')
            co['shape'] = [co_s.name(), co_s.get_params()]
            self.system_info['Constraints'].append(co)
        if len(self.system_info['Constraints']) == 0:
            self.system_info['Constraints'].append('None')

    def register_callback(self, cb, interval=1000):
        """Register timed callbacks.

        """
        self.timers.append((int(interval), cb))

    def screenshot(self, path):
        """Renders the current state into an image file at ``path`` with
        dimensions of ``specs['window_size']`` in PNG format.

        """
        # on first call: init and create buffers
        if not self.screenshot_initialized:
            self.screenshot_initialized = True
            self._init_opengl()

            # create buffers that can be larger than the screen frame buffer
            fbo = OpenGL.GL.glGenFramebuffers(1)
            OpenGL.GL.glBindFramebuffer(OpenGL.GL.GL_FRAMEBUFFER, fbo)
            # color buffer
            rbo = OpenGL.GL.glGenRenderbuffers(1)
            OpenGL.GL.glBindRenderbuffer(OpenGL.GL.GL_RENDERBUFFER, rbo)
            OpenGL.GL.glRenderbufferStorage(
                OpenGL.GL.GL_RENDERBUFFER,
                OpenGL.GL.GL_RGB,
                self.specs['window_size'][0],
                self.specs['window_size'][1])
            OpenGL.GL.glFramebufferRenderbuffer(
                OpenGL.GL.GL_FRAMEBUFFER,
                OpenGL.GL.GL_COLOR_ATTACHMENT0,
                OpenGL.GL.GL_RENDERBUFFER,
                rbo)
            # depth buffer
            dbo = OpenGL.GL.glGenRenderbuffers(1)
            OpenGL.GL.glBindRenderbuffer(OpenGL.GL.GL_RENDERBUFFER, dbo)
            OpenGL.GL.glRenderbufferStorage(
                OpenGL.GL.GL_RENDERBUFFER, OpenGL.GL.GL_DEPTH_COMPONENT,
                self.specs['window_size'][0], self.specs['window_size'][1])
            OpenGL.GL.glFramebufferRenderbuffer(
                OpenGL.GL.GL_FRAMEBUFFER,
                OpenGL.GL.GL_DEPTH_ATTACHMENT,
                OpenGL.GL.GL_RENDERBUFFER,
                dbo)

            self._reshape_window(
                self.specs['window_size'][0], self.specs['window_size'][1])
            OpenGL.GLUT.glutHideWindow()

        # initialize properties that depend on the ESPResSo system
        self._init_espresso_visualization()
        self._initial_espresso_updates()

        # draw the simulation box
        OpenGL.GL.glClear(
            OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT)
        OpenGL.GL.glLoadMatrixf(self.camera.modelview)
        self._draw_system()

        # read the pixels
        OpenGL.GL.glReadBuffer(OpenGL.GL.GL_COLOR_ATTACHMENT0)

        # save image
        self._make_screenshot(path)

    def run(self, integration_steps=1):
        """Convenience method with a simple integration thread.

        """

        def main():
            while True:
                self.update()
                if not self.paused:
                    try:
                        self.system.integrator.run(integration_steps)
                    except Exception as e:
                        print(e)
                        os._exit(1)

                # Necessary to regularly drop the Global Interpreter Lock (GIL)
                # which prevents freezing of the visualizer under certain
                # circumstances (see Issue #3764).
                time.sleep(1e-12)

        t = threading.Thread(target=main)
        t.daemon = True
        t.start()

        self.start()

    def start(self):
        """The blocking start method.

        """
        self._init_opengl()
        self._init_espresso_visualization()
        self._init_controls()
        self._init_OpenGL_callbacks()
        self._init_timers()

        # start the blocking main loop
        self.glut_main_loop_started = True
        OpenGL.GLUT.glutMainLoop()

    def update(self):
        """Update method to be called after integration.
        Changes of ESPResSo system can only happen here.

        """
        if self.glut_main_loop_started:

            # update on startup
            if not self.hasParticleData:
                self._initial_espresso_updates()
                self.hasParticleData = True

            # updates
            refresh_time = 1. / self.specs['update_fps']
            if (time.time() - self.last_update) > refresh_time:
                # update particle/bond data after system propagation,
                # or on pause to display particle info
                self._update_particles_and_bonds()

                # LB update
                if self.specs['LB_draw_velocity_plane']:
                    self._update_lb_velocity_plane()

                # box_l changed
                if not (self.last_box_l == self.system.box_l).all():
                    self.last_box_l = np.copy(self.system.box_l)
                    self._box_size_dependence()

                # keyboard callbacks may change ESPResSo system properties,
                # only safe to change here
                for c in self.keyboard_manager.userCallbackStack:
                    c()
                self.keyboard_manager.userCallbackStack = []

                self.last_update = time.time()

            # drag particles
            if self.specs['drag_enabled']:
                if self.trigger_set_particle_drag and self.drag_id != -1:
                    self.system.part.by_id(
                        self.drag_id).ext_force = self.dragExtForce
                    self.trigger_set_particle_drag = False
                elif self.trigger_reset_particle_drag and self.drag_id != -1:
                    self.system.part.by_id(
                        self.drag_id).ext_force = self.extForceOld
                    self.trigger_reset_particle_drag = False
                    self.drag_id = -1

        # <escape> key was pressed: wait for ESPResSo to complete current task,
        # then call sys exit from main thread
        if self.quit_safely:
            os._exit(1)

    # fetch data on startup
    def _initial_espresso_updates(self):
        self._update_particles_and_bonds()
        if self.has_particle_data['charge']:
            self._update_charge_color_range()
        if self.specs['draw_constraints']:
            self._update_constraints()
        if self.specs['draw_cells'] or self.specs['draw_nodes']:
            self._update_nodes()
        if self.specs['draw_cells']:
            self._update_cells()

    def _update_particles_and_bonds(self):
        """Fetch particle data from core and put it in the data dicts
        used by the visualizer.

        """
        parts_from_core = self.system.part.all()
        self._update_particles(parts_from_core)
        self._update_bonds(parts_from_core)

    def _update_particles(self, particle_data):
        """Updates particle data used for drawing particles.
        Do not call directly but use _update_particles_and_bonds()!

        """
        self.particles['pos'] = particle_data.pos_folded
        self.particles['type'] = particle_data.type
        # particle ids can be discontiguous, therefore create a dict that maps
        # particle ids to respective index in data arrays (used by
        # _draw_bonds())
        self.index_from_id = {
            id: ndx for ndx, id in enumerate(
                particle_data.id)}

        if self.has_particle_data['velocity']:
            self.particles['velocity'] = particle_data.v

        if self.has_particle_data['force']:
            self.particles['force'] = particle_data.f

        if self.has_particle_data['ext_force']:
            self.particles['ext_force'] = particle_data.ext_force

        if self.has_particle_data['charge']:
            self.particles['charge'] = particle_data.q

        if self.has_particle_data['director']:
            self.particles['director'] = particle_data.director

        if self.has_particle_data['node']:
            self.particles['node'] = particle_data.node

        if self.info_id != -1:
            # data array index of particle with id info_id
            info_index = self.index_from_id[self.info_id]
            for attr in self.particle_attributes:
                self.highlighted_particle[attr] = \
                    getattr(particle_data, attr)[info_index]

    def _update_lb_velocity_plane(self):
        if self.lb_is_cpu:
            self._update_lb_velocity_plane_cpu()
        else:
            self._update_lb_velocity_plane_gpu()

    def _update_lb_velocity_plane_cpu(self):
        agrid = self.lb_params['agrid']
        self.lb_plane_vel = []
        ng = self.specs['LB_plane_ngrid']
        for xi in range(ng):
            for xj in range(ng):
                pp = np.copy((self.lb_plane_p + xi * 1.0 / ng * self.lb_plane_b1 +
                              xj * 1.0 / ng * self.lb_plane_b2) % self.system.box_l)
                i, j, k = (int(ppp / agrid) for ppp in pp)
                lb_vel = np.copy(self.lb[i, j, k].velocity)
                self.lb_plane_vel.append([pp, lb_vel])

    def _update_lb_velocity_plane_gpu(self):
        ng = self.specs['LB_plane_ngrid']
        col_pos = []
        for xi in range(ng):
            for xj in range(ng):
                p = np.array((self.lb_plane_p + xi * 1.0 / ng * self.lb_plane_b1 +
                              xj * 1.0 / ng * self.lb_plane_b2) % self.system.box_l)
                col_pos.append(p)

        lb_vels = self.lb.get_interpolated_fluid_velocity_at_positions(
            np.array(col_pos))
        self.lb_plane_vel = []
        for p, v in zip(col_pos, lb_vels):
            self.lb_plane_vel.append([p, v])

    def _update_cells(self):
        self.cell_box_origins = []
        cell_system_state = self.system.cell_system.get_state()
        # Not all particle decompositions have a cell grid, if
        # it does not exist in the cell system state, we just
        # ignore it
        try:
            self.cell_size = cell_system_state['cell_size']
            for i in range(cell_system_state['cell_grid'][0]):
                for j in range(cell_system_state['cell_grid'][1]):
                    for k in range(cell_system_state['cell_grid'][2]):
                        self.cell_box_origins.append(
                            np.array([i, j, k]) * self.cell_size)
        except KeyError:
            pass

    def _update_nodes(self):
        self.node_box_origins = []
        self.local_box_l = np.array([0, 0, 0])
        for i in range(self.system.cell_system.node_grid[0]):
            for j in range(self.system.cell_system.node_grid[1]):
                for k in range(self.system.cell_system.node_grid[2]):
                    self.node_box_origins.append(
                        np.array([i, j, k]) * self.local_box_l)

    def _update_constraints(self):
        """Collect shapes and interaction type (for coloring) from constraints
        and update the list self.shapes with instances of the respective Shape
        (or child) classes.

        """
        self.shapes = []

        # helper functions and shape dictionary
        def unpack_shapes(shapes_list):
            """Return a list of shapes, where all unions have been unpacked.

            """
            shapes = []
            try:
                # recursively unpack unions
                for shape in shapes_list:
                    shapes.extend(unpack_shapes(shape))
            except TypeError:
                # otherwise just append the shape
                shapes.append(shapes_list)
            return shapes

        def shape_arguments(shape, part_type):
            """Helper function returning argument list for initialization of
            Shape class (or respective child classes).

            """
            arguments = [
                shape, part_type,
                self._modulo_indexing(self.specs['constraint_type_colors'],
                                      part_type),
                self.materials[self._modulo_indexing(
                    self.specs['constraint_type_materials'], part_type)],
                self.specs['quality_constraints'], self.system.box_l,
                self.specs['rasterize_resolution'],
                self.specs['rasterize_pointsize']
            ]
            return arguments

        shape_mapping = {
            'Shapes::Cylinder': Cylinder,
            'Shapes::Ellipsoid': Ellipsoid,
            'Shapes::HollowConicalFrustum': HollowConicalFrustum,
            'Shapes::SimplePore': SimplePore,
            'Shapes::Slitpore': Slitpore,
            'Shapes::Sphere': Sphere,
            'Shapes::SpheroCylinder': Spherocylinder,
            'Shapes::Wall': Wall
        }

        # collect shapes using the helper functions
        for constraint in self.system.constraints:
            if isinstance(constraint,
                          espressomd.constraints.ShapeBasedConstraint):
                part_type = constraint.get_parameter('particle_type')
                shape = constraint.get_parameter('shape')
                for sub_shape in unpack_shapes(shape):
                    arguments = shape_arguments(sub_shape, part_type)
                    try:
                        self.shapes.append(
                            shape_mapping[sub_shape.name()](*arguments))
                    except KeyError:
                        self.shapes.append(Shape(*arguments))

        if self.specs['LB_draw_boundaries']:
            ni = 0
            for constraint in self.system.lbboundaries:
                if isinstance(constraint, espressomd.lbboundaries.LBBoundary):
                    part_type = ni
                    ni += 1
                    shape = constraint.get_parameter('shape')
                    for sub_shape in unpack_shapes(shape):
                        arguments = shape_arguments(sub_shape, part_type)
                        try:
                            self.shapes.append(
                                shape_mapping[sub_shape.name()](*arguments))
                        except KeyError:
                            self.shapes.append(Shape(*arguments))

    def _update_bonds(self, particle_data):
        """Update bond data used for drawing bonds.
        Do not call directly but use
        :meth:`~espressomd.visualization_opengl.openGLLive._update_particles_and_bonds`!

        Updates data array ``self.bonds[]``; structure of elements is:
        ``[<id of first bond partner>, <id of second bond partner>, <bond type>]``

         """
        if self.specs['draw_bonds']:
            self.bonds = []
            for particle in particle_data:
                for bond in particle.bonds:
                    # input data:
                    # bond[0]: bond type, bond[1:] bond partners
                    bond_type = bond[0].type_number()
                    if len(bond) == 4:
                        self.bonds.append([particle.id, bond[1], bond_type])
                        self.bonds.append([particle.id, bond[2], bond_type])
                        self.bonds.append([bond[2], bond[3], bond_type])
                    else:
                        for bond_partner in bond[1:]:
                            self.bonds.append(
                                [particle.id, bond_partner, bond_type])

    def _draw_text(self, x, y, text, color,
                   font=OpenGL.GLUT.GLUT_BITMAP_9_BY_15):
        OpenGL.GL.glColor(color)
        OpenGL.GL.glWindowPos2f(x, y)
        for ch in text:
            OpenGL.GLUT.glutBitmapCharacter(font, ctypes.c_int(ord(ch)))

    # draw called automatically from GLUT display function
    def _draw_system(self):
        if self.specs['LB_draw_velocity_plane']:
            self._draw_lb_vel()

        if self.specs['draw_axis']:
            axis_fac = 0.2
            axis_r = np.min(self.system.box_l) / 50.0
            draw_arrow([0, 0, 0], [self.system.box_l[0] * axis_fac, 0, 0], axis_r,
                       [1, 0, 0, 1], self.materials['chrome'], self.specs['quality_arrows'])
            draw_arrow([0, 0, 0], [0, self.system.box_l[2] * axis_fac, 0], axis_r,
                       [0, 1, 0, 1], self.materials['chrome'], self.specs['quality_arrows'])
            draw_arrow([0, 0, 0], [0, 0, self.system.box_l[2] * axis_fac], axis_r,
                       [0, 0, 1, 1], self.materials['chrome'], self.specs['quality_arrows'])

        if self.specs['draw_bonds']:
            self._draw_bonds()
        self._draw_system_particles()

        if self.specs['draw_constraints']:
            self._draw_constraints()

        if self.specs['draw_box']:
            self._draw_system_box()
        if self.specs['draw_nodes']:
            self._draw_nodes()
        if self.specs['draw_cells']:
            self._draw_cells()
        if self.specs['LB_draw_nodes'] or self.specs['LB_draw_node_boundaries']:
            self._draw_lb_grid()

    def _draw_system_box(self):
        draw_box([0, 0, 0], self.system.box_l, self.inverse_bg_color,
                 self.materials['medium'], 2.0 * self.line_width_fac)

    def _draw_nodes(self):
        for n in self.node_box_origins:
            draw_box(n, self.local_box_l, self.node_box_color,
                     self.materials['transparent1'], 1.5 * self.line_width_fac)

    def _draw_cells(self):
        for n in self.node_box_origins:
            for c in self.cell_box_origins:
                draw_box(
                    c + n, self.cell_size, self.cell_box_color,
                    self.materials['transparent1'], 0.75 * self.line_width_fac)

    def _draw_lb_grid(self):
        a = self.lb_params['agrid']
        cell_size = np.array([a] * 3)
        dims = np.rint(np.array(self.system.box_l) / a)
        for i in range(int(dims[0])):
            for j in range(int(dims[1])):
                for k in range(int(dims[2])):
                    n = np.array([i, j, k]) * cell_size
                    if self.specs['LB_draw_node_boundaries'] \
                            and self.lb[i, j, k].boundary:
                        draw_box(n, cell_size, self.lb_box_color_boundary,
                                 self.materials['transparent2'], 5.0)
                    if self.specs['LB_draw_nodes'] \
                            and not self.lb[i, j, k].boundary:
                        draw_box(n, cell_size, self.lb_box_color,
                                 self.materials['transparent2'], 1.5)

    def _draw_constraints(self):

        # clip borders of simulation box
        for i in range(6):
            OpenGL.GL.glEnable(OpenGL.GL.GL_CLIP_PLANE0 + i)
            OpenGL.GL.glClipPlane(OpenGL.GL.GL_CLIP_PLANE0 + i,
                                  self.box_eqn[i, :])

        for shape in self.shapes:
            shape.draw()

        for i in range(6):
            OpenGL.GL.glDisable(OpenGL.GL.GL_CLIP_PLANE0 + i)

    def _determine_radius(self, part_type):
        def radius_by_lj(part_type):
            ia = self.system.non_bonded_inter[part_type, part_type]
            radius = 0.0
            if hasattr(ia, "lennard_jones"):
                radius = ia.lennard_jones.get_params()["sigma"] * 0.5
            if radius == 0.0 and hasattr(ia, "wca"):
                radius = ia.wca.get_params()["sigma"] * 0.5
            if radius == 0.0:
                radius = 0.5
            return radius

        if self.specs['particle_sizes'] == 'auto':
            radius = radius_by_lj(part_type)
        elif callable(self.specs['particle_sizes']):
            radius = self.specs['particle_sizes'](part_type)
        else:
            try:
                radius = self._modulo_indexing(
                    self.specs['particle_sizes'], part_type)
            except RuntimeError:
                radius = self.radius_by_lj(part_type)
        return radius

    def _draw_system_particles(self, color_by_id=False):
        part_type = -1
        reset_material = False

        for part_id, index in self.index_from_id.items():
            part_type_last = part_type
            part_type = int(self.particles['type'][index])

            # Only change material if type/charge has changed, color_by_id or
            # material was reset by arrows
            if reset_material or color_by_id or part_type != part_type_last or \
                    part_id == self.drag_id or part_id == self.info_id or \
                    self.specs['particle_coloring'] == 'node':
                reset_material = False

                radius = self._determine_radius(part_type)

                m = self._modulo_indexing(
                    self.specs['particle_type_materials'], part_type)
                material = self.materials[m]

                if color_by_id:
                    color = self._id_to_fcolor(part_id)
                    OpenGL.GL.glColor(color)
                else:
                    if self.specs['particle_coloring'] == 'auto':
                        # Color auto: Charge then Type
                        if self.has_particle_data['charge'] and self.particles['charge'][index] != 0:
                            color = self._color_by_charge(
                                self.particles['charge'][index])
                            reset_material = True
                        else:
                            color = self._modulo_indexing(
                                self.specs['particle_type_colors'], part_type)
                    elif self.specs['particle_coloring'] == 'charge':
                        color = self._color_by_charge(
                            self.particles['charge'][index])
                        reset_material = True
                    elif self.specs['particle_coloring'] == 'type':
                        color = self._modulo_indexing(
                            self.specs['particle_type_colors'], part_type)
                    elif self.specs['particle_coloring'] == 'node':
                        color = self._modulo_indexing(
                            self.specs['particle_type_colors'], self.particles['node'][index])
                    else:
                        raise ValueError(
                            f"Cannot process particle_coloring={self.specs['particle_coloring']}")

                    # Invert color of highlighted particle
                    if part_id == self.drag_id or part_id == self.info_id:
                        reset_material = True
                        color = [1. - color[0], 1. - color[1], 1. - color[2]]

                    set_solid_material(color, material)

                # Create a new display list, used until next material/color
                # change
                OpenGL.GL.glNewList(self.dl_sphere, OpenGL.GL.GL_COMPILE)
                OpenGL.GLUT.glutSolidSphere(
                    radius, self.specs['quality_particles'], self.specs['quality_particles'])
                OpenGL.GL.glEndList()

            self._redraw_sphere(self.particles['pos'][index])

            if self.has_images:
                for imx, imy, imz in itertools.product(*self.image_vectors):
                    if imx != 0 or imy != 0 or imz != 0:
                        offset = [imx, imy, imz] * self.system.box_l
                        self._redraw_sphere(
                            self.particles['pos'][index] + offset)

            if espressomd.has_features('EXTERNAL_FORCES'):
                if self.specs['ext_force_arrows'] or part_id == self.drag_id:
                    if any(v != 0 for v in self.particles['ext_force'][index]):
                        if part_id == self.drag_id:
                            sc = 1
                        else:
                            sc = self._modulo_indexing(
                                self.specs['ext_force_arrows_type_scale'], part_type)
                        if sc > 0:
                            arrow_col = self._modulo_indexing(
                                self.specs['ext_force_arrows_type_colors'], part_type)
                            arrow_radius = self._modulo_indexing(
                                self.specs['ext_force_arrows_type_radii'], part_type)
                            draw_arrow(self.particles['pos'][index], np.array(
                                self.particles['ext_force'][index]) * sc, arrow_radius, arrow_col,
                                self.materials['chrome'], self.specs['quality_arrows'])
                            reset_material = True

            if self.specs['velocity_arrows']:
                self._draw_arrow_property(
                    part_id, part_type, self.specs['velocity_arrows_type_scale'],
                    self.specs['velocity_arrows_type_colors'],
                    self.specs['velocity_arrows_type_materials'],
                    self.specs['velocity_arrows_type_radii'], 'velocity')
                reset_material = True

            if self.specs['force_arrows']:
                self._draw_arrow_property(
                    part_id, part_type, self.specs['force_arrows_type_scale'],
                    self.specs['force_arrows_type_colors'],
                    self.specs['force_arrows_type_materials'],
                    self.specs['force_arrows_type_radii'], 'force')
                reset_material = True

            if self.specs['director_arrows']:
                self._draw_arrow_property(
                    part_id, part_type, self.specs['director_arrows_type_scale'],
                    self.specs['director_arrows_type_colors'],
                    self.specs['director_arrows_type_materials'],
                    self.specs['director_arrows_type_radii'], 'director')
                reset_material = True

    def _draw_arrow_property(self, part_id, part_type, type_scale,
                             type_colors, type_materials, type_radii, prop):
        sc = self._modulo_indexing(type_scale, part_type)
        if sc > 0:
            v = self.particles[prop][self.index_from_id[part_id]]
            col = self._modulo_indexing(type_colors, part_type)
            radius = self._modulo_indexing(type_radii, part_type)
            material = self._modulo_indexing(type_materials, part_type)
            draw_arrow(self.particles['pos'][self.index_from_id[part_id]],
                       np.array(v, dtype=float) * sc, radius, col,
                       self.materials[material], self.specs['quality_arrows'])

    def _draw_bonds(self):
        half_box_l = self.system.box_l / 2.0
        quality_bonds = self.specs['quality_bonds']
        for b in self.bonds:
            col = self._modulo_indexing(self.specs['bond_type_colors'], b[2])
            mat = self.materials[self._modulo_indexing(
                self.specs['bond_type_materials'], b[2])]
            radius = self._modulo_indexing(
                self.specs['bond_type_radius'], b[2])
            # if particles are deleted in the core, index_from_id might
            # throw a KeyError -- e. g. when deleting a bond created by
            # collision_detection (using virtual site particles)
            try:
                x_a = self.particles['pos'][self.index_from_id[b[0]]]
                x_b = self.particles['pos'][self.index_from_id[b[1]]]
            except Exception:
                # skip this bond
                continue
            dx = x_b - x_a

            if np.all(np.abs(dx) < half_box_l):
                # bond completely inside box
                draw_cylinder(x_a, x_b, radius, col, mat, quality_bonds)
                if self.has_images:
                    for imx, imy, imz in itertools.product(
                            *self.image_vectors):
                        if imx != 0 or imy != 0 or imz != 0:
                            offset = [imx, imy, imz] * self.system.box_l
                            draw_cylinder(x_a + offset, x_b + offset, radius,
                                          col, mat, quality_bonds)
            else:
                # bond crosses the box boundaries
                d = self._cut_bond(x_a, dx)
                if d is np.inf:
                    continue

                s_a = x_a + d * dx
                s_b = x_b - (1 - d) * dx
                draw_cylinder(x_a, s_a, radius, col, mat, quality_bonds)
                draw_cylinder(x_b, s_b, radius, col, mat, quality_bonds)

                if self.has_images:
                    for imx, imy, imz in itertools.product(
                            *self.image_vectors):
                        if imx != 0 or imy != 0 or imz != 0:
                            offset = [imx, imy, imz] * self.system.box_l
                            draw_cylinder(x_a + offset, s_a + offset, radius,
                                          col, mat, quality_bonds)
                            draw_cylinder(x_b + offset, s_b + offset, radius,
                                          col, mat, quality_bonds)

    def _redraw_sphere(self, pos):
        OpenGL.GL.glPushMatrix()
        OpenGL.GL.glTranslatef(pos[0], pos[1], pos[2])
        OpenGL.GL.glCallList(self.dl_sphere)
        OpenGL.GL.glPopMatrix()

    # helper to draw periodic bonds
    def _cut_bond(self, x_a, dx):
        """
        Algorithm for the line-plane intersection problem. Given the unfolded
        positions of two particles, determine: 1) the box image that minimizes
        the folded bond length, 2) which side of the simulation box is crossed
        by the bond, 3) how much of the bond is inside the unit cell starting
        from the first particle. That scalar is returned by the function, and
        can be used to calculate the coordinates of the line-plane
        intersection point. The algorithm can be found at
        https://en.wikipedia.org/w/index.php?title=Line%E2%80%93plane_intersection&oldid=940427752#Algebraic_form
        """
        # corner case: the two particles occupy the same position
        if np.dot(dx, dx) < 1e-9:
            return np.inf
        # find the box image that minimizes the unfolded bond length
        shift = np.rint(dx / self.system.box_l)
        # get the unfolded bond vector
        dx -= shift * self.system.box_l
        # find which side of the simulation box is crossed by the bond
        best_d = np.inf
        for i in range(3):
            if dx[i] == 0:
                continue  # corner case: bond is parallel to a face
            elif dx[i] > 0:
                p0_i = self.system.box_l[i]
            else:
                p0_i = 0
            # calculate the length of the bond that is inside the box using
            # an optimized version of `np.dot(p0 - x0, n) / np.dot(dx, n)`
            # where the dot products decay to an array access, because `n`
            # is always a unit vector in rectangular boxes and its sign
            # is canceled out by the division
            d = (p0_i - x_a[i]) / dx[i]
            if d < best_d:
                best_d = d
        return best_d

    # arrows in a plane for LB velocities
    def _draw_lb_vel(self):

        for lb_pos, lb_vel in self.lb_plane_vel:
            draw_arrow(
                lb_pos,
                lb_vel * self.specs['LB_vel_scale'],
                self.lb_arrow_radius,
                self.specs['LB_arrow_color'],
                self.materials[self.specs['LB_arrow_material']],
                self.specs['LB_arrow_quality'])

    # use modulo if there are more particle types than type definitions for
    # colors, materials etc.
    @staticmethod
    def _modulo_indexing(l, t):
        return l[t % len(l)]

    # fade particle charge color from white (q=0) to plus_color (q=q_max) resp
    # minus_color (q=q_min)
    def _color_by_charge(self, q):
        if q < 0.:
            c = q / self.min_q
            minus_color = self.specs['particle_charge_colors'][0]
            return (1. - c) * np.ones(3) + c * np.array(minus_color)
        else:
            c = q / self.max_q
            plus_color = self.specs['particle_charge_colors'][1]
            return (1. - c) * np.ones(3) + c * np.array(plus_color)

    # on initialization, check q_max/q_min
    def _update_charge_color_range(self):
        if len(self.particles['charge'][:]) > 0:
            self.min_q = min(self.particles['charge'][:])
            self.max_q = max(self.particles['charge'][:])

    def _handle_screenshot(self):
        if self.take_screenshot:
            self.take_screenshot = False
            script_name = os.path.splitext(sys.argv[0])[0]

            i = 0
            while os.path.exists(f"{script_name}_{i:04d}.png"):
                i += 1
            file_name = f"{script_name}_{i:04d}.png"

            self._make_screenshot(file_name)

            self.screenshot_captured = True
            self.screenshot_capture_time = time.time()
            self.screenshot_capture_txt = f"Saved screenshot {file_name}"

    def _make_screenshot(self, filepath):
        data = OpenGL.GL.glReadPixels(0, 0, self.specs['window_size'][0],
                                      self.specs['window_size'][1],
                                      OpenGL.GL.GL_RGB, OpenGL.GL.GL_FLOAT)
        data = np.flipud(data.reshape((data.shape[1], data.shape[0], 3)))
        matplotlib.pyplot.imsave(filepath, data)

    def _display_all(self):

        OpenGL.GL.glClear(
            OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT)

        OpenGL.GL.glLoadMatrixf(self.camera.modelview)

        self._set_camera_spotlight()

        self._draw_system()
        self._draw_texts()

        OpenGL.GLUT.glutSwapBuffers()

        self._handle_screenshot()

    def _draw_texts(self):

        # draw user text
        for (x, y), text in self.user_texts:
            self._draw_text(x, y, text, self.text_color)

        # draw fps text
        if self.specs['draw_fps']:
            t = time.time()
            if t - self.fps_last > 1.0:
                self.fps_last = t
                self.fps = self.fps_count
                self.fps_count = 0

            self._draw_text(10, 10, f"{self.fps} fps", self.text_color)
            self._draw_text(10, 30, f"{1000.0 / self.fps} ms/frame",
                            self.text_color)
            self.fps_count += 1

        # draw particle info
        if self.show_system_info:
            self._draw_sysinfo_dict(self.system_info)
        elif self.info_id != -1:
            self._draw_particle_dict(
                self.highlighted_particle, self.max_len_attr)

        # indicate screen capture
        if self.screenshot_captured and not self.take_screenshot:
            col = np.array(self.text_color)
            ts = time.time() - self.screenshot_capture_time
            fadetime = 2.0
            col[3] = 1.0 - ts / fadetime
            if ts > fadetime:
                self.screenshot_captured = False
            else:
                self._draw_text(
                    self.specs['window_size'][0] - len(
                        self.screenshot_capture_txt) * 9.0 - 15,
                    self.specs['window_size'][1] - 15, self.screenshot_capture_txt, col)

    def _draw_sysinfo_dict(self, sysinfo_dict):
        y = 0
        for key, value_list in sysinfo_dict.items():
            # category title
            self._draw_text(10, self.specs['window_size'][1] - 10 - 15 * y,
                            f"{key}:", self.text_color)
            # item list
            for item in value_list:
                txt = str(item)
                n_lines = int(
                    len(txt) * 9.0 / self.specs['window_size'][0]) + 1
                chars_per_line = int(self.specs['window_size'][0] / 9) - 4
                if chars_per_line < 20:
                    break
                ls = 0
                for _ in range(n_lines):
                    y += 1
                    line_txt = txt[ls:ls + chars_per_line]
                    self._draw_text(
                        30, self.specs['window_size'][1] - 10 - 15 * y,
                        line_txt, self.text_color)
                    ls += chars_per_line
            y += 1.5

    def _draw_particle_dict(self, particle_dict, max_len):
        y = 0
        for k, v in particle_dict.items():
            txt = f"{k} {(max_len - len(k)) * ' '} {v}"
            self._draw_text(10, self.specs['window_size'][1] - 10 - 15 * y,
                            txt, self.text_color)
            y += 1

    # called on window position/size change
    def _reshape_window(self, w, h):
        OpenGL.GL.glViewport(0, 0, w, h)
        OpenGL.GL.glMatrixMode(OpenGL.GL.GL_PROJECTION)
        OpenGL.GL.glLoadIdentity()
        box_diag = np.linalg.norm(self.system.box_l)
        OpenGL.GLU.gluPerspective(40, 1.0 * w / h,
                                  self.specs['close_cut_distance'],
                                  self.specs['far_cut_distance'] * box_diag)
        OpenGL.GL.glMatrixMode(OpenGL.GL.GL_MODELVIEW)
        self.specs['window_size'][0] = w
        self.specs['window_size'][1] = h

    # inits for GLUT functions
    def _init_OpenGL_callbacks(self):
        # OpenGL callbacks
        def display():
            if self.hasParticleData and self.glut_main_loop_started:
                self._display_all()

        # pylint: disable=unused-argument
        def keyboard_up(button, x, y):
            if isinstance(button, bytes):
                button = button.decode("utf-8")
            self.keyboard_manager.keyboard_up(button)

        # pylint: disable=unused-argument
        def keyboard_down(button, x, y):
            if isinstance(button, bytes):
                button = button.decode("utf-8")
            self.keyboard_manager.keyboard_down(button)

        def mouse(button, state, x, y):
            self.mouse_manager.mouse_click(button, state, x, y)

        def motion(x, y):
            self.mouse_manager.mouse_move(x, y)

        def redraw_on_idle():
            # don't repost faster than 60 fps
            if (time.time() - self.last_draw) > 1.0 / 60.0:
                OpenGL.GLUT.glutPostRedisplay()
                self.last_draw = time.time()

        def reshape_callback(w, h):
            self._reshape_window(w, h)

        def close_window():
            os._exit(1)

        OpenGL.GLUT.glutDisplayFunc(display)
        OpenGL.GLUT.glutMouseFunc(mouse)
        OpenGL.GLUT.glutKeyboardFunc(keyboard_down)
        OpenGL.GLUT.glutKeyboardUpFunc(keyboard_up)
        OpenGL.GLUT.glutSpecialFunc(keyboard_down)
        OpenGL.GLUT.glutSpecialUpFunc(keyboard_up)
        OpenGL.GLUT.glutReshapeFunc(reshape_callback)
        OpenGL.GLUT.glutMotionFunc(motion)
        OpenGL.GLUT.glutWMCloseFunc(close_window)
        OpenGL.GLUT.glutIdleFunc(redraw_on_idle)

    def _init_timers(self):
        """
        Starts infinite loops of timed function callbacks.

        Using `OpenGL.GLUT.glutTimerFunc()` a timed function callback is
        registered that after (at least) the specified time calls a
        specific function, which re-registers a timed callback to itself
        plus does some other stuff.
        Per callback registered in `self.timers`, one such infinite
        callback loop is started.
        In addition, potential keyboard input is handled every (at least)
        17 ms, corresponding to a rate of ~60 1/s.

        """

        # timers for register_callback
        def dummy_timer(index):
            self.timers[index][1]()
            OpenGL.GLUT.glutTimerFunc(
                self.timers[index][0], dummy_timer, index)

        for index, timer in enumerate(self.timers):
            OpenGL.GLUT.glutTimerFunc(timer[0], dummy_timer, index)

        # handle input with 60 fps
        # pylint: disable=unused-argument
        def timed_handle_input(data):
            self.keyboard_manager.handle_input()
            OpenGL.GLUT.glutTimerFunc(17, timed_handle_input, -1)

        OpenGL.GLUT.glutTimerFunc(17, timed_handle_input, -1)

    # click on particle: drag; click on background: change camera angle.
    def _mouse_motion(self, mouse_pos, mouse_pos_old, mouse_button_state):

        if self.specs['drag_enabled'] and self.drag_id != -1:
            part_pos = self.particles['pos'][self.drag_id]
            viewport = OpenGL.GL.glGetIntegerv(OpenGL.GL.GL_VIEWPORT)
            mouse_world = OpenGL.GLU.gluUnProject(
                mouse_pos[0], viewport[3] - mouse_pos[1], self.depth)

            self.dragExtForce = self.specs['drag_force'] * \
                (np.asarray(mouse_world) - np.array(part_pos))
            self.trigger_set_particle_drag = True
        else:
            self.camera.rotate_camera(mouse_pos, mouse_pos_old,
                                      mouse_button_state)

    # re-draw scene without light to identify particle id by pixel color
    def _get_particle_id(self, pos):

        OpenGL.GL.glClearColor(0.0, 0.0, 0.0, 1.0)
        OpenGL.GL.glClear(
            OpenGL.GL.GL_COLOR_BUFFER_BIT | OpenGL.GL.GL_DEPTH_BUFFER_BIT)

        OpenGL.GL.glLoadMatrixf(self.camera.modelview)

        OpenGL.GL.glDisable(OpenGL.GL.GL_LIGHTING)
        OpenGL.GL.glDisable(OpenGL.GL.GL_LIGHT0)
        if self.specs['spotlight_enabled']:
            OpenGL.GL.glDisable(OpenGL.GL.GL_LIGHT1)
        self._draw_system_particles(color_by_id=True)
        viewport = OpenGL.GL.glGetIntegerv(OpenGL.GL.GL_VIEWPORT)

        read_pixel = OpenGL.GL.glReadPixelsui(
            pos[0], viewport[3] - pos[1], 1, 1,
            OpenGL.GL.GL_RGB, OpenGL.GL.GL_FLOAT)[0][0]
        depth = OpenGL.GL.glReadPixelsf(
            pos[0], viewport[3] - pos[1], 1, 1,
            OpenGL.GL.GL_DEPTH_COMPONENT, OpenGL.GL.GL_FLOAT)[0][0]

        part_id = self._fcolor_to_id(read_pixel)

        OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHTING)
        OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT0)
        if self.specs['spotlight_enabled']:
            OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT1)
        OpenGL.GL.glClearColor(self.specs['background_color'][0],
                               self.specs['background_color'][1],
                               self.specs['background_color'][2], 1.)

        return part_id, depth

    @staticmethod
    def _id_to_fcolor(part_id):
        part_id += 1
        return [int(part_id / 256 ** 2) / 255.0,
                int((part_id % 256 ** 2) / 256) / 255.0,
                (part_id % 256) / 255.0, 1.0]

    @staticmethod
    def _fcolor_to_id(fcolor):
        if (fcolor == [0, 0, 0]).all():
            return -1
        return int(fcolor[0] * 255) * 256 ** 2 + \
            int(fcolor[1] * 255) * 256 + \
            int(fcolor[2] * 255) - 1

    # pylint: disable=unused-argument
    def _set_particle_drag(self, pos, pos_old):
        part_id, depth = self._get_particle_id(pos)
        self.drag_id = part_id

        if part_id != -1:
            self.dragPosInitial = self.particles['pos'][self.drag_id]
            self.extForceOld = self.particles['ext_force'][self.drag_id][:]
            self.depth = depth

    # pylint: disable=unused-argument
    def _reset_particle_drag(self, pos, pos_old):
        if self.drag_id != -1:
            self.trigger_reset_particle_drag = True

    def _get_particle_info(self, pos, pos_old):
        part_id, _ = self._get_particle_id(pos)
        if self.show_system_info:
            self.show_system_info = False
        elif part_id == -1 and self.info_id == -1:
            self.show_system_info = True
            self.update_system_info()
        self.info_id = part_id

    def _next_particle_info(self):
        self.info_id = (self.info_id + 1) % len(self.particles['pos'])

    def _previous_particle_info(self):
        self.info_id = (self.info_id - 1) % len(self.particles['pos'])

    def _init_espresso_visualization(self):
        # initialize properties that depend on the ESPResSo system
        self.max_q = 0.
        self.min_q = 0.

        self.drag_id = -1
        self.info_id = -1
        self.show_system_info = False
        self.dragPosInitial = []
        self.extForceOld = []
        self.dragExtForceOld = []
        self.trigger_reset_particle_drag = False
        self.trigger_set_particle_drag = False

        self.depth = 0

        # LOOK FOR LB ACTOR
        lb_types = [espressomd.lb.LBFluid]
        if espressomd.has_features('CUDA'):
            lb_types.append(espressomd.lb.LBFluidGPU)
        for actor in self.system.actors:
            if isinstance(actor, tuple(lb_types)):
                self.lb_params = actor.get_params()
                self.lb = actor
                self.lb_is_cpu = isinstance(actor, espressomd.lb.LBFluid)
                break

        if self.specs['LB_draw_velocity_plane']:
            if self.specs['LB_plane_axis'] == 0:
                pn = [1.0, 0.0, 0.0]
                self.lb_plane_b1 = [0.0, 1.0, 0.0]
                self.lb_plane_b2 = [0.0, 0.0, 1.0]
            elif self.specs['LB_plane_axis'] == 1:
                pn = [0.0, 1.0, 0.0]
                self.lb_plane_b1 = [1.0, 0.0, 0.0]
                self.lb_plane_b2 = [0.0, 0.0, 1.0]
            else:
                pn = [0.0, 0.0, 1.0]
                self.lb_plane_b1 = [1.0, 0.0, 0.0]
                self.lb_plane_b2 = [0.0, 1.0, 0.0]

            self.lb_plane_b1 *= self.system.box_l
            self.lb_plane_b2 *= self.system.box_l
            self.lb_plane_p = np.array(pn) * self.specs['LB_plane_dist']

            self.lb_arrow_radius = self.system.box_l[
                self.specs['LB_plane_axis']] * self.specs['LB_vel_radius_scale']

            self.lb_min_vel = np.array([-1e-6] * 3)
            self.lb_max_vel = np.array([1e-6] * 3)
            self.lb_vel_range = self.lb_max_vel - self.lb_min_vel
            self.lb_min_dens = np.array([0] * 3)
            self.lb_max_dens = np.array([0] * 3)

            self._update_lb_velocity_plane()

        self._box_size_dependence()

    def _box_size_dependence(self):
        # initialize properties that depend on the box geometry

        if self.specs['draw_cells'] or self.specs['draw_nodes']:
            self._update_nodes()
            if self.specs['draw_cells']:
                self._update_cells()

        self.box_eqn = np.full((6, 4), np.nan)
        # set face normals of box
        self.box_eqn[:3, :3] = np.identity(3)
        self.box_eqn[3:, :3] = -np.identity(3)
        # set face parameters of box
        self.box_eqn[:3, 3] = 0.001 * self.system.box_l
        self.box_eqn[3:, 3] = 1.001 * self.system.box_l

        self.camera.center = np.array(self.system.box_l) * 0.5
        self.camera.update_modelview()

    def _init_controls(self):
        # initialize mouse/keyboard event listeners

        # mouse look/rotate/drag
        self.mouse_manager.register_button(MouseButtonEvent(
            None, MouseFireEvent.FreeMotion, self._mouse_motion))

        self.mouse_manager.register_button(MouseButtonEvent(
            3, MouseFireEvent.ButtonPressed, self.camera.move_backward))

        self.mouse_manager.register_button(MouseButtonEvent(
            4, MouseFireEvent.ButtonPressed, self.camera.move_forward))

        # left mouse button to start/stop drag
        if self.specs['drag_enabled']:
            self.mouse_manager.register_button(MouseButtonEvent(
                OpenGL.GLUT.GLUT_LEFT_BUTTON, MouseFireEvent.ButtonPressed, self._set_particle_drag, True))
            self.mouse_manager.register_button(MouseButtonEvent(
                OpenGL.GLUT.GLUT_LEFT_BUTTON, MouseFireEvent.ButtonReleased, self._reset_particle_drag, True))

        # left mouse button for particle information
        self.mouse_manager.register_button(MouseButtonEvent(
            OpenGL.GLUT.GLUT_LEFT_BUTTON, MouseFireEvent.DoubleClick, self._get_particle_info, True))

        # left/right arrows to cycle through particles
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            OpenGL.GLUT.GLUT_KEY_LEFT, KeyboardFireEvent.Pressed, self._previous_particle_info))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            OpenGL.GLUT.GLUT_KEY_RIGHT, KeyboardFireEvent.Pressed, self._next_particle_info))

        # <space> key to pause integration thread
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            ' ', KeyboardFireEvent.Pressed, self._pause, True))

        # <return> key to take screenshot
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            '\x0d', KeyboardFireEvent.Pressed, self._trigger_screenshot, True))

        # <escape> key to quit
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            '\x1b', KeyboardFireEvent.Pressed, self._quit, True))

        # camera control via keyboard
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'w', KeyboardFireEvent.Hold, self.camera.move_up, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            's', KeyboardFireEvent.Hold, self.camera.move_down, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'a', KeyboardFireEvent.Hold, self.camera.move_left, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'd', KeyboardFireEvent.Hold, self.camera.move_right, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'e', KeyboardFireEvent.Hold, self.camera.rotate_system_XR, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'q', KeyboardFireEvent.Hold, self.camera.rotate_system_XL, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'c', KeyboardFireEvent.Hold, self.camera.rotate_system_YR, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'z', KeyboardFireEvent.Hold, self.camera.rotate_system_YL, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'r', KeyboardFireEvent.Hold, self.camera.rotate_system_ZR, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'f', KeyboardFireEvent.Hold, self.camera.rotate_system_ZL, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            't', KeyboardFireEvent.Hold, self.camera.move_forward, True))
        self.keyboard_manager.register_button(KeyboardButtonEvent(
            'g', KeyboardFireEvent.Hold, self.camera.move_backward, True))

    def _quit(self):
        self.quit_safely = True

    def _pause(self):
        self.paused = not self.paused

    def _trigger_screenshot(self):
        self.take_screenshot = True

    # asynchronous parallel calls to glLight() causes segfaults, so only change
    # light at central display() method and trigger changes
    def _set_camera_spotlight(self):
        if self.specs['spotlight_enabled']:
            p = self.camera.cam_pos
            fp = [p[0], p[1], p[2], 1]
            OpenGL.GL.glLightfv(OpenGL.GL.GL_LIGHT1, OpenGL.GL.GL_POSITION, fp)
            OpenGL.GL.glLightfv(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_SPOT_DIRECTION,
                self.camera.state_target)

    def _init_camera(self):
        b = np.array(self.system.box_l)
        box_diag = np.linalg.norm(b)
        box_center = b * 0.5
        if self.specs['camera_position'] == 'auto':
            cp = [box_center[0], box_center[1], b[2] * 3]
        else:
            cp = self.specs['camera_position']

        if self.specs['camera_target'] == 'auto':
            ct = box_center
        else:
            ct = self.specs['camera_target']

        cr = np.array(self.specs['camera_right'])

        self._set_camera_spotlight()

        return Camera(cam_pos=np.array(cp), cam_target=ct, cam_right=cr,
                      move_speed=0.5 * box_diag / 17.0, center=box_center)

    def _init_opengl(self):
        OpenGL.GLUT.glutInit(self.specs['name'])
        OpenGL.GLUT.glutInitDisplayMode(
            OpenGL.GLUT.GLUT_DOUBLE | OpenGL.GLUT.GLUT_RGB | OpenGL.GLUT.GLUT_DEPTH)

        OpenGL.GLUT.glutInitWindowSize(self.specs['window_size'][0],
                                       self.specs['window_size'][1])

        OpenGL.GLUT.glutCreateWindow(b"ESPResSo visualization")

        OpenGL.GL.glClearColor(self.specs['background_color'][0],
                               self.specs['background_color'][1],
                               self.specs['background_color'][2], 1.)

        OpenGL.GL.glEnable(OpenGL.GL.GL_BLEND)
        OpenGL.GL.glBlendFunc(
            OpenGL.GL.GL_SRC_ALPHA,
            OpenGL.GL.GL_ONE_MINUS_SRC_ALPHA)

        OpenGL.GL.glEnable(OpenGL.GL.GL_POINT_SMOOTH)
        OpenGL.GL.glEnable(OpenGL.GL.GL_LINE_SMOOTH)
        OpenGL.GL.glHint(OpenGL.GL.GL_LINE_SMOOTH_HINT, OpenGL.GL.GL_NICEST)

        # bad for transparent particles
        # OpenGL.GL.glEnable(OpenGL.GL.GL_CULL_FACE)
        # OpenGL.GL.glCullFace(OpenGL.GL.GL_BACK)

        OpenGL.GL.glLineWidth(2.0)
        OpenGL.GLUT.glutIgnoreKeyRepeat(1)

        # setup lighting
        if self.specs['light_size'] == 'auto':
            box_diag = np.linalg.norm(self.system.box_l)
            self.specs['light_size'] = box_diag * 2.0

        OpenGL.GL.glEnable(OpenGL.GL.GL_DEPTH_TEST)
        OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHTING)

        # LIGHT0
        if self.specs['light_pos'] != 'auto':
            OpenGL.GL.glLightfv(OpenGL.GL.GL_LIGHT0, OpenGL.GL.GL_POSITION,
                                np.array(self.specs['light_pos']).tolist())
        else:
            OpenGL.GL.glLightfv(OpenGL.GL.GL_LIGHT0, OpenGL.GL.GL_POSITION,
                                (np.array(self.system.box_l) * 1.1).tolist())

        OpenGL.GL.glLightfv(
            OpenGL.GL.GL_LIGHT0,
            OpenGL.GL.GL_AMBIENT,
            self.specs['light_colors'][0])
        OpenGL.GL.glLightfv(
            OpenGL.GL.GL_LIGHT0,
            OpenGL.GL.GL_DIFFUSE,
            self.specs['light_colors'][1])
        OpenGL.GL.glLightfv(
            OpenGL.GL.GL_LIGHT0,
            OpenGL.GL.GL_SPECULAR,
            self.specs['light_colors'][2])

        OpenGL.GL.glLightf(
            OpenGL.GL.GL_LIGHT0, OpenGL.GL.GL_CONSTANT_ATTENUATION,
            1.0 / self.specs['light_brightness'])
        OpenGL.GL.glLightf(
            OpenGL.GL.GL_LIGHT0, OpenGL.GL.GL_LINEAR_ATTENUATION,
            1.0 / self.specs['light_size'])
        OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT0)

        # LIGHT1: spotlight on camera in look direction
        if self.specs['spotlight_enabled']:
            OpenGL.GL.glLightfv(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_POSITION,
                [0, 0, 0, 1])

            OpenGL.GL.glLightfv(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_AMBIENT,
                self.specs['spotlight_colors'][0])
            OpenGL.GL.glLightfv(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_DIFFUSE,
                self.specs['spotlight_colors'][1])
            OpenGL.GL.glLightfv(OpenGL.GL.GL_LIGHT1, OpenGL.GL.GL_SPECULAR,
                                self.specs['spotlight_colors'][2])

            OpenGL.GL.glLightf(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_SPOT_CUTOFF,
                self.specs['spotlight_angle'])
            OpenGL.GL.glLightfv(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_SPOT_DIRECTION,
                [1.0, 1.0, 1.0])
            OpenGL.GL.glLightf(OpenGL.GL.GL_LIGHT1, OpenGL.GL.GL_SPOT_EXPONENT,
                               self.specs['spotlight_focus'])

            OpenGL.GL.glLightf(
                OpenGL.GL.GL_LIGHT1, OpenGL.GL.GL_CONSTANT_ATTENUATION,
                1.0 / self.specs['spotlight_brightness'])
            OpenGL.GL.glLightf(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_LINEAR_ATTENUATION,
                0.0)
            OpenGL.GL.glLightf(
                OpenGL.GL.GL_LIGHT1,
                OpenGL.GL.GL_QUADRATIC_ATTENUATION,
                0.0)
            OpenGL.GL.glEnable(OpenGL.GL.GL_LIGHT1)

        self.dl_sphere = OpenGL.GL.glGenLists(1)


# END OF MAIN CLASS

class Shape():
    """
    Shape base class in the visualizer context.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        self.shape = shape
        self.particle_type = particle_type
        self.color = color
        self.material = material
        self.quality = quality
        self.box_l = box_l
        self.rasterize_resolution = rasterize_resolution
        self.pointsize = rasterize_pointsize
        self.rasterized_surface_points = None

    def draw(self):
        """
        Draw shape via rasterization. Used as a default draw method.
        Can and should be overwritten in child classes to implement a better
        draw method.

        """
        # get and store points of rasterized surface if not already present
        if self.rasterized_surface_points is None:
            self.rasterized_surface_points = self._rasterize_shape()

        set_solid_material(self.color, self.material)
        OpenGL.GL.glPointSize(self.pointsize)
        OpenGL.GL.glBegin(OpenGL.GL.GL_POINTS)
        for point in self.rasterized_surface_points:
            OpenGL.GL.glVertex3f(point[0], point[1], point[2])
        OpenGL.GL.glEnd()

    def _rasterize_shape(self):
        # rasterize brute force
        spacing = max(self.box_l) / self.rasterize_resolution
        resolution = np.array(self.box_l) / spacing

        points = []
        for i in range(int(resolution[0])):
            for j in range(int(resolution[1])):
                for k in range(int(resolution[2])):
                    # some shapes may not have a well-defined distance function
                    # in the whole domain and may throw a ValueError
                    try:
                        p = np.array([i, j, k]) * spacing
                        dist, vec = self.shape.call_method(
                            "calc_distance", position=p.tolist())
                        if not (np.isnan(vec).any() or np.isnan(dist)) \
                                and abs(dist) < spacing:
                            points.append((p - vec).tolist())
                    except ValueError:
                        continue
        return points

    def _has_gle_features(self, feature_names):
        for feature_name in feature_names:
            if not bool(getattr(OpenGL.GLE, feature_name)):
                return False
        return True


class Cylinder(Shape):
    """
    Drawable Shape Cylinder.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.center = np.array(self.shape.get_parameter('center'))
        self.axis = np.array(self.shape.get_parameter('axis'))
        self.length = self.shape.get_parameter('length')
        self.radius = self.shape.get_parameter('radius')
        self.open = self.shape.get_parameter('open')
        self.cap_center_1 = self.center - self.axis / \
            np.linalg.norm(self.axis) * 0.5 * self.length
        self.cap_center_2 = self.center + self.axis / \
            np.linalg.norm(self.axis) * 0.5 * self.length

    def draw(self):
        draw_cylinder(self.cap_center_1, self.cap_center_2,
                      self.radius, self.color, self.material,
                      self.quality, draw_caps=not self.open)


class Ellipsoid(Shape):
    """
    Drawable Shape Ellipsoid.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.center = np.array(self.shape.get_parameter('center'))
        self.semiaxis_a = np.array(self.shape.get_parameter('a'))
        self.semiaxis_b = np.array(self.shape.get_parameter('b'))
        self.semiaxis_c = np.array(self.shape.get_parameter('b'))

    def draw(self):
        set_solid_material(self.color, self.material)
        OpenGL.GL.glPushMatrix()
        OpenGL.GL.glTranslatef(self.center[0], self.center[1], self.center[2])
        OpenGL.GL.glScalef(self.semiaxis_a, self.semiaxis_b, self.semiaxis_c)
        OpenGL.GLUT.glutSolidSphere(1, self.quality, self.quality)
        OpenGL.GL.glPopMatrix()


class HollowConicalFrustum(Shape):
    """
    Drawable Shape HollowConicalFrustum.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        ctp = self.shape.get_parameter('cyl_transform_params')
        self.center = np.array(ctp.center)
        self.axis = np.array(ctp.axis)
        self.orientation = np.array(ctp.orientation)
        self.radius_1 = self.shape.get_parameter('r1')
        self.radius_2 = self.shape.get_parameter('r2')
        self.length = self.shape.get_parameter('length')
        self.thickness = self.shape.get_parameter('thickness')
        self.central_angle = self.shape.get_parameter('central_angle')
        self.use_gle = self._has_gle_features(
            ["gleSpiral", "gleSetNumSides", "gleSetJoinStyle"])

    def draw(self):
        """
        Draw using OpenGL Extrusion library, if available.
        Use rasterization of base class, otherwise.

        """
        if self.use_gle and self.central_angle == 0.:
            self._draw_using_gle()
        else:
            super().draw()

    # if available, use the GL Extrusion library
    def _draw_using_gle(self):
        set_solid_material(self.color, self.material)
        OpenGL.GL.glPushMatrix()
        # basic position and orientation
        OpenGL.GL.glTranslate(self.center[0], self.center[1], self.center[2])
        ax, rx, ry = rotation_helper(self.axis)
        OpenGL.GL.glRotatef(ax, rx, ry, 0.0)

        n = max(20, self.quality)
        rotation_angle = np.arctan(
            (self.radius_1 - self.radius_2) / self.length)
        l = self.length / np.cos(rotation_angle)

        contour = []
        for theta in np.linspace(-0.5 * np.pi, 0.5 * np.pi, n):
            contour.append([np.sin(theta) * 0.5 * self.thickness,
                            np.cos(theta) * 0.5 * self.thickness + 0.5 * l])
        for theta in np.linspace(0.5 * np.pi, -0.5 * np.pi, n):
            contour.append([np.sin(theta) * 0.5 * self.thickness,
                            -np.cos(theta) * 0.5 * self.thickness - 0.5 * l])
        contour = np.matmul(np.array(contour),
                            np.array([[np.cos(rotation_angle), -np.sin(rotation_angle)],
                                      [np.sin(rotation_angle), np.cos(rotation_angle)]]))

        normals = np.diff(np.array(contour), axis=0)
        normals /= np.linalg.norm(normals, ord=2, axis=1, keepdims=True)
        normals = np.roll(normals, 1, axis=1)

        OpenGL.GLE.gleSetJoinStyle(OpenGL.GLE.TUBE_JN_ANGLE)
        OpenGL.GLE.gleSetNumSides(max(90, 3 * self.quality))
        OpenGL.GLE.gleSpiral(contour, normals, [0, 0, 1], 0.5 * (self.radius_1 + self.radius_2), 0., 0., 0.,
                             [[1, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]], 0., 360)

        OpenGL.GL.glPopMatrix()


class SimplePore(Shape):
    """
    Drawable Shape SimplePore.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.center = np.array(self.shape.get_parameter('center'))
        self.axis = np.array(self.shape.get_parameter('axis'))
        self.length = np.array(self.shape.get_parameter('length'))
        self.radius = np.array(self.shape.get_parameter('radius'))
        self.smoothing_radius = np.array(
            self.shape.get_parameter('smoothing_radius'))
        self.max_box_l = max(box_l)
        self.use_gle = self._has_gle_features(
            ["gleSpiral", "gleSetNumSides", "gleSetJoinStyle"])

    def draw(self):
        """
        Draw using OpenGL Extrusion library, if available.
        Use OpenGL primitives + clip planes, otherwise.

        """
        set_solid_material(self.color, self.material)
        OpenGL.GL.glPushMatrix()
        # basic position and orientation
        OpenGL.GL.glTranslate(self.center[0], self.center[1], self.center[2])
        ax, rx, ry = rotation_helper(self.axis)
        OpenGL.GL.glRotatef(ax, rx, ry, 0.0)

        if self.use_gle:
            self._draw_using_gle()
        else:
            self._draw_using_primitives()

        OpenGL.GL.glPopMatrix()

    def _draw_using_gle(self):
        n = max(10, self.quality // 3)
        contour = [[0.5 * self.max_box_l, -0.5 * self.length]]
        for theta in np.linspace(0, 0.5 * np.pi, n):
            contour.append([(1. - np.sin(theta)) * self.smoothing_radius,
                            -0.5 * self.length + (1. - np.cos(theta)) * self.smoothing_radius])
        for theta in np.linspace(0.5 * np.pi, np.pi, n):
            contour.append([(1. - np.sin(theta)) * self.smoothing_radius,
                            0.5 * self.length - (1. + np.cos(theta)) * self.smoothing_radius])
        contour.append([0.5 * self.max_box_l, 0.5 * self.length])

        normals = np.diff(np.array(contour), axis=0)
        normals /= np.linalg.norm(normals, ord=2, axis=1, keepdims=True)
        normals = np.roll(normals, 1, axis=1)
        normals[:, 0] *= -1

        OpenGL.GLE.gleSetJoinStyle(OpenGL.GLE.TUBE_JN_ANGLE)
        OpenGL.GLE.gleSetNumSides(max(90, 3 * self.quality))
        OpenGL.GLE.gleSpiral(contour, normals, [0, 0, 1], self.radius, 0., 0., 0.,
                             [[1, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]], 0., 360)

    def _draw_using_primitives(self):
        clip_plane = get_extra_clip_plane()
        # cylinder
        OpenGL.GL.glTranslate(0, 0, -0.5 * self.length + self.smoothing_radius)
        quadric = OpenGL.GLU.gluNewQuadric()
        OpenGL.GLU.gluCylinder(quadric, self.radius, self.radius, self.length - 2 *
                               self.smoothing_radius, self.quality, self.quality)
        # torus segment
        OpenGL.GL.glEnable(clip_plane)
        OpenGL.GL.glClipPlane(clip_plane, (0, 0, -1, 0))
        OpenGL.GLUT.glutSolidTorus(
            self.smoothing_radius,
            self.radius + self.smoothing_radius,
            self.quality,
            self.quality)
        OpenGL.GL.glDisable(clip_plane)
        # wall
        OpenGL.GL.glTranslate(0, 0, -self.smoothing_radius)
        OpenGL.GLU.gluPartialDisk(quadric, self.radius + self.smoothing_radius,
                                  2.0 * self.max_box_l, self.quality, 1, 0, 360)
        # torus segment
        OpenGL.GL.glTranslate(0, 0, self.length - self.smoothing_radius)
        OpenGL.GL.glEnable(clip_plane)
        OpenGL.GL.glClipPlane(clip_plane, (0, 0, 1, 0))
        OpenGL.GLUT.glutSolidTorus(
            self.smoothing_radius,
            self.radius + self.smoothing_radius,
            self.quality,
            self.quality)
        OpenGL.GL.glDisable(clip_plane)
        # wall
        OpenGL.GL.glTranslate(0, 0, self.smoothing_radius)
        OpenGL.GLU.gluPartialDisk(quadric, self.radius + self.smoothing_radius,
                                  2.0 * self.max_box_l, self.quality, 1, 0, 360)

        OpenGL.GLU.gluDeleteQuadric(quadric)


class Slitpore(Shape):
    """
    Drawable Shape Slitpore.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.channel_width = np.array(
            self.shape.get_parameter('channel_width'))
        self.lower_smoothing_radius = np.array(
            self.shape.get_parameter('lower_smoothing_radius'))
        self.upper_smoothing_radius = np.array(
            self.shape.get_parameter('upper_smoothing_radius'))
        self.pore_length = np.array(self.shape.get_parameter('pore_length'))
        self.pore_mouth = np.array(self.shape.get_parameter('pore_mouth'))
        self.pore_width = np.array(self.shape.get_parameter('pore_width'))
        self.dividing_plane = np.array(
            self.shape.get_parameter('dividing_plane'))
        self.max_box_l = max(self.box_l)

    def draw(self):
        set_solid_material(self.color, self.material)
        # If pore is large, an additional wall is necessary
        if self.pore_width > 2. * self.lower_smoothing_radius:
            wall_0 = [
                [self.dividing_plane - 0.5 * self.pore_width + self.lower_smoothing_radius,
                 0., self.pore_mouth - self.pore_length],
                [self.dividing_plane + 0.5 * self.pore_width - self.lower_smoothing_radius,
                 0., self.pore_mouth - self.pore_length],
                [self.dividing_plane + 0.5 * self.pore_width - self.lower_smoothing_radius,
                 self.max_box_l, self.pore_mouth - self.pore_length],
                [self.dividing_plane - 0.5 * self.pore_width + self.lower_smoothing_radius,
                 self.max_box_l, self.pore_mouth - self.pore_length]]
            draw_plane(wall_0, self.color, self.material)

        # Add the remaining walls
        wall_1 = [
            [0., 0., self.channel_width + self.pore_mouth],
            [self.max_box_l, 0., self.channel_width + self.pore_mouth],
            [self.max_box_l, self.max_box_l, self.channel_width + self.pore_mouth],
            [0., self.max_box_l, self.channel_width + self.pore_mouth]]

        wall_2 = [
            [0., 0., self.pore_mouth],
            [self.dividing_plane - 0.5 * self.pore_width -
             self.upper_smoothing_radius, 0., self.pore_mouth],
            [self.dividing_plane - 0.5 * self.pore_width -
             self.upper_smoothing_radius, self.max_box_l, self.pore_mouth],
            [0., self.max_box_l, self.pore_mouth]]

        wall_3 = [
            [self.dividing_plane + 0.5 * self.pore_width +
             self.upper_smoothing_radius, 0., self.pore_mouth],
            [self.max_box_l, 0., self.pore_mouth],
            [self.max_box_l, self.max_box_l, self.pore_mouth],
            [self.dividing_plane + 0.5 * self.pore_width +
             self.upper_smoothing_radius, self.max_box_l, self.pore_mouth]]

        wall_4 = [
            [self.dividing_plane - 0.5 * self.pore_width, 0.,
             self.pore_mouth - self.upper_smoothing_radius],
            [self.dividing_plane - 0.5 * self.pore_width, self.max_box_l,
             self.pore_mouth - self.upper_smoothing_radius],
            [self.dividing_plane - 0.5 * self.pore_width, self.max_box_l,
             self.pore_mouth - self.pore_length + self.lower_smoothing_radius],
            [self.dividing_plane - 0.5 * self.pore_width, 0.,
             self.pore_mouth - self.pore_length + self.lower_smoothing_radius]]

        wall_5 = [
            [self.dividing_plane + 0.5 * self.pore_width, 0.,
             self.pore_mouth - self.upper_smoothing_radius],
            [self.dividing_plane + 0.5 * self.pore_width, self.max_box_l,
             self.pore_mouth - self.upper_smoothing_radius],
            [self.dividing_plane + 0.5 * self.pore_width, self.max_box_l,
             self.pore_mouth - self.pore_length + self.lower_smoothing_radius],
            [self.dividing_plane + 0.5 * self.pore_width, 0.,
             self.pore_mouth - self.pore_length + self.lower_smoothing_radius]]

        draw_plane(wall_1, self.color, self.material)
        draw_plane(wall_2, self.color, self.material)
        draw_plane(wall_3, self.color, self.material)
        draw_plane(wall_4, self.color, self.material)
        draw_plane(wall_5, self.color, self.material)

        # Add smooth edges via clipped cylinders
        ax, rx, ry = rotation_helper([0., 1., 0.])

        OpenGL.GL.glPushMatrix()
        quadric = OpenGL.GLU.gluNewQuadric()
        OpenGL.GL.glTranslate(self.dividing_plane - self.upper_smoothing_radius -
                              0.5 * self.pore_width, 0, self.pore_mouth - self.upper_smoothing_radius)
        OpenGL.GL.glRotatef(ax, rx, ry, 0.)

        # Upper edges
        clip_plane = get_extra_clip_plane()
        OpenGL.GL.glEnable(clip_plane)
        OpenGL.GL.glClipPlane(clip_plane,
                              (1, -1, 0, -self.upper_smoothing_radius))
        OpenGL.GLU.gluCylinder(quadric, self.upper_smoothing_radius,
                               self.upper_smoothing_radius, self.max_box_l, self.quality, self.quality)

        OpenGL.GL.glTranslate(self.pore_width + 2. * self.upper_smoothing_radius,
                              0, 0)
        OpenGL.GL.glClipPlane(clip_plane,
                              (-1, -1, 0, -self.upper_smoothing_radius))
        OpenGL.GLU.gluCylinder(quadric, self.upper_smoothing_radius,
                               self.upper_smoothing_radius, self.max_box_l, self.quality, self.quality)

        # Lower edges
        OpenGL.GL.glTranslate(- self.upper_smoothing_radius - self.lower_smoothing_radius,
                              self.pore_length - self.upper_smoothing_radius - self.lower_smoothing_radius, 0)
        OpenGL.GL.glClipPlane(clip_plane,
                              (1, 1, 0, -self.lower_smoothing_radius))
        OpenGL.GLU.gluCylinder(quadric, self.lower_smoothing_radius,
                               self.lower_smoothing_radius, self.max_box_l, self.quality, self.quality)

        OpenGL.GL.glTranslate(-self.pore_width + 2. *
                              self.lower_smoothing_radius, 0, 0)
        OpenGL.GL.glClipPlane(clip_plane,
                              (-1, 1, 0, -self.lower_smoothing_radius))
        OpenGL.GLU.gluCylinder(quadric, self.lower_smoothing_radius,
                               self.lower_smoothing_radius, self.max_box_l, self.quality, self.quality)

        OpenGL.GL.glDisable(clip_plane)
        OpenGL.GLU.gluDeleteQuadric(quadric)
        OpenGL.GL.glPopMatrix()


class Sphere(Shape):
    """
    Drawable Shape Sphere.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.center = self.shape.get_parameter('center')
        self.radius = self.shape.get_parameter('radius')

    def draw(self):
        OpenGL.GL.glPushMatrix()
        OpenGL.GL.glTranslatef(self.center[0], self.center[1], self.center[2])
        set_solid_material(self.color, self.material)
        OpenGL.GLUT.glutSolidSphere(self.radius, self.quality, self.quality)
        OpenGL.GL.glPopMatrix()


class Spherocylinder(Shape):
    """
    Drawable Shape Spherocylinder.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.center = np.array(self.shape.get_parameter('center'))
        self.axis = np.array(self.shape.get_parameter('axis'))
        self.length = self.shape.get_parameter('length')
        self.radius = self.shape.get_parameter('radius')
        self.cap_center_1 = self.center - self.axis / \
            np.linalg.norm(self.axis) * 0.5 * self.length
        self.cap_center_2 = self.center + self.axis / \
            np.linalg.norm(self.axis) * 0.5 * self.length

    def draw(self):
        set_solid_material(self.color, self.material)
        OpenGL.GL.glPushMatrix()
        quadric = OpenGL.GLU.gluNewQuadric()

        d = self.cap_center_2 - self.cap_center_1
        if d[2] == 0.0:
            d[2] = 0.0001

        v = np.linalg.norm(d)
        if v == 0:
            ax = 57.2957795
        else:
            ax = 57.2957795 * math.acos(d[2] / v)

        if d[2] < 0.0:
            ax = -ax
        rx = -d[1] * d[2]
        ry = d[0] * d[2]
        length = np.linalg.norm(d)
        OpenGL.GL.glTranslatef(
            self.cap_center_1[0],
            self.cap_center_1[1],
            self.cap_center_1[2])
        OpenGL.GL.glRotatef(ax, rx, ry, 0.0)

        # First hemispherical cap
        clip_plane = get_extra_clip_plane()
        OpenGL.GL.glEnable(clip_plane)
        OpenGL.GL.glClipPlane(clip_plane, (0, 0, -1, 0))
        OpenGL.GLU.gluSphere(quadric, self.radius, self.quality, self.quality)
        OpenGL.GL.glDisable(clip_plane)
        # Cylinder
        OpenGL.GLU.gluCylinder(quadric, self.radius, self.radius,
                               length, self.quality, self.quality)
        # Second hemispherical cap
        OpenGL.GL.glTranslatef(0, 0, v)
        OpenGL.GL.glEnable(clip_plane)
        OpenGL.GL.glClipPlane(clip_plane, (0, 0, 1, 0))
        OpenGL.GLU.gluSphere(quadric, self.radius, self.quality, self.quality)
        OpenGL.GL.glDisable(clip_plane)

        OpenGL.GLU.gluDeleteQuadric(quadric)
        OpenGL.GL.glPopMatrix()


class Wall(Shape):
    """
    Drawable Shape Wall.

    """

    def __init__(self, shape, particle_type, color, material,
                 quality, box_l, rasterize_resolution, rasterize_pointsize):
        super().__init__(shape, particle_type, color, material,
                         quality, box_l, rasterize_resolution, rasterize_pointsize)
        self.distance = self.shape.get_parameter('dist')
        self.normal = self.shape.get_parameter('normal')
        self.box_diag = np.linalg.norm(self.box_l)
        self.edges = self._edges_from_pn(self.distance * np.array(self.normal),
                                         self.normal, 2 * self.box_diag)

    @staticmethod
    def _get_tangents(n):
        n = np.array(n)
        v1 = np.random.randn(3)
        v1 -= v1.dot(n) * n / np.linalg.norm(n)**2
        v2 = np.cross(n, v1)
        v1 /= np.linalg.norm(v1)
        v2 /= np.linalg.norm(v2)
        return v1, v2

    def _edges_from_pn(self, p, n, diag):
        v1, v2 = self._get_tangents(n)
        edges = [p + diag * v1, p + diag * v2, p - diag * v1, p - diag * v2]
        return edges

    def draw(self):
        draw_plane(self.edges, self.color, self.material)


# OPENGL DRAW WRAPPERS

def set_solid_material(color, material=(0.6, 1.0, 0.1, 0.4, 1.0)):
    OpenGL.GL.glMaterialfv(OpenGL.GL.GL_FRONT_AND_BACK, OpenGL.GL.GL_AMBIENT, [
        color[0] * material[0], color[1] * material[0], color[2] * material[0], material[4]])
    OpenGL.GL.glMaterialfv(OpenGL.GL.GL_FRONT_AND_BACK, OpenGL.GL.GL_DIFFUSE, [
        color[0] * material[1], color[1] * material[1], color[2] * material[1], material[4]])
    OpenGL.GL.glMaterialfv(OpenGL.GL.GL_FRONT_AND_BACK, OpenGL.GL.GL_SPECULAR, [
        color[0] * material[2], color[1] * material[2], color[2] * material[2], material[4]])
    OpenGL.GL.glMaterialf(
        OpenGL.GL.GL_FRONT_AND_BACK,
        OpenGL.GL.GL_SHININESS,
        int(material[3] * 128))


def draw_box(p0, s, color, material, width):
    OpenGL.GL.glLineWidth(width)
    set_solid_material(color, material)
    OpenGL.GL.glPushMatrix()
    OpenGL.GL.glTranslatef(p0[0], p0[1], p0[2])
    OpenGL.GL.glBegin(OpenGL.GL.GL_LINE_LOOP)
    OpenGL.GL.glVertex3f(0.0, 0.0, 0.0)
    OpenGL.GL.glVertex3f(s[0], 0.0, 0.0)
    OpenGL.GL.glVertex3f(s[0], s[1], 0.0)
    OpenGL.GL.glVertex3f(0, s[1], 0.0)
    OpenGL.GL.glEnd()
    OpenGL.GL.glBegin(OpenGL.GL.GL_LINE_LOOP)
    OpenGL.GL.glVertex3f(0.0, 0.0, s[2])
    OpenGL.GL.glVertex3f(s[0], 0.0, s[2])
    OpenGL.GL.glVertex3f(s[0], s[1], s[2])
    OpenGL.GL.glVertex3f(0, s[1], s[2])
    OpenGL.GL.glEnd()
    OpenGL.GL.glBegin(OpenGL.GL.GL_LINES)
    OpenGL.GL.glVertex3f(0.0, 0.0, 0.0)
    OpenGL.GL.glVertex3f(0.0, 0.0, s[2])
    OpenGL.GL.glVertex3f(s[0], 0.0, 0.0)
    OpenGL.GL.glVertex3f(s[0], 0.0, s[2])
    OpenGL.GL.glVertex3f(s[0], s[1], 0.0)
    OpenGL.GL.glVertex3f(s[0], s[1], s[2])
    OpenGL.GL.glVertex3f(0.0, s[1], 0.0)
    OpenGL.GL.glVertex3f(0.0, s[1], s[2])
    OpenGL.GL.glEnd()

    OpenGL.GL.glPopMatrix()


def draw_plane(corners, color, material):

    set_solid_material(color, material)

    OpenGL.GL.glBegin(OpenGL.GL.GL_QUADS)
    for c in corners:
        OpenGL.GL.glVertex3f(c[0], c[1], c[2])
    OpenGL.GL.glEnd()


def draw_cylinder(posA, posB, radius, color, material, quality,
                  draw_caps=False):
    set_solid_material(color, material)
    OpenGL.GL.glPushMatrix()
    quadric = OpenGL.GLU.gluNewQuadric()

    d = posB - posA

    length = np.linalg.norm(d)
    OpenGL.GL.glTranslatef(posA[0], posA[1], posA[2])

    ax, rx, ry = rotation_helper(d)
    OpenGL.GL.glRotatef(ax, rx, ry, 0.0)
    OpenGL.GLU.gluCylinder(quadric, radius, radius, length, quality, quality)

    if draw_caps:
        OpenGL.GLU.gluDisk(quadric, 0, radius, quality, quality)
        OpenGL.GL.glTranslatef(0, 0, length)
        OpenGL.GLU.gluDisk(quadric, 0, radius, quality, quality)

    OpenGL.GLU.gluDeleteQuadric(quadric)
    OpenGL.GL.glPopMatrix()


def rotation_helper(d):
    # the rotation axis is the cross product between z and d
    vz = np.cross([0.0, 0.0, 1.0], d)
    # get the angle using a dot product
    norm = np.linalg.norm(d)
    angle = np.nan
    if norm != 0.:
        angle = 180.0 / np.pi * math.acos(d[2] / norm)

    return angle, vz[0], vz[1]


def get_extra_clip_plane():

    # ON SOME HARDWARE (e.g. MAC) only 6 CLIP PLANES ARE ALLOWED,
    # SO CAPPING OF BOX BOUNDARIES AND ADDITIONAL SHAPE CLIP PLANES
    # ARE NOT POSSIBLE. THIS WILL CAUSE THE SHAPES THAT NEED ADDITIONAL
    # CLIP PLANES TO NOT BE CLIPPED ON ONE FACE OF THE BOX

    if sys.platform == "darwin":
        return OpenGL.GL.GL_CLIP_PLANE0
    else:
        return OpenGL.GL.GL_CLIP_PLANE0 + 6


def draw_arrow(pos, d, radius, color, material, quality):
    pos2 = np.array(pos) + np.array(d)

    draw_cylinder(pos, pos2, radius, color, material, quality)

    ax, rx, ry = rotation_helper(d)
    if math.isnan(ax):
        return

    OpenGL.GL.glPushMatrix()
    OpenGL.GL.glTranslatef(pos2[0], pos2[1], pos2[2])
    OpenGL.GL.glRotatef(ax, rx, ry, 0.0)
    OpenGL.GLUT.glutSolidCone(radius * 3, radius * 3, quality, quality)
    OpenGL.GL.glPopMatrix()


# MOUSE EVENT MANAGER
class MouseFireEvent:
    """Event type of mouse button used for mouse callbacks.

    """

    ButtonPressed = 0
    FreeMotion = 1
    ButtonMotion = 2
    ButtonReleased = 3
    DoubleClick = 4


class MouseButtonEvent:
    """Mouse event used for mouse callbacks. Stores button and callback.

    """

    def __init__(self, button, fireEvent, callback, positional=False):
        self.button = button
        self.fireEvent = fireEvent
        self.callback = callback
        self.positional = positional


class MouseManager:
    """Handles mouse callbacks.

    """

    def __init__(self):
        self.mousePos = np.array([0, 0])
        self.mousePosOld = np.array([0, 0])
        self.mouseEventsPressed = []
        self.mouseEventsFreeMotion = []
        self.mouseEventsButtonMotion = []
        self.mouseEventsReleased = []
        self.mouseEventsDoubleClick = []
        self.mouseState = {OpenGL.GLUT.GLUT_LEFT_BUTTON: OpenGL.GLUT.GLUT_UP,
                           OpenGL.GLUT.GLUT_MIDDLE_BUTTON: OpenGL.GLUT.GLUT_UP,
                           OpenGL.GLUT.GLUT_RIGHT_BUTTON: OpenGL.GLUT.GLUT_UP,
                           '3': OpenGL.GLUT.GLUT_UP, '4': OpenGL.GLUT.GLUT_UP}
        self.pressedTime = {OpenGL.GLUT.GLUT_LEFT_BUTTON: 0,
                            OpenGL.GLUT.GLUT_MIDDLE_BUTTON: 0,
                            OpenGL.GLUT.GLUT_RIGHT_BUTTON: 0,
                            3: 0, 4: 0}
        self.pressedTimeOld = {OpenGL.GLUT.GLUT_LEFT_BUTTON: 0,
                               OpenGL.GLUT.GLUT_MIDDLE_BUTTON: 0,
                               OpenGL.GLUT.GLUT_RIGHT_BUTTON: 0, 3: 0, 4: 0}

    def register_button(self, mouseEvent):
        """Register mouse input callbacks.

        """
        if mouseEvent.fireEvent == MouseFireEvent.ButtonPressed:
            self.mouseEventsPressed.append(mouseEvent)
        elif mouseEvent.fireEvent == MouseFireEvent.ButtonReleased:
            self.mouseEventsReleased.append(mouseEvent)
        elif mouseEvent.fireEvent == MouseFireEvent.FreeMotion:
            self.mouseEventsFreeMotion.append(mouseEvent)
        elif mouseEvent.fireEvent == MouseFireEvent.ButtonMotion:
            self.mouseEventsButtonMotion.append(mouseEvent)
        elif mouseEvent.fireEvent == MouseFireEvent.DoubleClick:
            self.mouseEventsDoubleClick.append(mouseEvent)

    def mouse_click(self, button, state, x, y):
        self.mousePosOld = self.mousePos
        self.mousePos = np.array([x, y])

        self.mouseState[button] = state

        for me in self.mouseEventsPressed:
            if me.button == button and state == OpenGL.GLUT.GLUT_DOWN:
                if me.positional:
                    me.callback(self.mousePos, self.mousePosOld)
                else:
                    me.callback()
        for me in self.mouseEventsReleased:
            if me.button == button and state == OpenGL.GLUT.GLUT_UP:
                if me.positional:
                    me.callback(self.mousePos, self.mousePosOld)
                else:
                    me.callback()

        if state == OpenGL.GLUT.GLUT_DOWN:
            self.pressedTimeOld[button] = self.pressedTime[button]
            self.pressedTime[button] = time.time()

        for me in self.mouseEventsDoubleClick:
            if me.button == button and state == OpenGL.GLUT.GLUT_DOWN and self.pressedTime[
                    button] - self.pressedTimeOld[button] < 0.25:
                if me.positional:
                    me.callback(self.mousePos, self.mousePosOld)
                else:
                    me.callback()

    def mouse_move(self, x, y):
        self.mousePosOld = self.mousePos
        self.mousePos = np.array([x, y])

        for me in self.mouseEventsFreeMotion:
            me.callback(self.mousePos, self.mousePosOld, self.mouseState)


# KEYBOARD EVENT MANAGER

class KeyboardFireEvent:
    """Event type of button used for keyboard callbacks.

    """

    Pressed = 0
    Hold = 1
    Released = 2


class KeyboardButtonEvent:
    """Keyboard event used for keyboard callbacks. Stores button, event type and callback.

    """

    def __init__(self, button, fireEvent, callback, internal=False):
        self.button = button
        self.fireEvent = fireEvent
        self.callback = callback
        self.internal = internal


class KeyboardManager:
    """Handles keyboard callbacks.

    """

    def __init__(self):
        self.pressedKeys = set([])
        self.keyStateOld = {}
        self.keyState = {}
        self.buttonEventsPressed = []
        self.buttonEventsHold = []
        self.buttonEventsReleased = []
        self.userCallbackStack = []

    def register_button(self, buttonEvent):
        """Register keyboard input callbacks.

        """
        if buttonEvent.fireEvent == KeyboardFireEvent.Pressed:
            self.buttonEventsPressed.append(buttonEvent)
        elif buttonEvent.fireEvent == KeyboardFireEvent.Hold:
            self.buttonEventsHold.append(buttonEvent)
        elif buttonEvent.fireEvent == KeyboardFireEvent.Released:
            self.buttonEventsReleased.append(buttonEvent)

    def callback_on_button(self, be, b):
        if be.button == b:
            if be.internal:
                be.callback()
            else:
                self.userCallbackStack.append(be.callback)

    def handle_input(self):
        removeKeys = set([])
        for b in self.pressedKeys:
            if self.keyStateOld[b] == 0 and self.keyState[b] == 1:
                for be in self.buttonEventsPressed:
                    self.callback_on_button(be, b)
                for be in self.buttonEventsHold:
                    self.callback_on_button(be, b)

            elif self.keyStateOld[b] == 1 and self.keyState[b] == 1:
                for be in self.buttonEventsHold:
                    self.callback_on_button(be, b)

            elif self.keyStateOld[b] == 1 and self.keyState[b] == 0:
                for be in self.buttonEventsReleased:
                    self.callback_on_button(be, b)
                removeKeys.add(b)

            self.keyStateOld[b] = self.keyState[b]

        self.pressedKeys = self.pressedKeys.difference(removeKeys)

    def keyboard_up(self, button):
        self.keyState[button] = 0  # Key up

    def keyboard_down(self, button):
        self.pressedKeys.add(button)
        self.keyState[button] = 1  # Key down
        if button not in self.keyStateOld.keys():
            self.keyStateOld[button] = 0


# CAMERA

class Camera:

    def __init__(self, cam_pos=np.array([0, 0, 1]), cam_target=np.array([0, 0, 0]),
                 cam_right=np.array([1.0, 0.0, 0.0]), move_speed=0.5,
                 global_rot_speed=3.0, center=np.array([0, 0, 0])):
        self.cam_pos = cam_pos
        self.move_speed = move_speed
        self.global_rot_speed = global_rot_speed
        self.center = center

        self.modelview = np.identity(4, np.float32)

        t = cam_pos - cam_target
        r = np.linalg.norm(t)

        self.state_target = -t / r
        self.state_right = cam_right / np.linalg.norm(cam_right)
        self.state_up = np.cross(self.state_right, self.state_target)
        self.state_pos = np.array([0, 0, r])

        self.update_modelview()

    def move_forward(self):
        self.state_pos[2] += self.move_speed
        self.update_modelview()

    def move_backward(self):
        self.state_pos[2] -= self.move_speed
        self.update_modelview()

    def move_up(self):
        self.state_pos[1] += self.move_speed
        self.update_modelview()

    def move_down(self):
        self.state_pos[1] -= self.move_speed
        self.update_modelview()

    def move_left(self):
        self.state_pos[0] -= self.move_speed
        self.update_modelview()

    def move_right(self):
        self.state_pos[0] += self.move_speed
        self.update_modelview()

    def rotate_system_XL(self):
        self.rotate_system_y(0.01 * self.global_rot_speed)

    def rotate_system_XR(self):
        self.rotate_system_y(-0.01 * self.global_rot_speed)

    def rotate_system_YL(self):
        self.rotate_system_z(0.01 * self.global_rot_speed)

    def rotate_system_YR(self):
        self.rotate_system_z(-0.01 * self.global_rot_speed)

    def rotate_system_ZL(self):
        self.rotate_system_x(0.01 * self.global_rot_speed)

    def rotate_system_ZR(self):
        self.rotate_system_x(-0.01 * self.global_rot_speed)

    def rotate_camera(self, mouse_pos, mouse_pos_old, mouse_button_state):
        dm = mouse_pos - mouse_pos_old

        if mouse_button_state[OpenGL.GLUT.GLUT_LEFT_BUTTON] == OpenGL.GLUT.GLUT_DOWN:
            if dm[0] != 0:
                self.rotate_system_y(-0.001 * dm[0] * self.global_rot_speed)
            if dm[1] != 0:
                self.rotate_system_x(-0.001 * dm[1] * self.global_rot_speed)
        elif mouse_button_state[OpenGL.GLUT.GLUT_RIGHT_BUTTON] == OpenGL.GLUT.GLUT_DOWN:
            self.state_pos[0] -= 0.05 * dm[0] * self.move_speed
            self.state_pos[1] += 0.05 * dm[1] * self.move_speed
            self.update_modelview()
        elif mouse_button_state[OpenGL.GLUT.GLUT_MIDDLE_BUTTON] == OpenGL.GLUT.GLUT_DOWN:
            self.state_pos[2] += 0.05 * dm[1] * self.move_speed
            self.rotate_system_z(dm[0] * 0.001 * self.global_rot_speed)

    def get_camera_rotation_matrix(self, target_vec, up_vec):
        normalized_target_vec = target_vec / np.linalg.norm(target_vec)
        normalized_up_vec = up_vec / np.linalg.norm(up_vec)
        perpendicular_normal = np.cross(normalized_up_vec, target_vec)

        rotation_matrix = np.identity(4, np.float32)
        rotation_matrix[:3, :3] = np.array(
            [perpendicular_normal,
             np.cross(normalized_target_vec, perpendicular_normal),
             normalized_target_vec]).T

        return rotation_matrix

    def rotate_vector(self, vector, phi, axis):
        """
        Rotate vector around (unit vector) axis by angle phi.
        Uses Rodrigues' rotation formula.

        """
        rotated = vector * np.cos(phi) + np.cross(axis, vector) * np.sin(phi) \
            + axis * np.dot(axis, vector) * (1 - np.cos(phi))
        return rotated

    def rotate_system_x(self, angle):
        self.state_target = self.rotate_vector(self.state_target,
                                               angle, self.state_right)
        self.state_up = np.cross(self.state_right, self.state_target)
        self.update_modelview()

    def rotate_system_y(self, angle):
        self.state_target = self.rotate_vector(self.state_target,
                                               angle, self.state_up)
        self.state_right = np.cross(self.state_target, self.state_up)
        self.update_modelview()

    def rotate_system_z(self, angle):
        self.state_right = self.rotate_vector(self.state_right,
                                              angle, self.state_target)
        self.state_up = self.rotate_vector(self.state_up,
                                           angle, self.state_target)
        self.update_modelview()

    def update_modelview(self):

        self.state_up /= np.linalg.norm(self.state_up)
        self.state_right /= np.linalg.norm(self.state_right)
        self.state_target /= np.linalg.norm(self.state_target)

        # Center Box
        trans = np.identity(4, np.float32)
        trans[3, :3] -= self.center

        # Camera rotation
        rotate_cam = self.get_camera_rotation_matrix(
            -self.state_target, self.state_up)

        # System translation
        trans_cam = np.identity(4, np.float32)
        trans_cam[3, :3] -= self.state_pos

        self.modelview = trans.dot(rotate_cam.dot(trans_cam))

        c_xyz = -1 * np.mat(self.modelview[:3, :3]) * \
            np.mat(self.modelview[3, :3]).T
        self.cam_pos = np.array([c_xyz[0, 0], c_xyz[1, 0], c_xyz[2, 0]])
