"""
Edward Dale
2006-1-30
Animation Algorithms
Particle System

Copyright (c) 2005, Edward Dale
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of Edward Dale nor the names of its contributors
  may be used to endorse or promote products derived from this software
  without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from cgkit.all import *
from random import *
from math import pi,cos
from keyframe import *

class Particle:
    """
    A single particle in the system.
    """
    
    def __init__(self):
        """
        Initializes the particle to a bunch of empty values.
        """
        self.pos=vec3()
        self.vel=vec3()
        self.size=0
        self.lifetime=0
        self.initiallifetime=0

class System(Component):
    """
    The particle system, which holds all the particles and animates them
    over time.
    """
    
    def __init__(self, fp):
        """
        Creates all the particles and intializes their values.
        """
        Component.__init__(self, name="Particle System", auto_insert=True)
        self.particles=[]
        self.pause = False
        self.shrinking = False
        self.burst = False
        self.color = vec3(1,0,0)

        # Make the slots necessary for animation.
        self.time_slot = DoubleSlot()
        self.pos_slot = Vec3Slot()
        self.addSlot("pos", self.pos_slot)
        self.addSlot("time", self.time_slot)

        # Parameters about the generation rate of particles
        genrate = fp.readline().strip().split()
        self.avggenrate=int(genrate[0])
        self.genratedev=int(genrate[1])

        # Parameters about the particle lifetime
        lifetime=fp.readline().strip().split()
        self.lifetimeavg=int(lifetime[0])
        self.lifetimedev=int(lifetime[1])

        # Parameters about the emission cone
        cone=fp.readline().strip().split()
        self.coneangle=float(cone[0])
        self.coneheight=int(cone[1])

        # The number of particles to start out with
        initialparticles=int(fp.readline().strip())

        # Set up slot dependencies
        getScene().timer().time_slot.connect(self.time_slot)
        n=NotificationForwarder(self.tick)
        self.time_slot.addDependent(n)
        
        for i in xrange(initialparticles):
            newpart = Particle()
            self.initializeParticle(newpart)
            self.particles.append(newpart)

    exec slotPropertyCode("pos")
    exec slotPropertyCode("time")
    
    def initializeParticle(self, particle):
        """
        Sets all the random properties of the particle.
        """
        particle.size=uniform(0,1)
        particle.lifetime=max(1,int(normalvariate(self.lifetimeavg,self.lifetimedev)))
        particle.initiallifetime=float(particle.lifetime)
        
        # Use spherical coordinates to figure out emission cone
        r=uniform(0,self.coneheight)
        zen=2*pi*random()
        az=uniform(0, pi * self.coneangle)
        spherical=(az,zen,r)
        cartesian=sphericalToCartesian(spherical)

        particle.pos=self.pos
        particle.vel=cartesian
    
    def tick(self):
        """
        To be called at each clock tick.
        Needs to advance particles.
        """
        newparticles=[]
        drawClear()
        # Stats that don't change at each iteration
        drawText(pos=(-74, 120, 0), txt="Color %s"%self.color)
        drawText(pos=(-82, 120, 0), txt="Pause %s" %self.pause)
        drawText(pos=(-90, 120, 0), txt="Burst %s" %self.burst)
        drawText(pos=(-98, 120, 0), txt="Shrinking %s" %self.shrinking)
        
        drawText(pos=(-66, -64, 0), txt="%3d Avg. Lifetime"%self.lifetimeavg)
        drawText(pos=(-74, -64, 0), txt="%3d parts per tick"%self.avggenrate)

        # If the system isn't paused, birth new particles
        if (not self.pause and
              ( (self.burst and not self.particles) 
                or not self.burst)):
            numbirths=int(normalvariate(self.avggenrate, self.genratedev))
            for x in range(numbirths):
                i = Particle()
                self.initializeParticle(i)
                self.particles.append(i)
        else:
            numbirths=0
        drawText(pos=(-82, -64, 0), txt="%3d New Particles"%numbirths)

        dead=0
        for i in self.particles:
            i.lifetime -= 1 # Age the particle
            if i.lifetime <= 0:
                # The particle is dead, so remove it and record another death
                #self.particles.remove(i)
                dead += 1
            else:
                newparticles.append(i)
                # Advance the particle's position
                for a in range(3):
                    i.pos[a] += i.vel[a]
            # Figure out what color and size to use based on age
            age = i.lifetime/i.initiallifetime
            if self.shrinking:
                size = age * i.size
            else:
                size = i.size
            color = age*self.color
            drawMarker(pos=i.pos,color=color,size=size)

        # Display some more stats
        drawText(pos=(-90,-64, 0), txt="%3d Dead Particles"%dead)
        drawText(pos=(-98,-64, 0), txt="%3d Particles"%len(self.particles))
        
        self.particles=newparticles

def sphericalToCartesian(spherical):
    """
    Convert a point from spherical coordinates to cartesian coordinates.
    http://mathworld.wolfram.com/SphericalCoordinates.html
    """
    x=spherical[2]*cos(spherical[1])*sin(spherical[0])
    y=spherical[2]*sin(spherical[1])*sin(spherical[0])
    z=spherical[2]*cos(spherical[0])
    return vec3(x,y,z)
