/**
 * Copyright (c) 2005-2006, Sun Microsystems, Inc
 * 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 the TimingFramework project 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 org.jdesktop.animation.timing.interpolation;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;

/**
 * This class enables automating animation of object properties.
 * The class is a TimingTarget, and should be used as a target of
 * timing events from an Animator.  These events will be used to 
 * change a specified property over time, according to how the
 * PropertySetter is constructed.
 * <p>
 * For example, here is an animation of the "background" property
 * of some object "obj" from blue to red over a period of one second:
 * <pre>
 *  PropertySetter ps = new PropertySetter(obj, "background", Color.BLUE, 
 *                                         Color.RED);
 *  Animator anim = new Animator(1000, ps);
 *  anim.start();
 * </pre>
 * Here is the same animation, created using one of the utility
 * factory methods that returns an animator:
 * <pre>
 *  Animator animator = PropertySetter.createAnimator(1000, obj, "background", 
 *                                                    Color.BLUE, Color.RED);
 *  anim.start();
 * </pre>
 * <p>
 * More complex animations can be created by passing in multiple values
 * for the property to take on, for example:
 * <pre>
 *  Animator animator = PropertySetter.createAnimator(1000, obj, "background", 
 *                                                    Color.BLUE, Color.RED, 
 *                                                    Color.GREEN);
 *  anim.start();
 * </pre>
 * It is also possible to define more involved and tightly-controlled
 * steps in the animation, including the times between the values and
 * how the values are interpolated by using the constructor that takes
 * a {@link KeyFrames} object.  KeyFrames defines the fractional times at which
 * an object takes on specific values, the values to assume at those times,
 * and the method of interpolation between those values.  For example,
 * here is the same animation as above, specified through KeyFrames, where the
 * RED color will be set 10% of the way through the animation (note that
 * we are not setting an Interpolator, so the timing intervals will use the
 * default LinearInterpolator):
 * <pre>
 *  KeyValues vals = KeyValues.create(Color.BLUE, Color.RED, Color.GREEN);
 *  KeyTimes times = new KeyTimes(0.0f, .1f, 1.0f);
 *  KeyFrames frames = new KeyFrames(vals, times);
 *  Animator animator = PropertySetter.createAnimator(1000, obj, "background", 
 *                                                    frames);
 *  anim.start();
 * </pre>
 *
 * @author Chet
 */
public class PropertySetter extends TimingTargetAdapter {

    private Object object;
    private String propertyName;
    private KeyFrames keyFrames;
    private Method propertySetter;
    private Method propertyGetter;

    /**
     * Utility method that constructs a PropertySetter and an Animator using
     * that PropertySetter and returns the Animator
     * @param duration the duration, in milliseconds, of the animation
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in creating the
     * KeyValues for the keyFrames, the animation
     * will also need a "getFoo" method.
     * @param keyFrames the fractional times, values, and interpolation
     * to be used in calculating the values set on the object's property.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public static Animator createAnimator(int duration, Object object, 
            String propertyName, KeyFrames keyFrames) {
        PropertySetter ps = new PropertySetter(object, propertyName, keyFrames);
        Animator animator = new Animator(duration, ps);
        return animator;
    }

    /**
     * Utility method that constructs a PropertySetter and an Animator using
     * that PropertySetter and returns the Animator
     * @param duration the duration, in milliseconds, of the animation
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in creating the
     * KeyValues for the keyFrames, the animation
     * will also need a "getFoo" method.
     * @param params the values that the object will take on during the
     * animation.  Internally, a KeyFrames object will be created that
     * will use times that split the total duration evenly. Supplying
     * only one value for params implies that this is a "to" animation
     * whose intial value will be determined dynamically when the animation
     * starts.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public static <T> Animator createAnimator(int duration, 
            Object object, String propertyName, T... params) {
        PropertySetter ps = new PropertySetter(object, propertyName, params);
        Animator animator = new Animator(duration, ps);
        return animator;
    }
    
    /**
     * Utility method that constructs a PropertySetter and an Animator using
     * that PropertySetter and returns the Animator
     * 
     * @param duration the duration, in milliseconds, of the animation
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in creating the
     * KeyValues for the keyFrames, the animation
     * will also need a "getFoo" method.
     * @param evaluator KeyValues knows how to calculate intermediate values
     * for many built-in types, but if you want to supply values in 
     * types not understood by KeyValues, you will need to supply your
     * own Evaluator.
     * @param params the values that the object will take on during the
     * animation.  Internally, a KeyFrames object will be created that
     * will use times that split the total duration evenly. Supplying
     * only one value for params implies that this is a "to" animation
     * whose intial value will be determined dynamically when the animation
     * starts.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public static <T> Animator createAnimator(int duration, 
            Object object, String propertyName, 
            Evaluator evaluator, T... params) {
        PropertySetter ps = new PropertySetter(object, propertyName, evaluator, 
                params);
        Animator animator = new Animator(duration, ps);
        return animator;
    }

    /**
     * Constructor for a PropertySetter where the values the propert
     * takes on during the animation are specified in a {@link KeyFrames}
     * object.
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in creating the
     * KeyValues for the keyFrames, the animation
     * will also need a "getFoo" method.
     * @param keyFrames the fractional times, values, and interpolation
     * to be used in calculating the values set on the object's property.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public PropertySetter(Object object, String propertyName,
            KeyFrames keyFrames) {
        this.object = object;
        this.propertyName = propertyName;
        this.keyFrames = keyFrames;
        try {
            setupMethodInfo();
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Bad property name (" +
                    propertyName +"): could not find " +
                    "an appropriate setter or getter method for that property");
        }
    }
    
    /**
     * Constructor for a PropertySetter where the values the propert
     * takes on during the animation are specified in a {@link KeyFrames}
     * object.
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in params, the animation
     * will also need a "getFoo" method.
     * @param params the values that the object will take on during the
     * animation.  Internally, a KeyFrames object will be created that
     * will use times that split the total duration evenly. Supplying
     * only one value for params implies that this is a "to" animation
     * whose intial value will be determined dynamically when the animation
     * starts.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public <T> PropertySetter(Object object, String propertyName, T... params) {
        this(object, propertyName, new KeyFrames(KeyValues.create(params)));
    }
    
    /**
     * Constructor for a PropertySetter where the values the propert
     * takes on during the animation are specified in a {@link KeyFrames}
     * object.
     * 
     * @param object the object whose property will be animated
     * @param propertyName the name of the property to be animated.  For
     * any propertyName "foo" there must be an accessible "setFoo" method
     * on the object.  If only one value is supplied in params, the animation
     * will also need a "getFoo" method.
     * @param evaluator KeyValues knows how to calculate intermediate values
     * for many built-in types, but if you want to supply values in 
     * types not understood by KeyValues, you will need to supply your
     * own Evaluator.
     * @param params the values that the object will take on during the
     * animation.  Internally, a KeyFrames object will be created that
     * will use times that split the total duration evenly. Supplying
     * only one value for params implies that this is a "to" animation
     * whose intial value will be determined dynamically when the animation
     * starts.
     * @throws IllegalArgumentException if appropriate set/get methods
     * cannot be found for propertyName.
     */
    public <T> PropertySetter(Object object, String propertyName, 
            Evaluator evaluator, T... params) {
        this(object, propertyName, 
                new KeyFrames(KeyValues.create(evaluator, params)));
    }
    
