from ctypes import *
from OpenGL.GL import *
from PIL import Image
from math import copysign
from .matrix import Matrix
from . import glfw
from . import util
import cPickle as pickle
import os
import Queue
import threading
import time

def delete_all(obj):
    '''Calls `delete()` on all members of `obj` that are recognized as
    instances of `pg` objects.'''
    types = tuple([
        Shader,
        Mesh,
        VertexBuffer,
        IndexBuffer,
        Texture,
        Program,
        Context,
    ])
    for name in dir(obj):
        child = getattr(obj, name)
        if isinstance(child, types):
            child.delete()

class Shader(object):
    def __init__(self, shader_type, shader_source):
        if os.path.exists(shader_source):
            with open(shader_source, 'r') as fp:
                shader_source = fp.read()
        self.handle = glCreateShader(shader_type)
        glShaderSource(self.handle, shader_source)
        glCompileShader(self.handle)
        log = glGetShaderInfoLog(self.handle)
        if log:
            raise Exception(log)
    def delete(self):
        if self.handle is not None:
            glDeleteShader(self.handle)
            self.handle = None

class VertexShader(Shader):
    def __init__(self, shader_source):
        super(VertexShader, self).__init__(GL_VERTEX_SHADER, shader_source)

class FragmentShader(Shader):
    def __init__(self, shader_source):
        super(FragmentShader, self).__init__(GL_FRAGMENT_SHADER, shader_source)

class Cache(object):
    def __init__(self):
        self.data = {}
    def set(self, key, value):
        if key in self.data and self.data[key] == value:
            return False
        self.data[key] = value
        return True

class Mesh(object):
    @staticmethod
    def load_pickle(path):
        with open(path, 'rb') as fp:
            positions, normals, uvs = pickle.load(fp)
            return Mesh(positions, normals, uvs)
    def __init__(self, positions=None, normals=None, uvs=None):
        self.positions = positions or []
        self.normals = normals or []
        self.uvs = uvs or []
        self.index = None
        self.vertex_buffer = None
        self.slices = None
    def delete(self):
        if self.index:
            self.index.delete()
            self.index = None
        if self.vertex_buffer:
            self.vertex_buffer.delete()
            self.vertex_buffer = None
    def __add__(self, other):
        positions = self.positions + other.positions
        normals = self.normals + other.normals
        uvs = self.uvs + other.uvs
        return Mesh(positions, normals, uvs)
    def __rmul__(self, other):
        if isinstance(other, Matrix):
            return self.multiply(other)
        return NotImplemented
    def multiply(self, matrix):
        positions = [matrix * x for x in self.positions]
        normals = list(self.normals)
        uvs = list(self.uvs)
        return Mesh(positions, normals, uvs)
    def bounding_box(self):
        return util.bounding_box(self.positions)
    def center(self):
        positions = util.recenter(self.positions)
        normals = list(self.normals)
        uvs = list(self.uvs)
        return Mesh(positions, normals, uvs)
    def smooth_normals(self):
        positions = list(self.positions)
        normals = util.smooth_normals(self.positions, self.normals)
        uvs = list(self.uvs)
        return Mesh(positions, normals, uvs)
    def reverse_winding(self):
        positions = []
        for i in xrange(0, len(self.positions), 3):
            v1, v2, v3 = self.positions[i:i+3]
            positions.extend([v3, v2, v1])
        normals = [util.neg(x) for x in self.normals]
        uvs = []
        for i in xrange(0, len(self.uvs), 3):
            v1, v2, v3 = self.uvs[i:i+3]
            uvs.extend([v3, v2, v1])
        return Mesh(positions, normals, uvs)
    def swap_axes(self, i, j, k):
        si, sj, sk = copysign(1, i), copysign(1, j), copysign(1, k)
        i, j, k = abs(i), abs(j), abs(k)
        positions = [(v[i] * si, v[j] * sj, v[k] * sk) for v in self.positions]
        normals = [(v[i] * si, v[j] * sj, v[k] * sk) for v in self.normals]
        uvs = list(self.uvs)
        return Mesh(positions, normals, uvs)
    def save_pickle(self, path):
        obj = (self.positions, self.normals, self.uvs)
        with open(path, 'wb') as fp:
            pickle.dump(obj, fp, -1)
    def draw(self, context, mode=GL_TRIANGLES):
        if not self.vertex_buffer:
            self.index, self.vertex_buffer, self.slices = index(
                self.positions, self.normals, self.uvs)
        context.position, context.normal, context.uv = self.slices
        context.draw(mode, self.index)

