'''
Written using information from:
    http://www.markmorley.com/opengl/frustumculling.html
'''
import math

from OpenGL.GL import *
from Numeric import array as numarray

from util import normalise4

class Frustum:
    sides = 'right left top bottom back front'.split(' ')
    def __init__(self):
        self.calculate()

    def calculate(self):
        '''This extracts our frustum from the projection and modelview matrix.
        '''
        # get the projection matrix
        p = glGetFloatv(GL_PROJECTION_MATRIX)
        m = glGetFloatv(GL_MODELVIEW_MATRIX)

        # matrix mult those two to get the clipping matrix
        self.c = c = p #*m

        # Now we actually want to get the sides of the frustum.
        # To do this we take the clipping planes we received above and extract
        # the sides from them. We define the normals of the clipping planes
        # to be facing into the clipping box.
        self.right = normalise4(c[0][3]-c[0][0], c[1][3]-c[1][0],
            c[2][3]-c[2][0], c[3][3]-c[3][0])
        self.left = normalise4(c[0][3]+c[0][0], c[1][3]+c[1][0],
            c[2][3]+c[2][0], c[3][3]+c[3][0])
        self.top = normalise4(c[0][3]-c[0][1], c[1][3]-c[1][1],
            c[2][3]-c[2][1], c[3][3]-c[3][1])
        self.bottom = normalise4(c[0][3]+c[0][1], c[1][3]+c[1][1],
            c[2][3]+c[2][1], c[3][3]+c[3][1])
        self.back = normalise4(c[0][3]-c[0][2], c[1][3]-c[1][2],
            c[2][3]-c[2][2], c[3][3]-c[3][2])
        self.front = normalise4(c[0][3]+c[0][2], c[1][3]+c[1][2],
            c[2][3]+c[2][2], c[3][3]+c[3][2])

    def dump(self):
        print 'left x*%g, y*%g, z*%g = %g'%self.left
        print 'right x*%g, y*%g, z*%g = %g'%self.right
        print 'top x*%g, y*%g, z*%g = %g'%self.left
        print 'bottom x*%g, y*%g, z*%g = %g'%self.right
        return
        print 'clip =', self.c
        for attr in self.sides:
            print attr, getattr(self, attr)

    def isPointInside(self, x, y, z):
        '''The plane equations are (A*x + B*y + C*z + D = 0)

        If we plug in the point's values, the result of this equation will
        be:
            == 0   - point is on the plane
            > 0    - point is in front of the plane
            < 0    - point is behind the plane

        "Behind" in this case is outside the clipping box - outside the
        viewing area. We say it's outside the frustum if it's outside _any_
        of the clipping planes.
        '''
        for attr in self.sides:
            side = getattr(self, attr)
            result = side[0]*x + side[1]*y + side[2]*z + side[3]
            if result <= 0: return 0
        return 1

    def isSphereInside(self, x, y, z, radius):
        '''Similar to the point method, this one checks against the radius
        of the sphere.
        '''
        for attr in self.sides:
            side = getattr(self, attr)
            result = side[0]*x + side[1]*y, side[2]*z + side[3]
            if result <= -radius: return 0
        return 1

    def isCubeInside(self, x, y, z, size):
        '''Test a cube at position (x, y, z) with side length size*2 by
        just checking all the corners in turn. If all of the corners are
        behind any of the frustum planes, then the box is out of the
        frustum.

        This may return some false positives since we only check one plane
        at a time. A more complex method would be needed to determine if,
        eg.      /
                /
          Cube /._____
              / | Frustum
             /  |
        '''
        for attr in self.sides:
            side = getattr(self, attr)
            A, B, C, D = side
            # test all of the corners of the box in turn
            if A*(x - size) + B*(y - size) + C*(z - size) + D > 0:
                continue
            if A*(x + size) + B*(y - size) + C*(z - size) + D > 0:
                continue
            if A*(x - size) + B*(y + size) + C*(z - size) + D > 0:
                continue
            if A*(x + size) + B*(y + size) + C*(z - size) + D > 0:
                continue
            if A*(x - size) + B*(y - size) + C*(z + size) + D > 0:
                continue
            if A*(x + size) + B*(y - size) + C*(z + size) + D > 0:
                continue
            if A*(x - size) + B*(y + size) + C*(z + size) + D > 0:
                continue
            if A*(x + size) + B*(y + size) + C*(z + size) + D > 0:
                continue
            return 0
        return 1

    def isPolygonInside(self, l, xtrans=0, ytrans=0, ztrans=0):
        '''Do the isPoint check for each point in l, return true if any are
           inside.
        '''
        for x, y, z in l:
            x = x + xtrans
            y = y + ytrans
            z = z + ztrans
            for attr in self.sides:
                side = getattr(self, attr)
                result = side[0]*x + side[1]*y + side[2]*z + side[3]
                if result < 0:
                    break
            else:
                return 1
        return 0

