#-*- coding:utf-8 -*-

#  Pybik -- A 3 dimensional magic cube game.
#  Copyright © 2009-2013  B. Clausius <barcc@gmx.de>
#
#  This program 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.
#
#  This program 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/>.

# Ported from GNUbik
# Original filename: glarea.c
# Original copyright and license: 2003, 2004  John Darrington,  GPL3+


import os
from collections import namedtuple
import math
from time import sleep
from contextlib import contextmanager

# pylint: disable=W0614,W0401
from .debug import *

from PyQt4.QtOpenGL import *
from PyQt4.QtCore import *
from PyQt4.QtCore import pyqtSignal as Signal
from PyQt4.QtGui import *
# pylint: enable=W0614,W0401

# no need for OpenGL module for just two constants
GL_RGBA, GL_SRGB_ALPHA, GL_TEXTURE_2D = 6408, 35906, 3553
if DEBUG:
    try:
        from OpenGL import GL
    except Exception:
        pass
    else:
        assert GL.GL_RGBA == GL_RGBA
        assert GL.GL_SRGB_ALPHA == GL_SRGB_ALPHA
        assert GL.GL_TEXTURE_2D == GL_TEXTURE_2D

from . import config
from .textures import textures
from .settings import settings
from . import model


class Design:
    def __init__(self):
        self.bevel_color = (15, 15, 15)
        self.label_colors = [(0, 0, 0) for i in range(6)]
        self.imagemodes = [0] * 6
        self.texnames = [-1] * 6
        
        
class ShaderInterface:
    shader_prefix = b'''
#version 120
#line 0
'''
    
    def init_shaders(self, shadername):
        if shadername is None:
            self.load_shader()
        else:
            self.set_shader(shadername)
        if DEBUG_DRAW:
            debug('Creating "hud" shaders:')
            vertex_source = self._read_shader('hud.vert')
            fragment_source = self._read_shader('pick.frag')
            self.glarea.gl_create_hud_program(vertex_source, fragment_source)
        debug('Creating "pick" shaders:')
        vertex_source = self._read_shader('pick.vert')
        fragment_source = self._read_shader('pick.frag')
        self.glarea.gl_create_pick_program(vertex_source, fragment_source)
        
    @staticmethod
    def _read_shader(filename):
        filename = os.path.join(config.appdata_dir, 'shaders', filename)
        with open(filename, 'rb') as sfile:
            source = sfile.read()
        return ShaderInterface.shader_prefix + source
        
    def set_shader(self, shadername):
        self.shadername = shadername
        debug('Creating "%s" shaders:' % shadername)
        vertex_source = self._read_shader(shadername + '.vert')
        fragment_source = self._read_shader(shadername + '.frag')
        self.glarea.gl_create_render_program(vertex_source, fragment_source)
        
    def load_shader(self):
        shadername = 'lighting' if settings.draw.lighting else 'simple'
        self.set_shader(shadername)
        
        