class VertexBuffer(object):
    def __init__(self, data=None):
        self.handle = glGenBuffers(1)
        self.components = 0
        self.vertex_count = 0
        self.vertex_capacity = 0
        self.extend(data)
    def delete(self):
        if self.handle is not None:
            glDeleteBuffers(1, self.handle)
            self.handle = None
    def extend(self, data):
        if not data:
            return
        if self.components:
            if len(data[0]) != self.components:
                raise Exception
        else:
            self.components = len(data[0])
        offset = self.vertex_count * self.components
        size = len(data) * self.components
        flat = util.flatten(data)
        if len(flat) != size:
            raise Exception
        self.vertex_count += len(data)
        if self.vertex_count > self.vertex_capacity:
            old_size = self.components * self.vertex_capacity
            self.vertex_capacity = max(
                self.vertex_count, self.vertex_capacity * 2)
            new_size = self.components * self.vertex_capacity
            if old_size:
                self.resize(old_size, new_size)
            else:
                self.allocate(new_size)
        glBindBuffer(GL_ARRAY_BUFFER, self.handle)
        glBufferSubData(
            GL_ARRAY_BUFFER,
            sizeof(c_float) * offset,
            sizeof(c_float) * size,
            util.pack_list('<f', flat))
        glBindBuffer(GL_ARRAY_BUFFER, 0)
    def allocate(self, size):
        glBindBuffer(GL_ARRAY_BUFFER, self.handle)
        glBufferData(
            GL_ARRAY_BUFFER,
            sizeof(c_float) * size,
            None,
            GL_DYNAMIC_DRAW)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
    def resize(self, old_size, new_size):
        old_size = sizeof(c_float) * old_size
        new_size = sizeof(c_float) * new_size
        temp = (ctypes.c_byte * new_size)()
        glBindBuffer(GL_ARRAY_BUFFER, self.handle)
        data = glMapBuffer(GL_ARRAY_BUFFER, GL_READ_ONLY)
        memmove(temp, data, min(old_size, new_size))
        glUnmapBuffer(GL_ARRAY_BUFFER)
        glBufferData(GL_ARRAY_BUFFER, new_size, temp, GL_DYNAMIC_DRAW)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
    def set_data(self, data):
        old_size = self.components * self.vertex_capacity
        self.components = len(data[0])
        self.vertex_count = len(data)
        self.vertex_capacity = len(data)
        flat = util.flatten(data)
        size = len(flat)
        glBindBuffer(GL_ARRAY_BUFFER, self.handle)
        if size == old_size:
            glBufferSubData(
                GL_ARRAY_BUFFER,
                0,
                sizeof(c_float) * size,
                util.pack_list('<f', flat))
        else:
            glBufferData(
                GL_ARRAY_BUFFER,
                sizeof(c_float) * size,
                util.pack_list('<f', flat),
                GL_DYNAMIC_DRAW)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
    def slice(self, components, offset):
        return VertexBufferSlice(self, components, offset)
    def slices(self, *args):
        offset = 0
        result = []
        for components in args:
            if components:
                result.append(self.slice(components, offset))
                offset += components
            else:
                result.append(None)
        return result
    def bind(self, location):
        glBindBuffer(GL_ARRAY_BUFFER, self.handle)
        glVertexAttribPointer(
            location, self.components, GL_FLOAT, GL_FALSE,
            0, c_void_p())
        glBindBuffer(GL_ARRAY_BUFFER, 0)

class VertexBufferSlice(object):
    def __init__(self, parent, components, offset):
        self.parent = parent
        self.components = components
        self.offset = offset
    @property
    def vertex_count(self):
        return self.parent.vertex_count
    def bind(self, location):
        glBindBuffer(GL_ARRAY_BUFFER, self.parent.handle)
        glVertexAttribPointer(
            location, self.components, GL_FLOAT, GL_FALSE,
            sizeof(c_float) * self.parent.components,
            c_void_p(sizeof(c_float) * self.offset))
        glBindBuffer(GL_ARRAY_BUFFER, 0)

class IndexBuffer(object):
    def __init__(self, data=None):
        self.handle = glGenBuffers(1)
        if data is not None:
            self.set_data(data)
    def delete(self):
        if self.handle is not None:
            glDeleteBuffers(1, self.handle)
            self.handle = None
    def set_data(self, data):
        self.size = len(data)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.handle)
        glBufferData(
            GL_ELEMENT_ARRAY_BUFFER,
            sizeof(c_uint) * self.size,
            (c_uint * self.size)(*data),
            GL_STATIC_DRAW)
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)

