/**
 * @private
 * Base class for iOS and Android viewports.
 */
Ext.define('Ext.viewport.Default', {
    extend: 'Ext.Container',
 
    xtype: 'viewport',
 
    PORTRAIT: 'portrait',
 
    LANDSCAPE: 'landscape',
 
    requires: [
        'Ext.GlobalEvents',
        'Ext.LoadMask',
        'Ext.layout.Card',
        'Ext.util.InputBlocker'
    ],
 
    stripQuoteRe: /"/g,
 
    /**
     * @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} preventPanning 
         * Whether or not to always prevent default panning behavior of the
         * browser's viewport.
         * @accessor
         */
        preventPanning: 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%',
 
        useBodyElement: true,
 
        /**
         * An object of all the menus on this viewport.
         * @private
         */
        menus: {},
 
        /**
         * @private
         */
        orientation: null
    },
 
    getElementConfig: function() {
        var cfg = this.callParent(arguments);
 
        // Used in legacy browser that do not support matchMedia. Hidden element is used for checking of orientation 
        if (!Ext.feature.has.MatchMedia) {
            cfg.children.unshift({reference: 'orientationElement', className: 'x-orientation-inspector'});
        }
        return cfg;
    },
 
    /**
     * @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,
 
    focusedElement: null,
 
    /**
     * @private
     */
    fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',
 
    constructor: function(config) {
        var me = this,
            Component = Ext.Component,
            DomScroller = Ext.scroll.DomScroller;
 
        // By default document.body is monitored by a special DomScroller singleton so that 
        // the global scroll event fires when the document scrolls. 
        // A Viewport's Scroller will take over from this one. 
        if (DomScroller.document) {
            DomScroller.document = DomScroller.document.destroy();
        }
 
        me.doPreventPanning = me.doPreventPanning.bind(me);
        me.doPreventZooming = me.doPreventZooming.bind(me);
        me.doBlurInput = me.doBlurInput.bind(me);
 
        me.maximizeOnEvents = [
          'ready',
          'orientationchange'
        ];
 
      // set default devicePixelRatio if it is not explicitly defined 
        window.devicePixelRatio = window.devicePixelRatio || 1;
 
        me.callParent([config]);
 
        me.windowWidth = me.getWindowWidth();
        me.windowHeight = me.getWindowHeight();
        me.windowOuterHeight = me.getWindowOuterHeight();
 
        me.stretchHeights = me.stretchHeights || {};
 
        if (Ext.feature.has.OrientationChange) {
            me.addWindowListener('orientationchange', me.onOrientationChange.bind(me));
        }
 
        // Viewport is initialized before event system, we need to wait until the application is ready before 
        // we add the resize listener. Otherwise it will only fire if another resize listener is added later. 
        Ext.onReady(function() {
            me.addWindowListener('resize', me.onResize.bind(me));
        });
 
        document.addEventListener('focus', me.onElementFocus.bind(me), true);
        document.addEventListener('blur', me.onElementBlur.bind(me), true);
 
        Ext.onDocumentReady(me.onDomReady, me);
 
        if (!Component.on) {
            Ext.util.Observable.observe(Component);
        }
 
        Component.on('fullscreen', 'onItemFullscreenChange', me);
 
        return me;
    },
 
    initialize: function() {
        var me = this;
 
        me.addMeta('apple-mobile-web-app-capable', 'yes');
        me.addMeta('apple-touch-fullscreen', 'yes');
 
        me.callParent();
    },
 
    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();
        }
        if (Ext.browser.name === 'ChromeiOS') {
            this.setHeight('-webkit-calc(100% - ' + ((window.outerHeight - window.innerHeight) / 2) + 'px)');
        }
    },
 
    onElementFocus: function(e) {
        this.focusedElement = e.target;
    },
 
    onElementBlur: function() {
        this.focusedElement = null;
    },
 
    render: function() {
        if (!this.rendered) {
            var body = Ext.getBody(),
                clsPrefix = Ext.baseCSSPrefix,
                classList = [],
                osEnv = Ext.os,
                osName = osEnv.name.toLowerCase(),
                browserName = Ext.browser.name.toLowerCase(),
                osMajorVersion = osEnv.version.getMajor(),
                theme;
 
            this.renderTo(body);
 
            classList.push(clsPrefix + osEnv.deviceType.toLowerCase());
 
            if (osEnv.is.iPad) {
                classList.push(clsPrefix + 'ipad');
            }
 
            classList.push(clsPrefix + osName);
            classList.push(clsPrefix + browserName);
            if (Ext.toolkit) {
                classList.push(clsPrefix + Ext.toolkit);
            }
 
            if (osMajorVersion) {
                classList.push(clsPrefix + osName + '-' + osMajorVersion);
            }
 
            if (osEnv.is.BlackBerry) {
                classList.push(clsPrefix + 'bb');
                if (Ext.browser.userAgent.match(/Kbd/gi)) {
                    classList.push(clsPrefix + 'bb-keyboard');
                }
            }
 
            if (Ext.browser.is.WebKit) {
                classList.push(clsPrefix + 'webkit');
            }
 
            if (Ext.browser.is.WebView) {
                classList.push(clsPrefix + 'webview');
            }
 
            if (Ext.browser.is.Standalone) {
                classList.push(clsPrefix + 'standalone');
            }
 
            if (Ext.browser.is.AndroidStock) {
                classList.push(clsPrefix + 'android-stock');
            }
 
            if (Ext.browser.is.GoogleGlass) {
                classList.push(clsPrefix + 'google-glass');
            }
 
            this.setOrientation(this.determineOrientation());
            classList.push(clsPrefix + this.getOrientation());
 
            body.addCls(classList);
 
            theme = Ext.theme;
            if (theme && theme.getDocCls) {
                // hook for theme overrides to add css classes to the <html> element 
                Ext.fly(document.documentElement).addCls(theme.getDocCls());
            }
        }
    },
 
    updateAutoBlurInput: function(autoBlurInput) {
        var touchstart = Ext.feature.has.TouchEvents ? 'touchstart' : 'mousedown';
        this.toggleWindowListener(autoBlurInput, touchstart, this.doBlurInput, false);
    },
 
    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();
    },
 
    doBlurInput: function(e) {
        var target = e.target,
            focusedElement = this.focusedElement;
        //In IE9/10 browser window loses focus and becomes inactive if focused element is <body>. So we shouldn't call blur for <body> 
        // In FF, the focusedElement can be the document which doesn't have a blur method 
        if (focusedElement && focusedElement.blur && focusedElement.nodeName.toUpperCase() != 'BODY' && !this.isInputRegex.test(target.tagName)) {
            delete this.focusedElement;
            focusedElement.blur();
        }
    },
 
    doPreventPanning: function(e) {
        var 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 (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
            touch = e.touches[0];
            if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
                return;
            }
        }
 
        if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
            e.preventDefault();
        }
    },
 
    doPreventZooming: function(e) {
        // Don't prevent right mouse event 
        if ('button' in e && e.button !== 0) {
            return;
        }
 
        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,
            nativeOrientation;
 
        // 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;
            } else {
                return me.PORTRAIT;
            }
            // Second attempt will be to use MatchMedia and a media query 
        } else 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) 
        } else if (me.orientationElement) {
            return me.orientationElement.getStyle('content').replace(me.stripQuoteRe,'');
        }
 
        return null;
    },
 
    updateOrientation: function(newValue, oldValue) {
        if (oldValue) {
            this.fireOrientationChangeEvent(newValue, oldValue);
        }
    },
 
    fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
        var me = this,
            body = Ext.getBody(),
            clsPrefix = Ext.baseCSSPrefix;
 
        body.replaceCls(clsPrefix + oldOrientation, clsPrefix + newOrientation);
 
        me.updateSize();
 
        // Switched to using Width/Height of viewport as it is more consistent across Android and iOS 
        // using the inner window height/width caused iOS9 issues and was not updated to the correct value in Android Chrome 
        me.fireEvent('orientationchange', me, newOrientation, me.getWidth(), me.getHeight());
    },
 
    onResize: function() {
        var me = this;
 
        me.updateSize();
 
        // 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());
    },
 
    updateSize: function(width, height) {
        var me = this;
 
        me.windowWidth = width !== undefined ? width : me.getWindowWidth();
        me.windowHeight = height !== undefined ? height : me.getWindowHeight();
 
        return me;
    },
 
    waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
        if (!delay) {
            delay = 50;
        }
 
        if (!timeoutDuration) {
            timeoutDuration = 2000;
        }
 
        var scope = this,
            elapse = 0;
 
        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 {
            width: this.windowWidth,
            height: this.windowHeight
        };
    },
 
    onItemFullscreenChange: function(item) {
        item.addCls(this.fullscreenItemCls);
        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 removed.
     *
     * 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 {String} 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.
     */
    setMenu: function(menu, config) {
        config = config || {};
 
        var me = this,
            side = config.side,
            menus;
 
        // 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.Logger.error("You must specify a side to dock the menu.");
        }
 
        if (!side) {
            Ext.Logger.error("You must specify a side to dock the menu.");
        }
 
        if (['left', 'right', 'top', 'bottom'].indexOf(side) == -1) {
            Ext.Logger.error("You must specify a valid side (left, right, top or botom) to dock the menu.");
        }
        //</debug> 
 
        menus = me.getMenus();
 
        if (!menus) {
            menus = {};
        }
 
        if (!me.addedSwipeListener) {
            me.attachSwipeListeners();
            me.addedSwipeListener = true;
        }
 
        // If we have a menu cfg and no type was passed, we need to 
        // setup the type. This template method exists to defer 
        // for subclasses 
        if (!menu.isComponent) {
            if (!menu.xclass && !menu.xtype) {
                menu = me.getMenuCfg(menu, side);
            }
            menu = Ext.create(menu);
        }
 
        menus[side] = menu;
        menu.$reveal = Boolean(config.reveal);
        menu.$cover = config.cover !== false && !menu.$reveal;
        menu.$side = side;
 
        me.fixMenuSize(menu, side);
 
        if (side == 'left') {
            menu.setLeft(0);
            menu.setRight(null);
            menu.setTop(0);
            menu.setBottom(0);
        } else if (side == 'right') {
            menu.setLeft(null);
            menu.setRight(0);
            menu.setTop(0);
            menu.setBottom(0);
        } else if (side == 'top') {
            menu.setLeft(0);
            menu.setRight(0);
            menu.setTop(0);
            menu.setBottom(null);
        } else if (side == 'bottom') {
            menu.setLeft(0);
            menu.setRight(0);
            menu.setTop(null);
            menu.setBottom(0);
        }
 
        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
        });
    },
 
    getMenuCfg: function(menu, side) {
        return Ext.apply({
            xtype: 'menu'
        }, menu);
    },
 
    /**
     * Removes a menu from a specified side.
     * @param {String} side The side to remove the menu from
     */
    removeMenu: function(side) {
        var menus = this.getMenus() || {},
            menu = menus[side];
 
        if (menu) {
            this.hideMenu(side);
        }
        delete menus[side];
        this.setMenus(menus);
    },
 
    /**
     * @private
     * Changes the sizing of the specified menu so that it displays correctly when shown.
     */
    fixMenuSize: function(menu, side) {
        if (side == 'top' || side == 'bottom') {
            menu.setWidth('100%');
        } else if (side == 'left' || side == 'right') {
            menu.setHeight('100%');
        }
    },
 
    /**
     * Shows a menu specified by the menu's side.
     * @param {String} side The side which the menu is placed.
     */
    showMenu: function(side) {
        var me = this,
            menus = me.getMenus(),
            menu = menus[side],
            before, after,
            viewportBefore, viewportAfter, size;
 
        if (!menu || menu.isAnimating) {
            return;
        }
 
        me.hideOtherMenus(side);
 
        before = {
            translateX: 0,
            translateY: 0
        };
 
        after = {
            translateX: 0,
            translateY: 0
        };
 
        viewportBefore = {
            translateX: 0,
            translateY: 0
        };
 
        viewportAfter = {
            translateX: 0,
            translateY: 0
        };
 
        if (menu.$reveal) {
            Ext.getBody().insertFirst(menu.element);
        } else {
            Ext.Viewport.add(menu);
        }
 
        menu.show();
        menu.addCls('x-' + side);
 
        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
 
        if (side == 'left') {
            before.translateX = -size;
            viewportAfter.translateX = size;
        } else if (side == 'right') {
            before.translateX = size;
            viewportAfter.translateX = -size;
        } else if (side == 'top') {
            before.translateY = -size;
            viewportAfter.translateY = size;
        } else if (side == 'bottom') {
            before.translateY = size;
            viewportAfter.translateY = -size;
        }
 
        if (menu.$reveal) {
            if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
                menu.translate(0, 0);
            }
        } else {
            menu.translate(before.translateX, before.translateY);
        }
 
        if (menu.$cover) {
            menu.getTranslatable().on('animationend', function() {
                menu.isAnimating = false;
            }, me, {
                single: true
            });
 
            menu.translate(after.translateX, after.translateY, {
                preserveEndState: true,
                duration: 200
            });
 
        } else {
            me.translate(viewportBefore.translateX, viewportBefore.translateY);
 
 
            me.getTranslatable().on('animationend', function() {
                menu.isAnimating = false;
            }, me, {
                single: true
            });
 
            me.translate(viewportAfter.translateX, viewportAfter.translateY, {
                preserveEndState: true,
                duration: 200
            });
        }
 
        // Make the menu as animating 
        menu.isAnimating = true;
    },
 
    /**
     * Hides a menu specified by the menu's side.
     * @param {String} side The side which the menu is placed.
     */
    hideMenu: function(side, animate) {
        var me = this,
            menus = this.getMenus(),
            menu = menus[side],
            after, viewportAfter,
            size;
 
        animate = animate !== false;
 
        if (!menu || (menu.isHidden() || menu.isAnimating)) {
            return;
        }
 
        after = {
            translateX: 0,
            translateY: 0
        };
 
        viewportAfter = {
            translateX: 0,
            translateY: 0
        };
 
        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
 
        if (side == 'left') {
            after.translateX = -size;
        } else if (side == 'right') {
            after.translateX = size;
        } else if (side == 'top') {
            after.translateY = -size;
        } else if (side == 'bottom') {
            after.translateY = size;
        }
 
        if (menu.$cover) {
            if (animate) {
                menu.getTranslatable().on('animationend', function() {
                    menu.isAnimating = false;
                    menu.hide();
                }, me, {
                    single: true
                });
 
                menu.translate(after.translateX, after.translateY, {
                    preserveEndState: true,
                    duration: 200
                });
            } else {
                menu.translate(after.translateX, after.translateY);
                menu.hide()
            }
        } else {
            if (animate) {
                me.getTranslatable().on('animationend', function() {
                    menu.isAnimating = false;
                    menu.hide();
                }, me, {
                    single: true
                });
 
                me.translate(viewportAfter.translateX, viewportAfter.translateY, {
                    preserveEndState: true,
                    duration: 200
                });
            } else {
                me.translate(viewportAfter.translateX, viewportAfter.translateY);
                menu.hide();
            }
        }
    },
 
    /**
     * 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 {String} side         Side(s) not to hide
     * @param {String} animation    Animation to hide with
     */
    hideOtherMenus: function(side, animation){
        var menus = this.getMenus(),
            menu;
 
        for (menu in menus) {
            if (side !== menu) {
                this.hideMenu(menu, animation);
            }
        }
    },
 
    /**
     * Toggles the menu specified by side
     * @param {String} side The side which the menu is placed.
     */
    toggleMenu: function(side) {
        var menus = this.getMenus(), 
            menu;
 
        if (menus[side]) {
            menu = menus[side];
            if (menu.isHidden()) {
                this.showMenu(side);
            } else {
                this.hideMenu(side);
            }
        }
    },
 
    /**
     * @private
     */
    sideForDirection: function(direction) {
        if (direction === 'left') {
            return 'right';
        } else if (direction === 'right') {
            return 'left';
        } else if (direction == 'up') {
            return 'bottom';
        } else if (direction == 'down') {
            return 'top';
        }
    },
 
    /**
     * @private
     */
    sideForSwipeDirection: function(direction) {
        if (direction == 'up') {
            return  'top';
        } else if (direction == 'down') {
            return 'bottom';
        }
        return direction;
    },
 
    /**
     * @private
     */
    onTap: function(e) {
        // this.hideAllMenus(); 
    },
 
    /**
     * @private
     */
    onSwipeStart: function(e) {
        var side = this.sideForSwipeDirection(e.direction);
        this.hideMenu(side);
    },
 
    /**
     * @private
     */
    onEdgeSwipeStart: function(e) {
        var me = this,
            side = me.sideForDirection(e.direction),
            menus = me.getMenus(),
            menu = menus[side],
            menuSide, checkMenu, size,
            after, viewportAfter,
            transformStyleName, setTransform;
 
        if (!menu || !menu.isHidden()) {
            return;
        }
 
        for (menuSide in menus) {
            checkMenu = menus[menuSide];
            if (checkMenu.isHidden() !== false) {
                return;
            }
        }
 
        me.$swiping = true;
 
        me.hideAllMenus(false);
 
        // show the menu first so we can calculate the size 
        if (menu.$reveal) {
            Ext.getBody().insertFirst(menu.element);
        } else {
            Ext.Viewport.add(menu);
        }
        menu.show();
 
        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
 
        after = {
            translateX: 0,
            translateY: 0
        };
 
        viewportAfter = {
            translateX: 0,
            translateY: 0
        };
 
        if (side == 'left') {
            after.translateX = -size;
        } else if (side == 'right') {
            after.translateX = size;
        } else if (side == 'top') {
            after.translateY = -size;
        } else if (side == 'bottom') {
            after.translateY = size;
        }
 
        transformStyleName = 'webkitTransform' in document.createElement('div').style ? 'webkitTransform' : 'transform';
        setTransform = menu.element.dom.style[transformStyleName];
 
        if (setTransform) {
            menu.element.dom.style[transformStyleName] = '';
        }
 
        if (menu.$reveal) {
            if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
                menu.translate(0, 0);
            }
        } else {
            menu.translate(after.translateX, after.translateY);
        }
 
        if (!menu.$cover) {
            if (setTransform) {
                me.innerElement.dom.style[transformStyleName] = '';
            }
 
            me.translate(viewportAfter.translateX, viewportAfter.translateY);
        }
    },
 
    /**
     * @private
     */
    onEdgeSwipe: function(e) {
        var me = this,
            side = me.sideForDirection(e.direction),
            menu = me.getMenus()[side],
            size, after, viewportAfter,
            movement, viewportMovement;
 
        if (!menu || !me.$swiping) {
            return;
        }
 
        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
        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;
        }
 
        if (menu.$cover) {
            menu.translate(after.translateX, after.translateY);
        } else {
            me.translate(viewportAfter.translateX, viewportAfter.translateY);
        }
    },
 
    /**
     * @private
     */
    onEdgeSwipeEnd: function(e) {
        var me = this,
            side = me.sideForDirection(e.direction),
            menu = me.getMenus()[side],
            shouldRevert = false,
            size, velocity, movement, viewportMovement,
            after, viewportAfter;
 
        if (!menu) {
            return;
        }
 
        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();
        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;
 
        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;
        }
 
        // Move the viewport if cover is not enabled 
        if (menu.$cover) {
            menu.getTranslatable().on('animationend', function() {
                if (shouldRevert) {
                    menu.hide();
                }
            }, me, {
                single: true
            });
 
            menu.translate(after.translateX, after.translateY, {
                preserveEndState: true,
                duration: 200
            });
 
        } else {
            me.getTranslatable().on('animationend', function() {
                if (shouldRevert) {
                    menu.hide();
                }
            }, me, {
                single: true
            });
 
            me.translate(viewportAfter.translateX, viewportAfter.translateY, {
                preserveEndState: true,
                duration: 200
            });
        }
 
        me.$swiping = false;
    },
 
    privates: {
        addMeta: function(name, content) {
            var meta = document.createElement('meta');
 
            meta.setAttribute('name', name);
            meta.setAttribute('content', content);
            Ext.getHead().append(meta);
        },
 
        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]);
        },
 
        toggleWindowListener: function(on, eventName, fn, capturing) {
            if (on) {
                this.addWindowListener(eventName, fn, capturing);
            } else {
                this.removeWindowListener(eventName, fn, capturing);
            }
        }
    }
});