    /**
     * Translates the property name used in the PropertyRange object into
     * the appropriate Method in the Object to be modified.  This uses
     * standard JavaBean naming convention (e.g., propertyName would
     * become setPropertyName).
     * @throws NoSuchMethodException if there is no method on the
     * object with the appropriate name
     * @throws SecurityException if the application does not have
     * appropriate permissions to request access to the Method
     */
    private void setupMethodInfo() throws NoSuchMethodException {
        try {
            String firstChar = propertyName.substring(0, 1);
            String remainder = propertyName.substring(1);
            Class propertyType = getType();
            String propertySetterName = "set" + firstChar.toUpperCase() + remainder;

            PropertyDescriptor prop = new PropertyDescriptor(propertyName, object.getClass(),
                    null, propertySetterName);
            propertySetter = prop.getWriteMethod();
            if (isToAnimation()) {
                // Only need the getter for "to" animations
                String propertyGetterName = "get" + firstChar.toUpperCase() + 
                        remainder;
                prop = new PropertyDescriptor(propertyName, 
                        object.getClass(), propertyGetterName, null);
                propertyGetter = prop.getReadMethod();
            }
        } catch (Exception e) {
            throw new NoSuchMethodException("Cannot find property methods: " + e);
        }
    }
    
    //
    // TimingTargetAdapter overrides
    //
    
    /**
     * Called by Animator to signal that the timer is about to start.
     * The only operation performed in this method is setting an initial
     * value for the animation if appropriate; this accounts
     * for "to" animations, which need to start from the current value.
     * <p>
     * This method is not intended for use by application code.
     */
    public void begin() {
        if (isToAnimation()) {
            try {
                setStartValue(propertyGetter.invoke(object));
            } catch (Exception e) {
                System.out.println("Problem setting start value on object " +
                        object + ": " + e);
            }
        }
    }

    /**
     * Called from Animator to signal a timing event.  This
     * causes PropertySetter to invoke the property-setting method (as 
     * specified by the propertyName in the constructor) with the
     * appropriate value of the property given the range of values in the
     * KeyValues object and the fraction of the timing cycle that has
     * elapsed.
     * <p>
     * This method is not intended for use by application code.
     */
    public void timingEvent(float fraction) {
        try {
            setValue(object, propertySetter, fraction);
        } catch (Exception e) {
            System.out.println("Problem calling setValue in " +
                    "PropertySetter.timingEvent: " + e);
        }
    }
    
    private String getPropertyName() {
        return propertyName;
    }
    
    /**
     * Called during begin() if this is a "to" animation, to set the start
     * value of the animation to whatever the current value is.
     */
    private void setStartValue(Object object) {
        keyFrames.getKeyValues().setStartValue(object);
    }
    
    /**
     * Sets the appropriate value on the property given the current fraction
     */
    private void setValue(Object object, Method method, float fraction) {
        try {
            method.invoke(object, keyFrames.getValue(fraction));
        } catch (Exception e) {
            System.out.println("Problem invoking method " +
                    propertySetter + " in object " + object + 
                    " in setValue" + e);
        }
    }
        
    /**
     * Returns the type used in this property setter (defers to KeyFrames
     * for this information).
     */
    private Class getType() {
        return keyFrames.getType();
    }
    
    /**
     * Utility method for determining whether this is a "to" animation
     * (true if the first value is null).
     */
    private boolean isToAnimation() {
        return (keyFrames.getKeyValues().isToAnimation());
    }
    
}
