
"The abstract camera node."

from __future__ import with_statement

import ctypes

import OpenGL.GL as gl
import OpenGL.GLU as glu

import glitch
from glitch.context import Context

# XXX: buggy with extreme vertical aspect ratios

class Camera(glitch.Node):
    def __init__(self, eye=(0, 0, 5), ref=(0, 0, 0), up=(0, 1, 0), fovx=60,
            fovy=60, zNear=0.1, zFar=50, clear=(0, 0, 0, 0), **kwargs):
        glitch.Node.__init__(self, **kwargs)
        self.eye = list(eye)
        self.ref = list(ref)
        self.up = list(up)
        self.fovy = fovx
        self.fovx = fovy
        self.zNear = zNear
        self.zFar = zFar
        self.clear = clear
        self.context = Context()

    def draw(self, ctx):
        if ctx['w'] == 0:
            return

        gl.glClearColor(*self.clear)
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        gl.glColor(1, 1, 1)
        gl.glMaterialfv(
            gl.GL_FRONT_AND_BACK, gl.GL_AMBIENT_AND_DIFFUSE, (1, 1, 1, 1))

        if ctx.get('debug', False):
            gl.glLightModelfv(gl.GL_LIGHT_MODEL_AMBIENT, [.3, .3, .3, 1])
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE)
            gl.glColor(0, 1, 0, 1)
        else:
            gl.glLightModelfv(gl.GL_LIGHT_MODEL_AMBIENT, [0, 0, 0, 1])
            gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)

        gl.glViewport(0, 0, ctx['w'], ctx['h'])
        self.aspect = ctx['w'] / float(ctx['h'])

        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glLoadIdentity()
        glu.gluLookAt(*(self.eye + self.ref + self.up))
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()

        selection = ctx.get('selection')

        if selection is not None:
            (x, y, w, h) = selection
            glu.gluPickMatrix(x, y, w, h, (0, 0, ctx['w'], ctx['h']))

        fovy = max(self.fovy, self.fovx/self.aspect)
        glu.gluPerspective(fovy, self.aspect, self.zNear, self.zFar)
        gl.glEnable(gl.GL_DEPTH_TEST)

        gl.glMatrixMode(gl.GL_MODELVIEW)

    def select(self, parent_ctx, x, y, width=3, height=3):
        with self.context.push('selection', (x, y, width, height)):
            gl.glSelectBuffer(1000)
            gl.glRenderMode(gl.GL_SELECT)
            self.render(None)
            hits = gl.glRenderMode(gl.GL_RENDER)

        return [
            # The GL selection buffer contains the addresses of the Python node
            # objects on the stack when the hit occurred. Here we turn the
            # addresses back in to Python objects. This presumably won't work
            # outside CPython.
            ctypes.cast(int(id), ctypes.py_object).value
            for (_misc1, _misc2, path) in hits for id in path]

    def _save(self):
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glPushMatrix()
        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glPushMatrix()
        # XXX: Should we just save/restore everything?
        gl.glPushAttrib(gl.GL_VIEWPORT_BIT | gl.GL_TRANSFORM_BIT |
            gl.GL_ENABLE_BIT | gl.GL_LIGHTING_BIT | gl.GL_CURRENT_BIT)

    def _restore(self):
        gl.glPopAttrib()
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glPopMatrix()
        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glPopMatrix()

    def _all_lights_off(self, ctx):
        # Disable all lights in the parent scene, to prevent them affecting
        # the child scene.

        for light in ctx.get('lights', {}).values():
            gl.glDisable(gl.GL_LIGHT0 + light)

    def render(self, parent_ctx):
        # If this camera is part of an existing scene, we save and restore the
        # parent state.

        if parent_ctx is not None:
            self._save()

            parent_ctx.push('lights', {})
            parent_ctx.push('w', self.context['w'])
            parent_ctx.push('h', self.context['h'])
            glitch.Node.render(self, parent_ctx)
            parent_ctx.pop()
            parent_ctx.pop()
            parent_ctx.pop()

            self._restore()
        else:
            glitch.Node.render(self, self.context)

    def refresh(self):
        "Ask the camera to render whatever it renders to."

        raise NotImplementedError

    def run(self):
        """Display this camera, whatever that means.

        This might involve, for example, creating a window or starting a
        mainloop."""

        raise NotImplementedError