class CubeAreaBase (QGLWidget, ShaderInterface):
    animation_ending = Signal(bool)
    request_rotation = Signal((int, int, bool))
    request_swap_blocks = Signal((int, int, int, bool))
    request_rotate_block = Signal((int, bool))
    drop_color = Signal((int, str, str))
    drop_file = Signal((int, str, str))
    debug_text = Signal(str)
    
    def __init__(self, opts):
        glformat = QGLFormat()
        if settings.draw.samples > 0:
            glformat.setSampleBuffers(True)
            glformat.setSamples(2**settings.draw.samples)
        QGLWidget.__init__(self, glformat)
        if QGLFormat.openGLVersionFlags() & QGLFormat.OpenGL_Version_2_1:
            self.texture_format = GL_SRGB_ALPHA
        else:
            #FIXME: With OpenGL version < 2.1 images are not gamma correct
            self.texture_format = GL_RGBA
        
        self.shadername = opts.shader
        self.design = Design()
        self.model = model.empty_model
        
        self.last_mouse_x = -1
        self.last_mouse_y = -1
        self.button_down_background = False
        self.mouse_xy = -1, -1
        # Structure to hold copy of the last selection taken or None
        self.pickdata = None
        self.editing_model = False
        settings.keystore.changed.connect(self.on_settings_changed, Qt.QueuedConnection)
        
        if DEBUG_FPS:
            self.monotonic_time = QElapsedTimer()
            self.monotonic_time.start()
            self.render_count = 0
            self.fps = 0.
            
        self.speed = settings.draw.speed
        
        # Initialize the render engine
        self.gldraw, self.glarea = self.import_gl_engine()
        self.glarea.init_module()
        self.gldraw.init_module()
        self.rotation_x, self.rotation_y = self.glarea.set_rotation_xy(*settings.draw.default_rotation)
        self.glarea.set_antialiasing(self.format().sampleBuffers())
        self.init_design()
        
        self.setAcceptDrops(True)
        self.setFocusPolicy(Qt.StrongFocus)
        self.setMinimumSize(200, 200)
        
        self.cursors = None
        self.load_cursors()
        self.update_selection_pending = False
        self.timer_update_selection = QTimer(self)
        self.timer_update_selection.setSingleShot(True)
        self.timer_update_selection.timeout.connect(self.on_idle_update_selection)
        self.set_cursor()
        self.setMouseTracking(True)
        
    def import_gl_engine(self):
        if not DEBUG_PUREPYTHON:
            from . import _gldraw as gldraw
            from . import _glarea as glarea
        else:
            try:
                import OpenGL.GL
            except ImportError as e:
                print('The pure Python mode needs PyOpenGL (for Python 3):', e)
                raise SystemExit(1)
            from . import gldraw
            from . import glarea
        return gldraw, glarea
        
    def start(self):
        pass
        
    def stop(self):
        self.glarea.gl_exit()
        self.delete_textures()
        # this line prevents a segmentation fault with PySide if game->quit selected
        self.setMouseTracking(False)
        settings.keystore.changed.disconnect(self.on_settings_changed)
        self.timer_update_selection.timeout.disconnect(self.on_idle_update_selection)
        
    def load_cursors(self):
        cursors = []
        # Load 3 cursors from file (n - ne)
        for i, (x, y) in enumerate([(8, 0), (15, 0), (15, 0)]):
            filename = os.path.join(config.UI_DIR, 'mouse_{}.png'.format(i))
            image = QImage(filename)
            cursors.append((image, x, y))
        # 1 cursor (nnw)
        image, x, y = cursors[1]
        cursors.insert(0, (image.mirrored(True, False), 15-x, y))
        # 12 cursors (ene - nw)
        transform = QTransform()
        transform.rotate(90)
        for i in range(4, 16):
            image, x, y = cursors[-4]
            cursors.append((image.transformed(transform), 15-y, x))
        cursors.append(cursors[0])
        self.cursors = [QCursor(QPixmap.fromImage(image), x, y) for image, x, y in cursors[1:]]
        # cursor for center faces
        filename = os.path.join(config.UI_DIR, 'mouse_ccw.png')
        cursor = QCursor(QPixmap(filename), 7, 7)
        self.cursors.append(cursor)
        # background cursor
        cursor = QCursor()
        cursor.setShape(Qt.CrossCursor)
        self.cursors.append(cursor)
        
    def init_design(self):
        rgba = QColor()
        for i in range(6):
            rgba.setNamedColor(settings.theme.face[i].color)
            self.design.label_colors[i] = rgba.red(), rgba.green(), rgba.blue()
            self.design.imagemodes[i] = settings.theme.face[i].mode
        self.set_background_color(settings.theme.bgcolor)
        
    stock_texnames = [0]
    def set_face_texture(self, faceidx, imagefile):
        if imagefile.startswith('/'):
            pixbuf = textures.create_pixbuf_from_file(imagefile)
        else:
            pixbuf = textures.get_stock_pixbuf(imagefile)
        texname = self.bindTexture(pixbuf, GL_TEXTURE_2D, self.texture_format,
                                   QGLContext.LinearFilteringBindOption | QGLContext.MipmapBindOption)
        if not imagefile.startswith('/') and texname not in self.stock_texnames:
            self.stock_texnames.append(texname)
        if self.design.texnames[faceidx] not in self.stock_texnames:
            self.deleteTexture(self.design.texnames[faceidx])
        self.design.texnames[faceidx] = texname
        
    def delete_textures(self):
        for texname in self.design.texnames:
            if texname not in self.stock_texnames:
                self.deleteTexture(texname)
        del self.design.texnames[:]
        for texname in self.stock_texnames:
            if texname:
                self.deleteTexture(texname)
        self.stock_texnames[:] = [0]
        
    def set_model(self, model_, blocks=None):
        self.model = model_
        glmodeldata = self.get_glmodeldata(settings.draw.selection)
        with self.lock_glcontext():
            self.glarea.set_frustrum(self.model.bounding_sphere_radius, settings.draw.zoom)
            self.gldraw.gl_set_data(*glmodeldata)
            if blocks is not None:
                self.gldraw.set_transformations(blocks)
        self.update_selection()
        self.update()
        
    def get_glmodeldata(self, selection_mode):
        (   glvertices, glnormals, glcolors, gltexpos,
            labelinfos,
            idx_vbevel, cnts_block,
            idx_pick, cnt_pick,
            idx_debug,
        ) = self.model.gl_vertex_data(selection_mode, self.design)
        ptrnormals = len(glvertices)
        ptrcolors = ptrnormals + len(glnormals)
        ptrtexpos = ptrcolors + len(glcolors)
        return (len(self.model.blocks),
                glvertices + glnormals + glcolors + gltexpos,
                (ptrnormals, ptrcolors, ptrtexpos),
                (labelinfos, idx_vbevel, cnts_block, idx_pick, cnt_pick, idx_debug),
                self.design.texnames)
                
    def set_glmodel(self, selection_mode):
        glmodeldata = self.get_glmodeldata(selection_mode)
        with self.lock_glcontext():
            self.gldraw.gl_set_data(*glmodeldata)
        
    def set_transformations(self, cubestate):
        with self.lock():
            self.gldraw.set_transformations(cubestate.blocks)
        
    def do_initializeGL(self):
        if DEBUG_MSG:
            glcontext = self.context()
            glformat = glcontext.format()
            glrformat = glcontext.requestedFormat()
            def printglattr(name, *attrs):
                print('  {}: '.format(name), end='')
                def get_value(glformat, attr):
                    if isinstance(attr, str):
                        return getattr(glformat, attr)()
                    else:
                        return attr(glformat)
                values = [get_value(glformat, a) for a in attrs]
                rvalues = [get_value(glrformat, a) for a in attrs]
                if values == rvalues:
                    print(*values)
                else:
                    print(*values, end=' (')
                    print(*rvalues, end=')\n')
            print('OpenGL format:')
            printglattr('accum', 'accum', 'accumBufferSize')
            printglattr('alpha', 'alpha', 'alphaBufferSize')
            printglattr('rgb', 'redBufferSize', 'greenBufferSize', 'blueBufferSize')
            printglattr('depth', 'depth', 'depthBufferSize')
            printglattr('directRendering', 'directRendering')
            printglattr('doubleBuffer', 'doubleBuffer')
            printglattr('hasOverlay', 'hasOverlay')
            printglattr('version', lambda glformat: '{}.{} 0x{:x}'.format(
                                                            glformat.majorVersion(),
                                                            glformat.minorVersion(),
                                                            int(glformat.openGLVersionFlags())))
            printglattr('plane', 'plane')
            printglattr('profile', 'profile')
            printglattr('rgba', 'rgba')
            printglattr('samples', 'sampleBuffers', 'samples')
            printglattr('stencil', 'stencil', 'stencilBufferSize')
            printglattr('stereo', 'stereo')
            printglattr('swapInterval', 'swapInterval')
        self.init_shaders(self.shadername)
        for i in range(6):
            self.set_face_texture(i, settings.theme.face[i].image)
        self.glarea.gl_init()
        
    def do_paintGL(self):
        self.glarea.gl_render()
        if DEBUG:
            if DEBUG_DRAW:
                if self.pickdata and not self.pickdata.center:
                    self.glarea.gl_render_select_debug()
            if DEBUG_FPS:
                self.render_count += 1
                if self.monotonic_time.elapsed() > 1000:
                    elapsed = self.monotonic_time.restart()
                    self.fps = 1000. / elapsed * self.render_count
                    self.render_count = 0
                    self.debug_text.emit("FPS %.1f" % self.fps)
        
    def do_resizeGL(self, width, height):
        self.glarea.gl_resize(width, height)
        
    MODIFIER_MASK = int(Qt.ShiftModifier | Qt.ControlModifier)
    def keyPressEvent(self, event):
        modifiers = int(event.modifiers()) & self.MODIFIER_MASK
        if modifiers:
            return QGLWidget.keyPressEvent(self, event)
        elif event.key() == Qt.Key_Right:
            self.rotation_x += 2
        elif event.key() == Qt.Key_Left:
            self.rotation_x -= 2
        elif event.key() == Qt.Key_Up:
            self.rotation_y -= 2
        elif event.key() == Qt.Key_Down:
            self.rotation_y += 2
        else:
            return QGLWidget.keyPressEvent(self, event)
            
        self.rotation_x, self.rotation_y = self.glarea.set_rotation_xy(self.rotation_x, self.rotation_y)
        self.update()
        self.update_selection()
        
    PickData = namedtuple('PickData', 'maxis mslice mdir block symbol face center angle')
    def pick_polygons(self, x, y):
        '''Identify the block at screen co-ordinates x,y.'''
        
        height = self.height()
        with self.lock_glcontext():
            index = self.glarea.gl_pick_polygons(x, height-y)
        if not index:
            self.pickdata = None
            return
        maxis, mslice, mdir, face, center, block, symbol, face_center, edge_center = self.model.pick_data[index]
        
        if center:
            angle = None
        else:
            angle = self.glarea.get_cursor_angle(face_center, edge_center)
        self.pickdata = self.PickData(maxis, mslice, mdir, block, symbol, face, center, angle)
        if DEBUG_DRAW:
            text = ("block {}, face {} ({}), center {}\n"
                    "axis {}, slice {}, dir {}\n"
                    "angle {}\n"
                    "mouse {}, {}").format(
                    block.index, symbol, face, center,
                    maxis, mslice, mdir,
                    angle, *self.mouse_xy)
            self.debug_text.emit(text)
        
    def update_selection(self):
        '''This func determines which block the mouse is pointing at'''
        if self.animation_active:
            if self.pickdata is not None:
                self.pickdata = None
                self.set_cursor()
            return
        if self.update_selection_pending:
            return
        self.update_selection_pending = True
        self.timer_update_selection.start()
        
    def on_idle_update_selection(self):
        if self.animation_active:
            if self.pickdata is not None:
                self.pickdata = None
                self.set_cursor()
        else:
            self.pick_polygons(*self.mouse_xy)
            self.set_cursor()
            if DEBUG_DRAW:
                self.update()
        self.update_selection_pending = False
        
    def mouseMoveEvent(self, event):
        self.mouse_xy = event.x(), event.y()
        
        if not self.button_down_background:
            self.update_selection()
            return
            
        # perform rotation
        offset_x = event.x() - self.last_mouse_x
        offset_y = event.y() - self.last_mouse_y
        self.rotation_x, self.rotation_y = self.glarea.set_rotation_xy(
                                                        self.rotation_x + offset_x,
                                                        self.rotation_y + offset_y)
        self.rotation_x -= offset_x
        self.rotation_y -= offset_y
        self.update()
        
    def mousePressEvent(self, event):
        if self.pickdata is not None:
            if self.animation_active:
                return
            # make a move
            if self.editing_model:
                if event.modifiers() & Qt.ControlModifier:
                    if event.button() == Qt.LeftButton:
                        self.request_rotate_block.emit(self.pickdata.block.index, False)
                    elif event.button() == Qt.RightButton:
                        self.request_rotate_block.emit(self.pickdata.block.index, True)
                else:
                    if event.button() == Qt.LeftButton:
                        self.request_swap_blocks.emit(self.pickdata.block.index,
                                    self.pickdata.maxis, self.pickdata.mslice, self.pickdata.mdir)
            else:
                mslice = -1 if event.modifiers() & Qt.ControlModifier else self.pickdata.mslice
                if event.button() == Qt.LeftButton:
                    self.request_rotation.emit(self.pickdata.maxis, mslice, self.pickdata.mdir)
                elif event.button() == Qt.RightButton and settings.draw.selection_nick == 'simple':
                    self.request_rotation.emit(self.pickdata.maxis, mslice, not self.pickdata.mdir)
        elif event.button() == Qt.LeftButton:
            # start rotation
            self.button_down_background = True
            self.last_mouse_x = event.x()
            self.last_mouse_y = event.y()
        self.update()
        
    def mouseReleaseEvent(self, event):
        if event.button() != Qt.LeftButton:
            return
            
        if self.button_down_background:
            # end rotation
            self.rotation_x += event.x() - self.last_mouse_x
            self.rotation_y += event.y() - self.last_mouse_y
            self.button_down_background = False
            self.update_selection()
            
    def wheelEvent(self, event):
        if event.orientation() == Qt.Vertical:
            zoom = settings.draw.zoom * math.pow(1.1, -event.delta() / 120)
            zoom_min, zoom_max = settings.draw.zoom_range
            if zoom < zoom_min:
                zoom = zoom_min
            if zoom > zoom_max:
                zoom = zoom_max
            settings.draw.zoom = zoom
            
    def dragEnterEvent(self, event):
        debug('drag enter:', event.mimeData().formats())
        if (event.mimeData().hasFormat("text/uri-list") or
                event.mimeData().hasFormat("application/x-color")):
            event.acceptProposedAction()
            
    def dropEvent(self, event):
        # when a drag is in progress the pickdata is not updated, so do it now
        self.pick_polygons(event.pos().x(), event.pos().y())
        
        mime_data = event.mimeData()
        if mime_data.hasFormat("application/x-color"):
            color = mime_data.colorData()
            if color is None:
                debug("Received invalid color data")
                return
                
            if self.pickdata is not None:
                self.drop_color.emit(self.pickdata.block.index, self.pickdata.symbol, color.name())
            else:
                self.drop_color.emit(-1, '', color.name())
        elif mime_data.hasFormat("text/uri-list"):
            if self.pickdata is None:
                debug('Background image is not supported.')
                return
            uris = mime_data.urls()
            for uri in uris:
                if not uri.isLocalFile():
                    debug('filename "%s" not found or not a local file.' % uri.toString())
                    continue
                filename = uri.toLocalFile()
                if not filename or not os.path.exists(filename):
                    debug('filename "%s" not found.' % filename)
                    continue
                self.drop_file.emit(self.pickdata.block.index, self.pickdata.symbol, filename)
                break # For now,  just use the first one.
        # Ignore all others
        
    def set_cursor(self):
        if self.pickdata is None or self.button_down_background:
            index = -1
        elif self.pickdata.angle is None:
            index = -2
        else:
            index = int((self.pickdata.angle+180) / 22.5 + 0.5) % 16
        self.setCursor(self.cursors[index])
        
    def set_std_cursor(self):
        QTimer.singleShot(0, lambda: self.unsetCursor())
        
    def set_editing_mode(self, enable):
        self.editing_model = enable
        self.set_glmodel(0 if enable else settings.draw.selection)
        self.update_selection()
        
    def set_background_color(self, color):
        rgba = QColor()
        rgba.setNamedColor(color)
        self.glarea.set_background_color(rgba.redF(), rgba.greenF(), rgba.blueF())
        
    def reset_rotation(self):
        '''Reset cube rotation'''
        self.rotation_x, self.rotation_y = self.glarea.set_rotation_xy(*settings.draw.default_rotation)
        self.update()
        
    def reload_shader(self):
        with self.lock_glcontext():
            self.set_shader(self.shadername)
        self.update()
        
    def on_settings_changed(self, key):
        if key == 'draw.speed':
            self.speed = settings.draw.speed
        elif key == 'draw.lighting':
            with self.lock_glcontext():
                self.load_shader()
        elif key == 'draw.samples':
            if self.format().samples():
                samples = 2**settings.draw.samples
                if samples == 1:
                    self.glarea.set_antialiasing(False)
                elif samples == self.format().samples():
                    self.glarea.set_antialiasing(True)
        elif key == 'draw.selection':
            self.set_glmodel(settings.draw.selection)
            self.update_selection()
        elif key.startswith('theme.face.'):
            i = int(key.split('.')[2])
            if key == 'theme.face.{}.color'.format(i):
                color = QColor()
                color.setNamedColor(settings.theme.face[i].color)
                self.design.label_colors[i] = (color.red(), color.green(), color.blue())
                self.set_glmodel(settings.draw.selection)
            elif key == 'theme.face.{}.image'.format(i):
                with self.lock_glcontext():
                    self.set_face_texture(i, settings.theme.face[i].image)
                self.set_glmodel(settings.draw.selection)
            elif key == 'theme.face.{}.mode'.format(i):
                self.design.imagemodes[i] = settings.theme.face[i].mode
                self.set_glmodel(settings.draw.selection)
        elif key == 'theme.bgcolor':
            self.set_background_color(settings.theme.bgcolor)
        elif key == 'draw.zoom':
            self.glarea.set_frustrum(self.model.bounding_sphere_radius, settings.draw.zoom)
        else:
            debug('Unknown settings key changed:', key)
        self.update()
        
        
