/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * 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 'jMonkeyEngine' 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.
 */
package com.jme3.animation;

import com.jme3.math.FastMath;
import com.jme3.util.TempVars;
import java.util.BitSet;

/**
 * <code>AnimChannel</code> provides controls, such as play, pause,
 * fast forward, etc, for an animation. The animation
 * channel may influence the entire model or specific bones of the model's
 * skeleton. A single model may have multiple animation channels influencing
 * various parts of its body. For example, a character model may have an
 * animation channel for its feet, and another for its torso, and
 * the animations for each channel are controlled independently.
 * 
 * @author Kirill Vainer
 */
public final class AnimChannel {

    private static final float DEFAULT_BLEND_TIME = 0.15f;
    
    private AnimControl control;

    private BitSet affectedBones;

    private Animation animation;
    private Animation blendFrom;
    private float time;
    private float speed;
    private float timeBlendFrom;
    private float blendTime;
    private float speedBlendFrom;
    private boolean notified=false;

    private LoopMode loopMode, loopModeBlendFrom;
    
    private float blendAmount = 1f;
    private float blendRate   = 0;
    
    AnimChannel(AnimControl control){
        this.control = control;
    }

    /**
     * Returns the parent control of this AnimChannel.
     * 
     * @return the parent control of this AnimChannel.
     * @see AnimControl
     */
    public AnimControl getControl() {
        return control;
    }
    
    /**
     * @return The name of the currently playing animation, or null if
     * none is assigned.
     *
     * @see AnimChannel#setAnim(java.lang.String) 
     */
    public String getAnimationName() {
        return animation != null ? animation.getName() : null;
    }

    /**
     * @return The loop mode currently set for the animation. The loop mode
     * determines what will happen to the animation once it finishes
     * playing.
     * 
     * For more information, see the LoopMode enum class.
     * @see LoopMode
     * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
     */
    public LoopMode getLoopMode() {
        return loopMode;
    }

    /**
     * @param loopMode Set the loop mode for the channel. The loop mode
     * determines what will happen to the animation once it finishes
     * playing.
     *
     * For more information, see the LoopMode enum class.
     * @see LoopMode
     */
    public void setLoopMode(LoopMode loopMode) {
        this.loopMode = loopMode;
    }

    /**
     * @return The speed that is assigned to the animation channel. The speed
     * is a scale value starting from 0.0, at 1.0 the animation will play
     * at its default speed.
     *
     * @see AnimChannel#setSpeed(float)
     */
    public float getSpeed() {
        return speed;
    }

    /**
     * @param speed Set the speed of the animation channel. The speed
     * is a scale value starting from 0.0, at 1.0 the animation will play
     * at its default speed.
     */
    public void setSpeed(float speed) {
        this.speed = speed;
        if(blendTime>0){
            this.speedBlendFrom = speed;
            blendTime = Math.min(blendTime, animation.getLength() / speed);  
            blendRate = 1/ blendTime;
        }
    }

    /**
     * @return The time of the currently playing animation. The time
     * starts at 0 and continues on until getAnimMaxTime().
     *
     * @see AnimChannel#setTime(float)
     */
    public float getTime() {
        return time;
    }

    /**
     * @param time Set the time of the currently playing animation, the time
     * is clamped from 0 to {@link #getAnimMaxTime()}. 
     */
    public void setTime(float time) {
        this.time = FastMath.clamp(time, 0, getAnimMaxTime());
    }

    /**
     * @return The length of the currently playing animation, or zero
     * if no animation is playing.
     *
     * @see AnimChannel#getTime()
     */
    public float getAnimMaxTime(){
        return animation != null ? animation.getLength() : 0f;
    }

    /**
     * Set the current animation that is played by this AnimChannel.
     * <p>
     * This resets the time to zero, and optionally blends the animation
     * over <code>blendTime</code> seconds with the currently playing animation.
     * Notice that this method will reset the control's speed to 1.0.
     *
     * @param name The name of the animation to play
     * @param blendTime The blend time over which to blend the new animation
     * with the old one. If zero, then no blending will occur and the new
     * animation will be applied instantly.
     */
    public void setAnim(String name, float blendTime){
        if (name == null)
            throw new IllegalArgumentException("name cannot be null");

        if (blendTime < 0f)
            throw new IllegalArgumentException("blendTime cannot be less than zero");

        Animation anim = control.animationMap.get(name);
        if (anim == null)
            throw new IllegalArgumentException("Cannot find animation named: '"+name+"'");

        control.notifyAnimChange(this, name);

        if (animation != null && blendTime > 0f){
            this.blendTime = blendTime;
            // activate blending
            blendTime = Math.min(blendTime, anim.getLength() / speed);            
            blendFrom = animation;
            timeBlendFrom = time;
            speedBlendFrom = speed;
            loopModeBlendFrom = loopMode;
            blendAmount = 0f;
            blendRate   = 1f / blendTime;
        }else{
            blendFrom = null;
        }

        animation = anim;
        time = 0;
        speed = 1f;
        loopMode = LoopMode.Loop;
        notified = false;
    }

