"""
Edward Dale
2005-12-18
Animation Algorithms
Keyframing

Copyright (c) 2006, 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 cgkit.ri import *
from math import sin,pi
from bisect import bisect
from os import environ
from PIL import Image

def abstract():
    """
    Kludge to do some kind of abstract class stuff.
    Taken from:
    http://www.norvig.com/python-iaq.html
    """
    import inspect
    caller = inspect.getouterframes(inspect.currentframe())[1][3]
    raise NotImplementedError(caller + ' must be implemented in subclass')

class Keyframe:
    """
    A class representing a line in the keyframe file format from the
    assignment.
    """
    def fromInputLineArray(data):
        """
        Factory method to create a Keyframe from an array of the 
        constituent pieces.
        """
        if len(data)==4:
            return Keyframe(data[0],data[1],data[2],data[3])
        elif len(data)==8:
            return Keyframe(data[0],data[1],data[2],data[3],
                            data[4],data[5],data[6],data[7])
        else:
            raise "File not formatted correctly."
    fromInputLineArray = staticmethod( fromInputLineArray )
    
    def fromString(data):
        """
        Factory method to create a Keyframe from a line of the keyframe file.
        """
        return Keyframe.fromInputLine(data.split())
    fromString = staticmethod(fromString)

    def __init__(self, time=0, x=0, y=0, z=0, xa=0, ya=0, za=0, theta=0):
        """
        Creates a Keyframe from all of the pieces needed to define a keyframe.
        """
        self.time = float(time)
        self.theta = float(theta)
        self.pos = vec3(float(x),float(y),float(z))
        axis = vec3(float(xa),float(ya),float(za))
        self.rot = quat(float(theta), axis).normalize()
    
    def __cmp__(self, other):
        """
        Compares a Keyframe with another Keyframe.  Only the time is
        used in the comparison.  Can also compare this Keyframe with an int or
        float.
        """
        if isinstance(other,float) or isinstance(other,int):
            return cmp(self.time, other)
        
        return cmp(self.time, other.time)
    
    def __str__(self):
        """
        Pretty prints this Keyframe
        """
        return "time:%f, pos=%s, rot=%s" % (self.time, self.pos, self.rot) 
        
class Keyframes:
    """
    Represents the collection of Keyframe objects that characterize the animation.
    """
    def __init__(self, loop):
        self.boundingKeyframeCache = {}
        self.loop = loop
    
    def readFile(self, fp ):
        """
        Reads a simple textfile formatted using the keyframe
        format described in the assignment.
        Returns a list of Keyframe objects ordered by increasing frame number.
        Lines can also be commented out using a #.
        """
        self.blah = []
        for line in fp:
            if not line.startswith( "#" ):
                self.blah.append(Keyframe.fromInputLineArray(line.split()))
        
        self.blah.sort()
        self.maxtime = self.blah[-1].time # last frame is length of animation

    def getBoundingKeyframes(self,time, num=1):
        """
        Searches for the 'num' keyframes on either side of the frame at the
        given time that define the characteristics of the frame.
        Also returns the 'u' value of the immediate bounding keyframes.
        """
        if self.loop:
            time = time % self.maxtime
        # This should protect us from indexing errors
        if time < 0 or time > self.maxtime:
            raise ValueError("Outside of keyframe bounds.")
        
        mid = bisect(self.blah, time)
        lower = self.blah[:mid]
        upper = self.blah[mid:]

        # If there aren't enough keyframes on either side, then pad        
        while len(lower) < num:
            lower.insert(0, lower[0])
        while len(upper) < num:
            upper.append(upper[-1])
            
        span=time-lower[-1].time
        deltaT = upper[0].time-lower[-1].time
        u = span/deltaT

        # Return only the number of frames asked for
        return (lower[-num:],upper[:num], u)

class TheScene:
    """
    The simple scene that is being animated.
    Copied from the 'Sharing material definitions' tutorial at:
    http://cgkit.sourceforge.net/tutorials/code_examples/demo2.html
    """
    
    def __init__(self):
        """
        Creates the simple scene that will be animated.
        Stores a reference to the camera and the group of primitives being
        drawn.
        """
        scale = 10.0
        self.cam = TargetCamera(
            pos    = vec3(0,0,10)*scale,
            target = vec3(0,0,1)*scale,
            fov=90
        )
    
        GLPointLight( pos = vec3(2,3,4)*scale, intensity=0.8 )
        GLPointLight( pos = vec3(2,-3,1)*scale, intensity=0.8 )
        
        # Define a material for later use
        yellow = GLMaterial(
                     name      = "Yellow",
                     diffuse   = (1, 0.9, 0.3),
                     specular  = (1, 1, 1),
                     shininess = 80
                 )
        
        # Define the geometry...
        
        childs = []
        
        childs.append( Box(
            name = "Bottom",
            lx  = 1.0*scale,
            ly  = 1.0*scale,
            lz  = 0.2*scale,
            material = yellow
        ) )
        
        childs.append( Box(
            name = "Top",
            lx  = 1.0*scale,
            ly  = 1.0*scale,
            lz  = 0.2*scale,
            pos = vec3(0,0,2)*scale,
            material = yellow
        ) )
        
        childs.append( Sphere(
            name = "Middle",
            radius = 0.5,
            pos    = vec3(0,0,1)*scale,
            scale  = vec3(1,1,2.2)*scale,
            material = GLMaterial(
                           diffuse = vec3(1,0,0),
                           specular = vec3(1,1,1),
                           shininess = 30
                       )
        ) )
        
        self.group = Group( childs=childs )
    
class KeyframeInterpolator(Component):
    """
    Abstract superclass of the interpolators that are being written for the 
    keyframing assignment.  I guess this is where the grade actually comes from. 
    The rest is just plumbing to get the keyframing going.
    """
    
    def __init__(self, keyframes, name, slotType):
        """
        Creates an output slot that depends on the time.  The type of the
        output slot is determined by the slotType parameter.
        """
        Component.__init__(self, name=name, auto_insert=True)
        self.keyframes = keyframes
        
        self.time_slot = DoubleSlot() #input
        self.output_slot = slotType(self.computeOutput) #output

        # Add the slots to the component
        self.addSlot("output", self.output_slot)
        self.addSlot("time", self.output_slot)

        # Set up slot dependencies
        self.time_slot.addDependent(self.output_slot)
        getScene().timer().time_slot.connect(self.time_slot)

    def computeOutput(self):
        """
        The method to be overridden by the interpolators to provide 
        interpolation. This method is abstract, so you better override it.
        """
        abstract()

    # Create value attributes
    exec slotPropertyCode("time")
    exec slotPropertyCode("output")

class SphericalLinearRotation(KeyframeInterpolator):
    """
    Provides a simple slerp rotation using quaternions.
    """
    
    def __init__(self, keyframes):
        """
        Initializes the interpolator with the necessary keyframes.
        """
        KeyframeInterpolator.__init__(self, keyframes=keyframes, name="SphericalLinearRotation", slotType=ProceduralMat3Slot)

    def computeOutput(self):
        """
        Outputs a rotation matrix interpolated using slerp between the two 
        keyframes that bound the frame at this time.
        """
        lower,upper,u = self.keyframes.getBoundingKeyframes(self.time)
        middle=slerp(u, lower[0].rot, upper[0].rot)
        return middle.toMat3()

class LinearPosition(KeyframeInterpolator):
    """
    Simple linear position interpolator.
    """

    def __init__(self, keyframes):
        """
        Initializes the interpolator with the necessary keyframes.
        """
        KeyframeInterpolator.__init__(self, keyframes=keyframes, name="LinearPosition", slotType=ProceduralVec3Slot)

    def computeOutput(self):
        """
        Outputs a translation vector interpolated linearly between the two 
        keyframes that bound the frame at this time.
        """
        lower,upper,u = self.keyframes.getBoundingKeyframes(self.time)
        return lower[0].pos + u*(upper[0].pos-lower[0].pos)

class CatmullRomPosition(KeyframeInterpolator):
    """
    Interpolates between two positions using Catmull-Rom splines.
    """

    def __init__(self, keyframes, tension=0.5):
        """
        Initializes the interpolator with the necessary keyframes.
        """
        KeyframeInterpolator.__init__(self, keyframes=keyframes, name="CatmullRomPosition", slotType=ProceduralVec3Slot)
        self.basis=tension * mat4(-1,3,-3,1,2,-5,4,-1,-1,0,1,0,0,2,0,0)

    def computeOutput(self):
        """
        Outputs a translation vector using the basis and geometry matrices.
        """
        lower,upper,u=self.keyframes.getBoundingKeyframes(self.time,2)
        frames = [ frame.pos for frame in lower+upper ]
        times=vec4(u**3,u**2,u, 1.0)
        geometry = mat4(frames).transpose() # transpose to switch from row-major
                                            # to column-major 
        pos = times * self.basis * geometry
        return vec3(pos.x,pos.y,pos.z)
    
def setupAnimation():
    """
    Sets up all of the animation parameters like making the
    scene and connecting interpolators to slots.
    Configuration is done using environment variables.
    KEYFRAME_INFILE -> input file
    KEYFRAME_POS_ERP -> position interpolator (catmull, lerp)
    KEYFRAME_ROT_ERP -> rotation interpolator (slerp)
    """
    poserp = {"catmull":CatmullRomPosition, "linear":LinearPosition}
    roterp = {"slerp":SphericalLinearRotation}
    
    infile = environ.get("KEYFRAME_INFILE", "infile")
    posinterpolator = poserp.get( environ.get("KEYFRAME_POS_ERP", "catmull") )
    rotinterpolator = roterp.get( environ.get("KEYFRAME_ROT_ERP", "slerp") )
    
    # Setup the scene and read the keyframes from the file
    simpleScene = TheScene()
    keyframes = Keyframes()
    keyframes.readFile(infile)
    
    pos = posinterpolator(keyframes)
    pos.output_slot.connect( simpleScene.group.pos_slot )
    
    rot = rotinterpolator(keyframes)
    rot.output_slot.connect( simpleScene.group.rot_slot )
    
# We're probably being run from the viewer.py or render.py
# that comes with cgkit, so just setup the animation and let
# those programs handle all the rest.
# setupAnimation()
