
package com.clockwork.animation;

import com.clockwork.math.FastMath;
import com.clockwork.math.Quaternion;
import com.clockwork.math.Transform;
import com.clockwork.math.Vector3f;

/**
 * A convenience class to easily setup a spatial keyframed animation
 * you can add some keyFrames for a given time or a given keyFrameIndex, for translation rotation and scale.
 * The animationHelper will then generate an appropriate SpatialAnimation by interpolating values between the keyFrames.
 * 
 * Usage is : 
 * - Create the AnimationHelper
 * - add some keyFrames
 * - call the buildAnimation() method that will retruna new Animation
 * - add the generated Animation to any existing AnimationControl
 * 
 * Note that the first keyFrame (index 0) is defaulted with the identy transforms.
 * If you want to change that you have to replace this keyFrame with any transform you want.
 * 
 */
public class AnimationFactory {

    /**
     * step for splitting rotation that have a n ange above PI/2
     */
    private final static float EULER_STEP = FastMath.QUARTER_PI * 3;

    /**
     * enum to determine the type of interpolation
     */
    private enum Type {

        Translation, Rotation, Scale;
    }

    /**
     * Inner Rotation type class to kep track on a rotation Euler angle
     */
    protected class Rotation {

        /**
         * The rotation Quaternion
         */
        Quaternion rotation = new Quaternion();
        /**
         * This rotation expressed in Euler angles
         */
        Vector3f eulerAngles = new Vector3f();
        /**
         * the index of the parent key frame is this keyFrame is a splitted rotation
         */
        int masterKeyFrame = -1;

        public Rotation() {
            rotation.loadIdentity();
        }

        void set(Quaternion rot) {
            rotation.set(rot);
            float[] a = new float[3];
            rotation.toAngles(a);
            eulerAngles.set(a[0], a[1], a[2]);
        }

        void set(float x, float y, float z) {
            float[] a = {x, y, z};
            rotation.fromAngles(a);
            eulerAngles.set(x, y, z);
        }
    }
    /**
     * Name of the animation
     */
    protected String name;
    /**
     * frames per seconds
     */
    protected int fps;
    /**
     * Animation duration in seconds
     */
    protected float duration;
    /**
     * total number of frames
     */
    protected int totalFrames;
    /**
     * time per frame
     */
    protected float tpf;
    /**
     * Time array for this animation
     */
    protected float[] times;
    /**
     * Translation array for this animation
     */
    protected Vector3f[] translations;
    /**
     * rotation array for this animation
     */
    protected Quaternion[] rotations;
    /**
     * scales array for this animation
     */
    protected Vector3f[] scales;
    /**
     * The map of keyFrames to compute the animation. The key is the index of the frame
     */
    protected Vector3f[] keyFramesTranslation;
    protected Vector3f[] keyFramesScale;
    protected Rotation[] keyFramesRotation;

    /**
     * Creates and AnimationHelper
     * @param duration the desired duration for the resulting animation
     * @param name the name of the resulting animation
     */
    public AnimationFactory(float duration, String name) {
        this(duration, name, 30);
    }

    /**
     * Creates and AnimationHelper
     * @param duration the desired duration for the resulting animation
     * @param name the name of the resulting animation
     * @param fps the number of frames per second for this animation (default is 30)
     */
    public AnimationFactory(float duration, String name, int fps) {
        this.name = name;
        this.duration = duration;
        this.fps = fps;
        totalFrames = (int) (fps * duration) + 1;
        tpf = 1 / (float) fps;
        times = new float[totalFrames];
        translations = new Vector3f[totalFrames];
        rotations = new Quaternion[totalFrames];
        scales = new Vector3f[totalFrames];
        keyFramesTranslation = new Vector3f[totalFrames];
        keyFramesTranslation[0] = new Vector3f();
        keyFramesScale = new Vector3f[totalFrames];
        keyFramesScale[0] = new Vector3f(1, 1, 1);
        keyFramesRotation = new Rotation[totalFrames];
        keyFramesRotation[0] = new Rotation();

    }

