/**
 * @private
 *
 * This easing is typically used for {@link Ext.scroll.Scroller}. It's a combination of
 * {@link Ext.fx.easing.Momentum} and {@link Ext.fx.easing.Bounce}, which emulates deceleration
 * when the animated element is still within its boundary, then bouncing back (snapping) when
 * it's out-of-bound.
 */
Ext.define('Ext.fx.easing.BoundMomentum', {
    extend: 'Ext.fx.easing.Abstract',
 
    requires: [
        'Ext.fx.easing.Momentum',
        'Ext.fx.easing.Bounce'
    ],
 
    config: {
        /**
         * @cfg {Object} momentum
         * A valid config object for {@link Ext.fx.easing.Momentum}
         * @accessor
         */
        momentum: null,
 
        /**
         * @cfg {Object} bounce
         * A valid config object for {@link Ext.fx.easing.Bounce}
         * @accessor
         */
        bounce: null,
 
        minMomentumValue: 0,
 
        maxMomentumValue: 0,
 
        /**
         * @cfg {Number} minVelocity
         * The minimum velocity to end this easing
         * @accessor
         */
        minVelocity: 0.01,
 
        /**
         * @cfg {Number} startVelocity
         * The start velocity
         * @accessor
         */
        startVelocity: 0
    },
 
    applyMomentum: function(config, currentEasing) {
        return Ext.factory(config, Ext.fx.easing.Momentum, currentEasing);
    },
 
    applyBounce: function(config, currentEasing) {
        return Ext.factory(config, Ext.fx.easing.Bounce, currentEasing);
    },
 
    updateStartTime: function(startTime) {
        this.getMomentum().setStartTime(startTime);
 
        this.callParent(arguments);
    },
 
    updateStartVelocity: function(startVelocity) {
        this.getMomentum().setStartVelocity(startVelocity);
    },
 
    updateStartValue: function(startValue) {
        this.getMomentum().setStartValue(startValue);
    },
 
    reset: function() {
        this.lastValue = null;
 
        this.isBouncingBack = false;
 
        this.isOutOfBound = false;
 
        return this.callParent(arguments);
    },
 
    getValue: function() {
        var momentum = this.getMomentum(),
            bounce = this.getBounce(),
            startVelocity = momentum.getStartVelocity(),
            direction = startVelocity > 0 ? 1 : -1,
            minValue = this.getMinMomentumValue(),
            maxValue = this.getMaxMomentumValue(),
            boundedValue = (direction === 1) ? maxValue : minValue,
            lastValue = this.lastValue,
            value, velocity;
 
        if (startVelocity === 0) {
            return this.getStartValue();
        }
 
        if (!this.isOutOfBound) {
            value = momentum.getValue();
            velocity = momentum.getVelocity();
 
            if (Math.abs(velocity) < this.getMinVelocity()) {
                this.isEnded = true;
            }
 
            if (value >= minValue && value <= maxValue) {
                return value;
            }
 
            this.isOutOfBound = true;
 
            bounce.setStartTime(Ext.Date.now())
                .setStartVelocity(velocity)
                .setStartValue(boundedValue);
        }
 
        value = bounce.getValue();
 
        if (!this.isEnded) {
            if (!this.isBouncingBack) {
                if (lastValue !== null) {
                    if ((direction === 1 && value < lastValue) ||
                        (direction === -1 && value > lastValue)) {
                        this.isBouncingBack = true;
                    }
                }
            }
            else {
                if (Math.round(value) === boundedValue) {
                    this.isEnded = true;
                }
            }
        }
 
        this.lastValue = value;
 
        return value;
    }
});