MAX_OBJECTS_PER_BUCKET = 64

class Octree:
    def __init__(self, center, size):
        '''"size" is the edge length of the box centered at "center"
           "objects" is the storage of the objects that occupy this node.
           "nodes" holds the sub-nodes of this node. It and "objects" are
           mutually exlusive.
        '''
        self.center = center
        self.size = size
        self.half_size = size/2
        self.objects = []
        self.nodes = []

    def subdivide(self):
        '''Make eight new nodes and divvy up the objects contents to each of
           the new nodes.
        '''
        hs = self.half_size
        qs = hs / 2
        cx, cy, cz = self.center
        # make the eight new nodes
        self.nodes.append(Octree((cx-qs, cy-qs, cz-qs), hs))
        self.nodes.append(Octree((cx-qs, cy-qs, cz+qs), hs))
        self.nodes.append(Octree((cx-qs, cy+qs, cz-qs), hs))
        self.nodes.append(Octree((cx-qs, cy+qs, cz+qs), hs))
        self.nodes.append(Octree((cx+qs, cy-qs, cz-qs), hs))
        self.nodes.append(Octree((cx+qs, cy-qs, cz+qs), hs))
        self.nodes.append(Octree((cx+qs, cy+qs, cz-qs), hs))
        self.nodes.append(Octree((cx+qs, cy+qs, cz+qs), hs))

        # re-insert now that we have sub-nodes
        for point, object in self.objects:
            for node in self.nodes:
                if node.isPointInside(point):
                    node.insertObject(point, object)
                    break
        self.objects = []

    def insertObject(self, point, object):
        '''Figure where the point lies in the octree and insert the object
           into the objects there.
        '''
        # add the object to the correct node
        if self.nodes:
            for node in self.nodes:
                if node.isPointInside(point):
                    node.insertObject(point, object)
                    break
            else:
                raise ValueError, "Point %r lies outside %r size %r"%(
                    point, self.center, self.size)
        else:
            # belongs in this node's objects
            self.objects.append((point, object))

        # now check to see if we're full
        if len(self.objects) > MAX_OBJECTS_PER_BUCKET:
            self.subdivide()

    def isPointInside(self, point):
        '''Determine if the given point lies inside this node
        '''
        c, s = self.center, self.half_size
        if point[0] < c[0] - s: return 0
        if point[0] > c[0] + s: return 0
        if point[1] < c[1] - s: return 0
        if point[1] > c[1] + s: return 0
        if point[2] < c[2] - s: return 0
        if point[2] > c[2] + s: return 0
        return 1

    def insertPolygon(self, poly):
        '''Insert a polygon - defined by a sequence of vertex 3-tuples.
        The polygon object must also have a render method.
        
        In the C implementation, a reference to this method is obtained when
        the object is added to the tree, so changing it on the object after
        adding will have no effect.
        '''
        for entry in poly.vertexes:
            self.insertObject(entry, poly)

    def clearRenderFlag(self):
        '''Clear the "rendered" flag from all objects
        '''
        for node in self.nodes:
            node.clearRenderFlag()
        for pos, object in self.objects:
            object.rendered = 0

    def renderClippedToFrustum(self, frustum):
        '''Render all nodes, clipped to the given frustum
        '''
        x, y, z = self.center
        if not frustum.isCubeInside(x, y, z, self.half_size):
            return 0
        drawn = 0
        if self.nodes:
            for node in self.nodes:
                drawn = drawn + node.renderClippedToFrustum(frustum)
        else:
            for position, object in self.objects:
                if object.rendered: continue
                object.render()
                drawn = drawn + 1
        return drawn

    def summary(self):
        '''Figure a summary about this octree structure
        '''
        print "Nodes: %d\nObjects: %d\nEmpty nodes: %d"%(
            self._countNodes(), self._countObjects(),
            self._countEmpties())

    def _countNodes(self):
        count = 1
        for node in self.nodes:
            count += node._countNodes()
        return count

    def _countEmpties(self):
        count=0
        if not self.objects:
            count=1
        for node in self.nodes:
            count += node._countEmpties()
        return count

    def _countObjects(self):
        count = len(self.objects)
        for node in self.nodes:
            count += node._countObjects()
        return count

