
"Framebuffer objects, for offscreen rendering."

from __future__ import with_statement

import OpenGL.GL as gl
import OpenGL.GL.EXT.framebuffer_object as fbo

from glitch.camera import Camera
from glitch.texture import Texture
from glitch.multicontext import MultiContext

class RenderBuffer(MultiContext):
    "An abstract OpenGL render buffer."

    _context_key = 'render_buffer'

    def __init__(self, width, height):
        MultiContext.__init__(self)
        self.width = width
        self.height = height

    def _context_create(self, ctx):
        return fbo.glGenRenderbuffersEXT(1)

    def _context_delete(self, ctx, id):
        fbo.glDeleteRenderbuffersEXT(1, [id])

    def bind(self, ctx):
        id = self.context_get(ctx)
        fbo.glBindRenderbufferEXT(gl.GL_RENDERBUFFER_EXT, id)
        #assert fbo.glIsRenderbufferEXT(id)
        return id

    def unbind(self, ctx):
         fbo.glBindRenderbufferEXT(gl.GL_RENDERBUFFER_EXT, 0)

class DepthRenderBuffer(RenderBuffer):
    "An OpenGL depth render buffer."

    def _context_create(self, ctx):
        id = RenderBuffer._context_create(self, ctx)
        fbo.glBindRenderbufferEXT(gl.GL_RENDERBUFFER_EXT, id)
        fbo.glRenderbufferStorageEXT(gl.GL_RENDERBUFFER_EXT,
            gl.GL_DEPTH_COMPONENT, self.width, self.height)
        return id

class ColorRenderBuffer(RenderBuffer):
    "An OpenGL color render buffer."

    format = gl.GL_RGBA8

    def _context_create(self, ctx):
        id = RenderBuffer._context_create(self, ctx)
        fbo.glBindRenderbufferEXT(gl.GL_RENDERBUFFER_EXT, id)
        fbo.glRenderbufferStorageEXT(gl.GL_RENDERBUFFER_EXT,
            self.format, self.width, self.height)
        return id

class FrameBuffer(MultiContext):
    "An OpenGL frame buffer."

    _context_key = 'framebuffer'

    def __init__(self, color_buffer, depth_buffer=None):
        MultiContext.__init__(self)
        self.color_buffer = color_buffer
        self.depth_buffer = depth_buffer

    def _context_create(self, ctx):
        id = fbo.glGenFramebuffersEXT(1)
        fbo.glBindFramebufferEXT(gl.GL_FRAMEBUFFER_EXT, id)

        if isinstance(self.color_buffer, Texture):
            with self.color_buffer.bind(ctx, 0) as color_buffer_id:
                # Attach texture to render to.
                fbo.glFramebufferTexture2DEXT(gl.GL_FRAMEBUFFER_EXT,
                    gl.GL_COLOR_ATTACHMENT0_EXT, gl.GL_TEXTURE_2D,
                    color_buffer_id, 0)
        else:
            color_buffer_id = self.color_buffer.bind(ctx)

            # Attach color buffer to render to.
            fbo.glFramebufferRenderbufferEXT(gl.GL_FRAMEBUFFER_EXT,
                gl.GL_COLOR_ATTACHMENT0_EXT, gl.GL_RENDERBUFFER_EXT,
                color_buffer_id)

        if self.depth_buffer is not None:
            depth_buffer_id = self.depth_buffer.bind(ctx)
            #self.depth_buffer.unbind(ctx)
            fbo.glFramebufferRenderbufferEXT(gl.GL_FRAMEBUFFER_EXT,
                gl.GL_DEPTH_ATTACHMENT_EXT, gl.GL_RENDERBUFFER_EXT,
                depth_buffer_id)

        self._check_status()
        return id

    def _context_delete(self, ctx, id):
        fbo.glDeleteFramebuffersEXT([id])

    def bind(self, ctx):
        # XXX: Put this on a stack.
        self.previous_binding = gl.glGetIntegerv(
            fbo.GL_FRAMEBUFFER_BINDING_EXT)
        id = self.context_get(ctx)
        fbo.glBindFramebufferEXT(gl.GL_FRAMEBUFFER_EXT, id)

    def _check_status(self):
        status = fbo.glCheckFramebufferStatusEXT(gl.GL_FRAMEBUFFER_EXT)

        if status != fbo.GL_FRAMEBUFFER_COMPLETE_EXT:
            errors = dict((int(getattr(fbo, name)), getattr(fbo, name))
                for name in dir(fbo) if 'INCOMPLETE' in name)
            raise RuntimeError(errors[status])

    def unbind(self, ctx):
        fbo.glBindFramebufferEXT(gl.GL_FRAMEBUFFER_EXT, self.previous_binding)

class CameraTexture(Texture):
    """Render a scene to a texture.

    A CameraTexture behaves like a Camera, in that it takes the same
    parameters as a Camera, and it behaves like a Texture, in that it can be
    passed to ApplyTexture.
    """

    _context_key = 'camera_texture'

    def __init__(self, width, height, **kw):
        Texture.__init__(self, width, height, None)
        # XXX: Should probably just take a camera as an argument, or have a
        # texture as a property.
        self.camera = Camera(**kw)

    def _context_create(self, ctx):
        depth_buffer = DepthRenderBuffer(self.width, self.height)

        # Data is None: texture will be allocated but no texels loaded.
        texture = Texture(self.width, self.height, None)

        with texture.bind(ctx, 0):
            pass

        fbo = FrameBuffer(texture, depth_buffer)
        self._context_update(ctx, fbo)
        return fbo

    def _context_update(self, ctx, fbo):
        fbo.bind(ctx)
        self.camera.context['w'] = self.width
        self.camera.context['h'] = self.height
        self.camera.render(ctx)
        fbo.unbind(ctx)
        return fbo

    def _context_delete(self, ctx, fbo):
        fbo.color_buffer.context_delete(ctx)
        fbo.depth_buffer.context_delete(ctx)
        fbo.context_delete(ctx)

    def bind(self, ctx, unit):
        fbo = self.context_get(ctx)
        return fbo.color_buffer.bind(ctx, unit)

    def refresh(self):
        self.version += 1

class BufferCamera(Camera, MultiContext):
    "A camera that renders to a ColorRenderBuffer."

    _context_key = 'buffer_camera'

    def __init__(self, width, height, **kw):
        self.width = width
        self.height = height
        MultiContext.__init__(self)
        Camera.__init__(self, **kw)

    def _context_create(self, ctx):
        color_buffer = ColorRenderBuffer(self.width, self.height)
        depth_buffer = DepthRenderBuffer(self.width, self.height)
        fbo = FrameBuffer(color_buffer, depth_buffer)
        self._context_update(self, ctx, fbo)
        return fbo

    def _context_update(self, ctx, fbo):
        fbo.bind(ctx)
        self.context['w'] = self.width
        self.context['h'] = self.height
        Camera.render(self, ctx)
        fbo.unbind(ctx)
        return fbo

    def render(self, parent_ctx=None):
        self.context_get(self.context)