class AnimationBaseInterface:
    def __init__(self):
        self.stop_requested = False
        self.animation_active = False
        
    def animate_rotation(self, move_data, blocks, stop_after):
        self.stop_requested = stop_after
        axis = (self.model.axesI if move_data.dir else self.model.axes)[move_data.axis]
        angle = self.model.symmetry[move_data.axis]
        self.gldraw.set_animation_start(blocks, angle, *axis)
        self.animation_active = True
            
    def animation_end(self):
        self.animation_active = False
        
    def _on_animate(self):
        increment = self.speed * 1e-02 * 20
        increment = min(increment, 45)
        unfinished = self.gldraw.set_animation_next(increment)
        if unfinished:
            return True
            
        # we have finished the animation sequence now
        return False
        
    def animate_abort(self, update=True):
        if update:
            self.animation_ending.emit(self.stop_requested)
            self.update()
            self.update_selection()
        else:
            self.animation_end()
        self.stop_requested = False
        
        
class AnimationInterface (AnimationBaseInterface):
    def __init__(self):
        AnimationBaseInterface.__init__(self)
        self.timer_animate = QTimer() # the animate timer
        self.timer_animate.timeout.connect(self._on_animate)
        
    def animate_rotation(self, move_data, blocks, stop_after):
        AnimationBaseInterface.animate_rotation(self, move_data, blocks, stop_after)
        self.timer_animate.start(0 if DEBUG_VFPS else 20)
        self.update_selection()
        
    def _on_animate(self):
        if AnimationBaseInterface._on_animate(self):
            self.update()
            return
            
        # we have finished the animation sequence now
        self.animation_ending.emit(self.stop_requested)
        self.update()
        self.update_selection()
        self.stop_requested = False
        
    def animation_end(self):
        AnimationBaseInterface.animation_end(self)
        self.timer_animate.stop()
        
        
