"""
windowed.py
---------------

Provides a pyglet- based windowed viewer to preview
Trimesh, Scene, PointCloud, and Path objects.

Works on all major platforms: Windows, Linux, and OSX.
"""

import collections

import numpy as np
import pyglet

# pyglet 2.0 is close to a re-write moving from fixed-function
# to shaders and we will likely support it by forking an entirely
# new viewer `trimesh.viewer.shaders` and then basically keeping
# `windowed` around for backwards-compatibility with no changes
if int(pyglet.version.split(".")[0]) >= 2:
    raise ImportError('`trimesh.viewer.windowed` requires `pip install "pyglet<2"`')

from .. import rendering, util
from ..transformations import translation_matrix
from ..typed import ArrayLike, Callable, Dict, Iterable, Number, Optional
from ..visual import to_rgba
from .trackball import Trackball

pyglet.options["shadow_window"] = False

import pyglet.gl as gl  # NOQA

# help message for the viewer
_HELP_MESSAGE = """
# SceneViewer Controls

| Input                      | Action                                        |
|----------------------------|-----------------------------------------------|
| `mouse click + drag`       | Rotates the view                              |
| `ctl + mouse click + drag` | Pans the view                                 |
| `mouse wheel`              | Zooms the view                                |
| `z`                        | Resets to the initial view                    |
| `w`                        | Toggles wireframe mode                        |
| `c`                        | Toggles backface culling                      |
| `g`                        | Toggles an XY grid with Z set to lowest point |
| `a`                        | Toggles an XYZ-RGB axis marker between: off,  |
|                            | at world frame, or at every frame and world,  |
|                            | and at every frame                            |
| `f`                        | Toggles between fullscreen and windowed mode  |
| `h`                        | Prints this help message                      |
| `m`                        | Maximizes the window                          |
| `q`                        | Closes the window                             |
|----------------------------|-----------------------------------------------|
"""