def index(*args):
    sizes = [len(x[0]) if x else None for x in args]
    data = util.interleave(*filter(None, args))
    unique = list(util.distinct(data))
    lookup = dict((x, i) for i, x in enumerate(unique))
    indices = [lookup[x] for x in data]
    vertex_buffer = VertexBuffer(unique)
    index_buffer = IndexBuffer(indices)
    return index_buffer, vertex_buffer, vertex_buffer.slices(*sizes)

class Texture(object):
    UNITS = [
        GL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2, GL_TEXTURE3,
        GL_TEXTURE4, GL_TEXTURE5, GL_TEXTURE6, GL_TEXTURE7,
        GL_TEXTURE8, GL_TEXTURE9, GL_TEXTURE10, GL_TEXTURE11,
        GL_TEXTURE12, GL_TEXTURE13, GL_TEXTURE14, GL_TEXTURE15,
        GL_TEXTURE16, GL_TEXTURE17, GL_TEXTURE18, GL_TEXTURE19,
        GL_TEXTURE20, GL_TEXTURE21, GL_TEXTURE22, GL_TEXTURE23,
        GL_TEXTURE24, GL_TEXTURE25, GL_TEXTURE26, GL_TEXTURE27,
        GL_TEXTURE28, GL_TEXTURE29, GL_TEXTURE30, GL_TEXTURE31,
    ]
    def __init__(
        self, unit, im,
        min_filter=GL_LINEAR, mag_filter=GL_LINEAR,
        wrap_s=GL_REPEAT, wrap_t=GL_REPEAT,
        mipmap=False):
        self.unit = unit
        if isinstance(im, basestring):
            im = Image.open(im)
        im = im.convert('RGBA').transpose(Image.FLIP_TOP_BOTTOM)
        self.size = width, height = im.size
        data = im.tobytes()
        self.handle = glGenTextures(1)
        self.bind()
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_s)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t)
        glTexImage2D(
            GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
            GL_UNSIGNED_BYTE, data)
        if mipmap:
            glGenerateMipmap(GL_TEXTURE_2D)
    def delete(self):
        if self.handle is not None:
            glDeleteTextures(1, self.handle)
            self.handle = None
    def get_uniform_value(self):
        return self.unit
    def bind(self):
        glActiveTexture(Texture.UNITS[self.unit])
        glBindTexture(GL_TEXTURE_2D, self.handle)

class Attribute(object):
    def __init__(self, location, name, size, data_type):
        self.location = location
        self.name = name
        self.size = size
        self.data_type = data_type
    def bind(self, value):
        glEnableVertexAttribArray(self.location)
        cache = App.instance.current_window.cache
        if not cache.set(self.location, value):
            return
        value.bind(self.location)
    def unbind(self):
        glDisableVertexAttribArray(self.location)
    def __repr__(self):
        return 'Attribute%s' % str(
            (self.location, self.name, self.size, self.data_type))

class Uniform(object):
    FLOATS = set([GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4])
    INTS = set([GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4])
    BOOLS = set([GL_BOOL, GL_BOOL_VEC2, GL_BOOL_VEC3, GL_BOOL_VEC4])
    MATS = set([GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4])
    SAMPLERS = set([GL_SAMPLER_2D, GL_SAMPLER_CUBE])
    def __init__(self, location, name, size, data_type):
        self.location = location
        self.name = name
        self.size = size
        self.data_type = data_type
    def bind(self, value):
        if self.size > 1:
            self.bind_array(value)
            return
        if hasattr(value, 'get_uniform_value'):
            value = value.get_uniform_value()
        try:
            count = len(value)
        except Exception:
            value = [value]
            count = 1
        cache = App.instance.current_window.current_program.cache
        if not cache.set(self.location, value):
            return
        if self.data_type in Uniform.MATS:
            funcs = {
                4: glUniformMatrix2fv,
                9: glUniformMatrix3fv,
                16: glUniformMatrix4fv,
            }
            funcs[count](self.location, 1, False, (c_float * count)(*value))
        elif self.data_type in Uniform.FLOATS:
            funcs = {
                1: glUniform1f,
                2: glUniform2f,
                3: glUniform3f,
                4: glUniform4f,
            }
            funcs[count](self.location, *value)
        elif self.data_type in Uniform.INTS or self.data_type in Uniform.BOOLS:
            funcs = {
                1: glUniform1i,
                2: glUniform2i,
                3: glUniform3i,
                4: glUniform4i,
            }
            funcs[count](self.location, *value)
        elif self.data_type in Uniform.SAMPLERS:
            glUniform1i(self.location, *value)
    def bind_array(self, value):
        first = value[0]
        size = min(len(value), self.size)
        value = value[:size]
        try:
            count = len(first)
            value = util.flatten(value)
        except Exception:
            count = 1
        if len(value) != size * count:
            raise Exception
        value = (c_float * len(value))(*value)
        if self.data_type in Uniform.FLOATS:
            funcs = {
                1: glUniform1fv,
                2: glUniform2fv,
                3: glUniform3fv,
                4: glUniform4fv,
            }
            funcs[count](self.location, size, value)
        elif self.data_type in Uniform.INTS or self.data_type in Uniform.BOOLS:
            funcs = {
                1: glUniform1iv,
                2: glUniform2iv,
                3: glUniform3iv,
                4: glUniform4iv,
            }
            funcs[count](self.location, size, value)
        elif self.data_type in Uniform.SAMPLERS:
            glUniform1iv(self.location, size, value)
    def __repr__(self):
        return 'Uniform%s' % str(
            (self.location, self.name, self.size, self.data_type))

