"""
a plant/tree featuring simple procedural modelling (l-system like). 
this version for kalleria3d. 

made for interactive controls: besides growing, and 'ungrow' (revert).

done:
- simple working 3d, using coordsys transformations
  .. perhaps this works as a soya coordsys tutorial, as there is none other?
- branch class to get reference to the previous one when removing branches
  * now also almost all of the growth code there

todo/wishlist:
- parameterzation of the growth
 * this is little/perhaps similar to Weber&Penn (p119-weber.pdf, siggraph '9x)
   where lot of control and nice results. that technique could be drawn from
- mesh support for body (now edges/faces (lines) only)
  * change the current branches from lines to vectors (new_vector now there)
    construct meshes based on those (Weber&Penn uses cones etc.)
- leaves (one solution also in Weber&Penn)
- smooth growing/reverting (animate)
  * make 'todo' in grow(), work based on that in advance_time
    - many in parallel or still one by one?
- ..
- good interactive controls (for VJing)
- better camera automatization, + controls
- support use from outside, e.g. for generating game scenes
 * some nice generating method / class, instead of step-by-step growing
   .. or would some other model be better for that, if this is for VJing?

"""

import random
import soya, soya.sdlconst
from math import *



class Plant(soya.World):
    """a plant/tree featuring simple procedural modelling (l-system like).

    made for interactive controls: besides growing, and 'ungrow' (revert).
    """

    def __init__(self, scene):
        soya.World.__init__(self, scene)

        self.trunk = Branch(self, None, #this is the only branch with no prev
                            (soya.Point(self, 0,0,0),
                            soya.Point(self, 0,1,0)))
        
        self.branches = [self.trunk] #all branches
        self.branchfrom = self.trunk #the branch which the next new will extend
        self.branchfrom_queue = [] #queue for coming branchpoints, used as FIFO

        #options (no random yet)
        #self.d_angle = 90 - 35 #not used now (in 3d version)
        self.length_ratio = 0.8
        self.branchamount = 3

    def next_branchfrom(self):
        """updates the internal pointer to next branchpoint,
        based on the internal queue of points to branch from.
        """
        try:
            self.branchfrom = self.branchfrom_queue.pop(0)
        except IndexError:
            self.branchfrom = self.trunk
            print "branchfrom queue empty, branching set to trunk."

    def grow(self):
        if len(self.branchfrom.branches) == self.branchamount: #branching limit achieved
            self.next_branchfrom()

        #making 3d - endpoints on a circle.
        #should make use d_angle again
        interval_angle = 360.0 / self.branchamount
        branch = self.branchfrom.branch(interval_angle, self.length_ratio)

        self.branches.append(branch) #needed? just to get latest in remove?
        self.branchfrom_queue.append(branch)

        return True

    def revert(self):
        try:
            latest = self.branches.pop() #takes latest branch away
                
        except IndexError: #this is the trunk
            pass #print "no branches to remove"
            
        else: #a branch was removed
            try:
                self.branchfrom_queue.remove(latest) #also from the branchpoint queue
            except ValueError:
                pass #print "removed branch not in branchfrom_queue", latest, self.branchfrom_queue

            latest.remove() #finally the actual removal (Plant, prev.branches)
            #this is safe because the trunk is not in Plant.branches
            #XXX this is not safe now, crashes when removing 0/1 branch!
                        
            if len(latest.prev.branches) == 0: #was last in there
                self.branchfrom = latest.prev #from what removed branched
                #this is why the whole Branch class was introduced..

            #otherwise branchfrom remains (is same as latest.prev?)
                                     
