Show:
/* Copyright © 2015-2016 David Valdman */

define(function (require, exports, module) {
    var dirtyQueue = require('./queues/dirtyQueue');
    var preTickQueue = require('./queues/preTickQueue');
    var tickQueue = require('./queues/tickQueue');
    var EventHandler = require('../events/EventHandler');
    var SimpleStream = require('../streams/SimpleStream');
    var Stream = require('../streams/Stream');

    var Tween = require('../transitions/Tween');
    var Spring = require('../transitions/Spring');
    var Inertia = require('../transitions/Inertia');

    var transitionMethods = {
        tween: Tween,
        spring: Spring,
        inertia: Inertia
    };

    /**
     * A way to transition numeric values and arrays of numbers between start and end states.
     *  Transitions are given an easing curve and a duration.
     *  Non-numeric values are ignored.
     *
     *  @example
     *
     *      var transitionable = new Transitionable(0);
     *
     *      transitionable.set(100, {duration : 1000, curve : 'easeIn'});
     *
     *      transitionable.on('start', function(value){
     *          console.log(value); // 0
     *      });
     *
     *      transitionable.on('update', function(value){
     *          console.log(value); // numbers between 0 and 100
     *      });
     *
     *      transitionable.on('end', function(value){
     *          console.log(value); // 100
     *      });
     *
     * @class Transitionable
     * @constructor
     * @extends Streams.SimpleStream
     * @param value {Number|Number[]}   Starting value
     */
    function Transitionable(value) {
        this.value = value || 0;
        this.velocity = 0;
        this._callback = undefined;
        this._method = null;
        this._active = false;
        this._currentActive = false;

        var hasUpdated = false;
        this.updateMethod = undefined;

        this._eventInput = new EventHandler();
        this._eventOutput = new EventHandler();
        EventHandler.setInputHandler(this, this._eventInput);
        EventHandler.setOutputHandler(this, this._eventOutput);

        this._eventInput.on('start', function (value) {
            this._currentActive = true;
            if (!this._active) {
                this.emit('start', value);
                this._active = true;
            }
        }.bind(this));

        this._eventInput.on('update', function (value) {
            hasUpdated = true;
            this.value = value;
            this.velocity = this._engineInstance.getVelocity();
            this.emit('update', value);
        }.bind(this));

        this._eventInput.on('end', function (value) {
            this.value = value;
            this._currentActive = false;

            if (this._callback) {
                var callback = this._callback;
                this._callback = undefined;
                callback();
            }

            if (!this._currentActive){
                this._active = false;
                hasUpdated = false;

                if (this._engineInstance)
                    this.velocity = this._engineInstance.getVelocity();

                this._active = false;
                this.emit('end', value);
            }
        }.bind(this));

        if (value !== undefined) {
            this.value = value;
            preTickQueue.push(function () {
                this.trigger('start', value);

                dirtyQueue.push(function () {
                    if (hasUpdated) return;
                    this.trigger('end', value);
                }.bind(this));
            }.bind(this));
        }
    }

    Transitionable.prototype = Object.create(SimpleStream.prototype);
    Transitionable.prototype.constructor = Transitionable;

    /**
     * Constructor method. A way of registering other engines that can interpolate
     *  between start and end values. For instance, a physics engine.
     *
     *  @method register
     *  @param name {string}    Identifier for the engine
     *  @param constructor      Constructor for the engine
     */
    Transitionable.register = function register(name, constructor) {
        if (!(name in transitionMethods))
            transitionMethods[name] = constructor;
    };

    /**
     * Constructor method. Unregister an interpolating engine.
     *  Undoes work of `register`.
     *
     *  @method unregister
     *  @param name {string}    Identifier for the engine
     */
    Transitionable.unregister = function unregister(name) {
        if (name in transitionMethods) {
            delete transitionMethods[name];
            return true;
        }
        else return false;
    };

    /**
     * Define a new end value that will be transitioned towards with the prescribed
     *  transition. An optional callback can fire when the transition completes.
     *
     * @method set
     * @param value {Number|Number[]}           End value
     * @param [transition] {Object}             Transition definition
     * @param [transition.curve] {string}       Easing curve name, e.g., "easeIn"
     * @param [transition.duration] {string}    Duration of transition
     * @param [callback] {Function}             Callback
     */
    Transitionable.prototype.set = function set(value, transition, callback) {
        if (!transition) {
            this.value = value;
            if (callback) callback();
            if (!this.isActive()){
                this.trigger('start', value);

                dirtyQueue.push(function () {
                    this.trigger('end', value);
                }.bind(this));
            }
            return;
        }

        if (callback) this._callback = callback;

        var curve = transition.curve;

        var method = (curve && transitionMethods[curve])
            ? transitionMethods[curve]
            : Tween;

        if (this._method !== method) {
            if (this._engineInstance){
                if (this.updateMethod){
                    var index = tickQueue.indexOf(this.updateMethod);
                    if (index >= 0) tickQueue.splice(index, 1);
                }
                this.unsubscribe(this._engineInstance);
            }

            if (this.value instanceof Array) {
                var dimensions = this.value.length;
                this._engineInstance = (dimensions < method.DIMENSIONS)
                    ? new method(this.value)
                    : new NDTransitionable(this.value, method);
            }
            else this._engineInstance = new method(this.value);

            this.subscribe(this._engineInstance);
            this.updateMethod = this._engineInstance.update.bind(this._engineInstance);
            tickQueue.push(this.updateMethod);

            this._method = method;
        }

        if (!transition.velocity) transition.velocity = this.velocity;

        this._engineInstance.set(value, transition);
    };

    /**
     * Return the current state of the transition.
     *
     * @method get
     * @return {Number|Number[]}    Current state
     */
    Transitionable.prototype.get = function get() {
        return this.value;
    };

    /**
     * Return the current velocity of the transition.
     *
     * @method getVelocity
     * @return {Number|Number[]}    Current state
     */
    Transitionable.prototype.getVelocity = function getVelocity(){
        return this.velocity;
    };

    /**
     * Sets the value and velocity of the transition without firing any events.
     *
     * @method reset
     * @param value {Number|Number[]}       New value
     * @param [velocity] {Number|Number[]}  New velocity
     */
    Transitionable.prototype.reset = function reset(value, velocity){
        this.value = value;
        this.velocity = velocity || 0;
        this._callback = null;
        this._method = null;
        if (this._engineInstance) this._engineInstance.reset(value, velocity);
    };

    /**
     * Ends the transition in place.
     *
     * @method halt
     */
    Transitionable.prototype.halt = function () {
        if (!this._active) return;
        this.reset(this.get());
        this.trigger('end', this.value);

        //TODO: refactor this
        if (this._engineInstance) {
            if (this.updateMethod) {
                var index = tickQueue.indexOf(this.updateMethod);
                if (index >= 0) tickQueue.splice(index, 1);
            }
            this.unsubscribe(this._engineInstance);
        }
    };

    /**
     * Determine is the transition is ongoing, or has completed.
     *
     * @method isActive
     * @return {Boolean}
     */
    Transitionable.prototype.isActive = function isActive() {
        return this._active;
    };

    /**
     * Combine multiple transitions to be executed sequentially. Pass an optional
     *  callback to fire on completion. Provide the transitions as an array of
     *  transition definition pairs: [value, method]
     *
     *  @example
     *
     *  transitionable.setMany([
     *      [0, {curve : 'easeOut', duration : 500}],
     *      [1, {curve : 'spring', period : 100, damping : 0.5}]
     *  ]);
     *
     * @method setMany
     * @param transitions {Array}   Array of transitions
     * @param [callback] {Function} Callback
     */
    Transitionable.prototype.setMany = function (transitions, callback) {
        var transition = transitions.shift();
        if (transitions.length === 0) {
            this.set(transition[0], transition[1], callback);
        }
        else this.set(transition[0], transition[1], this.setMany.bind(this, transitions, callback));
    };

    /**
     * Loop indefinitely between values with provided transitions array.
     *
     *  @example
     *
     *  transitionable.loop([
     *      [0, {curve : 'easeOut', duration : 500}],
     *      [1, {curve : 'spring', period : 100, damping : 0.5}]
     *  ]);
     *
     * @method loop
     * @param transitions {Array}   Array of transitions
     */
    Transitionable.prototype.loop = function (transitions) {
        var arrayClone = transitions.slice(0);
        this.setMany(transitions, this.loop.bind(this, arrayClone));
    };

    /**
     * Postpone a transition, and fire it by providing it in the callback parameter.
     *
     * @method delay
     * @param callback {Function}   Callback
     * @param duration {Number}     Duration of delay (in millisecons)
     */
    Transitionable.prototype.delay = function delay(callback, duration) {
        this.set(this.get(), {
                duration: duration,
                curve: function () { return 0; }
            },
            callback
        );
    };

    function NDTransitionable(value, method) {
        this._eventOutput = new EventHandler();
        EventHandler.setOutputHandler(this, this._eventOutput);

        this.sources = [];
        for (var i = 0; i < value.length; i++) {
            var source = new method(value[i]);
            this.sources.push(source);
        }

        this.stream = Stream.merge(this.sources);
        this._eventOutput.subscribe(this.stream);
    }

    NDTransitionable.prototype.set = function (value, transition) {
        var velocity = transition.velocity ? transition.velocity.slice() : undefined;
        for (var i = 0; i < value.length; i++){
            if (velocity) transition.velocity = velocity[i];
            this.sources[i].set(value[i], transition);
        }
    };

    NDTransitionable.prototype.getVelocity = function () {
        var velocity = [];
        for (var i = 0; i < this.sources.length; i++)
            velocity[i] = this.sources[i].getVelocity();
        return velocity;
    };

    NDTransitionable.prototype.update = function () {
        for (var i = 0; i < this.sources.length; i++)
            this.sources[i].update();
    };

    NDTransitionable.prototype.reset = function (value) {
        for (var i = 0; i < this.sources.length; i++) {
            var source = this.sources[i];
            source.reset(value[i]);
        }
    };

    module.exports = Transitionable;
});