/**
 * @private
 */
Ext.define('Ext.layout.container.boxOverflow.Scroller', {

    /* Begin Definitions */

    extend: 'Ext.layout.container.boxOverflow.None',
    requires: ['Ext.util.ClickRepeater', 'Ext.Element'],
    alternateClassName: 'Ext.layout.boxOverflow.Scroller',
    mixins: {
        observable: 'Ext.util.Observable'
    },
    
    /* End Definitions */

    /**
     * @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,

    /**
     * @cfg {String} beforeCtCls
     * CSS class added to the beforeCt element. This is the element that holds any special items such as scrollers,
     * which must always be present at the leftmost edge of the Container
     */

    /**
     * @cfg {String} afterCtCls
     * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
     * which must always be present at the rightmost edge of the Container
     */

    /**
     * @cfg {String} [scrollerCls='x-box-scroller']
     * CSS class added to both scroller elements if enableScroll is used
     */
    scrollerCls: Ext.baseCSSPrefix + 'box-scroller',

    /**
     * @cfg {String} beforeScrollerCls
     * CSS class added to the left scroller element if enableScroll is used
     */

    /**
     * @cfg {String} afterScrollerCls
     * CSS class added to the right scroller element if enableScroll is used
     */

    constructor: function(layout, config) {
        var me = this;

        me.layout = layout;
        Ext.apply(me, config || {});

        // Dont pass the config so that it is not applied to 'this' again
        me.mixins.observable.constructor.call(me);

        me.addEvents(
            /**
             * @event scroll
             * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller
             * @param {Number} newPosition The new position of the scroller
             * @param {Boolean/Object} animate If animating or not. If true, it will be a animation configuration, else it will be false
             */
            'scroll'
        );
        me.scrollPosition = 0;
        me.scrollSize = 0;
    },

    getPrefixConfig: function() {
        var me = this,
            owner = me.layout.owner,
            cls;

        me.initCSSClasses();
        cls = Ext.layout.container.Box.prototype.innerCls + ' ' + me.beforeCtCls;
        if (owner.plain) {
            // Add plain class for components that need separate "plain" styling (e.g. tab bar)
            cls += ' ' + me.scrollerCls + '-plain';
        }
        return {
            cls: cls,
            cn : {
                id : owner.id + '-before-scroller',
                cls: me.scrollerCls + ' ' + me.beforeScrollerCls,
                style: 'display:none'
            }
        };
    },

    getSuffixConfig: function() {
        var me = this,
            owner = me.layout.owner,
            cls = Ext.layout.container.Box.prototype.innerCls + ' ' + me.afterCtCls;

        if (owner.plain) {
            // Add plain class for components that need separate "plain" styling (e.g. tab bar)
            cls += ' ' + me.scrollerCls + '-plain';
        }
        return {
            cls: cls,
            cn : {
                id : me.layout.owner.id + '-after-scroller',
                cls: me.scrollerCls + ' ' + me.afterScrollerCls,
                style: 'display:none'
            }
        };
    },

    getOverflowCls: function() {
        return Ext.baseCSSPrefix + this.layout.direction + '-box-overflow-body';
    },

    initCSSClasses: function() {
        var me = this,
            prefix = Ext.baseCSSPrefix,
            layout = me.layout,
            names = layout.names,
            beforeXName = names.beforeX,
            afterXName = names.afterX,
            type = me.getOwnerType(layout.owner);

        me.beforeCtCls = me.beforeCtCls || prefix + 'box-scroller-' + beforeXName;
        me.afterCtCls  = me.afterCtCls  || prefix + 'box-scroller-' + afterXName;
        
        me.beforeScrollerCls = me.beforeScrollerCls || prefix + type + '-scroll-' + beforeXName;
        me.afterScrollerCls  = me.afterScrollerCls  || prefix + type + '-scroll-' + afterXName;
    },

    beginLayout: function (ownerContext) {
        ownerContext.innerCtScrollPos = this.getElScrollPos();
        this.callParent(arguments);
    },

    completeLayout: function(ownerContext) {
        var me = this,
            plan = ownerContext.state.boxPlan,
            names = me.layout.names,
            last;

        // If there is overflow...
        if (plan && plan.tooNarrow) {
            last = ownerContext.childItems[ownerContext.childItems.length - 1];

            // capture this before callParent since it calls handle/clearOverflow:
            me.scrollSize = last.props[names.x] + last.props[names.width];
            me.updateScrollButtons();
        }
        this.callParent(arguments);
    },

    finishedLayout: function(ownerContext) {
        var scrollPos = Math.min(this.getMaxScrollPosition(), ownerContext.innerCtScrollPos);

        this.setElScrollPos(scrollPos);
    },
    
    setElScrollPos: function(scrollPos) {
        var layout = this.layout;
        if (layout.direction === 'vertical') {
            layout.innerCt.dom.scrollTop = scrollPos;
        } else {
            layout.innerCt.setScrollLeft(scrollPos);
        }
    },
    
    getElScrollPos: function(){
        var layout = this.layout;
        if (layout.direction === 'vertical') {
            return layout.innerCt.dom.scrollTop;
        } else {
            return layout.innerCt.getScrollLeft();
        }
    },

    handleOverflow: function(ownerContext) {
        var me = this,
            methodName = me.layout.names.getWidth;

        me.showScrollers();
        return {
            reservedSpace: me.beforeCt[methodName]() + me.afterCt[methodName]()
        };
    },

    /**
     * @private
     * Gets references to the beforeCt and afterCt elements if they have not already been captured
     * and creates click handlers for them.
     */
    captureChildElements: function() {
        var me = this,
            el = me.layout.owner.el,
            before, after, hoverCls, pressedSuffix, pressedCls, hoverSuffix;

        // Grab the scroll click receiving elements
        if (!me.beforeCt) {
            hoverSuffix = '-hover';
            pressedSuffix = '-pressed';
            hoverCls = me.scrollerCls + hoverSuffix;
            pressedCls = me.scrollerCls + pressedSuffix;
            before = me.beforeScroller = el.getById(me.layout.owner.id + '-before-scroller');
            after = me.afterScroller = el.getById(me.layout.owner.id + '-after-scroller');
            me.beforeCt = before.up('');
            me.afterCt = after.up('');
            me.createWheelListener();

            before.addClsOnOver(hoverCls);
            before.addClsOnOver(me.beforeScrollerCls + hoverSuffix);
            before.addClsOnClick(pressedCls);
            before.addClsOnClick(me.beforeScrollerCls + pressedSuffix);
            after.addClsOnOver(hoverCls);
            after.addClsOnOver(me.afterScrollerCls + hoverSuffix);
            after.addClsOnClick(pressedCls);
            after.addClsOnClick(me.afterScrollerCls + pressedSuffix);

            before.setVisibilityMode(Ext.Element.DISPLAY);
            after.setVisibilityMode(Ext.Element.DISPLAY);

            me.beforeRepeater = new Ext.util.ClickRepeater(before, {
                interval: me.scrollRepeatInterval,
                handler : me.scrollLeft,
                scope   : me
            });

            me.afterRepeater = new Ext.util.ClickRepeater(after, {
                interval: me.scrollRepeatInterval,
                handler : me.scrollRight,
                scope   : me
            });
        }
    },

    /**
     * @private
     * Sets up an listener to scroll on the layout's innerCt mousewheel event
     */
    createWheelListener: function() {
        var me = this;
        me.layout.innerCt.on({
            mousewheel: function(e) {
                me.scrollBy(e.getWheelDelta() * me.wheelIncrement * -1, false);
            },
            stopEvent: true
        });
    },

    /**
     * @private
     */
    clearOverflow: function () {
        this.hideScrollers();
    },

    /**
     * @private
     * Shows the scroller elements in the beforeCt and afterCt. Creates the scrollers first if they are not already
     * present. 
     */
    showScrollers: function() {
        var me = this;

        me.captureChildElements();
        me.beforeScroller.show();
        me.afterScroller.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 in the beforeCt and afterCt
     */
    hideScrollers: function() {
        var me = this;

        if (me.beforeScroller !== undefined) {
            me.beforeScroller.hide();
            me.afterScroller.hide();
            me.layout.owner.removeClsWithUI(me.layout.direction === 'vertical' ? 'vertical-scroller' : 'scroller');
            // TODO - this may invalidates data in the ContextItem's styleCache
        }
    },

    /**
     * @private
     */
    destroy: function() {
        var me = this;

        Ext.destroy(me.beforeRepeater, me.afterRepeater, me.beforeScroller, me.afterScroller, me.beforeCt, me.afterCt);
    },

    /**
     * @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
     */
    scrollBy: function(delta, animate) {
        this.scrollTo(this.getScrollPosition() + delta, animate);
    },

    /**
     * @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,
            beforeMeth,
            afterMeth,
            beforeCls,
            afterCls,
            disabledCls,
            suffix = '-disabled';
            
        if (me.beforeScroller == null || me.afterScroller == null) {
            return;
        }

        beforeMeth = me.atExtremeBefore()  ? 'addCls' : 'removeCls';
        afterMeth  = me.atExtremeAfter() ? 'addCls' : 'removeCls';
        disabledCls = me.scrollerCls + suffix;
        beforeCls = [disabledCls, me.beforeScrollerCls + suffix];
        afterCls = [disabledCls, me.afterScrollerCls  + suffix];

        me.beforeScroller[beforeMeth](beforeCls);
        me.afterScroller[afterMeth](afterCls);
        me.scrolling = false;
    },

    /**
     * @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 me = this,
            layout = me.layout,
            result;

        // Until we actually scroll, the scroll[Top|Left] is stored as zero to avoid DOM hits, after that it's NaN.
        if (isNaN(me.scrollPosition)) {
            result = me.getElScrollPos();
        } else {
            result = me.scrollPosition;
        }
        return result;
    },

    /**
     * @private
     * Returns the maximum value we can scrollTo
     * @return {Number} The max scroll value
     */
    getMaxScrollPosition: function() {
        var me = this,
            layout = me.layout,
            maxScrollPos = me.scrollSize - layout.innerCt[layout.names.getWidth]();

        return (maxScrollPos < 0) ? 0 : maxScrollPos;
    },
    
    /**
     * @private
     * Returns true if the innerCt scroll is already at its left-most point
     * @return {Boolean} True if already at furthest left point
     */
    atExtremeBefore: function() {
        return !this.getScrollPosition();
    },

    /**
     * @private
     * Returns true if the innerCt scroll is already at its right-most point
     * @return {Boolean} True if already at furthest right point
     */
    atExtremeAfter: function() {
        return this.getScrollPosition() >= this.getMaxScrollPosition();
    },

    /**
     * @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 me = this,
            layout = me.layout,
            oldPosition = me.getScrollPosition(),
            newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition());

        if (newPosition != oldPosition && !me.scrolling) {
            me.scrollPosition = NaN;
            if (animate === undefined) {
                animate = me.animateScroll;
            }

            layout.innerCt.scrollTo(layout.names.beforeX, newPosition, animate ? me.getScrollAnim() : false);
            if (animate) {
                me.scrolling = true;
            } else {
                me.updateScrollButtons();
            }
            me.fireEvent('scroll', me, newPosition, animate ? me.getScrollAnim() : 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) {
        var me = this,
            layout = me.layout,
            owner = layout.owner,
            names = layout.names,
            visibility,
            box,
            newPos;

        item = me.getItem(item);
        if (item !== undefined) {
            if (item == owner.items.first()) {
                newPos = 0
            } else if (item === owner.items.last()) {
                newPos = me.getMaxScrollPosition();
            } else {
                visibility = me.getItemVisibility(item);
                if (!visibility.fullyVisible) {
                    box = item.getBox(false, true);
                    newPos = box[names.x];
                    if (visibility.hiddenEnd) {
                        newPos -= (me.layout.innerCt[names.getWidth]() - box[names.width]);
                    }
                }
            }
            me.scrollTo(newPos, animate);
        }
    },

    /**
     * @private
     * For a given item in the container, return an object with information on whether the item is visible
     * with the current innerCt scroll value.
     * @param {Ext.Component} item The item
     * @return {Object} Values for fullyVisible, hiddenStart and hiddenEnd
     */
    getItemVisibility: function(item) {
        var me          = this,
            box         = me.getItem(item).getBox(true, true),
            layout      = me.layout,
            names       = layout.names,
            itemStart   = box[names.x],
            itemEnd     = itemStart + box[names.width],
            scrollStart = me.getScrollPosition(),
            scrollEnd   = scrollStart + layout.innerCt[names.getWidth]();

        return {
            hiddenStart : itemStart < scrollStart,
            hiddenEnd   : itemEnd > scrollEnd,
            fullyVisible: itemStart > scrollStart && itemEnd < scrollEnd
        };
    }
});