    /**
     * Adds a key frame for the given Transform at the given time
     * @param time the time at which the keyFrame must be inserted
     * @param transform the transforms to use for this keyFrame
     */
    public void addTimeTransform(float time, Transform transform) {
        addKeyFrameTransform((int) (time / tpf), transform);
    }

    /**
     * Adds a key frame for the given Transform at the given keyFrame index
     * @param keyFrameIndex the index at which the keyFrame must be inserted
     * @param transform the transforms to use for this keyFrame
     */
    public void addKeyFrameTransform(int keyFrameIndex, Transform transform) {
        addKeyFrameTranslation(keyFrameIndex, transform.getTranslation());
        addKeyFrameScale(keyFrameIndex, transform.getScale());
        addKeyFrameRotation(keyFrameIndex, transform.getRotation());
    }

    /**
     * Adds a key frame for the given translation at the given time
     * @param time the time at which the keyFrame must be inserted
     * @param translation the translation to use for this keyFrame
     */
    public void addTimeTranslation(float time, Vector3f translation) {
        addKeyFrameTranslation((int) (time / tpf), translation);
    }

    /**
     * Adds a key frame for the given translation at the given keyFrame index
     * @param keyFrameIndex the index at which the keyFrame must be inserted
     * @param translation the translation to use for this keyFrame
     */
    public void addKeyFrameTranslation(int keyFrameIndex, Vector3f translation) {
        Vector3f t = getTranslationForFrame(keyFrameIndex);
        t.set(translation);
    }

    /**
     * Adds a key frame for the given rotation at the given time
     * This can't be used if the interpolated angle is higher than PI (180°)
     * Use #addTimeRotationAngles(float time, float x, float y, float z)}  instead that uses Euler angles rotations.     * 
     * @param time the time at which the keyFrame must be inserted
     * @param rotation the rotation Quaternion to use for this keyFrame
     * see #addTimeRotationAngles(float time, float x, float y, float z) 
     */
    public void addTimeRotation(float time, Quaternion rotation) {
        addKeyFrameRotation((int) (time / tpf), rotation);
    }

    /**
     * Adds a key frame for the given rotation at the given keyFrame index
     * This can't be used if the interpolated angle is higher than PI (180°)
     * Use #addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z)} instead that uses Euler angles rotations.
     * @param keyFrameIndex the index at which the keyFrame must be inserted
     * @param rotation the rotation Quaternion to use for this keyFrame
     * see #addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z) 
     */
    public void addKeyFrameRotation(int keyFrameIndex, Quaternion rotation) {
        Rotation r = getRotationForFrame(keyFrameIndex);
        r.set(rotation);
    }

    /**
     * Adds a key frame for the given rotation at the given time.
     * Rotation is expressed by Euler angles values in radians.
     * Note that the generated rotation will be stored as a quaternion and interpolated using a spherical linear interpolation (slerp)
     * Hence, this method may create intermediate keyFrames if the interpolation angle is higher than PI to ensure continuity in animation
     * 
     * @param time the time at which the keyFrame must be inserted
     * @param x the rotation around the x axis (aka yaw) in radians
     * @param y the rotation around the y axis (aka roll) in radians
     * @param z the rotation around the z axis (aka pitch) in radians
     */
    public void addTimeRotationAngles(float time, float x, float y, float z) {
        addKeyFrameRotationAngles((int) (time / tpf), x, y, z);
    }

