/**
 * @private
 * Base class for iOS and Android viewports.
 */
Ext.define('Ext.viewport.Default', function() {
    var TOP = 1,
        RIGHT = 2,
        BOTTOM = 4,
        LEFT = 8,
        sideMap = {
            top: TOP,
            right: RIGHT,
            bottom: BOTTOM,
            left: LEFT
        },
        oppositeSide = {
            "1": BOTTOM,
            "2": LEFT,
            "4": TOP,
            "8": RIGHT
        },
        oppositeSideNames = {
            left: 'right',
            right: 'left',
            top: 'bottom',
            bottom: 'top',
            up: 'bottom',
            down: 'top'
        };
 
    return {
        extend: 'Ext.Container',
 
        xtype: 'viewport',
 
        PORTRAIT: 'portrait',
 
        LANDSCAPE: 'landscape',
 
        requires: [
            'Ext.GlobalEvents',
            'Ext.layout.Card',
            'Ext.util.InputBlocker'
        ],
 
        nameHolder: true,
 
        /**
         * @event ready
         * Fires when the Viewport is in the DOM and ready.
         * @param {Ext.Viewport} this 
         */
 
        /**
         * @event maximize
         * Fires when the Viewport is maximized.
         * @param {Ext.Viewport} this 
         */
 
        /**
         * @event orientationchange
         * Fires when the Viewport orientation has changed.
         * @param {Ext.Viewport} this 
         * @param {String} newOrientation The new orientation.
         * @param {Number} width The width of the Viewport.
         * @param {Number} height The height of the Viewport.
         */
 
        config: {
            /**
             * @private
             */
            autoMaximize: false,
 
            /**
             * @private
             *
             * Auto blur the focused element when touching on a non-input. This is used to
             * work around Android bugs where the virtual keyboard is not hidden when tapping
             * outside an input.
             */
            autoBlurInput: true,
 
            /**
             * @cfg {Boolean} preventZooming
             * `true` to attempt to stop zooming when you double tap on the screen on mobile
             * devices, typically HTC devices with HTC Sense UI.
             * @accessor
             */
            preventZooming: false,
 
            /**
             * @cfg
             * @private
             */
            autoRender: true,
 
            /**
             * @cfg {Object/String} layout Configuration for this Container's layout. Example:
             *
             *     Ext.create('Ext.Container', {
             *         layout: {
             *             type: 'hbox',
             *             align: 'middle'
             *         },
             *         items: [
             *             {
             *                 xtype: 'panel',
             *                 flex: 1,
             *                 style: 'background-color: red;'
             *             },
             *             {
             *                 xtype: 'panel',
             *                 flex: 2,
             *                 style: 'background-color: green'
             *             }
             *         ]
             *     });
             *
             * @accessor
             */
            layout: 'card',
 
            /**
             * @cfg
             * @private
             */
            width: '100%',
 
            /**
             * @cfg
             * @private
             */
            height: '100%',
 
            /**
             * An object of all the menus on this viewport.
             * @private
             */
            menus: {},
 
            /**
             * @private
             */
            orientation: null,
 
            /**
             * @cfg {Number} swipeThreshold
             * The minimum distance an edge swipe must traverse in order to trigger showing
             * an edge menu.
             *
             * Note that reversing an edge swipe gesture back towards the edge aborts showing
             * that side's edge menu.
             */
            swipeThreshold: 30
        },
 
        classCls: Ext.baseCSSPrefix + 'viewport',
 
        getTemplate: function() {
            var template = this.callParent();
 
            // Used in legacy browser that do not support matchMedia. Hidden element is used
            // for checking of orientation
            if (!Ext.feature.has.MatchMedia) {
                template.unshift({
                    reference: 'orientationElement',
                    className: Ext.baseCSSPrefix + 'orientation-inspector',
                    children: [{
                        className: Ext.baseCSSPrefix + 'orientation-inspector-landscape'
                    }]
                });
            }
 
            return template;
        },
 
        /**
         * @property {Boolean} isReady
         * `true` if the DOM is ready.
         */
        isReady: false,
 
        isViewport: true,
 
        isMaximizing: false,
 
        id: 'ext-viewport',
 
        isInputRegex: /^(input|textarea|select|a)$/i,
 
        isInteractiveWebComponentRegEx: /^(audio|video)$/i,
 
        notScalableRe: /user-scalable=no/,
 
        focusable: false,
        focusEl: null,
        ariaEl: null,
 
        allSidesCls: [
            Ext.baseCSSPrefix + 'top',
            Ext.baseCSSPrefix + 'right',
            Ext.baseCSSPrefix + 'bottom',
            Ext.baseCSSPrefix + 'left'
        ],
 
        sideClsMap: {
            top: Ext.baseCSSPrefix + 'top',
            right: Ext.baseCSSPrefix + 'right',
            bottom: Ext.baseCSSPrefix + 'bottom',
            left: Ext.baseCSSPrefix + 'left'
        },
 
        hasViewportCls: Ext.baseCSSPrefix + 'has-viewport',
        fixedCls: Ext.baseCSSPrefix + 'fixed-viewport',
 
        /**
         * @private
         */
        fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',
 
        constructor: function(config) {
            var me = this,
                scroller;
 
            me.doPreventPanning = me.doPreventPanning.bind(me);
            me.doPreventZooming = me.doPreventZooming.bind(me);
 
            me.maximizeOnEvents = [
                'ready',
                'orientationchange'
            ];
 
            // set default devicePixelRatio if it is not explicitly defined
            window.devicePixelRatio = window.devicePixelRatio || 1;
 
            me.callParent([config]);
 
            me.updateSize();
            me.windowOuterHeight = me.getWindowOuterHeight();
 
            // The global scroller is our scroller.
            // We must provide a non-scrolling one if we are not configured to scroll,
            // otherwise the deferred ready listener in Scroller will create
            // one with scroll: true
            scroller = me.getScrollable();
 
            if (!scroller) {
                scroller = Ext.getViewportScroller().setConfig({
                    x: false,
                    y: false
                });
                scroller.component = me;
            }
 
            Ext.setViewportScroller(scroller);
 
            // The body has to be overflow:hidden
            Ext.getBody().setStyle('overflow', 'hidden');
 
            Ext.get(document.documentElement).addCls(me.hasViewportCls);
 
            me.stretchHeights = me.stretchHeights || {};
 
            if (Ext.feature.has.OrientationChange) {
                me.addWindowListener('orientationchange', me.onOrientationChange.bind(me));
            }
 
            if (!Ext.os.is.iOS || !me.isScalable()) {
                Ext.get(document.documentElement).addCls(me.fixedCls);
            }
 
            // Tale over firing the resize event to sync the Viewport first, then fire the event.
            Ext.GlobalEvents.on('resize', 'onWindowResize', me, { priority: 1000 });
 
            Ext.onDocumentReady(me.onDomReady, me);
 
            return me;
        },
 
        initialize: function() {
            var me = this;
 
            me.addMeta('apple-mobile-web-app-capable', 'yes');
            me.addMeta('apple-touch-fullscreen', 'yes');
 
            me.callParent();
        },
 
        getRefItems: function(deep) {
            var menus = this.getMenus(),
                result = this.callParent([deep]),
                side, menu;
 
            for (side in menus) {
                menu = menus[side];
 
                if (menu) {
                    Ext.Array.include(result, menu);
                }
            }
 
            return result;
        },
 
        initInheritedState: function(inheritedState, inheritedStateInner) {
            var me = this,
                root = Ext.rootInheritedState;
 
            if (inheritedState !== root) {
                // We need to go at this again but with the rootInheritedState object. Let
                // any derived class poke on the proper object!
                me.initInheritedState(
                    me.inheritedState = root,
                    me.inheritedStateInner = Ext.Object.chain(root));
            }
            else {
                me.callParent([inheritedState, inheritedStateInner]);
            }
        },
 
        onAppLaunch: function() {
            var me = this;
 
            if (!me.isReady) {
                me.onDomReady();
            }
        },
 
        onDomReady: function() {
            var me = this;
 
            if (me.isReady) {
                return;
            }
 
            me.isReady = true;
            me.updateSize();
            me.onReady();
            me.fireEvent('ready', me);
            Ext.GlobalEvents.fireEvent('viewportready', me);
        },
 
        onReady: function() {
            if (this.getAutoRender()) {
                this.render();
            }
        },
 
        render: function() {
            var me = this,
                body = Ext.getBody();
 
            if (!me.rendered) {
                // Render ourself *before* any existing floatRoot so that floateds
                // are always on top.
                me.callParent([body, Ext.floatRoot]);
 
                me.setOrientation(me.determineOrientation());
                Ext.getBody().addCls(Ext.baseCSSPrefix + me.getOrientation());
            }
        },
 
        applyAutoMaximize: function(autoMaximize) {
            return Ext.browser.is.WebView ? false : autoMaximize;
        },
 
        updateAutoMaximize: function(autoMaximize) {
            var me = this;
 
            if (autoMaximize) {
                me.on('ready', 'doAutoMaximizeOnReady', me, { single: true });
                me.on('orientationchange', 'doAutoMaximizeOnOrientationChange', me);
            }
            else {
                me.un('ready', 'doAutoMaximizeOnReady', me);
                me.un('orientationchange', 'doAutoMaximizeOnOrientationChange', me);
            }
        },
 
        updatePreventPanning: function(preventPanning) {
            this.toggleWindowListener(preventPanning, 'touchmove', this.doPreventPanning, false);
        },
 
        updatePreventZooming: function(preventZooming) {
            var touchstart = Ext.feature.has.TouchEvents ? 'touchstart' : 'mousedown';
 
            this.toggleWindowListener(preventZooming, touchstart, this.doPreventZooming, false);
        },
 
        doAutoMaximizeOnReady: function() {
            var me = this;
 
            me.isMaximizing = true;
 
            me.on('maximize', function() {
                me.isMaximizing = false;
 
                me.updateSize();
 
                me.fireEvent('ready', me);
            }, me, { single: true });
 
            me.maximize();
        },
 
        doAutoMaximizeOnOrientationChange: function() {
            var me = this;
 
            me.isMaximizing = true;
 
            me.on('maximize', function() {
                me.isMaximizing = false;
 
                me.updateSize();
            }, me, { single: true });
 
            me.maximize();
        },
 
        doPreventPanning: function(e) {
            var me = this,
                target = e.target,
                touch;
 
            // If we have an interaction on a WebComponent we need to check the actual shadow
            // dom element selected to determine if it is an input before preventing default
            // behavior.
            // Side effect to this is if the shadow input does not do anything with touchmove
            // the user could pan the screen.
            if (me.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches &&
                e.touches.length > 0) {
                touch = e.touches[0];
 
                if (touch && touch.target && me.isInputRegex.test(touch.target.tagName)) {
                    return;
                }
            }
 
            if (target && target.nodeType === 1 && !me.isInputRegex.test(target.tagName)) {
                e.preventDefault();
            }
        },
 
        doPreventZooming: function(e) {
            // Don't prevent right mouse event
            if ('button' in e && e.button !== 0) {
                return;
            }
 
            // eslint-disable-next-line vars-on-top
            var target = e.target,
                inputRe = this.isInputRegex,
                touch;
 
            if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches &&
                e.touches.length > 0) {
                touch = e.touches[0];
 
                if (touch && touch.target && inputRe.test(touch.target.tagName)) {
                    return;
                }
            }
 
            if (target && target.nodeType === 1 && !inputRe.test(target.tagName)) {
                e.preventDefault();
            }
        },
 
        addWindowListener: function(eventName, fn, capturing) {
            window.addEventListener(eventName, fn, Boolean(capturing));
        },
 
        removeWindowListener: function(eventName, fn, capturing) {
            window.removeEventListener(eventName, fn, Boolean(capturing));
        },
 
        supportsOrientation: function() {
            return Ext.feature.has.Orientation;
        },
 
        supportsMatchMedia: function() {
            return Ext.feature.has.MatchMedia;
        },
 
        onOrientationChange: function() {
            this.setOrientation(this.determineOrientation());
        },
 
        determineOrientation: function() {
            var me = this,
                orientationElement = me.orientationElement,
                nativeOrientation, visible;
 
            // First attempt will be to use Native Orientation information
            if (me.supportsOrientation()) {
                nativeOrientation = me.getWindowOrientation();
 
                // 90 || -90 || 270 is landscape
                if (Math.abs(nativeOrientation) === 90 || nativeOrientation === 270) {
                    return me.LANDSCAPE;
                }
 
                return me.PORTRAIT;
            }
 
            // Second attempt will be to use MatchMedia and a media query
            if (me.supportsMatchMedia()) {
                return window.matchMedia('(orientation : landscape)').matches
                    ? me.LANDSCAPE
                    : me.PORTRAIT;
            }
 
            // Fall back on hidden element with media query attached to it (media query in
            // Base Theme)
            if (orientationElement) {
                visible = orientationElement.first().isVisible();
 
                return visible ? me.LANDSCAPE : me.PORTRAIT;
            }
 
            return null;
        },
 
        updateOrientation: function(newValue, oldValue) {
            if (oldValue) {
                this.fireOrientationChangeEvent(newValue, oldValue);
            }
        },
 
        fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
            var me = this,
                newSize = me.updateSize();
 
            Ext.getBody().replaceCls(Ext.baseCSSPrefix + oldOrientation,
                                     Ext.baseCSSPrefix + newOrientation);
 
            me.fireEvent('orientationchange', me, newOrientation, newSize.width, newSize.height);
        },
 
        onWindowResize: function(width, height) {
            var me = this,
                oldWidth = me.lastSize.width,
                oldHeight = me.lastSize.height;
 
            me.updateSize(width, height);
 
            // On devices that do not support native orientation we use resize.
            // orientationchange events are only dispatched when there is an actual change
            // in orientation value so in cases on devices with orientation change events,
            // the setter is called an extra time, but stopped after.
            me.setOrientation(me.determineOrientation());
 
            // Only fire the event if we have actually resized.
            if (width != null) {
                me.fireEvent('resize', this, width, height, oldWidth, oldHeight);
            }
        },
 
        updateSize: function(width, height) {
            var lastSize = this.lastSize;
 
            lastSize.width = width !== undefined ? width : this.getWindowWidth();
            lastSize.height = height !== undefined ? height : this.getWindowHeight();
 
            return lastSize;
        },
 
        waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
            var scope = this,
                elapse = 0;
 
            if (!delay) {
                delay = 50;
            }
 
            if (!timeoutDuration) {
                timeoutDuration = 2000;
            }
 
            Ext.defer(function repeat() {
                elapse += delay;
 
                if (condition.call(scope) === true) {
                    if (onSatisfied) {
                        onSatisfied.call(scope);
                    }
                }
                else {
                    if (elapse >= timeoutDuration) {
                        if (onTimeout) {
                            onTimeout.call(scope);
                        }
                    }
                    else {
                        Ext.defer(repeat, delay);
                    }
                }
            }, delay);
        },
 
        maximize: function() {
            this.fireMaximizeEvent();
        },
 
        fireMaximizeEvent: function() {
            this.updateSize();
            this.fireEvent('maximize', this);
        },
 
        updateHeight: function(height, oldHeight) {
            Ext.getBody().setHeight(height);
            this.callParent([height, oldHeight]);
        },
 
        updateWidth: function(width, oldWidth) {
            Ext.getBody().setWidth(width);
            this.callParent([width, oldWidth]);
        },
 
        scrollToTop: function() {
            window.scrollTo(0, -1);
        },
 
        /**
         * Retrieves the document width.
         * @return {Number} width in pixels.
         */
        getWindowWidth: function() {
            return window.innerWidth;
        },
 
        /**
         * Retrieves the document height.
         * @return {Number} height in pixels.
         */
        getWindowHeight: function() {
            return window.innerHeight;
        },
 
        getWindowOuterHeight: function() {
            return window.outerHeight;
        },
 
        getWindowOrientation: function() {
            return window.orientation;
        },
 
        getSize: function() {
            return this.lastSize;
        },
 
        setItemFullScreen: function(item) {
            item.addCls(this.fullscreenItemCls);
            item.setTop(0);
            item.setRight(0);
            item.setBottom(0);
            item.setLeft(0);
            this.add(item);
        },
 
        /**
         * Sets a menu for a given side of the Viewport.
         *
         * Adds functionality to show the menu by swiping from the side of the screen from
         * the given side.
         *
         * If a menu is already set for a given side, it will be replaced by the passed menu.
         *
         * Available sides are: `left`, `right`, `top`, and `bottom`.
         *
         * **Note:** The `cover` and `reveal` animation configs are mutually exclusive.
         * Include only one animation config or omit both to default to `cover`.
         *
         * @param {Ext.Menu/Object} menu The menu instance or config to assign to the viewport.
         * @param {Object} config The configuration for the menu.
         * @param {'top'/'bottom'/'left'/'right'} config.side The side to put the menu on.
         * @param {Boolean} config.cover True to cover the viewport content. Defaults to `true`.
         * @param {Boolean} config.reveal True to push the menu alongside the viewport
         * content. Defaults to `false`.
         *
         * @return {Ext.Menu} The menu set for the passed side.
         */
        setMenu: function(menu, config) {
            var me = this,
                side, menus, oldMenu;
 
            config = config || {};
 
            //<debug>
            if (config.reveal && config.cover) {
                Ext.raise('[Ext.Viewport] setMenu(): Only one of reveal or cover is allowed');
            }
            //</debug>
 
            // Temporary workaround for body shifting issue
            if (Ext.os.is.iOS && !me.hasiOSOrientationFix) {
                me.hasiOSOrientationFix = true;
                me.on('orientationchange', function() {
                    window.scrollTo(0, 0);
                }, me);
            }
 
            //<debug>
            if (!menu) {
                Ext.raise("You must specify a menu configuration.");
            }
            //</debug>
 
            menus = me.getMenus();
 
            if (!me.addedSwipeListener) {
                me.attachSwipeListeners();
                me.addedSwipeListener = true;
            }
 
            // Either create the menu, or reconfigure a passed instance
            // according to the config settings for side, cover, and reveal.
            menu = me.configureMenu(menu, config);
            side = menu.getSide();
 
            // We we already have a menu for this side, ensure it's hidden
            // and reconfgure it using setConfig which will either use
            // the config in the default situation, that the menu is a Sheet,
            // or update the private property.
            oldMenu = menus[side];
 
            if (oldMenu && !oldMenu.destroyed && oldMenu !== menu) {
                me.hideMenu(side);
                oldMenu.setSide(null);
            }
 
            menus[side] = menu;
 
            me.setMenus(menus);
 
            return menu;
        },
 
        attachSwipeListeners: function() {
            var me = this;
 
            me.element.on({
                tap: me.onTap,
                swipestart: me.onSwipeStart,
                edgeswipestart: me.onEdgeSwipeStart,
                edgeswipe: me.onEdgeSwipe,
                edgeswipeend: me.onEdgeSwipeEnd,
                scope: me
            });
        },
 
        configureMenu: function(menu, config) {
            // We may be creating or reconfiguring a menu here.
            // If reconfiguring, only change configs that are present in the passed config.
            var isInstanced = menu.isComponent,
                // If an instance is being reconfigured, and the config is silent about
                // reveal, cover, or side, we must use the instance's current setting.
                reveal = isInstanced && !('reveal' in config) ? menu.getReveal() : !!config.reveal,
                cover = (isInstanced && !('cover' in config)
                    ? menu.getCover()
                    : config.cover) !== false && !reveal,
                side = isInstanced && !('side' in config) ? menu.getSide() : config.side,
                wasFloated;
 
            //<debug>
            if (!side) {
                Ext.raise("You must specify a side to dock the menu.");
            }
 
            if (!sideMap[side]) {
                Ext.raise('Invalid side; must left, right, top or bottom.');
            }
            //</debug>
 
            // Upgrade the config object to have the correct configurations as defaulted
            // in from the existing instance if it is an instance.
            config = {
                hideAnimation: null,
                showAnimation: null,
                hidden: true,
                floated: cover,
                zIndex: cover ? null : 5,
                reveal: reveal,
                cover: cover,
                side: side
            };
            config[oppositeSideNames[side]] = null;
 
            if (isInstanced) {
                wasFloated = menu.getFloated();
 
                // Flipping modes - the menu has to be derendered.
                if (config.floated !== wasFloated) {
                    if (menu.rendered) {
                        // If menu was covering the viewport, then it was a floated child
                        // just remove it non-destructively. It will be derendered and lose
                        // its parent reference.
                        if (wasFloated) {
                            this.remove(menu, false);
                        }
                        // If we're in the non-standard menu insertion mode, we need to derender.
                        // Floated setRender(false) does unwrap the component from its floatWrap.
                        else {
                            menu.el.dom.parentNode.removeChild(menu.el.dom);
                            menu.setRendered(false);
                        }
                    }
 
                    // Clear down old positioning
                    menu.setConfig({
                        top: null,
                        right: null,
                        bottom: null,
                        left: null
                    });
                }
 
                // Reconfigure instance according to config.
                // Use strict: false because if the menu is not an instance of Sheet,
                // the reveal, cover and side configs are merely private properties.
                menu.setConfig(config, null, {
                    strict: false
                });
            }
            else {
                config.xtype = 'actionsheet';
                menu = Ext.create(Ext.apply(config, menu));
            }
 
            // Update the positioning configs *after* the floatedness has been fully setttled
            // If applied during configuration, these imply positioned, and *not* floated.
            config = {
                left: 0,
                right: 0,
                top: 0,
                bottom: 0
            };
            config[oppositeSideNames[side]] = null;
            menu.setConfig(config);
 
            menu.toggleCls(menu.floatingCls, !menu.getFloated());
            menu.removeCls(this.getLayout().itemCls);
            menu.toggleCls(Ext.baseCSSPrefix + 'menu-cover', cover);
            menu.toggleCls(Ext.baseCSSPrefix + 'menu-reveal', reveal);
            menu.replaceCls(this.allSidesCls, this.sideClsMap[side]);
            menu.isViewportMenu = true;
 
            return menu;
        },
 
        /**
         * Removes a menu from a specified side.
         * @param {'top'/'bottom'/'left'/'right'} side The side to remove the menu from
         * @param {Boolean} animation Pass `true` to animate the menu out of view
         */
        removeMenu: function(side, animation) {
            var me = this,
                menus = me.getMenus() || {},
                menu = menus[side];
 
            if (menu) {
                me.hideMenu(side, animation);
                menu.removeCls(me.sideClsMap[side]);
            }
 
            delete menus[side];
            me.setMenus(menus);
        },
 
        /**
         * Shows the menu that has been {@link #method!setMenu set} on the passed side.
         *
         * If no menu has been set on the passed side, nothing hapens.
         * @param {'top'/'bottom'/'left'/'right'} side The side to show the menu for.
         * @param {Boolean} animation if false, the menu will be shown without animation.
         */
        showMenu: function(side, animation) {
            var me = this,
                sideValue = sideMap[side],
                menu = me.getMenus()[side],
                viewportAfter = {
                    translateX: 0,
                    translateY: 0
                },
                size;
 
            if (!menu || !menu.isHidden()) {
                return;
            }
 
            if (animation === false) {
                menu.show(false, {
                    side: null
                });
 
                return;
            }
 
            // Ensures Menu is rendered, and positioned for animation
            me.beforeMenuAnimate(menu);
 
            size = menu.element.measure(sideValue & (LEFT | RIGHT) ? 'w' : 'h');
 
            if (sideValue === LEFT) {
                viewportAfter.translateX = size;
            }
            else if (sideValue === RIGHT) {
                viewportAfter.translateX = -size;
            }
            else if (sideValue === TOP) {
                viewportAfter.translateY = size;
            }
            else if (sideValue === BOTTOM) {
                viewportAfter.translateY = -size;
            }
 
            // Menu always animates in. Animate to an untranslated state.
            menu.translate(0, 0, {
                duration: 200
            });
 
            // Viewport only animates out of its way if menu is not not floated.
            if (!menu.getFloated()) {
                me.translate(viewportAfter.translateX, viewportAfter.translateY, {
                    duration: 200
                });
            }
        },
 
        /**
         * Hides a menu specified by the menu's side.
         * @param {'top'/'bottom'/'left'/'right'} side The side which the menu is placed.
         * @param {Boolean} animation if false, the menu will be hidden without animation.
         */
        hideMenu: function(side, animation) {
            var me = this,
                sideValue = sideMap[side],
                menu = me.getMenus()[side],
                after = {
                    translateX: 0,
                    translateY: 0
                },
                size;
 
            animation = animation !== false;
 
            if (!menu || menu.isHidden()) {
                return;
            }
 
            size = menu.element.measure(sideValue & (LEFT | RIGHT) ? 'w' : 'h');
 
            if (sideValue === LEFT) {
                after.translateX = -size;
            }
            else if (sideValue === RIGHT) {
                after.translateX = size;
            }
            else if (sideValue === TOP) {
                after.translateY = -size;
            }
            else if (sideValue === BOTTOM) {
                after.translateY = size;
            }
 
            // Animate menu out of view if told to
            if (animation) {
                menu.revertFocus();
                menu.translate(after.translateX, after.translateY, {
                    duration: 200,
                    callback: function() {
                        if (!menu.destroyed) {
                            menu.translate(0, 0);
                            menu.setHidden(true);
                        }
                    }
                });
            }
            // Otherwise hide it immediately
            else {
                menu.getTranslatable().stopAnimation();
                menu.setHidden(true);
            }
 
            // Viewport only has to move back into place if menu is not not floated
            // Return it to an unstranslated state
            if (!menu.getFloated()) {
                me.translate(0, 0, animation ? { duration: 200 } : null);
            }
        },
 
        /**
         * Hides all visible menus.
         */
        hideAllMenus: function(animation) {
            var menus = this.getMenus(),
                side;
 
            for (side in menus) {
                this.hideMenu(side, animation);
            }
        },
 
        /**
         * Hides all menus except for the side specified
         * @param {'top'/'bottom'/'left'/'right'} side Side not to hide.
         * @param {Boolean} animate if false, the menu will be hidden without animation.
         */
        hideOtherMenus: function(side, animate) {
            var menus = this.getMenus(),
                menu;
 
            for (menu in menus) {
                if (side !== menu) {
                    this.hideMenu(menu, animate);
                }
            }
        },
 
        /**
         * Toggles the menu specified by side
         * @param {'top'/'bottom'/'left'/'right'} side The side which the menu is placed.
         */
        toggleMenu: function(side) {
            var menus = this.getMenus(),
                menu;
 
            if (menus[side]) {
                menu = menus[side];
 
                menu.setDisplayed(menu.isHidden());
            }
        },
 
        applyScrollable: function(scrollable) {
            return this.callParent([ scrollable, Ext.getViewportScroller() ]);
        },
 
        doDestroy: function() {
            var me = this,
                docEl = Ext.get(document.documentElement),
                scroller = me._scrollable;
 
            docEl.removeCls(me.hasViewportCls);
            docEl.removeCls(me.fixedCls);
 
            // We acquired usage of the global body scroller through our applyScrollable.
            // Just relinquish it here and allow it to live on.
            if (scroller) {
                // Return the body scroller to default; X and Y scrolling
                scroller.setConfig({
                    x: true,
                    y: true
                });
                me._scrollable = null;
            }
 
            Ext.un('resize', 'onWindowResize', me);
 
            me.callParent();
 
            Ext.Viewport = null;
        },
 
        privates: {
            addMeta: function(name, content) {
                var meta = document.createElement('meta');
 
                meta.setAttribute('name', name);
                meta.setAttribute('content', content);
                Ext.getHead().append(meta);
            },
 
            /**
             * Sets up the before conditions to begin animating a menu into view
             * whether from {@link #method!showMenu}, or {@link #method!beginEdgeSwipe}
             * @param {Ext.Sheet} menu The menu to setup the animation.
             * @private
             */
            beforeMenuAnimate: function(menu) {
                var me = this,
                    side = menu.getSide(),
                    sideValue = sideMap[side],
                    before = {
                        translateX: 0,
                        translateY: 0
                    },
                    size, modal;
 
                me.hideOtherMenus(side);
 
                // Ensure the menu is in place as a floated child, or programmatically render it.
                if (menu.getFloated()) {
                    me.add(menu);
                }
                else {
                    // We're going off the reservation by programmatically adding to the
                    // document body.
                    // Usually onAdded does this stuff. We must render the menu and its modal mask.
                    Ext.getBody().insertFirst(menu.element);
 
                    if (!menu.rendered) {
                        menu.setRendered(true);
                    }
 
                    modal = menu.getModal();
 
                    if (modal) {
                        Ext.getBody().insertFirst(modal.element);
 
                        if (!modal.rendered) {
                            modal.setRendered(true);
                        }
                    }
 
                    // Must initialize the translatable config to be able to animate
                    me.translate(0, 0);
                }
 
                menu.removeCls(me.getLayout().itemCls);
 
                // Now show the edge menu through the normal component show pathway
                // which will fire the expected template methods and events.
                menu.show(false, {
                    side: null
                });
 
                size = menu.element.measure(sideValue & (LEFT | RIGHT) ? 'w' : 'h');
 
                if (sideValue === LEFT) {
                    before.translateX = -size;
                }
                else if (sideValue === RIGHT) {
                    before.translateX = size;
                }
                else if (sideValue === TOP) {
                    before.translateY = -size;
                }
                else if (sideValue === BOTTOM) {
                    before.translateY = size;
                }
 
                menu.translate(before.translateX, before.translateY);
            },
 
            /**
             * @private
             */
            disableTabbing: function() {
                if (this.tabbingDisabled) {
                    return;
                }
 
                this.tabbingDisabled = true;
 
                this.callParent();
            },
 
            /**
             * @private
             */
            enableTabbing: function(reset) {
                if (!this.tabbingDisabled) {
                    return;
                }
 
                this.tabbingDisabled = false;
 
                this.callParent([reset]);
            },
 
            doAddListener: function(eventName, fn, scope, options, order, caller, manager) {
                var me = this;
 
                if (eventName === 'ready' && me.isReady && !me.isMaximizing) {
                    fn.call(scope);
 
                    return me;
                }
 
                me.callParent([eventName, fn, scope, options, order, caller, manager]);
            },
 
            /**
             * Returns true if the user can zoom the viewport
             * @private
             */
            isScalable: function() {
                var me = this,
                    metas = document.querySelectorAll('meta[name="viewport"]'),
                    // if there are multiple viewport tags the last one wins.
                    meta = metas.length && metas[metas.length - 1],
                    scalable = true,
                    content;
 
                if (meta) {
                    content = meta.getAttribute('content');
 
                    scalable = !(content && me.notScalableRe.test(content));
                }
 
                return scalable;
            },
 
            /**
             * @private
             */
            onTap: function(e) {
                // this.hideAllMenus();
            },
 
            /**
             * @private
             */
            onSwipeStart: function(e) {
                var side = this.sideForSwipeDirection(e.direction),
                    menu = this.getMenus()[side];
 
                // preventing menu scrolling from being captured as viewport swiping
                if (menu && !menu.owns(e)) {
                    this.hideMenu(side);
                }
            },
 
            /**
             * @private
             */
            onEdgeSwipeStart: function(e) {
                var me = this,
                    menus = me.getMenus(),
                    menu = menus[oppositeSideNames[e.direction]],
                    menuSide, checkMenu;
 
                if (!menu || !menu.isHidden()) {
                    return;
                }
 
                // Claim the gesture to prevent viewport panning.
                e.claimGesture();
 
                for (menuSide in menus) {
                    checkMenu = menus[menuSide];
 
                    if (checkMenu.isVisible()) {
                        return;
                    }
                }
 
                me.$swiping = true;
 
                // Ensures Menu is rendered, and positioned for animation
                me.beforeMenuAnimate(menu);
            },
 
            /**
             * @private
             */
            onEdgeSwipe: function(e) {
                var me = this,
                    side = me.sideForDirection(e.direction),
                    menu = me.getMenus()[oppositeSideNames[e.direction]],
                    size, after, viewportAfter,
                    movement, viewportMovement;
 
                if (!menu || !me.$swiping) {
                    return;
                }
 
                // Claim the gesture to prevent viewport panning.
                e.claimGesture();
 
                // See if the swipe has been reversed
                if (e.distance !== me.lastSwipeDistance) {
                    me.reverseSwiping = e.distance < me.lastSwipeDistance;
                }
 
                me.lastSwipeDistance = e.distance;
                size = menu.element.measure(side & (LEFT | RIGHT) ? 'w' : 'h');
                movement = Math.min(e.distance - size, 0);
                viewportMovement = Math.min(e.distance, size);
 
                after = {
                    translateX: 0,
                    translateY: 0
                };
 
                viewportAfter = {
                    translateX: 0,
                    translateY: 0
                };
 
                if (side === LEFT) {
                    after.translateX = movement;
                    viewportAfter.translateX = viewportMovement;
                }
                else if (side === RIGHT) {
                    after.translateX = -movement;
                    viewportAfter.translateX = -viewportMovement;
                }
                else if (side === TOP) {
                    after.translateY = movement;
                    viewportAfter.translateY = viewportMovement;
                }
                else if (side === BOTTOM) {
                    after.translateY = -movement;
                    viewportAfter.translateY = -viewportMovement;
                }
 
                menu.translate(after.translateX, after.translateY);
 
                // Viewport only animates out of its way if menu is not not floated.
                if (!menu.getFloated()) {
                    me.translate(viewportAfter.translateX, viewportAfter.translateY);
                }
            },
 
            /**
             * @private
             */
            onEdgeSwipeEnd: function(e) {
                var me = this,
                    side = me.sideForDirection(e.direction),
                    menu = me.getMenus()[oppositeSideNames[e.direction]],
                    shouldRevert = me.reverseSwiping || (e.distance < me.getSwipeThreshold()),
                    after = {
                        translateX: 0,
                        translateY: 0
                    },
                    viewportAfter = {
                        translateX: 0,
                        translateY: 0
                    },
                    size, velocity, movement, viewportMovement;
 
                if (!menu) {
                    return;
                }
 
                size = menu.element.measure(side & (LEFT | RIGHT) ? 'w' : 'h');
                velocity = (e.flick) ? e.flick.velocity : 0;
 
                // check if continuing in the right direction
                if (side === RIGHT) {
                    if (velocity.x > 0) {
                        shouldRevert = true;
                    }
                }
                else if (side === LEFT) {
                    if (velocity.x < 0) {
                        shouldRevert = true;
                    }
                }
                else if (side === TOP) {
                    if (velocity.y < 0) {
                        shouldRevert = true;
                    }
                }
                else if (side === BOTTOM) {
                    if (velocity.y > 0) {
                        shouldRevert = true;
                    }
                }
 
                movement = shouldRevert ? size : 0;
                viewportMovement = shouldRevert ? 0 : -size;
 
                if (side === LEFT) {
                    after.translateX = -movement;
                    viewportAfter.translateX = -viewportMovement;
                }
                else if (side === RIGHT) {
                    after.translateX = movement;
                    viewportAfter.translateX = viewportMovement;
                }
                else if (side === TOP) {
                    after.translateY = -movement;
                    viewportAfter.translateY = -viewportMovement;
                }
                else if (side === BOTTOM) {
                    after.translateY = movement;
                    viewportAfter.translateY = viewportMovement;
                }
 
                // Menu always animates in (or out if reverting)
                menu.translate(after.translateX, after.translateY, {
                    duration: 200,
                    callback: function() {
                        if (shouldRevert) {
                            menu.setHidden(true);
                        }
                    }
                });
 
                // Viewport only animates out of menu's way if menu is not not floated.
                if (!menu.getFloated()) {
                    me.translate(viewportAfter.translateX, viewportAfter.translateY, {
                        duration: 200
                    });
                }
 
                me.$swiping = false;
            },
 
            /**
             * @private
             */
            sideForDirection: function(direction) {
                if (direction === 'up') {
                    direction = 'top';
                }
                else if (direction === 'down') {
                    direction = 'bottom';
                }
 
                return oppositeSide[sideMap[direction]];
            },
 
            /**
             * @private
             */
            sideForSwipeDirection: function(direction) {
                if (direction === 'up') {
                    return 'top';
                }
 
                if (direction === 'down') {
                    return 'bottom';
                }
 
                return direction;
            },
 
            toggleWindowListener: function(on, eventName, fn, capturing) {
                if (on) {
                    this.addWindowListener(eventName, fn, capturing);
                }
                else {
                    this.removeWindowListener(eventName, fn, capturing);
                }
            }
        }
    };
});