/**
 * @private
 */
Ext.define('Ext.scroll.scroller.Abstract', {

    requires: [
        'Ext.scroll.easing.BoundMomentum',
        'Ext.scroll.easing.EaseOut',
        'Ext.util.SizeMonitor'
    ],

    mixins: [
        'Ext.mixin.Observable'
    ],

    config: {
        element: null,

        /**
         * @cfg {String} direction
         * Possible values: 'auto', 'vertical', 'horizontal', or 'both'
         * @accessor
         */
        direction: 'auto',

        momentumEasing: {
            momentum: {
                acceleration: 30,
                friction: 0.5
            },

            bounce: {
                acceleration: 30,
                springTension: 0.3
            },

            minVelocity: 0.2
        },

        snapEasing: {
            duration: 400,
            exponent: 4
        },

        outOfBoundRestrictFactor: 0.5,

        startMomentumResetTime: 300,

        fps: 60,

        maxAbsoluteVelocity: 2,

        containerSize: 'auto',

        containerScrollSize: 'auto',

        size: 'auto',

        snap: null,

        snapOffset: {
            x: 0,
            y: 0
        },

        disabled: null,

        autoRefresh: true,

        cls: Ext.baseCSSPrefix + 'scroll-scroller',

        containerCls: Ext.baseCSSPrefix + 'scroll-container'
    },

    dragStartTime: 0,

    dragEndTime: 0,

    activeEasing: null,

    isDragging: false,

    isAnimating: false,

    constructor: function(config) {
        var element = config && config.element;

        this.doAnimationFrame = Ext.Function.bind(this.doAnimationFrame, this);

        this.listeners = {
            scope: this,
            touchstart: 'onTouchStart',
            dragstart : 'onDragStart',
            drag      : 'onDrag',
            dragend   : 'onDragEnd'
        };

        this.startPosition = { x: 0, y: 0 };

        this.size = { x: 0, y: 0 };

        this.position = { x: 0, y: 0 };

        this.velocity = { x: 0, y: 0 };

        this.isAxisEnabledFlags = { x: false, y: false };

        this.activeEasing = { x: null, y: null };

        this.flickStartPosition = { x: 0, y: 0 };

        this.flickStartTime = { x: 0, y: 0 };

        this.lastDragPosition = { x: 0, y: 0 };

        this.dragDirection = { x: 0, y: 0};

        this.initialConfig = config;

        if (element) {
            delete config.element;
            this.initElement(element);
        }

        return this;
    },

    applyElement: function(element) {
        if (!element) {
            return;
        }

        return Ext.get(element);
    },

    updateElement: function(element) {
        element.addCls(this.getCls());

        this.initConfig(this.initialConfig);

        this.onAfterInitialized();

        return this;
    },

    onAfterInitialized: function() {
        if (!this.getDisabled()) {
            this.attachListeneners();
        }

        this.onConfigUpdate(['containerSize', 'size'], 'refreshMaxPosition');

        this.on('maxpositionchange', 'snapToBoundary');
    },

    attachListeneners: function() {
        this.getContainer().on(this.listeners);
    },

    detachListeners: function() {
        this.getContainer().un(this.listeners);
    },

    updateDisabled: function(disabled) {
        if (disabled) {
            this.detachListeners();
        }
        else {
            this.attachListeneners();
        }
    },

    updateFps: function(fps) {
        this.animationInterval = 1000 / fps;
    },

    applyDirection: function(direction) {
        var maxPosition = this.getMaxPosition(),
            isHorizontal, isVertical;

        this.givenDirection = direction;

        if (direction === 'auto') {
            isHorizontal = maxPosition.x > 0;
            isVertical = maxPosition.y > 0;

            if (isHorizontal && isVertical) {
                direction = 'both';
            }
            else if (isHorizontal) {
                direction = 'horizontal';
            }
            else {
                direction = 'vertical';
            }
        }

        return direction;
    },

    updateDirection: function(direction) {
        var isAxisEnabled = this.isAxisEnabledFlags;

        isAxisEnabled.x = (direction === 'both' || direction === 'horizontal');
        isAxisEnabled.y = (direction === 'both' || direction === 'vertical');
    },

    isAxisEnabled: function(axis) {
        this.getDirection();

        return this.isAxisEnabledFlags[axis];
    },

    applyMomentumEasing: function(easing) {
        var defaultEasingClass = Ext.scroll.easing.BoundMomentum;

        if (!(easing instanceof Ext.scroll.easing.Easing)) {
            return {
                x: new defaultEasingClass(easing),
                y: new defaultEasingClass(easing)
            };
        }

        return {
            x: easing,
            y: easing.clone()
        };
    },

    applySnapEasing: function(easing) {
        var defaultEasingClass = Ext.scroll.easing.EaseOut;

        if (!(easing instanceof Ext.scroll.easing.Easing)) {
            return {
                x: new defaultEasingClass(easing),
                y: new defaultEasingClass(easing)
            };
        }

        return {
            x: easing,
            y: easing.clone()
        };
    },

    getMaxPosition: function() {
        var maxPosition = this.maxPosition,
            size, containerSize;

        if (!maxPosition) {
            size = this.getSize();
            containerSize = this.getContainerSize();

            this.maxPosition = maxPosition = {
                x: Math.max(0, size.x - containerSize.x),
                y: Math.max(0, size.y - containerSize.y)
            };

            this.fireEvent('maxpositionchange', this, maxPosition);
        }

        return maxPosition;
    },

    refreshMaxPosition: function() {
        this.maxPosition = null;
        this.getMaxPosition();
    },

    applyContainerSize: function(size) {
        var containerDom, x, y;

        this.givenContainerSize = size;

        if (size === 'auto') {
            containerDom = this.getContainer().dom;

            x = containerDom.offsetWidth;
            y = containerDom.offsetHeight;
        }
        else {
            x = size.x;
            y = size.y;
        }

        return {
            x: x,
            y: y
        };
    },

    applySize: function(size) {
        var dom, x, y;

        this.givenSize = size;

        if (size === 'auto') {
            dom = this.getElement().dom;

            x = dom.offsetWidth;
            y = dom.offsetHeight;
        }
        else {
            x = size.x;
            y = size.y;
        }

        return {
            x: x,
            y: y
        };
    },

    applyContainerScrollSize: function(size) {
        var containerDom, x, y;

        this.givenContainerScrollSize = size;

        if (size === 'auto') {
            containerDom = this.getContainer().dom;

            x = containerDom.scrollWidth;
            y = containerDom.scrollHeight;
        }
        else {
            x = size.x;
            y = size.y;
        }

        return {
            x: x,
            y: y
        };
    },

    getContainer: function() {
        var container = this.container;

        if (!container) {
            container = this.container = this.getElement().getParent();
            container.addCls(this.getContainerCls());
        }

        return container;
    },

    updateAutoRefresh: function(autoRefresh) {
        var SizeMonitor = Ext.util.SizeMonitor;

        if (autoRefresh) {
            this.sizeMonitors = {
                element: new SizeMonitor({
                    element: this.getElement(),
                    callback: this.doRefresh,
                    scope: this
                }),
                container: new SizeMonitor({
                    element: this.getContainer(),
                    callback: this.doRefresh,
                    scope: this
                })
            };
        }
    },

    doRefresh: function() {
        this.stopAnimation();

        this.setSize(this.givenSize);
        this.setContainerSize(this.givenContainerSize);
        this.setContainerScrollSize(this.givenContainerScrollSize);
        this.setDirection(this.givenDirection);

        this.fireEvent('refresh');
    },

    refresh: function() {
        var sizeMonitors = this.sizeMonitors;

        if (sizeMonitors) {
            sizeMonitors.element.refresh();
            sizeMonitors.container.refresh();
        }

        this.doRefresh();
    },

    scrollTo: function(x, y) {
        //<deprecated product=touch since=2.0>
        if (typeof x != 'number' && arguments.length === 1) {
            y = x.y;
            x = x.x;
        }
        //</deprecated>

        var position = this.position,
            positionChanged = false,
            actualX = null,
            actualY = null;

        if (this.isAxisEnabled('x')) {
            if (typeof x != 'number') {
                x = position.x;
            }
            else {
                if (position.x !== x) {
                    position.x = x;
                    positionChanged = true;
                }
            }

            actualX = x;
        }

        if (this.isAxisEnabled('y')) {
            if (typeof y != 'number') {
                y = position.y;
            }
            else {
                if (position.y !== y) {
                    position.y = y;
                    positionChanged = true;
                }
            }

            actualY = y;
        }

        if (positionChanged) {
            this.fireEvent('scroll', this, position.x, position.y);
            this.doScrollTo(actualX, actualY);
        }

        return this;
    },

    doScrollTo: function(x, y) {
        var containerDom = this.getContainer().dom;

        if (x !== null) {
            containerDom.scrollLeft = x;
        }

        if (y !== null) {
            containerDom.scrollTop = y;
        }
    },

    onTouchStart: function() {
        this.stopAnimation();
    },

    onDragStart: function() {
        this.stopAnimation(true);

        var startPosition = this.startPosition,
            flickStartPosition = this.flickStartPosition,
            flickStartTime = this.flickStartTime,
            lastDragPosition = this.lastDragPosition,
            currentPosition = this.position,
            dragDirection = this.dragDirection,
            x = currentPosition.x,
            y = currentPosition.y,
            now = Ext.Date.now();

        lastDragPosition.x = x;
        lastDragPosition.y = y;

        flickStartPosition.x = x;
        flickStartPosition.y = y;

        startPosition.x = x;
        startPosition.y = y;

        flickStartTime.x = now;
        flickStartTime.y = now;

        dragDirection.x = 0;
        dragDirection.y = 0;

        this.dragStartTime = now;

        this.isDragging = true;

        this.fireEvent('scrollstart');
    },

    onAxisDrag: function(axis, delta) {
        if (!this.isAxisEnabled(axis)) {
            return;
        }

        var flickStartPosition = this.flickStartPosition,
            flickStartTime = this.flickStartTime,
            lastDragPosition = this.lastDragPosition,
            dragDirection = this.dragDirection,
            old = this.position[axis],
            max = this.getMaxPosition()[axis],
            start = this.startPosition[axis],
            last = lastDragPosition[axis],
            current = start - delta,
            lastDirection = dragDirection[axis],
            restrictFactor = this.getOutOfBoundRestrictFactor(),
            startMomentumResetTime = this.getStartMomentumResetTime(),
            now = Ext.Date.now(),
            distance;

        if (current < 0) {
            current *= restrictFactor;
        }
        else if (current > max) {
            distance = current - max;
            current = max + distance * restrictFactor;
        }

        if (current > last) {
            dragDirection[axis] = 1;
        }
        else if (current < last) {
            dragDirection[axis] = -1;
        }

        if ((lastDirection !== 0 && (dragDirection[axis] !== lastDirection)) || (now - flickStartTime[axis]) > startMomentumResetTime) {
            flickStartPosition[axis] = old;
            flickStartTime[axis] = now;
        }

        lastDragPosition[axis] = current;
    },

    onDrag: function(e) {
        if (!this.isDragging) {
            return;
        }

        var lastDragPosition = this.lastDragPosition;

        this.onAxisDrag('x', e.deltaX);
        this.onAxisDrag('y', e.deltaY);

        this.scrollTo(lastDragPosition.x, lastDragPosition.y);
    },

    onDragEnd: function(e) {
        var animationX, animationY;

        if (!this.isDragging) {
            return;
        }

        this.dragEndTime = Ext.Date.now();

        this.onDrag(e);

        this.isDragging = false;

        animationX = this.prepareAnimation('x');
        animationY = this.prepareAnimation('y');

        if (!(animationX === false || animationY === false)) {
            this.isScrolling = true;
        }

        this.startAnimation();
    },

    prepareAnimation: function(axis) {
        if (!this.isAxisEnabled(axis)) {
            return this;
        }

        var currentPosition = this.position[axis],
            flickStartPosition = this.flickStartPosition[axis],
            flickStartTime = this.flickStartTime[axis],
            maxPosition = this.getMaxPosition()[axis],
            maxAbsVelocity = this.getMaxAbsoluteVelocity(),
            boundValue = null,
            easing, velocity, duration;

        if (currentPosition < 0) {
            boundValue = 0;
        }
        else if (currentPosition > maxPosition) {
            boundValue = maxPosition;
        }

        // Out of bound, to be pulled back
        if (boundValue !== null) {
            easing = this.getSnapEasing()[axis];
            easing.setConfig({
                startTime: this.dragEndTime,
                startValue: currentPosition,
                endValue: boundValue
            });
        }
        // Still within boundary, start deceleration
        else {
            duration = this.dragEndTime - flickStartTime;

            if (duration === 0) {
                return false;
            }

            velocity = (currentPosition - flickStartPosition) / (this.dragEndTime - flickStartTime);

            if (velocity === 0) {
                return;
            }

            if (velocity < -maxAbsVelocity) {
                velocity = -maxAbsVelocity;
            }
            else if (velocity > maxAbsVelocity) {
                velocity = maxAbsVelocity;
            }

            easing = this.getMomentumEasing()[axis];
            easing.setConfig({
                startTime: this.dragEndTime,
                startValue: currentPosition,
                startVelocity: velocity,
                minMomentumValue: 0,
                maxMomentumValue: maxPosition
            });
        }

        this.activeEasing[axis] = easing;

        return this;
    },

    prepareSnapAnimation: function(axis) {
        if (!this.isAxisEnabled(axis)) {
            return false;
        }

        var currentPosition = this.position[axis],
            containerSize = this.getContainerSize()[axis],
            containerScrollSize = this.getContainerScrollSize()[axis],
            snap = this.getSnap(),
            offset = this.getSnapOffset()[axis],
            easing, endValue;

        endValue = Math.round((currentPosition + offset) / snap) * snap;

        //if the currentPosition is less than the containerScrollSize - containerSize (so it is at the end of the list), then use it
        if ((containerScrollSize - containerSize) <= currentPosition) {
            return false;
        }

        easing = this.getSnapEasing()[axis];
        easing.setConfig({
            startTime : Ext.Date.now(),
            startValue: currentPosition,
            endValue  : endValue - offset
        });

        this.activeEasing[axis] = easing;

        return endValue;
    },

    startAnimation: function() {
        this.isAnimating = true;
        this.animationTimer = setInterval(this.doAnimationFrame, this.animationInterval);
        this.doAnimationFrame();
    },

    doAnimationFrame: function() {
        if (!this.isAnimating) {
            return;
        }

        var easing = this.activeEasing,
            easingX = easing.x,
            easingY = easing.y,
            isEasingXEnded = easingX === null || easingX.isEnded,
            isEasingYEnded = easingY === null || easingY.isEnded,
            x = null,
            y = null;

        if (isEasingXEnded && isEasingYEnded) {
            this.stopAnimation();
            return;
        }

        if (!isEasingXEnded) {
            x = easingX.getValue();
        }

        if (!isEasingYEnded) {
            y = easingY.getValue();
        }

        this.scrollTo(x, y);
    },

    stopAnimation: function(isOnTouchStart) {
        if (!this.isAnimating) {
            return;
        }

        var activeEasing = this.activeEasing;

        activeEasing.x = null;
        activeEasing.y = null;

        this.isAnimating = false;

        clearInterval(this.animationTimer);

        this.snapToBoundary();

        if (!isOnTouchStart) {
            if (this.onScrollEnd()) {
                this.fireEvent('scrollend', this, this.position);
            }
        }

        this.isScrolling = false;
    },

    scrollToAnimated: function(x, y) {
        var currentPosition = this.position,
            easingX, easingY;

        easingX = this.getSnapEasing().x;
        easingX.setConfig({
            startTime : Ext.Date.now(),
            startValue: currentPosition.x,
            endValue  : x
        });

        easingY = this.getSnapEasing().y;
        easingY.setConfig({
            startTime : Ext.Date.now(),
            startValue: currentPosition.y,
            endValue  : y
        });

        this.activeEasing.x = easingX;
        this.activeEasing.y = easingY;

        this.startAnimation();
    },

    onScrollEnd: function() {
        if (this.isSnapping) {
            this.isSnapping = false;
            return true;
        }

        // Check the current position and calculate the snapping position
        var snap = this.getSnap(),
            snapX, snapY;

        if (!snap) {
            return true;
        }

        snapX = this.prepareSnapAnimation('x');
        snapY = this.prepareSnapAnimation('y');

        if ((snapX || snapY) && snap && snap > 0) {
            this.isSnapping = true;

            this.startAnimation();

            return false;
        }

        return true;
    },

    snapValueForAxis: function(value, axis) {
        var containerSize = this.getContainerSize()[axis],
            containerScrollSize = this.getContainerScrollSize()[axis],
            snap = this.getSnap(),
            offset = this.getSnapOffset()[axis],
            easing;

        value = Math.round((value + offset) / snap) * snap;

        return value;
    },

    snapToBoundary: function() {
        var position    = this.position,
            maxPosition = this.getMaxPosition(),
            maxX = maxPosition.x,
            maxY = maxPosition.y,
            x = position.x,
            y = position.y;

        if (x < 0) {
            x = 0;
        }
        else if (x > maxX) {
            x = maxX;
        }

        if (y < 0) {
            y = 0;
        }
        else if (y > maxY) {
            y = maxY;
        }

        this.scrollTo(x, y);
    },

    destroy: function() {
        var element = this.getElement(),
            sizeMonitors = this.sizeMonitors;

        if (sizeMonitors) {
            sizeMonitors.element.destroy();
            sizeMonitors.container.destroy();
        }

        if (element) {
            element.removeCls(this.getCls());
            this.getContainer().removeCls(this.getContainerCls());
        }

        this.callParent(arguments);
    }

}, function() {
    //<deprecated product=touch since=2.0>

    this.override({
        constructor: function(config) {
            var acceleration, friction, springTension, minVelocity;

            if (config.hasOwnProperty('acceleration')) {
                acceleration = config.acceleration;
                delete config.acceleration;
                //<debug warn>
                Ext.Logger.deprecate("'acceleration' config is deprecated, set momentumEasing.momentum.acceleration and momentumEasing.bounce.acceleration configs instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { acceleration: acceleration },
                        bounce: { acceleration: acceleration }
                    }
                });
            }

            if (config.hasOwnProperty('friction')) {
                friction = config.friction;
                delete config.friction;
                //<debug warn>
                Ext.Logger.deprecate("'friction' config is deprecated, set momentumEasing.momentum.friction config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { friction: friction }
                    }
                });
            }

            if (config.hasOwnProperty('springTension')) {
                springTension = config.springTension;
                delete config.springTension;
                //<debug warn>
                Ext.Logger.deprecate("'springTension' config is deprecated, set momentumEasing.momentum.springTension config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        momentum: { springTension: springTension }
                    }
                });
            }

            if (config.hasOwnProperty('minVelocityForAnimation')) {
                minVelocity = config.minVelocityForAnimation;
                delete config.minVelocityForAnimation;
                //<debug warn>
                Ext.Logger.deprecate("'minVelocityForAnimation' config is deprecated, set momentumEasing.minVelocity config instead");
                //</debug>

                Ext.merge(config, {
                    momentumEasing: {
                        minVelocity: minVelocity
                    }
                });
            }

            this.callOverridden(arguments);
        },

        updateBoundary: function() {
            //<debug warn>
            Ext.Logger.deprecate("updateBoundary() is deprecated, use refresh() instead");
            //</debug>
            return this.refresh();
        },

        scrollBy: function(offset) {
            var position = this.position;

            return this.scrollTo(position.x + offset.x, position.y + offset.y);
        },

        setOffset: function(offset) {
            return this.scrollTo(-offset.x, -offset.y);
        },

        //TODO
        snapToSlot: function() {

        }
    });

    //</deprecated>
});