    /**
     * Adds a key frame for the given rotation at the given key frame index.
     * Rotation is expressed by Euler angles values in radians.
     * Note that the generated rotation will be stored as a quaternion and interpolated using a spherical linear interpolation (slerp)
     * Hence, this method may create intermediate keyFrames if the interpolation angle is higher than PI to ensure continuity in animation
     * 
     * @param keyFrameIndex the index at which the keyFrame must be inserted
     * @param x the rotation around the x axis (aka yaw) in radians
     * @param y the rotation around the y axis (aka roll) in radians
     * @param z the rotation around the z axis (aka pitch) in radians
     */
    public void addKeyFrameRotationAngles(int keyFrameIndex, float x, float y, float z) {
        Rotation r = getRotationForFrame(keyFrameIndex);
        r.set(x, y, z);

        // if the delta of euler angles is higher than PI, we create intermediate keyframes
        // since we are using quaternions and slerp for rotation interpolation, we cannot interpolate over an angle higher than PI
        int prev = getPreviousKeyFrame(keyFrameIndex, keyFramesRotation);
        if (prev != -1) {
            //previous rotation keyframe
            Rotation prevRot = keyFramesRotation[prev];
            //the maximum delta angle (x,y or z)
            float delta = Math.max(Math.abs(x - prevRot.eulerAngles.x), Math.abs(y - prevRot.eulerAngles.y));
            delta = Math.max(delta, Math.abs(z - prevRot.eulerAngles.z));
            //if delta > PI we have to create intermediates key frames
            if (delta >= FastMath.PI) {
                //frames delta
                int dF = keyFrameIndex - prev;
                //angle per frame for x,y ,z
                float dXAngle = (x - prevRot.eulerAngles.x) / (float) dF;
                float dYAngle = (y - prevRot.eulerAngles.y) / (float) dF;
                float dZAngle = (z - prevRot.eulerAngles.z) / (float) dF;

                // the keyFrame step
                int keyStep = (int) (((float) (dF)) / delta * (float) EULER_STEP);
                // the current keyFrame
                int cursor = prev + keyStep;
                while (cursor < keyFrameIndex) {
                    //for each step we create a new rotation by interpolating the angles
                    Rotation dr = getRotationForFrame(cursor);
                    dr.masterKeyFrame = keyFrameIndex;
                    dr.set(prevRot.eulerAngles.x + cursor * dXAngle, prevRot.eulerAngles.y + cursor * dYAngle, prevRot.eulerAngles.z + cursor * dZAngle);
                    cursor += keyStep;
                }

            }
        }

    }

    /**
     * Adds a key frame for the given scale at the given time
     * @param time the time at which the keyFrame must be inserted
     * @param scale the scale to use for this keyFrame
     */
    public void addTimeScale(float time, Vector3f scale) {
        addKeyFrameScale((int) (time / tpf), scale);
    }

    /**
     * Adds a key frame for the given scale at the given keyFrame index
     * @param keyFrameIndex the index at which the keyFrame must be inserted
     * @param scale the scale to use for this keyFrame
     */
    public void addKeyFrameScale(int keyFrameIndex, Vector3f scale) {
        Vector3f s = getScaleForFrame(keyFrameIndex);
        s.set(scale);
    }