class Program(object):
    def __init__(self, vs, fs):
        if not isinstance(vs, Shader):
            vs = VertexShader(vs)
        if not isinstance(fs, Shader):
            fs = FragmentShader(fs)
        self.vs = vs
        self.fs = fs
        self.handle = glCreateProgram()
        glAttachShader(self.handle, self.vs.handle)
        glAttachShader(self.handle, self.fs.handle)
        glLinkProgram(self.handle)
        log = glGetProgramInfoLog(self.handle)
        if log:
            raise Exception(log)
        self.cache = Cache()
    def delete(self):
        if self.handle is not None:
            glDeleteProgram(self.handle)
            self.handle = None
        self.vs.delete()
        self.fs.delete()
    def use(self):
        glUseProgram(self.handle)
        App.instance.current_window.set_current_program(self)
    def set_defaults(self, context):
        pass
    def get_attributes(self):
        result = []
        count = glGetProgramiv(self.handle, GL_ACTIVE_ATTRIBUTES)
        name = create_string_buffer(256)
        size = c_int()
        data_type = c_int()
        for index in xrange(count):
            glGetActiveAttrib(
                self.handle, index, 256, None,
                byref(size), byref(data_type), name)
            location = glGetAttribLocation(self.handle, name.value)
            attribute = Attribute(
                location, name.value, size.value, data_type.value)
            result.append(attribute)
        return result
    def get_uniforms(self):
        result = []
        count = glGetProgramiv(self.handle, GL_ACTIVE_UNIFORMS)
        for index in xrange(count):
            name, size, data_type = glGetActiveUniform(self.handle, index)
            if name.endswith('[0]'):
                name = name[:-3]
            location = glGetUniformLocation(self.handle, name)
            uniform = Uniform(location, name, size, data_type)
            result.append(uniform)
        return result

class Context(object):
    def __init__(self, program):
        self._program = program
        self._attributes = dict((x.name, x) for x in program.get_attributes())
        self._uniforms = dict((x.name, x) for x in program.get_uniforms())
        self._attribute_values = {}
        self._uniform_values = {}
        self._program.set_defaults(self)
    def delete(self):
        self._program.delete()
    def __setattr__(self, name, value):
        if name.startswith('_'):
            super(Context, self).__setattr__(name, value)
        elif name in self._attributes:
            self._attribute_values[name] = value
        elif name in self._uniforms:
            self._uniform_values[name] = value
        else:
            super(Context, self).__setattr__(name, value)
    def __getattr__(self, name):
        if name.startswith('_'):
            super(Context, self).__getattr__(name)
        elif name in self._attributes:
            return self._attribute_values[name]
        elif name in self._uniforms:
            return self._uniform_values[name]
        else:
            super(Context, self).__getattr__(name)
    def draw(self, mode=GL_TRIANGLES, index_buffer=None):
        self._program.use()
        for name, value in self._uniform_values.iteritems():
            if value is not None:
                self._uniforms[name].bind(value)
        for name, value in self._attribute_values.iteritems():
            if value is not None:
                self._attributes[name].bind(value)
        if index_buffer is None:
            vertex_count = min(x.vertex_count for x in
                self._attribute_values.itervalues() if x is not None)
            glDrawArrays(mode, 0, vertex_count)
        else:
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer.handle)
            glDrawElements(mode, index_buffer.size, GL_UNSIGNED_INT, c_void_p())
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
        for name, value in self._attribute_values.iteritems():
            if value is not None:
                self._attributes[name].unbind()

