/**
 * @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;
    }
});