    /**
     * Set the current animation that is played by this AnimChannel.
     * <p>
     * See {@link #setAnim(java.lang.String, float)}.
     * The blendTime argument by default is 150 milliseconds.
     * 
     * @param name The name of the animation to play
     */
    public void setAnim(String name){
        setAnim(name, DEFAULT_BLEND_TIME);
    }

    /**
     * Add all the bones of the model's skeleton to be
     * influenced by this animation channel.
     */
    public void addAllBones() {
        affectedBones = null;
    }

    /**
     * Add a single bone to be influenced by this animation channel.
     */
    public void addBone(String name) {
        addBone(control.getSkeleton().getBone(name));
    }

    /**
     * Add a single bone to be influenced by this animation channel.
     */
    public void addBone(Bone bone) {
        int boneIndex = control.getSkeleton().getBoneIndex(bone);
        if(affectedBones == null) {
            affectedBones = new BitSet(control.getSkeleton().getBoneCount());
        }
        affectedBones.set(boneIndex);
    }

    /**
     * Add bones to be influenced by this animation channel starting from the
     * given bone name and going toward the root bone.
     */
    public void addToRootBone(String name) {
        addToRootBone(control.getSkeleton().getBone(name));
    }

    /**
     * Add bones to be influenced by this animation channel starting from the
     * given bone and going toward the root bone.
     */
    public void addToRootBone(Bone bone) {
        addBone(bone);
        while (bone.getParent() != null) {
            bone = bone.getParent();
            addBone(bone);
        }
    }

    /**
     * Add bones to be influenced by this animation channel, starting
     * from the given named bone and going toward its children.
     */
    public void addFromRootBone(String name) {
        addFromRootBone(control.getSkeleton().getBone(name));
    }

    /**
     * Add bones to be influenced by this animation channel, starting
     * from the given bone and going toward its children.
     */
    public void addFromRootBone(Bone bone) {
        addBone(bone);
        if (bone.getChildren() == null)
            return;
        for (Bone childBone : bone.getChildren()) {
            addBone(childBone);
            addFromRootBone(childBone);
        }
    }

    BitSet getAffectedBones(){
        return affectedBones;
    }
    
    public void reset(boolean rewind){
        if(rewind){
            setTime(0);        
            if(control.getSkeleton()!=null){
                control.getSkeleton().resetAndUpdate();
            }else{
                TempVars vars = TempVars.get();
                update(0, vars);
                vars.release();    
            }
        }
        animation = null;
        notified = false;
    }

    void update(float tpf, TempVars vars) {
        if (animation == null)
            return;

        if (blendFrom != null && blendAmount != 1.0f){
            // The blendFrom anim is set, the actual animation
            // playing will be set 
//            blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
            blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
            
            timeBlendFrom += tpf * speedBlendFrom;
            timeBlendFrom = AnimationUtils.clampWrapTime(timeBlendFrom,
                                          blendFrom.getLength(),
                                          loopModeBlendFrom);
            if (timeBlendFrom < 0){
                timeBlendFrom = -timeBlendFrom;
                speedBlendFrom = -speedBlendFrom;
            }

            blendAmount += tpf * blendRate;
            if (blendAmount > 1f){
                blendAmount = 1f;
                blendFrom = null;
            }
        }
        
        animation.setTime(time, blendAmount, control, this, vars);
        time += tpf * speed;      
        if (animation.getLength() > 0){
            if (!notified && (time >= animation.getLength() || time < 0)) {
                if (loopMode == LoopMode.DontLoop) {
                    // Note that this flag has to be set before calling the notify
                    // since the notify may start a new animation and then unset
                    // the flag.
                    notified = true;
                }
                control.notifyAnimCycleDone(this, animation.getName());
            } 
        }
        time = AnimationUtils.clampWrapTime(time, animation.getLength(), loopMode);
        if (time < 0){
            // Negative time indicates that speed should be inverted
            // (for cycle loop mode only)
            time = -time;
            speed = -speed;
        }
    }
}