class Scene(object):
    def __init__(self, window):
        self.window = window
        self.listeners = []
        self.call('setup')
    def __del__(self):
        self.call('teardown')
    def call(self, name, *args, **kwargs):
        for listener in self.listeners + [self]:
            if hasattr(listener, name):
                if getattr(listener, name)(*args, **kwargs):
                    return
    # listener functions
    def setup(self):
        pass
    def enter(self):
        pass
    def update(self, t, dt):
        pass
    def draw(self):
        pass
    def exit(self):
        pass
    def teardown(self):
        pass
    def on_size(self, width, height):
        pass
    def on_cursor_pos(self, x, y):
        pass
    def on_mouse_button(self, button, action, mods):
        pass
    def on_key(self, key, scancode, action, mods):
        pass
    def on_char(self, codepoint):
        pass

class Worker(object):
    def __init__(self):
        self.handle = glfw.create_window(1, 1, 'Worker', None, None)
        if not self.handle:
            raise Exception
    def use(self):
        glfw.make_context_current(self.handle)
    def destroy(self):
        glfw.destroy_window(self.handle)
    def start(self):
        async(self.thread_main)
    def thread_main(self):
        self.use()
        try:
            self.run()
        finally:
            self.destroy()
    def run(self):
        pass

class Window(object):
    def __init__(
        self, size=(800, 600), title='Python Graphics', visible=True,
        share=None, full_screen=False):
        self.app = App.instance
        if full_screen:
            monitor = glfw.get_primary_monitor()
            size = glfw.get_video_mode(monitor)[0]
        else:
            monitor = None
        self.size = width, height = size
        self.aspect = float(width) / height
        glfw.window_hint(glfw.VISIBLE, visible)
        share = share and share.handle
        self.handle = glfw.create_window(width, height, title, monitor, share)
        if not self.handle:
            raise Exception
        self.app.add_window(self)
        self.cache = Cache()
        self.current_program = None
        self.use()
        self.framebuffer_size = glfw.get_framebuffer_size(self.handle)
        self.configure()
        self.exclusive = False
        self.listeners = []
        self.scene_stack = []
        self.set_callbacks()
        self.call('setup')
    @property
    def current_scene(self):
        return self.scene_stack[-1] if self.scene_stack else None
    def push_scene(self, scene):
        if scene.window != self:
            raise Exception
        self.scene_stack.append(scene)
        scene.call('enter')
    def pop_scene(self):
        scene = self.current_scene
        scene.call('exit')
        self.scene_stack.pop()
    def set_scene(self, scene):
        if self.current_scene:
            self.pop_scene()
        self.push_scene(scene)
    @property
    def t(self):
        return self.app.ticker.t
    @property
    def dt(self):
        return self.app.ticker.dt
    @property
    def fps(self):
        return self.app.ticker.fps
    @property
    def ticks(self):
        return self.app.ticker.ticks
    def configure(self):
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_CULL_FACE)
        # glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
    def close(self):
        glfw.set_window_should_close(self.handle, 1)
    def set_exclusive(self, exclusive=True):
        if exclusive == self.exclusive:
            return
        self.exclusive = exclusive
        if exclusive:
            glfw.set_input_mode(self.handle, glfw.CURSOR, glfw.CURSOR_DISABLED)
        else:
            glfw.set_input_mode(self.handle, glfw.CURSOR, glfw.CURSOR_NORMAL)
    def set_current_program(self, program):
        self.current_program = program
    def use(self):
        glfw.make_context_current(self.handle)
        self.app.set_current_window(self)
    def clear(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    def clear_color_buffer(self):
        glClear(GL_COLOR_BUFFER_BIT)
    def clear_depth_buffer(self):
        glClear(GL_DEPTH_BUFFER_BIT)
    def set_clear_color(self, r, g, b, a=1.0):
        glClearColor(r, g, b, a)
    def tick(self):
        self.use()
        if glfw.window_should_close(self.handle):
            self.call('teardown')
            self.app.remove_window(self)
            glfw.destroy_window(self.handle)
            return
        self.call('update', self.t, self.dt)
        self.redraw()
    def redraw(self):
        self.call('draw')
        glfw.swap_buffers(self.handle)
    def save_image(self, path):
        width, height = self.size
        data = (c_ubyte * (width * height * 3))()
        glReadBuffer(GL_BACK)
        glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, data)
        im = Image.frombytes('RGB', (width, height), data)
        im = im.transpose(Image.FLIP_TOP_BOTTOM)
        im.save(path)
    def screenshot(self):
        counter = 0
        while True:
            path = 'pg%04d.png' % counter
            if not os.path.exists(path):
                self.save_image(path)
                break
            counter += 1
    def set_callbacks(self):
        glfw.set_window_size_callback(self.handle, self._on_size)
        glfw.set_framebuffer_size_callback(self.handle, self._on_framebuffer_size)
        glfw.set_cursor_pos_callback(self.handle, self._on_cursor_pos)
        glfw.set_mouse_button_callback(self.handle, self._on_mouse_button)
        glfw.set_key_callback(self.handle, self._on_key)
        glfw.set_char_callback(self.handle, self._on_char)
    def call(self, name, *args, **kwargs):
        for listener in self.listeners + [self]:
            if hasattr(listener, name):
                if getattr(listener, name)(*args, **kwargs):
                    return
        if name in ['setup', 'teardown']:
            return
        scene = self.current_scene
        if scene is not None:
            scene.call(name, *args, **kwargs)
    def _on_size(self, window, width, height):
        self.size = (width, height)
        self.aspect = float(width) / height
        self.call('on_size', width, height)
    def _on_framebuffer_size(self, window, width, height):
        self.framebuffer_size = (width, height)
        self.call('on_framebuffer_size', width, height)
    def _on_cursor_pos(self, window, x, y):
        self.call('on_cursor_pos', x, y)
    def _on_mouse_button(self, window, button, action, mods):
        self.call('on_mouse_button', button, action, mods)
    def _on_key(self, window, key, scancode, action, mods):
        self.call('on_key', key, scancode, action, mods)
        if action == glfw.PRESS and key == glfw.KEY_F12:
            self.screenshot()
    def _on_char(self, window, codepoint):
        self.call('on_char', codepoint)
    # listener functions
    def setup(self):
        pass
    def update(self, t, dt):
        pass
    def draw(self):
        pass
    def teardown(self):
        pass
    def on_size(self, width, height):
        pass
    def on_framebuffer_size(self, width, height):
        pass
    def on_cursor_pos(self, x, y):
        pass
    def on_mouse_button(self, button, action, mods):
        pass
    def on_key(self, key, scancode, action, mods):
        pass
    def on_char(self, codepoint):
        pass