class SceneViewer(pyglet.window.Window):
    def __init__(
        self,
        scene,
        smooth: bool = True,
        flags: Optional[Dict] = None,
        visible: bool = True,
        resolution: Optional[ArrayLike] = None,
        fullscreen: bool = False,
        resizable: bool = True,
        start_loop: bool = True,
        callback: Optional[Callable] = None,
        callback_period: Optional[Number] = None,
        caption: Optional[str] = None,
        fixed: Optional[Iterable] = None,
        offset_lines: bool = True,
        line_settings: Optional[Dict] = None,
        background=None,
        window_conf=None,
        profile: bool = False,
        record: bool = False,
        **kwargs,
    ):
        """
        Create a window that will display a trimesh.Scene object
        in an OpenGL context via pyglet.

        Parameters
        ---------------
        scene : trimesh.scene.Scene
          Scene with geometry and transforms
        smooth
          If True try to smooth shade things
        flags
          If passed apply keys to self.view:
          ['cull', 'wireframe', etc]
        visible
          Display window or not
        resolution
          Initial resolution of window
        fullscreen
          Determines whether the window is rendered in fullscreen mode.
        resizable
          Determines whether the rendered window can be resized by the user.
        start_loop
          Call pyglet.app.run() at the end of init
        callback
          A function which can be called periodically to
          update things in the scene
        callback_period
          How often to call the callback, in seconds
        caption
          Caption for the window title
        fixed
          List of keys in scene.geometry to skip view
          transform on to keep fixed relative to camera
        offset_lines
          If True, will offset lines slightly so if drawn
          coplanar with mesh geometry they will be visible
        line_settings
          Override default line width and point size with keys
          'line_width' and 'point_size' in pixels
        background
          Color for background
        window_conf
          Passed to window init
        profile
          If set will run a `pyinstrument` profile for
          every call to `on_draw` and print the output.
        record
          If True, will save a list of `png` bytes to
          a list located in `scene.metadata['recording']`
        kwargs
          Additional arguments to pass, including
          'background' for to set background color
        """
        self.scene = self._scene = scene

        self.callback = callback
        self.callback_period = callback_period
        self.scene._redraw = self._redraw
        self.offset_lines = bool(offset_lines)
        self.background = background
        # save initial camera transform
        self._initial_camera_transform = scene.camera_transform.copy()

        # a transform to offset lines slightly to avoid Z-fighting
        self._line_offset = translation_matrix(
            [0, 0, scene.scale / 1000 if self.offset_lines else 0]
        )

        self.reset_view()
        self.batch = pyglet.graphics.Batch()
        self._smooth = smooth

        self._profile = bool(profile)
        if self._profile:
            from pyinstrument import Profiler

            self.Profiler = Profiler

        self._record = bool(record)
        if self._record:
            # will save bytes here
            self.scene.metadata["recording"] = []

        # store kwargs
        self.kwargs = kwargs

        # store a vertexlist for an axis marker
        self._axis = None
        # store a vertexlist for a grid display
        self._grid = None
        # store scene geometry as vertex lists
        self.vertex_list = {}
        # store geometry hashes
        self.vertex_list_hash = {}
        # store geometry rendering mode
        self.vertex_list_mode = {}
        # store meshes that don't rotate relative to viewer
        self.fixed = fixed
        # store a hidden (don't not display) node.
        self._nodes_hidden = set()
        # name : texture
        self.textures = {}

        # if resolution isn't defined set a default value
        if resolution is None:
            resolution = scene.camera.resolution
        else:
            scene.camera.resolution = resolution

        if caption is None:
            caption = "Trimesh SceneViewer (`h` for help)"

        # set the default line settings to a fraction
        # of our resolution so the points aren't tiny
        scale = max(resolution)
        self.line_settings = {"point_size": scale / 200, "line_width": scale / 400}
        # if we've been passed line settings override the default
        if line_settings is not None:
            self.line_settings.update(line_settings)

        # no window conf was passed so try to get the best looking one
        if window_conf is None:
            try:
                # try enabling antialiasing
                # if you have a graphics card this will probably work
                conf = gl.Config(
                    sample_buffers=1, samples=4, depth_size=24, double_buffer=True
                )
                super().__init__(
                    config=conf,
                    visible=visible,
                    fullscreen=fullscreen,
                    resizable=resizable,
                    width=resolution[0],
                    height=resolution[1],
                    caption=caption,
                )
            except pyglet.window.NoSuchConfigException:
                conf = gl.Config(double_buffer=True)
                super().__init__(
                    config=conf,
                    fullscreen=fullscreen,
                    resizable=resizable,
                    visible=visible,
                    width=resolution[0],
                    height=resolution[1],
                    caption=caption,
                )
        else:
            # window config was manually passed
            super().__init__(
                config=window_conf,
                fullscreen=fullscreen,
                resizable=resizable,
                visible=visible,
                width=resolution[0],
                height=resolution[1],
                caption=caption,
            )

        # add scene geometry to viewer geometry
        self._update_vertex_list()

        # call after geometry is added
        self.init_gl()
        self.set_size(*resolution)
        if flags is not None:
            self.reset_view(flags=flags)
        self.update_flags()

        # someone has passed a callback to be called periodically
        if self.callback is not None:
            # if no callback period is specified set it to default
            if callback_period is None:
                # 30 times per second
                callback_period = 1.0 / 30.0
            # set up a do-nothing periodic task which will
            # trigger `self.on_draw` every `callback_period`
            # seconds if someone has passed a callback
            pyglet.clock.schedule_interval(lambda x: x, callback_period)
        if start_loop:
            pyglet.app.run()

    def _redraw(self):
        self.on_draw()

    def _update_vertex_list(self):
        # update vertex_list if needed
        for name, geom in self.scene.geometry.items():
            if geom.is_empty:
                continue
            if _geometry_hash(geom) == self.vertex_list_hash.get(name):
                continue
            self.add_geometry(name=name, geometry=geom, smooth=bool(self._smooth))

    def _update_meshes(self):
        # call the callback if specified
        if self.callback is not None:
            self.callback(self.scene)
            self._update_vertex_list()
            self._update_perspective(self.width, self.height)

    def add_geometry(self, name, geometry, **kwargs):
        """
        Add a geometry to the viewer.

        Parameters
        --------------
        name : hashable
          Name that references geometry
        geometry : Trimesh, Path2D, Path3D, PointCloud
          Geometry to display in the viewer window
        kwargs **
          Passed to rendering.convert_to_vertexlist
        """
        try:
            # convert geometry to constructor args
            args = rendering.convert_to_vertexlist(geometry, **kwargs)
        except BaseException:
            util.log.warning(f"failed to add geometry `{name}`", exc_info=True)
            return

        # create the indexed vertex list
        self.vertex_list[name] = self.batch.add_indexed(*args)
        # save the hash of the geometry
        self.vertex_list_hash[name] = _geometry_hash(geometry)
        # save the rendering mode from the constructor args
        self.vertex_list_mode[name] = args[1]

        # get the visual if the element has it
        visual = getattr(geometry, "visual", None)
        if hasattr(visual, "uv") and hasattr(visual, "material"):
            try:
                tex = rendering.material_to_texture(visual.material)
                if tex is not None:
                    self.textures[name] = tex
            except BaseException:
                util.log.warning("failed to load texture", exc_info=True)

    def cleanup_geometries(self):
        """
        Remove any stored vertex lists that no longer
        exist in the scene.
        """
        # shorthand to scene graph
        graph = self.scene.graph
        # which parts of the graph still have geometry
        geom_keep = {graph[node][1] for node in graph.nodes_geometry}
        # which geometries no longer need to be kept
        geom_delete = [geom for geom in self.vertex_list if geom not in geom_keep]
        for geom in geom_delete:
            # remove stored vertex references
            self.vertex_list.pop(geom, None)
            self.vertex_list_hash.pop(geom, None)
            self.vertex_list_mode.pop(geom, None)
            self.textures.pop(geom, None)

    def unhide_geometry(self, node):
        """
        If a node is hidden remove the flag and show the
        geometry on the next draw.

        Parameters
        -------------
        node : str
          Node to display
        """
        self._nodes_hidden.discard(node)

    def hide_geometry(self, node):
        """
        Don't display the geometry contained at a node on
        the next draw.

        Parameters
        -------------
        node : str
          Node to not display
        """
        self._nodes_hidden.add(node)

    def reset_view(self, flags=None):
        """
        Set view to the default view.

        Parameters
        --------------
        flags : None or dict
          If any view key passed override the default
          e.g. {'cull': False}
        """
        self.view = {
            "cull": True,
            "axis": False,
            "grid": False,
            "fullscreen": False,
            "wireframe": False,
            "ball": Trackball(
                pose=self._initial_camera_transform,
                size=self.scene.camera.resolution,
                scale=self.scene.scale,
                target=self.scene.centroid,
            ),
        }
        try:
            # if any flags are passed override defaults
            if isinstance(flags, dict):
                for k, v in flags.items():
                    if k in self.view:
                        self.view[k] = v
                self.update_flags()
        except BaseException:
            pass

    def init_gl(self):
        """
        Perform the magic incantations to create an
        OpenGL scene using pyglet.
        """

        # if user passed a background color use it
        if self.background is None:
            # default background color is white
            background = np.ones(4)
        else:
            # convert to (4,) uint8 RGBA
            background = to_rgba(self.background)
            # convert to 0.0-1.0 float
            background = background.astype(np.float64) / 255.0

        self._gl_set_background(background)
        # use camera setting for depth
        self._gl_enable_depth(self.scene.camera)
        self._gl_enable_color_material()
        self._gl_enable_blending()
        self._gl_enable_smooth_lines(**self.line_settings)
        self._gl_enable_lighting(self.scene)

    @staticmethod
    def _gl_set_background(background):
        gl.glClearColor(*background)

    @staticmethod
    def _gl_unset_background():
        gl.glClearColor(*[0, 0, 0, 0])

    @staticmethod
    def _gl_enable_depth(camera):
        """
        Enable depth test in OpenGL using distances
        from `scene.camera`.
        """
        gl.glClearDepth(1.0)
        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glDepthFunc(gl.GL_LEQUAL)

        gl.glEnable(gl.GL_DEPTH_TEST)
        gl.glEnable(gl.GL_CULL_FACE)

    @staticmethod
    def _gl_enable_color_material():
        # do some openGL things
        gl.glColorMaterial(gl.GL_FRONT_AND_BACK, gl.GL_AMBIENT_AND_DIFFUSE)
        gl.glEnable(gl.GL_COLOR_MATERIAL)
        gl.glShadeModel(gl.GL_SMOOTH)

        gl.glMaterialfv(
            gl.GL_FRONT,
            gl.GL_AMBIENT,
            rendering.vector_to_gl(0.192250, 0.192250, 0.192250),
        )
        gl.glMaterialfv(
            gl.GL_FRONT,
            gl.GL_DIFFUSE,
            rendering.vector_to_gl(0.507540, 0.507540, 0.507540),
        )
        gl.glMaterialfv(
            gl.GL_FRONT,
            gl.GL_SPECULAR,
            rendering.vector_to_gl(0.5082730, 0.5082730, 0.5082730),
        )

        gl.glMaterialf(gl.GL_FRONT, gl.GL_SHININESS, 0.4 * 128.0)

    @staticmethod
    def _gl_enable_blending():
        # enable blending for transparency
        gl.glEnable(gl.GL_BLEND)
        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)

    @staticmethod
    def _gl_enable_smooth_lines(line_width=4, point_size=4):
        # make the lines from Path3D objects less ugly
        gl.glEnable(gl.GL_LINE_SMOOTH)
        gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
        # set the width of lines to 4 pixels
        gl.glLineWidth(line_width)
        # set PointCloud markers to 4 pixels in size
        gl.glPointSize(point_size)

    @staticmethod
    def _gl_enable_lighting(scene):
        """
        Take the lights defined in scene.lights and
        apply them as openGL lights.
        """
        gl.glEnable(gl.GL_LIGHTING)
        # opengl only supports 7 lights?
        for i, light in enumerate(scene.lights[:7]):
            # the index of which light we have
            lightN = eval(f"gl.GL_LIGHT{i}")

            # get the transform for the light by name
            matrix = scene.graph.get(light.name)[0]

            # convert light object to glLightfv calls
            multiargs = rendering.light_to_gl(
                light=light, transform=matrix, lightN=lightN
            )

            # enable the light in question
            gl.glEnable(lightN)
            # run the glLightfv calls
            for args in multiargs:
                gl.glLightfv(*args)

    def toggle_culling(self):
        """
        Toggle back face culling.

        It is on by default but if you are dealing with
        non- watertight meshes you probably want to be able
        to see the back sides.
        """
        self.view["cull"] = not self.view["cull"]
        self.update_flags()

    def toggle_wireframe(self):
        """
        Toggle wireframe mode

        Good for  looking inside meshes, off by default.
        """
        self.view["wireframe"] = not self.view["wireframe"]
        self.update_flags()

    def toggle_fullscreen(self):
        """
        Toggle between fullscreen and windowed mode.
        """
        self.view["fullscreen"] = not self.view["fullscreen"]
        self.update_flags()

    def toggle_axis(self):
        """
        Toggle a rendered XYZ/RGB axis marker:
        off, world frame, every frame
        """
        # cycle through three axis states
        states = [False, "world", "all", "without_world"]
        # the state after toggling
        index = (states.index(self.view["axis"]) + 1) % len(states)
        # update state to next index
        self.view["axis"] = states[index]
        # perform gl actions
        self.update_flags()

    def toggle_grid(self):
        """
        Toggle a rendered grid.
        """
        # update state to next index
        self.view["grid"] = not self.view["grid"]
        # perform gl actions
        self.update_flags()

    def update_flags(self):
        """
        Check the view flags, and call required GL functions.
        """
        # view mode, filled vs wirefrom
        if self.view["wireframe"]:
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
        else:
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)

        # set fullscreen or windowed
        self.set_fullscreen(fullscreen=self.view["fullscreen"])

        # backface culling on or off
        if self.view["cull"]:
            gl.glEnable(gl.GL_CULL_FACE)
        else:
            gl.glDisable(gl.GL_CULL_FACE)

        # case where we WANT an axis and NO vertexlist
        # is stored internally
        if self.view["axis"] and self._axis is None:
            from .. import creation

            # create an axis marker sized relative to the scene
            axis = creation.axis(origin_size=self.scene.scale / 100)
            # create ordered args for a vertex list
            args = rendering.mesh_to_vertexlist(axis)
            # store the axis as a reference
            self._axis = self.batch.add_indexed(*args)
        # case where we DON'T want an axis but a vertexlist
        # IS stored internally
        elif not self.view["axis"] and self._axis is not None:
            # remove the axis from the rendering batch
            self._axis.delete()
            # set the reference to None
            self._axis = None

        if self.view["grid"] and self._grid is None:
            try:
                # create a grid marker
                from ..path.creation import grid

                bounds = self.scene.bounds
                center = bounds.mean(axis=0)
                # set the grid to the lowest Z position
                # also offset by the scale to avoid interference
                center[2] = bounds[0][2] - (np.ptp(bounds[:, 2]) / 100)
                # choose the side length by maximum XY length
                side = np.ptp(bounds, axis=0)[:2].max()
                # create an axis marker sized relative to the scene
                grid_mesh = grid(side=side, count=4, transform=translation_matrix(center))
                # convert the path to vertexlist args
                args = rendering.convert_to_vertexlist(grid_mesh)
                # create ordered args for a vertex list
                self._grid = self.batch.add_indexed(*args)
            except BaseException:
                util.log.warning("failed to create grid!", exc_info=True)
        elif not self.view["grid"] and self._grid is not None:
            self._grid.delete()
            self._grid = None

    def _update_perspective(self, width, height):
        try:
            # for high DPI screens viewport size
            # will be different then the passed size
            width, height = self.get_viewport_size()
        except BaseException:
            # older versions of pyglet may not have this
            pass

        # set the new viewport size
        gl.glViewport(0, 0, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()

        # get field of view and Z range from camera
        camera = self.scene.camera

        # set perspective from camera data
        gl.gluPerspective(
            camera.fov[1], width / float(height), camera.z_near, camera.z_far
        )
        gl.glMatrixMode(gl.GL_MODELVIEW)

        return width, height

    def on_resize(self, width, height):
        """
        Handle resized windows.
        """
        width, height = self._update_perspective(width, height)
        self.scene.camera.resolution = (width, height)
        self.view["ball"].resize(self.scene.camera.resolution)
        self.scene.camera_transform = self.view["ball"].pose

    def on_mouse_press(self, x, y, buttons, modifiers):
        """
        Set the start point of the drag.
        """
        self.view["ball"].set_state(Trackball.STATE_ROTATE)
        if buttons == pyglet.window.mouse.LEFT:
            ctrl = modifiers & pyglet.window.key.MOD_CTRL
            shift = modifiers & pyglet.window.key.MOD_SHIFT
            if ctrl and shift:
                self.view["ball"].set_state(Trackball.STATE_ZOOM)
            elif shift:
                self.view["ball"].set_state(Trackball.STATE_ROLL)
            elif ctrl:
                self.view["ball"].set_state(Trackball.STATE_PAN)
        elif buttons == pyglet.window.mouse.MIDDLE:
            self.view["ball"].set_state(Trackball.STATE_PAN)
        elif buttons == pyglet.window.mouse.RIGHT:
            self.view["ball"].set_state(Trackball.STATE_ZOOM)

        self.view["ball"].down(np.array([x, y]))
        self.scene.camera_transform = self.view["ball"].pose

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        """
        Pan or rotate the view.
        """
        self.view["ball"].drag(np.array([x, y]))
        self.scene.camera_transform = self.view["ball"].pose

    def on_mouse_scroll(self, x, y, dx, dy):
        """
        Zoom the view.
        """
        self.view["ball"].scroll(dy)
        self.scene.camera_transform = self.view["ball"].pose

    def on_key_press(self, symbol, modifiers):
        """
        Call appropriate functions given key presses.
        """
        magnitude = 10
        if symbol == pyglet.window.key.W:
            self.toggle_wireframe()
        elif symbol == pyglet.window.key.Z:
            self.reset_view()
        elif symbol == pyglet.window.key.C:
            self.toggle_culling()
        elif symbol == pyglet.window.key.A:
            self.toggle_axis()
        elif symbol == pyglet.window.key.G:
            self.toggle_grid()
        elif symbol == pyglet.window.key.Q:
            self.on_close()
        elif symbol == pyglet.window.key.M:
            self.maximize()
        elif symbol == pyglet.window.key.F:
            self.toggle_fullscreen()
        elif symbol == pyglet.window.key.H:
            print(_HELP_MESSAGE)  # noqa: T201

        if symbol in [
            pyglet.window.key.LEFT,
            pyglet.window.key.RIGHT,
            pyglet.window.key.DOWN,
            pyglet.window.key.UP,
        ]:
            self.view["ball"].down([0, 0])
            if symbol == pyglet.window.key.LEFT:
                self.view["ball"].drag([-magnitude, 0])
            elif symbol == pyglet.window.key.RIGHT:
                self.view["ball"].drag([magnitude, 0])
            elif symbol == pyglet.window.key.DOWN:
                self.view["ball"].drag([0, -magnitude])
            elif symbol == pyglet.window.key.UP:
                self.view["ball"].drag([0, magnitude])
            self.scene.camera_transform = self.view["ball"].pose

    def on_draw(self):
        """
        Run the actual draw calls.
        """

        if self._profile:
            profiler = self.Profiler()
            profiler.start()

        self._update_meshes()
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        gl.glLoadIdentity()

        # pull the new camera transform from the scene
        transform_camera = np.linalg.inv(self.scene.camera_transform)

        # apply the camera transform to the matrix stack
        gl.glMultMatrixf(rendering.matrix_to_gl(transform_camera))

        # we want to render fully opaque objects first,
        # followed by objects which have transparency
        node_names = collections.deque(self.scene.graph.nodes_geometry)
        # how many nodes did we start with
        count_original = len(node_names)
        count = -1

        # if we are rendering an axis marker at the world
        if self._axis and not self.view["axis"] == "without_world":
            # we stored it as a vertex list
            self._axis.draw(mode=gl.GL_TRIANGLES)
        if self._grid:
            self._grid.draw(mode=gl.GL_LINES)

        # save a reference outside of the loop
        geometry = self.scene.geometry
        graph = self.scene.graph

        while len(node_names) > 0:
            count += 1
            current_node = node_names.popleft()

            if current_node in self._nodes_hidden:
                continue

            # get the transform from world to geometry and mesh name
            transform, geometry_name = graph.get(current_node)
            # if no geometry at this frame continue without rendering
            if geometry_name is None or geometry_name not in self.vertex_list_mode:
                continue

            # if a geometry is marked as fixed apply the inverse view transform
            if self.fixed is not None and geometry_name in self.fixed:
                # remove altered camera transform from fixed geometry
                transform_fix = np.linalg.inv(
                    np.dot(self._initial_camera_transform, transform_camera)
                )
                # apply the transform so the fixed geometry doesn't move
                transform = np.dot(transform, transform_fix)

            # get a reference to the mesh so we can check transparency
            mesh = geometry[geometry_name]
            if mesh.is_empty:
                continue
            # get the GL mode of the current geometry
            mode = self.vertex_list_mode[geometry_name]

            # if you draw a coplanar line with a triangle it will z-fight
            # the best way to do this is probably a shader but this works fine
            if mode == gl.GL_LINES:
                # apply the offset in camera space
                transform = util.multi_dot(
                    [
                        transform,
                        np.linalg.inv(transform_camera),
                        self._line_offset,
                        transform_camera,
                    ]
                )

            # add a new matrix to the model stack
            gl.glPushMatrix()
            # transform by the nodes transform
            gl.glMultMatrixf(rendering.matrix_to_gl(transform))

            # draw an axis marker for each mesh frame
            if self.view["axis"] == "all":
                self._axis.draw(mode=gl.GL_TRIANGLES)
            elif self.view["axis"] == "without_world":
                if not util.allclose(transform, np.eye(4), atol=1e-5):
                    self._axis.draw(mode=gl.GL_TRIANGLES)

            # transparent things must be drawn last
            if (
                hasattr(mesh, "visual")
                and hasattr(mesh.visual, "transparency")
                and mesh.visual.transparency
            ):
                # put the current item onto the back of the queue
                if count < count_original:
                    # add the node to be drawn last
                    node_names.append(current_node)
                    # pop the matrix stack for now
                    gl.glPopMatrix()
                    # come back to this mesh later
                    continue

            # if we have texture enable the target texture
            texture = None
            if geometry_name in self.textures:
                texture = self.textures[geometry_name]
                gl.glEnable(texture.target)
                gl.glBindTexture(texture.target, texture.id)

            # draw the mesh with its transform applied
            self.vertex_list[geometry_name].draw(mode=mode)
            # pop the matrix stack as we drew what we needed to draw
            gl.glPopMatrix()

            # disable texture after using
            if texture is not None:
                gl.glDisable(texture.target)

        if self._profile:
            profiler.stop()
            util.log.debug(profiler.output_text(unicode=True, color=True))

    def flip(self):
        super().flip()
        if self._record:
            # will save a PNG-encoded bytes
            img = self.save_image(util.BytesIO())
            # seek start of file-like object
            img.seek(0)
            # save the bytes from the file object
            self.scene.metadata["recording"].append(img.read())

    def save_image(self, file_obj):
        """
        Save the current color buffer to a file object
        in PNG format.

        Parameters
        -------------
        file_obj: file name, or file- like object
        """
        manager = pyglet.image.get_buffer_manager()
        colorbuffer = manager.get_color_buffer()
        # if passed a string save by name
        if hasattr(file_obj, "write"):
            colorbuffer.save(file=file_obj)
        else:
            colorbuffer.save(filename=file_obj)
        return file_obj


def _geometry_hash(geometry):
    """
    Get a hash for a geometry object

    Parameters
    ------------
    geometry : object

    Returns
    ------------
    hash : str
    """
    h = str(hash(geometry))
    if hasattr(geometry, "visual"):
        # if visual properties are defined
        h += str(hash(geometry.visual))

    return h


def render_scene(
    scene, resolution=None, visible=True, fullscreen=False, resizable=True, **kwargs
):
    """
    Render a preview of a scene to a PNG. Note that
    whether this works or not highly variable based on
    platform and graphics driver.

    Parameters
    ------------
    scene : trimesh.Scene
      Geometry to be rendered
    resolution : (2,) int or None
      Resolution in pixels or set from scene.camera
    visible : bool
      Show a window during rendering. Note that MANY
      platforms refuse to render with hidden windows
      and will likely return a blank image; this is a
      platform issue and cannot be fixed in Python.
    fullscreen : bool
      Determines whether the window is rendered in fullscreen mode.
      Defaults to False (windowed).
    resizable : bool
      Determines whether the rendered window can be resized by the user.
      Defaults to True (resizable).
    kwargs : **
      Passed to SceneViewer

    Returns
    ---------
    render : bytes
      Image in PNG format
    """
    window = SceneViewer(
        scene,
        start_loop=False,
        visible=visible,
        resolution=resolution,
        fullscreen=fullscreen,
        resizable=resizable,
        **kwargs,
    )

    from ..util import BytesIO

    # need to run loop twice to display anything
    for save in [False, False, True]:
        pyglet.clock.tick()
        window.switch_to()
        window.dispatch_events()
        window.dispatch_event("on_draw")
        window.flip()
        if save:
            # save the color buffer data to memory
            file_obj = BytesIO()
            window.save_image(file_obj)
            file_obj.seek(0)
            render = file_obj.read()
    window.close()

    return render