class Branch:
    """Plants consist of these. To hold references to prev branches,
    to know the 'right' order when 'ungrowing'

    might not make sense - was not adding this to avoid 'over-OO'.

    now does include all the 3d data, hopefully nice when adding shape
    """

    def __init__(self, world, prev, line):
        #for removing
        self.world = world
        
        #for the linked list / tree structure
        self.prev = prev
        self.branches = []

        #the visual 3d data
        v1 = soya.Vertex(world, diffuse = (0.0, 1.0, 1.0, 1.0))
        v1.move(line[0])
        v2 = soya.Vertex(world, diffuse = (0.0, 1.0, 0.0, 1.0))
        v2.move(line[1]) #when animating, this will happen gradually
        self.mesh = soya.Face(world, (v1, v2))
        self.cs = soya.CoordSyst(self.world) #applies Plant rotation

        #info for Plant.grow .. that now actually is in self.branch
        self.begin = self.mesh.vertices[0]
        self.end = self.mesh.vertices[-1]
        self.update_info()

    def update_info(self): #for trunk, called  when mouse has moved .end
        #local coordsys for branches of this branch to use
        #i.e. for making upcoming new branch relative to thís old
        self.cs.move(self.begin) #beginning of this branch
        self.cs.look_at_y(self.end) #orientation to match .. self
        self.cs.move(self.end) #beginning of next branch
        #.. as there is no .orientation to set&get, this an easy way to do..

    def branch(self, anglemultiplier, length_ratio):
        length = (self.begin >> self.end).length() * length_ratio
        angle = anglemultiplier * len(self.branches)

        dx = sin(radians(angle))
        dz = cos(radians(angle))
        dy = 1 #all to same height (first?). this could apply d_angle
                
        endpoint = soya.Point(self.cs, dx, dy, dz)
        new_vector = self.end >> endpoint #to adjust length
        new_vector.set_length(length)

        end = soya.Point(self.world, #new_vector in self.cs translated to Plant
                         self.end.x + new_vector.x,
                         self.end.y + new_vector.y,
                         self.end.z + new_vector.z)

        branch = Branch(self.world, self, (self.end, end))
        self.branches.append(branch)
        return branch
    
    def remove(self):
        self.prev.branches.remove(self)
        self.world.remove(self.mesh)


class PlantToy(soya.World):
    from soya.sdlconst import *
    keybindings = {K_ESCAPE: "soya.IDLER.stop()",
                   K_LEFT: "plant.turn_lateral(-10)",
                   K_RIGHT: "plant.turn_lateral(10)",
                   K_UP: "plant.grow()",
                   K_DOWN: "plant.revert()",
                   K_a: "camera.set_xyz(camera.x, camera.y, camera.z - 0.1)",
                   K_z: "camera.set_xyz(camera.x, camera.y, camera.z + 0.1)",
                   }
    
    def __init__(self): #should get plant etc. (current globals)
        soya.World.__init__(self)
        soya.set_grab_input(1)
        
    def begin_round(self):
        soya.World.begin_round(self) #is there anything?
        for event in soya.process_event():
            if event[0] == soya.sdlconst.KEYDOWN:
                try:
                    cmd = self.keybindings[event[1]]
                except KeyError:
                    pass
                else:
                    eval(cmd)
                    
            elif event[0] == soya.sdlconst.MOUSEMOTION:
                rel_x = event[3]
                rel_y = event[4]

                if len(plant.branches) == 0: #freely move trunk end
                    mouse_x = event[1]
                    mouse_y = event[2]
                    mouse_pos = camera.coord2d_to_3d(mouse_x, mouse_y,
                                                     (plant % camera).z)
                    mouse_pos.convert_to(self)
                    plant.branchfrom.end.x = mouse_pos.x
                    plant.branchfrom.update_info()

                else: #spin the tree with x movement
                    soya.set_mouse_pos(300, 300) #window middle would be nice
                    soya.process_event() #resets rel mouse
                    plant.turn_lateral(rel_x)

                #growth control in y axis
                if rel_y > 0:
                    if plant.grow():
                        camera.z += 0.05

                if rel_y < 0:
                    plant.revert()
                    if camera.z > 2:
                        camera.z -= 0.05

if __name__ == '__main__':
    soya.init(width = 700, height = 700)

    scene = PlantToy()
    plant = Plant(scene)

    #globals (show for scene & plant)
    light = soya.Light(scene)
    light.set_xyz(1.0, 0.7, 1.0)

    light2 = soya.Light(scene)
    light2.set_xyz(-1.0, 0.9, 0.7)

    light3 = soya.Light(scene)
    light3.set_xyz(0.3, 0.8, -0.7)

    camera = soya.Camera(scene)
    camera.set_xyz(0.0, 1.2, 2.0)
    soya.set_root_widget(camera)

    #Start
    idler = soya.Idler(scene)
    idler.idle()