class Ticker(object):
    def __init__(self):
        self.start_time = time.time()
        self.last_time = self.start_time
        self.t = 0
        self.dt = 0
        self.ticks = 0
        self.fps_time = self.start_time
        self.fps_ticks = 0
        self.fps = 0
    def tick(self):
        now = time.time()
        self.t = now - self.start_time
        self.dt = now - self.last_time
        self.last_time = now
        self.ticks += 1
        self.fps_ticks += 1
        if now - self.fps_time >= 1:
            self.fps = self.fps_ticks / (now - self.fps_time)
            self.fps_ticks = 0
            self.fps_time = now

class App(object):
    instance = None
    def __init__(self):
        if not glfw.init():
            raise Exception
        App.instance = self
        self.windows = []
        self.current_window = None
        self.queue = Queue.Queue()
        self.ticker = Ticker()
    def add_window(self, window):
        self.windows.append(window)
    def remove_window(self, window):
        self.windows.remove(window)
    def set_current_window(self, window):
        self.current_window = window
    def call_after(self, func, *args, **kwargs):
        self.queue.put((func, args, kwargs))
    def process_queue(self):
        while self.queue.qsize():
            func, args, kwargs = self.queue.get()
            func(*args, **kwargs)
    def run(self):
        while self.windows:
            self.tick()
        glfw.terminate()
    def tick(self):
        self.ticker.tick()
        poll_events()
        self.process_queue()
        for window in list(self.windows):
            window.tick()

def poll_events():
    glfw.poll_events()

def call_after(func, *args, **kwargs):
    App.instance.call_after(func, *args, **kwargs)

def async(func, *args, **kwargs):
    thread = threading.Thread(target=func, args=args, kwargs=kwargs)
    thread.setDaemon(True)
    thread.start()

def run(cls, *args, **kwargs):
    app = App()
    if issubclass(cls, Window):
        window = cls(*args, **kwargs)
    else:
        window = Window()
    if issubclass(cls, Scene):
        scene = cls(window, *args, **kwargs)
        window.set_scene(scene)
    app.run()