/**
 * @private
 */
Ext.define('Ext.layout.container.boxOverflow.Scroller', {
    extend: 'Ext.layout.container.boxOverflow.None',
    alternateClassName: 'Ext.layout.boxOverflow.Scroller',
 
    alias: [
        'box.overflow.scroller',
        'box.overflow.Scroller' // capitalized for 4.x compat
    ],
 
    requires: [
        'Ext.util.ClickRepeater',
        'Ext.Element'
    ],
 
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    /**
     * @cfg {Boolean} animateScroll
     * True to animate the scrolling of items within the layout (ignored if enableScroll is false)
     */
    animateScroll: false,
 
    /**
     * @cfg {Number} scrollIncrement
     * The number of pixels to scroll by on scroller click
     */
    scrollIncrement: 20,
 
    /**
     * @cfg {Number} wheelIncrement
     * The number of pixels to increment on mouse wheel scrolling.
     */
    wheelIncrement: 10,
 
    /**
     * @cfg {Number} scrollRepeatInterval
     * Number of milliseconds between each scroll while a scroller button is held down
     */
    scrollRepeatInterval: 60,
 
    /**
     * @cfg {Number} scrollDuration
     * Number of milliseconds that each scroll animation lasts
     */
    scrollDuration: 400,
 
    /**
     * @private
     */
    scrollerCls: Ext.baseCSSPrefix + 'box-scroller',
    beforeSuffix: '-before-scroller',
    afterSuffix: '-after-scroller',
 
    /**
     * @event scroll
     * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller
     * @param {Number} newPosition The new position of the scroller
     */
 
    constructor: function(config) {
        var me = this;
 
        me.mixins.observable.constructor.call(me, config);
        me.layout.owner.on({
            afterrender: me.onOwnerRender,
            scope: me,
            single: true
        });
        me.layout.owner.getOverflowEl = me.ownerGetOverflowImpl;
 
        me.scrollPosition = 0;
        me.scrollSize = 0;
    },
 
    onOwnerRender: function(owner) {
        var me = this,
            scrollable = {
                isBoxOverflowScroller: true,
                x: false,
                y: false,
                listeners: {
                    scrollend: this.onScrollEnd,
                    scope: this
                }
            };
 
        // If no obstrusive scrollbars, allow natural scrolling on mobile touch devices
        if (!Ext.scrollbar.width() && !Ext.platformTags.desktop) {
            scrollable[owner.layout.horizontal ? 'x' : 'y'] = true;
        }
        else {
            me.wheelListener = me.layout.innerCt.on(
                'wheel', me.onMouseWheel, me, { destroyable: true }
            );
        }
 
        owner.setScrollable(scrollable);
    },
 
    getPrefixConfig: function() {
        return {
            role: 'presentation',
            id: this.layout.owner.id + this.beforeSuffix,
            cls: this.createScrollerCls('beforeX'),
            style: 'display:none'
        };
    },
 
    getSuffixConfig: function() {
        return {
            role: 'presentation',
            id: this.layout.owner.id + this.afterSuffix,
            cls: this.createScrollerCls('afterX'),
            style: 'display:none'
        };
    },
 
    createScrollerCls: function(xName) {
        var me = this,
            layout = me.layout,
            owner = layout.owner,
            type = me.getOwnerType(owner),
            scrollerCls = me.scrollerCls,
            cls =
                scrollerCls + ' ' +
                scrollerCls + '-' + layout.names[xName] + ' ' +
                scrollerCls + '-' + type + ' ' +
                scrollerCls + '-' + type + '-' + owner.ui;
 
        if (owner.plain) {
            // Add plain class for components that need separate "plain" styling (e.g. tab bar)
            cls += ' ' + scrollerCls + '-plain';
        }
 
        return cls;
    },
 
    getOverflowCls: function(direction) {
        return this.scrollerCls + '-body-' + direction;
    },
 
    beginLayout: function(ownerContext) {
        ownerContext.innerCtScrollPos = this.getScrollPosition();
 
        this.callParent(arguments);
    },
 
    finishedLayout: function(ownerContext) {
        var me = this,
            plan = ownerContext.state.boxPlan,
            layout = me.layout,
            names = layout.names,
            scrollPos = Math.min(me.getMaxScrollPosition(), ownerContext.innerCtScrollPos),
            lastProps;
 
        // If there is overflow...
        if (plan && plan.tooNarrow) {
            lastProps = ownerContext.childItems[ownerContext.childItems.length - 1].props;
 
            // capture this before callParent since it calls handle/clearOverflow:
            me.scrollSize = lastProps[names.x] + lastProps[names.width];
            me.updateScrollButtons();
 
            // Restore pre layout scroll position
            layout.innerCt[names.setScrollLeft](scrollPos);
        }
 
        me.callParent([ownerContext]);
    },
 
    handleOverflow: function(ownerContext) {
        var me = this,
            names = me.layout.names,
            getWidth = names.getWidth,
            parallelMargins = names.parallelMargins,
            scrollerWidth, targetPaddingWidth, beforeScroller, afterScroller;
 
        me.showScrollers();
 
        beforeScroller = me.getBeforeScroller();
        afterScroller = me.getAfterScroller();
 
        scrollerWidth = beforeScroller[getWidth]() + afterScroller[getWidth]() +
            beforeScroller.getMargin(parallelMargins) + afterScroller.getMargin(parallelMargins);
 
        targetPaddingWidth = ownerContext.targetContext.getPaddingInfo()[names.width];
 
        return {
            reservedSpace: Math.max(scrollerWidth - targetPaddingWidth, 0)
        };
    },
 
    /**
     * @private
     * Returns a reference to the "before" scroller element.  Creates click handlers on
     * the first call.
     */
    getBeforeScroller: function() {
        var me = this;
 
        return me._beforeScroller || (me._beforeScroller =
            me.createScroller(me.beforeSuffix, 'beforeRepeater', 'scrollLeft'));
    },
 
    /**
     * @private
     * Returns a reference to the "after" scroller element.  Creates click handlers on
     * the first call.
     */
    getAfterScroller: function() {
        var me = this;
 
        return me._afterScroller || (me._afterScroller =
            me.createScroller(me.afterSuffix, 'afterRepeater', 'scrollRight'));
    },
 
    createScroller: function(suffix, repeaterName, scrollHandler) {
        var me = this,
            owner = me.layout.owner,
            scrollerCls = me.scrollerCls,
            scrollerEl;
 
        scrollerEl = owner.el.getById(owner.id + suffix);
 
        scrollerEl.addClsOnOver(scrollerCls + '-hover');
        scrollerEl.addClsOnClick(scrollerCls + '-pressed');
 
        scrollerEl.setVisibilityMode(Ext.Element.DISPLAY);
 
        me[repeaterName] = new Ext.util.ClickRepeater(scrollerEl, {
            interval: me.scrollRepeatInterval,
            handler: scrollHandler,
            scope: me,
            mousedownPreventDefault: true // Stop IE from scrolling to improperly focused element
        });
 
        return scrollerEl;
    },
 
    onMouseWheel: function(e) {
        var cmp = Ext.Component.from(e.target),
            cmpScroller = cmp.getScrollable && cmp.getScrollable();
 
        // Only stop the event if we are not scrolling a scrollable component
        // inside this container.
        if (!cmpScroller || (cmpScroller === this.layout.owner.getScrollable())) {
            e.stopEvent();
            this.scrollBy(this.getWheelDelta(e) * this.wheelIncrement, false);
        }
    },
 
    getWheelDelta: function(e) {
        return e.getWheelDelta();
    },
 
    /**
     * @private
     */
    clearOverflow: function() {
        this.hideScrollers();
    },
 
    /**
     * @private
     * Shows the scroller elements. Creates the scrollers first if they are not already present.
     */
    showScrollers: function() {
        var me = this;
 
        me.getBeforeScroller().show();
        me.getAfterScroller().show();
        me.layout.owner.addClsWithUI(
            me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller'
        );
        // TODO - this may invalidates data in the ContextItem's styleCache
    },
 
    /**
     * @private
     * Hides the scroller elements.
     */
    hideScrollers: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller();
 
        if (beforeScroller) {
            beforeScroller.hide();
            afterScroller.hide();
            me.layout.owner.removeClsWithUI(
                me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller'
            );
            // TODO - this may invalidates data in the ContextItem's styleCache
        }
    },
 
    destroy: function() {
        Ext.destroyMembers(this, 'beforeRepeater', 'afterRepeater', '_beforeScroller',
                           '_afterScroller', 'wheelListener');
        this.callParent();
    },
 
    /**
     * @private
     * Scrolls left or right by the number of pixels specified
     * @param {Number} delta Number of pixels to scroll to the right by.
     * Use a negative number to scroll left
     * @param {Boolean} animate 
     */
    scrollBy: function(delta, animate) {
        var layout = this.layout,
            scroller = layout.owner.getScrollable(),
            args = [0, 0, animate ? this.getScrollAnim() : false];
 
        args[layout.horizontal ? 0 : 1] = delta;
        scroller.scrollBy.apply(scroller, args);
    },
 
    /**
     * @private
     * @return {Object} Object passed to scrollTo when scrolling
     */
    getScrollAnim: function() {
        return {
            duration: this.scrollDuration,
            callback: this.updateScrollButtons,
            scope: this
        };
    },
 
    /**
     * @private
     * Enables or disables each scroller button based on the current scroll position
     */
    updateScrollButtons: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller(),
            scrollPos = me.getScrollPosition(),
            disabledCls;
 
        if (!beforeScroller || !afterScroller) {
            return;
        }
 
        disabledCls = me.scrollerCls + '-disabled';
 
        beforeScroller[scrollPos ? 'removeCls' : 'addCls'](disabledCls);
        afterScroller[scrollPos >= me.getMaxScrollPosition() ? 'addCls' : 'removeCls'](disabledCls);
    },
 
    /**
     * @private
     * Scrolls to the left by the configured amount
     */
    scrollLeft: function() {
        this.scrollBy(-this.scrollIncrement, false);
    },
 
    /**
     * @private
     * Scrolls to the right by the configured amount
     */
    scrollRight: function() {
        this.scrollBy(this.scrollIncrement, false);
    },
 
    /**
     * Returns the current scroll position of the innerCt element
     * @return {Number} The current scroll position
     */
    getScrollPosition: function() {
        var layout = this.layout;
 
        return layout.owner.getScrollable().getPosition()[layout.horizontal ? 'x' : 'y'];
    },
 
    /**
     * @private
     * Returns the maximum value we can scrollTo
     * @return {Number} The max scroll value
     */
    getMaxScrollPosition: function() {
        var layout = this.layout;
 
        return layout.owner.getScrollable().getMaxPosition()[layout.horizontal ? 'x' : 'y'];
    },
 
    /**
     * @private
     */
    setVertical: function() {
        var me = this,
            beforeScroller = me.getBeforeScroller(),
            afterScroller = me.getAfterScroller(),
            names = me.layout.names,
            scrollerCls = me.scrollerCls;
 
        beforeScroller.removeCls(scrollerCls + '-' + names.beforeY);
        afterScroller.removeCls(scrollerCls + '-' + names.afterY);
 
        beforeScroller.addCls(scrollerCls + '-' + names.beforeX);
        afterScroller.addCls(scrollerCls + '-' + names.afterX);
 
        me.callParent();
    },
 
    /**
     * @private
     * Scrolls to the given position. Performs bounds checking.
     * @param {Number} position The position to scroll to. This is constrained.
     * @param {Boolean} animate True to animate. If undefined, falls back to value
     * of this.animateScroll
     */
    scrollTo: function(position, animate) {
        var layout = this.layout,
            scroller = layout.owner.getScrollable(),
            args = [0, 0, animate ? this.getScrollAnim() : false];
 
        args[layout.horizontal ? 0 : 1] = position;
        scroller.scrollTo.apply(scroller, args);
    },
 
    onScrollEnd: function(scroller, x, y) {
        this.updateScrollButtons();
        this.fireEvent('scroll', this, this.layout.horizontal ? x : y, false);
    },
 
    /**
     * Scrolls to the given component.
     * @param {String/Number/Ext.Component} item The item to scroll to. Can be a numerical index,
     * component id or a reference to the component itself.
     * @param {Boolean} animate True to animate the scrolling
     */
    scrollToItem: function(item, animate) {
        item = this.getItem(item);
 
        if (item !== undefined) {
            this.layout.owner.getScrollable().ensureVisible(item.el, {
                animation: animate
            });
        }
    },
 
    privates: {
        // This is injected into the owner component because the scroller
        // must be applied to the element this this class scrolls
        ownerGetOverflowImpl: function() {
            return this.layout.innerCt;
        }
    }
});