/*global define*/
define([
        '../Core/clone',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/EasingFunction',
        '../Core/getTimestamp',
        '../Core/TimeConstants',
        '../ThirdParty/Tween'
    ], function(
        clone,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        EasingFunction,
        getTimestamp,
        TimeConstants,
        TweenJS) {
    "use strict";

    /**
     * A tween is an animation that interpolates the properties of two objects using an {@link EasingFunction}.  Create
     * one using {@link Scene#tweens} and {@link TweenCollection#add} and related add functions.
     *
     * @alias Tween
     * @constructor
     *
     * @private
     */
    var Tween = function(tweens, tweenjs, startObject, stopObject, duration, delay, easingFunction, update, complete, cancel) {
        this._tweens = tweens;
        this._tweenjs = tweenjs;

        this._startObject = clone(startObject);
        this._stopObject = clone(stopObject);

        this._duration = duration;
        this._delay = delay;
        this._easingFunction = easingFunction;

        this._update = update;
        this._complete = complete;

        /**
         * The callback to call if the tween is canceled either because {@link Tween#cancelTween}
         * was called or because the tween was removed from the collection.
         *
         * @type {TweenCollection~TweenCancelledCallback}
         */
        this.cancel = cancel;

        /**
         * @private
         */
        this.needsStart = true;
    };

    defineProperties(Tween.prototype, {
        /**
         * An object with properties for initial values of the tween.  The properties of this object are changed during the tween's animation.
         * @memberof Tween.prototype
         *
         * @type {Object}
         * @readonly
         */
        startObject : {
            get : function() {
                return this._startObject;
            }
        },

        /**
         * An object with properties for the final values of the tween.
         * @memberof Tween.prototype
         *
         * @type {Object}
         * @readonly
         */
        stopObject : {
            get : function() {
                return this._stopObject;
            }
        },

        /**
         * The duration, in seconds, for the tween.  The tween is automatically removed from the collection when it stops.
         * @memberof Tween.prototype
         *
         * @type {Number}
         * @readonly
         */
        duration : {
            get : function() {
                return this._duration;
            }
        },

        /**
         * The delay, in seconds, before the tween starts animating.
         * @memberof Tween.prototype
         *
         * @type {Number}
         * @readonly
         */
        delay : {
            get : function() {
                return this._delay;
            }
        },

        /**
         * Determines the curve for animtion.
         * @memberof Tween.prototype
         *
         * @type {EasingFunction}
         * @readonly
         */
        easingFunction : {
            get : function() {
                return this._easingFunction;
            }
        },

        /**
         * The callback to call at each animation update (usually tied to the a rendered frame).
         * @memberof Tween.prototype
         *
         * @type {TweenCollection~TweenUpdateCallback}
         * @readonly
         */
        update : {
            get : function() {
                return this._update;
            }
        },

        /**
         * The callback to call when the tween finishes animating.
         * @memberof Tween.prototype
         *
         * @type {TweenCollection~TweenCompleteCallback}
         * @readonly
         */
        complete : {
            get : function() {
                return this._complete;
            }
        },

        /**
         * @memberof Tween.prototype
         *
         * @private
         */
        tweenjs : {
            get : function() {
                return this._tweenjs;
            }
        }
    });

    /**
     * Cancels the tween calling the {@link Tween#cancel} callback if one exists.  This
     * has no effect if the tween finished or was already canceled.
     */
    Tween.prototype.cancelTween = function() {
        this._tweens.remove(this);
    };

    /**
     * A collection of tweens for animating properties.  Commonly accessed using {@link Scene#tweens}.
     *
     * @alias TweenCollection
     * @constructor
     *
     * @private
     */
    var TweenCollection = function() {
        this._tweens = [];
    };

    defineProperties(TweenCollection.prototype, {
        /**
         * The number of tweens in the collection.
         * @memberof TweenCollection.prototype
         *
         * @type {Number}
         * @readonly
         */
        length : {
            get : function() {
                return this._tweens.length;
            }
        }
    });

    /**
     * Creates a tween for animating between two sets of properties.  The tween starts animating at the next call to {@link TweenCollection#update}, which
     * is implicit when {@link Viewer} or {@link CesiumWidget} render the scene.
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Object} options.startObject An object with properties for initial values of the tween.  The properties of this object are changed during the tween's animation.
     * @param {Object} options.stopObject An object with properties for the final values of the tween.
     * @param {Number} options.duration The duration, in seconds, for the tween.  The tween is automatically removed from the collection when it stops.
     * @param {Number} [options.delay=0.0] The delay, in seconds, before the tween starts animating.
     * @param {EasingFunction} [options.easingFunction=EasingFunction.LINEAR_NONE] Determines the curve for animtion.
     * @param {TweenCollection~TweenUpdateCallback} [options.update] The callback to call at each animation update (usually tied to the a rendered frame).
     * @param {TweenCollection~TweenCompleteCallback} [options.complete] The callback to call when the tween finishes animating.
     * @param {TweenCollection~TweenCancelledCallback} [options.cancel] The callback to call if the tween is canceled either because {@link Tween#cancelTween} was called or because the tween was removed from the collection.
     * @returns {Tween} The tween.
     *
     * @exception {DeveloperError} options.duration must be positive.
     */
    TweenCollection.prototype.add = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        //>>includeStart('debug', pragmas.debug);
        if (!defined(options.startObject) || !defined(options.stopObject)) {
            throw new DeveloperError('options.startObject and options.stopObject are required.');
        }

        if (!defined(options.duration) || options.duration < 0.0) {
            throw new DeveloperError('options.duration is required and must be positive.');
        }
        //>>includeEnd('debug');

        if (options.duration === 0.0) {
            if (defined(options.complete)) {
                options.complete();
            }
            return new Tween(this);
        }

        var duration = options.duration / TimeConstants.SECONDS_PER_MILLISECOND;
        var delayInSeconds = defaultValue(options.delay, 0.0);
        var delay = delayInSeconds / TimeConstants.SECONDS_PER_MILLISECOND;
        var easingFunction = defaultValue(options.easingFunction, EasingFunction.LINEAR_NONE);

        var value = options.startObject;
        var tweenjs = new TweenJS.Tween(value);
        tweenjs.to(clone(options.stopObject), duration);
        tweenjs.delay(delay);
        tweenjs.easing(easingFunction);
        if (defined(options.update)) {
            tweenjs.onUpdate(function() {
                options.update(value);
            });
        }
        tweenjs.onComplete(defaultValue(options.complete, null));
        tweenjs.repeat(defaultValue(options._repeat, 0.0));

        var tween = new Tween(this, tweenjs, options.startObject, options.stopObject, options.duration, delayInSeconds, easingFunction, options.update, options.complete, options.cancel);
        this._tweens.push(tween);
        return tween;
    };

    /**
     * Creates a tween for animating a scalar property on the given object.  The tween starts animating at the next call to {@link TweenCollection#update}, which
     * is implicit when {@link Viewer} or {@link CesiumWidget} render the scene.
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Object} options.object The object containing the property to animate.
     * @param {String} options.property The name of the property to animate.
     * @param {Number} options.startValue The initial value.
     * @param {Number} options.stopValue The final value.
     * @param {Number} [options.duration=3.0] The duration, in seconds, for the tween.  The tween is automatically removed from the collection when it stops.
     * @param {Number} [options.delay=0.0] The delay, in seconds, before the tween starts animating.
     * @param {EasingFunction} [options.easingFunction=EasingFunction.LINEAR_NONE] Determines the curve for animtion.
     * @param {TweenCollection~TweenUpdateCallback} [options.update] The callback to call at each animation update (usually tied to the a rendered frame).
     * @param {TweenCollection~TweenCompleteCallback} [options.complete] The callback to call when the tween finishes animating.
     * @param {TweenCollection~TweenCancelledCallback} [options.cancel] The callback to call if the tween is canceled either because {@link Tween#cancelTween} was called or because the tween was removed from the collection.
     * @returns {Tween} The tween.
     *
     * @exception {DeveloperError} options.object must have the specified property.
     * @exception {DeveloperError} options.duration must be positive.
     */
    TweenCollection.prototype.addProperty = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var object = options.object;
        var property = options.property;
        var startValue = options.startValue;
        var stopValue = options.stopValue;

        //>>includeStart('debug', pragmas.debug);
        if (!defined(object) || !defined(options.property)) {
            throw new DeveloperError('options.object and options.property are required.');
        }
        if (!defined(object[property])) {
            throw new DeveloperError('options.object must have the specified property.');
        }
        if (!defined(startValue) || !defined(stopValue)) {
            throw new DeveloperError('options.startValue and options.stopValue are required.');
        }
        //>>includeEnd('debug');

        function update(value) {
            object[property] = value.value;
        }

        return this.add({
            startObject : {
                value : startValue
            },
            stopObject : {
                value : stopValue
            },
            duration : defaultValue(options.duration, 3.0),
            delay : options.delay,
            easingFunction : options.easingFunction,
            update : update,
            complete : options.complete,
            cancel : options.cancel,
            _repeat : options._repeat
        });
    };

    /**
     * Creates a tween for animating the alpha of all color uniforms on a {@link Material}.  The tween starts animating at the next call to {@link TweenCollection#update}, which
     * is implicit when {@link Viewer} or {@link CesiumWidget} render the scene.
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Material} options.material The material to animate.
     * @param {Number} [options.startValue=0.0] The initial alpha value.
     * @param {Number} [options.stopValue=1.0] The final alpha value.
     * @param {Number} [options.duration=3.0] The duration, in seconds, for the tween.  The tween is automatically removed from the collection when it stops.
     * @param {Number} [options.delay=0.0] The delay, in seconds, before the tween starts animating.
     * @param {EasingFunction} [options.easingFunction=EasingFunction.LINEAR_NONE] Determines the curve for animtion.
     * @param {TweenCollection~TweenUpdateCallback} [options.update] The callback to call at each animation update (usually tied to the a rendered frame).
     * @param {TweenCollection~TweenCompleteCallback} [options.complete] The callback to call when the tween finishes animating.
     * @param {TweenCollection~TweenCancelledCallback} [options.cancel] The callback to call if the tween is canceled either because {@link Tween#cancelTween} was called or because the tween was removed from the collection.
     * @returns {Tween} The tween.
     *
     * @exception {DeveloperError} material has no properties with alpha components.
     * @exception {DeveloperError} options.duration must be positive.
     */
    TweenCollection.prototype.addAlpha = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var material = options.material;

        //>>includeStart('debug', pragmas.debug);
        if (!defined(material)) {
            throw new DeveloperError('options.material is required.');
        }
        //>>includeEnd('debug');

        var properties = [];

        for (var property in material.uniforms) {
            if (material.uniforms.hasOwnProperty(property) &&
                defined(material.uniforms[property]) &&
                defined(material.uniforms[property].alpha)) {
                properties.push(property);
            }
        }

        //>>includeStart('debug', pragmas.debug);
        if (properties.length === 0) {
            throw new DeveloperError('material has no properties with alpha components.');
        }
        //>>includeEnd('debug');

        function update(value) {
            var length = properties.length;
            for (var i = 0; i < length; ++i) {
                material.uniforms[properties[i]].alpha = value.alpha;
            }
        }

        return this.add({
            startObject : {
                alpha : defaultValue(options.startValue, 0.0)  // Default to fade in
            },
            stopObject : {
                alpha : defaultValue(options.stopValue, 1.0)
            },
            duration : defaultValue(options.duration, 3.0),
            delay : options.delay,
            easingFunction : options.easingFunction,
            update : update,
            complete : options.complete,
            cancel : options.cancel
        });
    };

    /**
     * Creates a tween for animating the offset uniform of a {@link Material}.  The tween starts animating at the next call to {@link TweenCollection#update}, which
     * is implicit when {@link Viewer} or {@link CesiumWidget} render the scene.
     *
     * @param {Object} [options] Object with the following properties:
     * @param {Material} options.material The material to animate.
     * @param {Number} options.startValue The initial alpha value.
     * @param {Number} options.stopValue The final alpha value.
     * @param {Number} [options.duration=3.0] The duration, in seconds, for the tween.  The tween is automatically removed from the collection when it stops.
     * @param {Number} [options.delay=0.0] The delay, in seconds, before the tween starts animating.
     * @param {EasingFunction} [options.easingFunction=EasingFunction.LINEAR_NONE] Determines the curve for animtion.
     * @param {TweenCollection~TweenUpdateCallback} [options.update] The callback to call at each animation update (usually tied to the a rendered frame).
     * @param {TweenCollection~TweenCancelledCallback} [options.cancel] The callback to call if the tween is canceled either because {@link Tween#cancelTween} was called or because the tween was removed from the collection.
     * @returns {Tween} The tween.
     *
     * @exception {DeveloperError} material.uniforms must have an offset property.
     * @exception {DeveloperError} options.duration must be positive.
     */
    TweenCollection.prototype.addOffsetIncrement = function(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var material = options.material;

        //>>includeStart('debug', pragmas.debug);
        if (!defined(material)) {
            throw new DeveloperError('material is required.');
        }
        if (!defined(material.uniforms.offset)) {
            throw new DeveloperError('material.uniforms must have an offset property.');
        }
        //>>includeEnd('debug');

        var uniforms = material.uniforms;
        return this.addProperty({
            object : uniforms,
            property : 'offset',
            startValue : uniforms.offset,
            stopValue :  uniforms.offset + 1,
            duration : options.duration,
            delay : options.delay,
            easingFunction : options.easingFunction,
            update : options.update,
            cancel : options.cancel,
            _repeat : Infinity
        });
    };

    /**
     * Removes a tween from the collection.
     * <p>
     * This calls the {@link Tween#cancel} callback if the tween has one.
     * </p>
     *
     * @param {Tween} tween The tween to remove.
     * @returns {Boolean} <code>true</code> if the tween was removed; <code>false</code> if the tween was not found in the collection.
     */
    TweenCollection.prototype.remove = function(tween) {
        if (!defined(tween)) {
            return false;
        }

        var index = this._tweens.indexOf(tween);
        if (index !== -1) {
            tween.tweenjs.stop();
            if (defined(tween.cancel)) {
                tween.cancel();
            }
            this._tweens.splice(index, 1);
            return true;
        }

        return false;
    };

    /**
     * Removes all tweens from the collection.
     * <p>
     * This calls the {@link Tween#cancel} callback for each tween that has one.
     * </p>
     */
    TweenCollection.prototype.removeAll = function() {
        var tweens = this._tweens;

        for (var i = 0; i < tweens.length; ++i) {
            var tween = tweens[i];
            tween.tweenjs.stop();
            if (defined(tween.cancel)) {
                tween.cancel();
            }
        }
        tweens.length = 0;
    };

    /**
     * Determines whether this collection contains a given tween.
     *
     * @param {Tween} tween The tween to check for.
     * @returns {Boolean} <code>true</code> if this collection contains the tween, <code>false</code> otherwise.
     */
    TweenCollection.prototype.contains = function(tween) {
        return defined(tween) && (this._tweens.indexOf(tween) !== -1);
    };

    /**
     * Returns the tween in the collection at the specified index.  Indices are zero-based
     * and increase as tweens are added.  Removing a tween shifts all tweens after
     * it to the left, changing their indices.  This function is commonly used to iterate over
     * all the tween in the collection.
     *
     * @param {Number} index The zero-based index of the tween.
     * @returns {Tween} The tween at the specified index.
     *
     * @example
     * // Output the duration of all the tweens in the collection.
     * var tweens = scene.tweens;
     * var length = tweens.length;
     * for (var i = 0; i < length; ++i) {
     *   console.log(tweens.get(i).duration);
     * }
     */
    TweenCollection.prototype.get = function(index) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(index)) {
            throw new DeveloperError('index is required.');
        }
        //>>includeEnd('debug');

        return this._tweens[index];
    };

    /**
     * Updates the tweens in the collection to be at the provide time.  When a tween finishes, it is removed
     * from the collection.
     *
     * @param {Number} [time=getTimestamp()] The time in seconds.  By default tweens are synced to the system clock.
     */
    TweenCollection.prototype.update = function(time) {
        var tweens = this._tweens;

        var i = 0;
        time = defined(time) ? time / TimeConstants.SECONDS_PER_MILLISECOND : getTimestamp();
        while (i < tweens.length) {
            var tween = tweens[i];
            var tweenjs = tween.tweenjs;

            if (tween.needsStart) {
                tween.needsStart = false;
                tweenjs.start(time);
            } else {
                if (tweenjs.update(time)) {
                    i++;
                } else {
                    tweenjs.stop();
                    tweens.splice(i, 1);
                }
            }
        }
    };

    /**
     * A function that will execute when a tween completes.
     * @callback TweenCollection~TweenCompleteCallback
     */

    /**
     * A function that will execute when a tween updates.
     * @callback TweenCollection~TweenUpdateCallback
     */

    /**
     * A function that will execute when a tween is cancelled.
     * @callback TweenCollection~TweenCancelledCallback
     */

    return TweenCollection;
});