/**
 * Overflow scroller enables scrolling within a box layout.
 *
 * Desktop devices use arrows by default. These arrows scroll the content.
 * Mobile devices use touch scrolling by default.
 *
 * For example usage see {@link Ext.layout.Box#overflow}.
 */
Ext.define('Ext.layout.overflow.Scroller', {
    alias: 'layout.overflow.scroller',
 
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    requires: [
        'Ext.Tool',
        'Ext.util.ClickRepeater'
    ],
 
    config: {
 
        /**
         * @cfg {Boolean}
         * `true` to show the scroller arrow tools
         */
        arrows: Ext.os.is.Desktop,
 
        /**
         * @cfg {Object/Ext.Tool}
         */
        backwardTool: {
            xtype: 'tool',
            ui: 'boxscroller',
            focusable: false
        },
 
        /**
         * @cfg {Boolean/Object} [animation=false]
         * Animation to use when scrolling using the scroller tools
         */
        animation: true,
 
        /**
         * @cfg {Object/Ext.Tool}
         */
        forwardTool: {
            xtype: 'tool',
            ui: 'boxscroller',
            focusable: false
        },
 
        /**
         * @cfg {'stretch'/'center'/'start'/'end'}
         * The alignment of the {@link #forwardTool forward} and {@link #backwardTool backward}
         * tools on the box layout's cross axis
         */
        toolAlign: 'stretch',
 
        /**
         * @cfg {Number/'item'/'page'}
         * The number of pixels to scroll when a scroller tool is clicked
         * Also can be the string 'item' to scroll the next tab into view or 'page'
         * to scroll the next full page of items into view.
         * Default is 'item'
         */
        increment: 'item',
 
        /**
         * @cfg {Number}
         * The time (in milliseconds) to repeat a click when a scroller tool is pressed down
         */
        repeatInterval: 500,
 
        /**
         * @cfg {Boolean}
         * `true` to allow horizontal scrolling in response to vertical mouse wheel events
         */
        mouseWheel: true,
 
        /**
         * @cfg {Number}
         * The number of pixels to scroll on each mouse `wheel` event when mouse wheel
         * scrolling is {@link #mouseWheel enabled}.
         */
        wheelIncrement: 10,
 
        /**
         * @private
         */
        vertical: null,
 
        /**
         * @private
         * The owning {@link Ext.layout.Box Box Layout}
         */
        owner: null,
 
        /**
         * @private
         * The owning {@link Ext.layout.Box Box Layout}'s Container
         */
        container: null,
 
        /**
         * @private
         * @readonly
         */
        wrap: true
    },
 
    cls: Ext.baseCSSPrefix + 'boxscroller',
    bodyCls: Ext.baseCSSPrefix + 'boxscroller-body',
 
    toolAlignCls: {
        start: Ext.baseCSSPrefix + 'tool-align-start',
        end: Ext.baseCSSPrefix + 'tool-align-end',
        center: Ext.baseCSSPrefix + 'tool-align-center',
        stretch: Ext.baseCSSPrefix + 'tool-align-stretch'
    },
 
    orientMap: {
        false: {
            cls: Ext.baseCSSPrefix + 'horizontal',
            crossSize: 'height',
            getSize: 'getWidth',
            setCrossSize: 'setHeight',
            measureCross: 'h',
            scrollSize: 'scrollWidth',
            scrollbarMargin: 'margin-bottom',
            forwardTool: 'scroll-right',
            backwardTool: 'scroll-left',
            startPad: 'l',
            endPad: 'r',
            posProp: 'x'
        },
        true: {
            cls: Ext.baseCSSPrefix + 'vertical',
            crossSize: 'width',
            getSize: 'getHeight',
            setCrossSize: 'setWidth',
            measureCross: 'w',
            scrollSize: 'scrollHeight',
            scrollbarMargin: 'margin-right',
            forwardTool: 'scroll-down',
            backwardTool: 'scroll-up',
            startPad: 't',
            endPad: 'b',
            posProp: 'y'
        }
    },
 
    constructor: function(config) {
        var me = this;
 
        me.repeaters = [];
 
        me.initConfig(config);
        me.syncMouseWheel();
 
        //<debug>
        if (me.getOwner().getPack() !== 'start') {
            Ext.raise('Overflow scroller requires box layout to be packed \'start\'');
        }
 
        if (me.getContainer().getAutoSize() === false) {
            Ext.raise('Overflow scroller is not compatible with autoSize: false');
        }
        //</debug>
 
        me.getContainer().$onScrollerContainerVisible = me.sync.bind(me);
    },
 
    applyWrap: function() {
        var me = this,
            container = me.getContainer(),
            containerBody = me.getContainer().bodyElement,
            wrap = Ext.Element.create({
                cls: me.cls,
                children: [{
                    cls: me.bodyCls
                }]
            }),
            uiCls = 'boxscroller',
            bodyEl = wrap.first();
 
        // Add the wrap el as a "reference element" on the Container so it can participate
        // in the Container's "UI"
        container.boxScrollerElement = wrap;
 
        // Add the boxScrollerElement to the Container's uiReferences map so that it's CSS
        // class names will be automatically synchronized when the container's UI changes.
        container.uiReferences.boxScrollerElement = uiCls;
 
        container.initUiReference('boxScrollerElement', uiCls);
 
        if (!container.isConfiguring) {
            // If the container has already been configured its UI classes have already been
            // synchronized on its reference elements.  Since we are adding the boxScrollerElement
            // to the Container references we need too sync its UI cls now.
            container.syncUiCls({
                boxScrollerElement: 'boxscroller'
            });
        }
 
        this._body = bodyEl;
 
        wrap.insertBefore(containerBody);
        bodyEl.appendChild(containerBody);
 
        return wrap;
    },
 
    getBody: function() {
        var body = this._body;
 
        if (!body) {
            this.getWrap();
        }
 
        return this._body;
    },
 
    onActiveItemChange: function(render, item) {
        var me = this;
 
        if (me.getContainer().rendered) {
            me.ensureVisible(item);
        }
    },
 
    onActiveTabChange: function(render, tab) {
        var me = this;
 
        if (me.getContainer().rendered) {
            me.ensureVisible(tab);
        }
    },
 
    ensureVisible: function(item, animation) {
        var me = this;
 
        if (animation === undefined) {
            animation = me.getAnimation();
        }
 
        me.getOwner().ensureVisible(item, { animation: animation });
    },
 
    createForwardTool: function(config) {
        var me = this;
 
        return Ext.apply({
            $initParent: me.getContainer(),
            hidden: true,
            preventRefocus: true
        }, config);
    },
 
    createBackwardTool: function(config) {
        var me = this;
 
        return Ext.apply({
            $initParent: me.getContainer(),
            hidden: true,
            preventRefocus: true
        }, config);
    },
 
    scrollToItemOffset: function(offset, page) {
        var animate = this.getAnimation();
 
        this.getOwner().ensureVisible({
            offset: offset,
            scroll: page ? 'max' : 'min',
            animation: animate
        });
    },
 
    applyVertical: function(vertical) {
        return !!vertical;
    },
 
    updateVertical: function(vertical) {
        var me = this,
            orientMap = me.orientMap[vertical];
 
        me.getWrap().replaceCls(me.orientMap[!vertical].cls, orientMap.cls);
 
        if (me.getArrows()) {
            me.getForwardTool().setType(orientMap.forwardTool);
            me.getBackwardTool().setType(orientMap.backwardTool);
        }
 
        if (!me.isConfiguring) {
            me.syncContainerScrollable();
            me.syncMouseWheel();
        }
    },
 
    applyForwardTool: function(tool, oldTool) {
        var ct = this.getContainer();
 
        tool = Ext.updateWidget(oldTool, tool, this, 'createForwardTool');
        delete tool.$initParent;
        tool.ownerCmp = ct;
 
        tool.doInheritUi();
        tool.addUi('boxscroller-' + ct.xtype);
 
        return tool;
    },
 
    applyBackwardTool: function(tool, oldTool) {
        var ct = this.getContainer();
 
        tool = Ext.updateWidget(oldTool, tool, this, 'createBackwardTool');
        delete tool.$initParent;
        tool.ownerCmp = ct;
 
        tool.doInheritUi();
        tool.addUi('boxscroller-' + ct.xtype);
 
        return tool;
    },
 
    updateArrows: function(arrows) {
        var me = this,
            container = me.getContainer(),
            el = me.getWrap(),
            forward = me.getForwardTool(),
            backward = me.getBackwardTool();
 
        el.insertFirst(backward.el);
        el.append(forward.el);
        me.addClickListener(forward, me.onForwardClick);
        me.addClickListener(backward, me.onBackwardClick);
 
        if (container.rendered) {
            me.sync();
        }
        else {
            container.whenVisible('$onScrollerContainerVisible');
            container.on({
                painted: 'sync',
                scope: me,
                single: true
            });
        }
    },
 
    updateMouseWheel: function() {
        if (!this.isConfiguring) {
            this.syncMouseWheel();
        }
    },
 
    updateOwner: function(owner) {
        var me = this,
            container = owner.getContainer();
 
        container.on({
            activeitemchange: 'onActiveItemChange',
            activetabchange: 'onActiveTabChange', // is this needed?
            scope: me
        });
 
        Ext.override(container, {
            getRefItems: function(deep) {
                var refItems = this.callParent([deep]), // this, not me!
                    forward = me.getForwardTool(),      // me, not this!
                    backward = me.getBackwardTool();    // me, not this!
 
                refItems.push(forward, backward);
 
                return refItems;
            }
        });
 
        me.syncContainerScrollable();
 
        container.getScrollable().on('scrollend', 'sync', me);
 
        container.on('resize', 'sync', me);
        container.bodyElement.on('resize', 'sync', me);
    },
 
    updateToolAlign: function(toolAlign, oldToolAlign) {
        var map = this.toolAlignCls;
 
        this.getWrap().replaceCls(map[oldToolAlign], map[toolAlign]);
    },
 
    getContainer: function() {
        return this.getOwner().getContainer();
    },
 
    destroy: function() {
        var me = this;
 
        Ext.destroy(me.repeaters);
        me.getForwardTool().destroy();
        me.getBackwardTool().destroy();
        me.callParent();
    },
 
    privates: {
        addClickListener: function(tool, clickFn) {
            var me = this,
                repeat = me.getRepeatInterval(),
                repeater;
 
            if (repeat) {
                repeater = new Ext.util.ClickRepeater(Ext.apply({
                    target: tool,
                    preventDefault: true,
                    listeners: {
                        click: clickFn,
                        scope: me
                    }
                }, repeat));
 
                me.repeaters.push(repeater);
            }
            else {
                tool.on({
                    click: clickFn,
                    scope: me
                });
            }
        },
 
        doMoveFromClick: function(offset) {
            var me = this,
                scrollable = me.getContainer().getScrollable(),
                animate = me.getAnimation(),
                isVertical = me.getVertical(),
                increment = me.getIncrement(),
                dx, dy;
 
            if (isNaN(increment)) {
                me.scrollToItemOffset(offset, increment === 'page');
            }
            else {
                dx = !isVertical ? increment : 0;
                dy = isVertical ? increment : 0;
                scrollable.scrollBy(dx * offset, dy * offset, animate);
            }
        },
 
        onBackwardClick: function() {
            this.doMoveFromClick(-1);
        },
 
        onForwardClick: function() {
            this.doMoveFromClick(1);
        },
 
        onMouseWheel: function(e) {
            var me = this,
                scrollable = me.getContainer().getScrollable(),
                isVertical = me.getVertical(),
                delta = e.getWheelDelta() * me.getWheelIncrement() * -1,
                dx = !isVertical ? delta : 0,
                dy = isVertical ? delta : 0;
 
            scrollable.scrollBy(dx, dy);
        },
 
        syncMouseWheel: function() {
            var me = this,
                target = me.getContainer().getScrollerTarget(),
                fn = me.getMouseWheel() && !me.getVertical() ? 'on' : 'un';
 
            target[fn]('wheel', 'onMouseWheel', me);
        },
 
        syncContainerScrollable: function() {
            var me = this,
                isVertical = me.getVertical(),
                container = me.getContainer();
 
            container.setScrollable({
                element: me.getBody(),
                x: !isVertical,
                y: isVertical
            });
        },
 
        sync: function() {
            var me = this,
                container = me.getContainer(),
                target = container.getScrollerTarget(),
                orientMap = me.orientMap[me.getVertical()],
                elementSize = container.el[orientMap.getSize](),
                targetSize = target.dom[orientMap.scrollSize],
                forwardTool = me.getForwardTool(),
                backwardTool = me.getBackwardTool(),
                scrollbarSize = Ext.getScrollbarSize()[orientMap.crossSize],
                hasOverflow = targetSize > elementSize,
                posProp = orientMap.posProp,
                scrollable, maxPos, pos;
 
            if (me.getArrows() && hasOverflow) {
                forwardTool.show();
                backwardTool.show();
 
                scrollable = container.getScrollable();
                maxPos = scrollable.getMaxPosition()[posProp];
                pos = scrollable.getPosition()[posProp];
 
                forwardTool.setDisabled(pos + target.getPadding(orientMap.endPad) >= maxPos);
                backwardTool.setDisabled(pos <= target.getPadding(orientMap.startPad));
            }
            else {
                forwardTool.hide();
                backwardTool.hide();
            }
 
            // The stylesheet uses padding and negative margin on the x-boxscroller-body
            // element in order to hide the scrollbar or floating scroll indicator, whichever
            // the browser may have. On browsers that have scrollbars we need an additional
            // adjustment to account for the additional height that the scrollbar adds to
            // the x-boxscroller-body element.
            container.bodyElement.setStyle(
                orientMap.scrollbarMargin,
                hasOverflow ? (-scrollbarSize + 'px') : 0
            );
 
            // Forces the height of the box scroller, this will remove any flicker
            // when changing heights/resizing later
            me.getBody()[orientMap.setCrossSize](
                container.bodyElement.measure(orientMap.measureCross)
            );
        }
    }
});