    /**
     * returns the translation for a given frame index
     * creates the translation if it doesn't exists
     * @param keyFrameIndex index
     * @return the translation
     */
    private Vector3f getTranslationForFrame(int keyFrameIndex) {
        if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
            throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
        }
        Vector3f v = keyFramesTranslation[keyFrameIndex];
        if (v == null) {
            v = new Vector3f();
            keyFramesTranslation[keyFrameIndex] = v;
        }
        return v;
    }

    /**
     * returns the scale for a given frame index
     * creates the scale if it doesn't exists
     * @param keyFrameIndex index
     * @return the scale
     */
    private Vector3f getScaleForFrame(int keyFrameIndex) {
        if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
            throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
        }
        Vector3f v = keyFramesScale[keyFrameIndex];
        if (v == null) {
            v = new Vector3f();
            keyFramesScale[keyFrameIndex] = v;
        }
        return v;
    }

    /**
     * returns the rotation for a given frame index
     * creates the rotation if it doesn't exists
     * @param keyFrameIndex index
     * @return the rotation
     */
    private Rotation getRotationForFrame(int keyFrameIndex) {
        if (keyFrameIndex < 0 || keyFrameIndex > totalFrames) {
            throw new ArrayIndexOutOfBoundsException("keyFrameIndex must be between 0 and " + totalFrames + " (received " + keyFrameIndex + ")");
        }
        Rotation v = keyFramesRotation[keyFrameIndex];
        if (v == null) {
            v = new Rotation();
            keyFramesRotation[keyFrameIndex] = v;
        }
        return v;
    }

    /**
     * Creates an Animation based on the keyFrames previously added to the helper.
     * @return the generated animation 
     */
    public Animation buildAnimation() {
        interpolateTime();
        interpolate(keyFramesTranslation, Type.Translation);
        interpolate(keyFramesRotation, Type.Rotation);
        interpolate(keyFramesScale, Type.Scale);

        SpatialTrack spatialTrack = new SpatialTrack(times, translations, rotations, scales);

        //creating the animation
        Animation spatialAnimation = new Animation(name, duration);
        spatialAnimation.setTracks(new SpatialTrack[]{spatialTrack});

        return spatialAnimation;
    }

    /**
     * interpolates time values
     */
    private void interpolateTime() {
        for (int i = 0; i < totalFrames; i++) {
            times[i] = i * tpf;
        }
    }

    /**
     * Interpolates over the key frames for the given keyFrame array and the given type of transform
     * @param keyFrames the keyFrames array
     * @param type the type of transforms
     */
    private void interpolate(Object[] keyFrames, Type type) {
        int i = 0;
        while (i < totalFrames) {
            //fetching the next keyFrame index transform in the array
            int key = getNextKeyFrame(i, keyFrames);
            if (key != -1) {
                //computing the frame span to interpolate over
                int span = key - i;
                //interating over the frames
                for (int j = i; j <= key; j++) {
                    // computing interpolation value
                    float val = (float) (j - i) / (float) span;
                    //interpolationg depending on the transform type
                    switch (type) {
                        case Translation:
                            translations[j] = FastMath.interpolateLinear(val, (Vector3f) keyFrames[i], (Vector3f) keyFrames[key]);
                            break;
                        case Rotation:
                            Quaternion rot = new Quaternion();
                            rotations[j] = rot.slerp(((Rotation) keyFrames[i]).rotation, ((Rotation) keyFrames[key]).rotation, val);
                            break;
                        case Scale:
                            scales[j] = FastMath.interpolateLinear(val, (Vector3f) keyFrames[i], (Vector3f) keyFrames[key]);
                            break;
                    }
                }
                //jumping to the next keyFrame
                i = key;
            } else {
                //No more key frame, filling the array witht he last transform computed.
                for (int j = i; j < totalFrames; j++) {

                    switch (type) {
                        case Translation:
                            translations[j] = ((Vector3f) keyFrames[i]).clone();
                            break;
                        case Rotation:
                            rotations[j] = ((Quaternion) ((Rotation) keyFrames[i]).rotation).clone();
                            break;
                        case Scale:
                            scales[j] = ((Vector3f) keyFrames[i]).clone();
                            break;
                    }
                }
                //we're done
                i = totalFrames;
            }
        }
    }

    /**
     * Get the index of the next keyFrame that as a transform
     * @param index the start index
     * @param keyFrames the keyFrames array
     * @return the index of the next keyFrame
     */
    private int getNextKeyFrame(int index, Object[] keyFrames) {
        for (int i = index + 1; i < totalFrames; i++) {
            if (keyFrames[i] != null) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Get the index of the previous keyFrame that as a transform
     * @param index the start index
     * @param keyFrames the keyFrames array
     * @return the index of the previous keyFrame
     */
    private int getPreviousKeyFrame(int index, Object[] keyFrames) {
        for (int i = index - 1; i >= 0; i--) {
            if (keyFrames[i] != null) {
                return i;
            }
        }
        return -1;
    }
}