class CubeArea (CubeAreaBase, AnimationInterface):
    def __init__(self, opts):
        CubeAreaBase.__init__(self, opts)
        AnimationInterface.__init__(self)
        
    def stop(self):
        CubeAreaBase.stop(self)
        self.timer_animate.timeout.disconnect(self._on_animate)
        
    initializeGL = CubeAreaBase.do_initializeGL
    resizeGL = CubeAreaBase.do_resizeGL
    paintGL = CubeAreaBase.do_paintGL
    
    @contextmanager
    def lock(self):
        yield
        
    lock_glcontext = lock
    
    
class AnimationThreadedInterface (AnimationBaseInterface):
    def animate_rotation(self, move_data, blocks, stop_after):
        with self.lock():
            AnimationBaseInterface.animate_rotation(self, move_data, blocks, stop_after)
        self.render_condition.wakeAll()
        self.update_selection()
        
    def _on_animate(self):
        if AnimationBaseInterface._on_animate(self):
            return
            
        # we have finished the animation sequence now
        self.animation_active = False
        self.animation_ending.emit(self.stop_requested)
        self.update_selection()
        self.stop_requested = False
        
        
class CubeAreaThreaded (CubeAreaBase, AnimationThreadedInterface):
    def __init__(self, opts):
        CubeAreaBase.__init__(self, opts)
        AnimationThreadedInterface.__init__(self)
        
        self.render_thread = RenderThread(self.thread_loop)
        
        self.request_render = True
        self.request_resize = False
        self.render_mutex = QMutex()
        self.render_condition = QWaitCondition()
        # Buffer swap is handled in the rendering thread
        self.setAutoBufferSwap(False)
        
    def start(self):
        self.render_thread.start()
        self.render_condition.wakeAll()
        
    def stop(self):
        '''Performs a save shutdown of the render thread'''
        self.request_render = False
        self.render_condition.wakeAll()
        self.render_thread.wait()
        CubeAreaBase.stop(self)
        
    @contextmanager
    def lock(self):
        self.render_mutex.lock()
        yield
        self.render_mutex.unlock()
        
    @contextmanager
    def lock_glcontext(self):
        self.render_mutex.lock()
        self.makeCurrent()
        yield
        self.doneCurrent()
        self.render_mutex.unlock()
        
    @contextmanager
    def release_glcontext(self):
        self.doneCurrent()
        self.render_mutex.unlock()
        yield
        self.render_mutex.lock()
        self.makeCurrent()
        
    def paintEvent(self, event):
        self.render_condition.wakeAll()
        
    def resizeEvent(self, event):
        self.requested_size = event.size()
        self.request_resize = True
        self.render_condition.wakeAll()
        
    def thread_loop(self):
        with self.lock_glcontext():
            self._thread_loop()
            
    def _thread_loop(self):
        self.do_initializeGL()
        while self.request_render:
            if self.request_resize:
                self.do_resizeGL(self.requested_size.width(), self.requested_size.height())
                self.request_resize = False
            self.do_paintGL()
            self.swapBuffers()
            if self.animation_active:
                with self.release_glcontext():
                    if not DEBUG_VFPS:
                        sleep(.02)
                    self._on_animate()
            else:
                self.doneCurrent()
                self.render_condition.wait(self.render_mutex)
                self.makeCurrent()
        
        
class RenderThread (QThread):
    def __init__(self, func):
        QThread.__init__(self)
        self.func = func
        
    def run(self):
        self.func()
        
        

