/**
 * This component presents a month calendar and allows the user to browse and select a valid
 * date. It is used as a `floated` popup by {@link Ext.field.Date datefield} but can be created
 * and used directly.
 * @since 6.5.0
 */
Ext.define('Ext.panel.Date', {
    extend: 'Ext.Panel',
    xtype: 'datepanel',
 
    requires: [
        'Ext.layout.Carousel',
        'Ext.panel.DateView',
        'Ext.panel.DateTitle',
        'Ext.panel.YearPicker'
    ],
 
    config: {
        /**
         * @cfg {Boolean} [animation=true]
         * Set to `false` to disable animations.
         */
        animation: true,
 
        /**
         * @cfg {Boolean} [autoConfirm=false]
         * When set to `true`, clicking or tapping on
         * a date cell in the calendar will confirm selection and dismiss the picker.
         * When set to `false`, user will have to click OK button after selecting the date.
         */
        autoConfirm: false,
 
        /**
         * @cfg {String} [captionFormat="F Y"]
         * Date format for calendar pane captions.
         */
        captionFormat: {
            $value: 'F Y',
            cached: true
        },
 
        /**
         * @cfg {String} dateCellFormat
         * The date format to use for date cells, compatible with {@link Ext.Date#format} method.
         * This format usually includes only day of month information.
         * @locale
         */
        dateCellFormat: {
            $value: 'j',
            cached: true
        },
 
        /**
         * @cfg {Date[]/String[]/RegExp} disabledDates
         * An array of dates to disable. This array can contain Date objects, stringified dates
         * in {@link #format}, or RegExp patterns that would match strings in {@link #format}.
         * Date objects can be used to disable specific dates, while strings will be used to build
         * a regular expression to match dates against.
         * Some examples:
         *
         *   - ['03/08/2003', new Date(2003, 8, 16)] would disable those exact dates
         *   - ['03/08', '09/16'] would disable those days for every year
         *   - ['^03/08'] would only match the beginning (useful if you are using short years)
         *   - [/03\/..\/2006/] would disable every day in March 2006
         *   - /^03/ would disable every day in every March
         *
         * Note that the format of the dates included in the array should exactly match the
         * {@link #format} config.
         */
        disabledDates: null,
 
        /**
         * @cfg {Number[]} [disabledDays]
         * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday.
         * See {@link #disabledDates}.
         */
        disabledDays: null,
 
        /**
         * @cfg {Date} focusableDate
         * The date that is currently focusable.
         *
         * @private
         * @since 6.5.1
         */
        focusableDate: null,
 
        /**
         * @cfg {String} format
         * The default date format string which can be overriden for localization support.
         * The format must be valid according to {@link Ext.Date#parse}
         * (defaults to {@link Ext.Date#defaultFormat}).
         * @locale
         */
        format: {
            $value: Ext.Date.defaultFormat,
            cached: true
        },
 
        /**
         * @cfg {Function} [handler]
         * A function that will handle the change in value.
         * The function will receive the following parameters:
         *
         * @param {Ext.panel.Date} handler.this this
         * @param {Date} handler.date The selected date
         */
        handler: null,
 
        /**
         * @cfg {String} headerFormat
         * The format to display the current value in the title.
         * The format must be valid according to {@link Ext.Date#parse}.
         *
         * @locale
         */
        headerFormat: {
            $value: 'D, M j Y',
            cached: true
        },
 
        /**
         * @cfg {Number} [headerLength=1]
         * Length of day names in header cells.
         */
        headerLength: 1,
 
        /**
         * @cfg {Boolean} hideCaptions
         * Set to `true` to hide calendar pane captions displaying
         * the month and year shown in each pane.
         */
        hideCaptions: true,
 
        /**
         * @cfg {Boolean} hideOutside
         * `true` to hide dates outside of the current month. This means no classes
         * (other than the base cell class) will be used on the cells.
         *
         * @since 6.5.1
         */
        hideOutside: false,
 
        /**
         * @cfg {Date/String} [maxDate]
         * Maximum allowable date as Date object or a string in {@link #format}.
         */
        maxDate: null,
 
        /**
         * @cfg {Date/String} [minDate]
         * Minimum allowable date as Date object or a string in {@link #format}.
         */
        minDate: null,
 
        /**
         * @cfg {'header'/'caption'} navigationPosition
         * The position for the {@link #tools}.
         *
         * @since 6.5.1
         */
        navigationPosition: 'header',
 
        /**
         * @cfg {String} nextText
         * The next month navigation button tooltip.
         * @locale
         */
        nextText: 'Next Month (Control+Right)',
 
        /**
         * @cfg {Number} [panes=1]
         * Number of calendar panes to display in the picker.
         */
        panes: 1,
 
        /**
         * @cfg {String} prevText
         * The previous month navigation button tooltip.
         * @locale
         */
        prevText: 'Previous Month (Control+Left)',
 
        /**
         * @cfg {Boolean} selectOnNavigate
         * `true` to keep the selection on the current pane.
         *
         * @since  6.5.1
         */
        selectOnNavigate: true,
 
        /**
         * @cfg {Boolean} [showAfterMaxDate]
         * Set to `true` to allow navigating to months coming after {@link #maxDate}.
         * This has no effect when `maxDate` is not set.
         */
        showAfterMaxDate: false,
 
        /**
         * @cfg {Boolean} [showBeforeMinDate]
         * Set to `true` to allow navigating to months preceding {@link #minDate}.
         * This has no effect when `minDate` is not set.
         */
        showBeforeMinDate: false,
 
        /**
         * @cfg {Boolean} [showFooter]
         * Set to `true` to always show footer bar with OK,
         * Cancel, and Today buttons. If this config is not provided, footer will be shown
         * or hidden automatically depending on {@link #autoConfirm}.
         */
        showFooter: null,
 
        /**
         * @cfg {Boolean} [showTodayButton]
         * Set to `true` to show the Today button. Location
         * will depend on {@link #showFooter} config: if the footer is shown, Today button
         * will be placed in the footer; otherwise the button will be placed in picker header.
         */
        showTodayButton: null,
 
        /**
         * @cfg {Date[]/String[]/RegExp[]} [specialDates]
         * An array of Date objects, strings, or
         * RegExp patterns designating special dates like holidays. These dates will have
         * 'x-special-day' CSS class added to their cells, allowing for visually distinct styling.
         *
         * If you want to disallow selecting these dates you would need to include them in
         * {@link #disabledDates} config as well.
         */
        specialDates: null,
 
        /**
         * @cfg {Number[]} [specialDays]
         * An array of days to mark as special, 0-based. For example, [0, 6] disables Sunday
         * and Saturday. See {@link #specialDates}.
         *
         * @since 6.5.1
         */
        specialDays: null,
 
        /**
         * @cfg {Boolean} splitTitle
         * `true` to split the year vertically from the main title. The {@link #headerFormat}
         * should be modified to reflect this.
         *
         * @since 6.5.1
         */
        splitTitle: false,
 
        /**
         * @cfg {Number} [startDay]
         * Day index at which the week should begin, 0-based.
         *
         * Defaults to the value of {@link Ext.Date.firstDayOfWeek}.
         * @locale
         */
        startDay: {
            $value: Ext.Date.firstDayOfWeek,
            cached: true
        },
 
        /**
         * @cfg {Boolean/Object} titleAnimation
         * The animation for the title. If not specified, the default
         * {@link #animation} configuration is used. This is not compatible
         * when using splitting titles using {@link #splitTitle}.
         *
         * @since 6.5.1
         */
        titleAnimation: null,
 
        /**
         * @cfg {Date} [value]
         * The value of this picker. Defaults to today.
         */
        value: undefined,
 
        /**
         * @cfg {Number[]} [weekendDays]
         * Array of weekend day indices, 0-based.
         *
         * Defaults to the value of {@link Ext.Date.weekendDays}
         * @locale
         */
        weekendDays: {
            $value: Ext.Date.weekendDays,
            cached: true
        },
 
        /**
         * @cfg {Object} yearPicker
         * A configuration for the {@link Ext.panel.YearPicker}. `null` to
         * disable the year picker.
         *
         * @since 6.5.1
         */
        yearPicker: {
            lazy: true,
            $value: {}
        },
 
        /**
         * @cfg {Object} yearPickerDefaults
         * The default configuration options for the {@link #yearPicker}.
         *
         * @since 6.5.1
         */
        yearPickerDefaults: null
    },
 
    /**
     * @cfg {Object} scope
     * The scope in which {@link #handler} function will be called.
     */
 
    /**
     * @cfg {Function} [transformCellCls]
     * A function that will be called during cell rendering
     * to allow modifying CSS classes applied to the cell.
     *
     * @param {Date} transformCellCls.date 
     * Date for which a cell is being rendered.
     * @param {String[]} transformCellCls.classes Array of standard CSS classes for this cell,
     * including class names for {@link #specialDates}{@link #disabledDates}, etc.
     * You can add custom classes or remove some standard class names as desired.
     */
 
    focusable: true,
    tabIndex: 0,
 
    border: false,
    mouseWheelBuffer: 500,
 
    autoSize: null,
 
    headerCls: Ext.baseCSSPrefix + 'datepanelheader',
    titleCls: Ext.baseCSSPrefix + 'datetitle',
    toolCls: [
        Ext.baseCSSPrefix + 'paneltool',
        Ext.baseCSSPrefix + 'datepaneltool'
    ],
 
    header: {
        title: {
            xtype: 'datetitle'
        }
    },
 
    tools: {
        previousMonth: {
            reference: 'navigatePrevMonth',
            iconCls: 'x-fa fa-angle-left',
            cls: Ext.baseCSSPrefix + 'left-year-tool ',
            weight: -100,
            increment: -1,
            focusable: false,
            tabIndex: null,
            forceTabIndex: true,
            listeners: {
                click: 'onMonthToolClick'
            }
        },
        previousYear: {
            reference: 'navigatePrevYear',
            iconCls: 'x-fa fa-angle-double-left',
            cls: Ext.baseCSSPrefix + 'left-month-tool',
            weight: -90,
            increment: -12,
            focusable: false,
            tabIndex: null,
            forceTabIndex: true,
            listeners: {
                click: 'onMonthToolClick'
            }
        },
        nextYear: {
            reference: 'navigateNextYear',
            iconCls: 'x-fa fa-angle-double-right',
            cls: Ext.baseCSSPrefix + 'right-month-tool',
            weight: 90,
            increment: 12,
            focusable: false,
            tabIndex: null,
            forceTabIndex: true,
            listeners: {
                click: 'onMonthToolClick'
            }
        },
        nextMonth: {
            reference: 'navigateNextMonth',
            iconCls: 'x-fa fa-angle-right',
            cls: Ext.baseCSSPrefix + 'right-year-tool',
            weight: 100,
            increment: 1,
            focusable: false,
            tabIndex: null,
            forceTabIndex: true,
            listeners: {
                click: 'onMonthToolClick'
            }
        }
    },
 
    keyMapTarget: 'bodyElement',
 
    // Ctrl-PageUp and Ctrl-PageDown are often used in browser to switch tabs
    // so we support both Shift- and Ctrl-PageUp/PageDown for switching years
    keyMap: {
        '*+LEFT': 'onLeftArrowKey',
        '*+RIGHT': 'onRightArrowKey',
        UP: 'onUpArrowKey',
        DOWN: 'onDownArrowKey',
        "*+PAGE_UP": 'onPageUpKey',
        "*+PAGE_DOWN": 'onPageDownKey',
        HOME: 'onHomeKey',
        END: 'onEndKey',
        ENTER: 'onEnterKey',
        SPACE: 'onSpaceKey',
        BACKSPACE: 'onBackspaceKey',
        "*+TAB": 'onTabKey',
 
        scope: 'this'
    },
 
    paneXtype: 'dateview',
 
    classCls: Ext.baseCSSPrefix + 'datepanel',
 
    layout: {
        type: 'carousel',
        animation: {
            duration: 100
        }
    },
 
    defaultListenerScope: true,
    referenceHolder: true,
 
    buttonToolbar: {
        enableFocusableContainer: false,
        cls: Ext.baseCSSPrefix + 'datepanel-footer',
        reference: 'footer'
    },
 
    buttons: {
        footerTodayButton: {
            text: 'Today',
            tabIndex: -1,
            hidden: true,
            weight: -20,
            handler: 'onTodayButtonClick',
            reference: 'footerTodayButton'
        },
        spacer: {
            xtype: 'component',
            weight: -10,
            flex: 1
        },
        ok: {
            tabIndex: -1,
            handler: 'onOkButtonClick'
        },
        cancel: {
            tabIndex: -1,
            handler: 'onCancelButtonClick'
        }
    },
 
    sidePaneOffset: 2,
 
    initialize: function() {
        var me = this,
            value = me.getValue();
 
        me.callParent();
 
        me.setToolText('navigatePrevMonth', me.getPrevText());
        me.setToolText('navigateNextMonth', me.getNextText());
 
        me.bodyElement.on({
            click: {
                delegate: me.cellSelector,
                fn: 'onDateClick'
            },
            focus: 'onBodyFocus',
 
            // Some browsers/platforms like desktop Mac will send a lot of
            // wheel events in sequence, causing very rapid calendar transitions.
            // Throttled functions begin executing immediately upon call and
            // thereafter, repeated calls are throttled to the passed buffer quantum.
            wheel: Ext.Function.createThrottled(me.onMouseWheel, me.mouseWheelBuffer),
            scope: me
        });
 
        // Make sure the panes are refreshed
        me.getShowFooter();
 
        me.preventAnim = true;
        me.setFocusableDate(value);
        me.preventAnim = false;
        me.setTitleByDate(value);
        Ext.fly(me.getCellByDate(value)).addCls(me.selectedCls);
    },
 
    onRender: function() {
        this.callParent();
        this.measurePaneSize();
    },
 
    doDestroy: function() {
        var me = this;
 
        Ext.destroy(me.animTitle, me.animBody);
 
        me.callParent();
    },
 
    focusDate: function(date) {
        var me = this;
 
        me.doFocus = true;
        me.setFocusableDate(date);
        me.doFocus = false;
    },
 
    updateAnimation: function(animate) {
        this.getLayout().setAnimation(animate);
    },
 
    updateAutoConfirm: function(autoConfirm) {
        var me = this;
 
        me.getButtons();
 
        if (!autoConfirm) {
            me.setShowFooter(true);
        }
        else {
            me.setShowFooter(me.initialConfig.showFooter);
        }
    },
 
    updateCaptionFormat: function(format) {
        this.broadcastConfig('captionFormat', format);
    },
 
    updateDateCellFormat: function(format) {
        this.broadcastConfig('dateCellFormat', format);
    },
 
    applyDisabledDates: function(dates) {
        if (!dates) {
            return dates;
        }
 
        // eslint-disable-next-line vars-on-top
        var cfg = {
                dates: {}
            },
            re = [],
            item, i, len;
 
        if (dates instanceof RegExp) {
            cfg.re = dates;
        }
        else {
            if (!Ext.isArray(dates)) {
                dates = [dates];
            }
 
            for (= 0, len = dates.length; i < len; i++) {
                item = dates[i];
 
                if (item instanceof Date) {
                    item = Ext.Date.clearTime(item);
                    cfg.dates[item.getTime()] = true;
                }
                else if (item instanceof RegExp) {
                    re.push(item.source);
                }
                else {
                    re.push(Ext.String.escapeRegex(item));
                }
            }
 
            if (re.length) {
                cfg.re = new RegExp('(?:' + re.join('|') + ')');
            }
        }
 
        return cfg;
    },
 
    updateDisabledDates: function() {
        this.refreshPanes();
    },
 
    applyDisabledDays: function(days) {
        return days ? Ext.Array.toMap(days) : days;
    },
 
    updateDisabledDays: function() {
        this.refreshPanes();
    },
 
    updateFormat: function(format) {
        this.broadcastConfig('format', format);
    },
 
    updateHeader: function(header, oldHeader) {
        this.callParent([header, oldHeader]);
 
        header.getTitle().on({
            scope: this,
            yeartap: 'onYearTitleTap',
            titletap: 'onTitleTap'
        });
    },
 
    applyMaxDate: function(date) {
        if (typeof date === 'string') {
            date = Ext.Date.parse(date, this.getFormat());
        }
 
        return date;
    },
 
    updateMaxDate: function() {
        this.refreshPanes();
    },
 
    applyMinDate: function(date) {
        if (typeof date === 'string') {
            date = Ext.Date.parse(date, this.getFormat());
        }
 
        return date;
    },
 
    updateMinDate: function() {
        this.refreshPanes();
    },
 
    updateNavigationPosition: function(pos) {
        var me = this,
            toolList = me.toolList,
            len = toolList.length,
            isHeader = pos === 'header',
            ct = isHeader ? me.toolCt : me.getHeader(),
            tools, i, c;
 
        if (isHeader && me.isConfiguring) {
            return;
        }
 
        me.getTools();
 
        tools = [];
 
        for (= 0; i < len; ++i) {
            c = me.lookup(toolList[i]);
 
            if (c) {
                tools.push(c);
                ct.remove(c, false);
                c.toggleCls(me.toolCls, isHeader);
            }
        }
 
        me.toolCt = Ext.destroy(me.toolCt);
 
        if (pos === 'header') {
            me.getHeader().add(tools);
        }
        else {
            tools.push({
                xtype: 'component',
                flex: 1,
                weight: 0
            });
 
            me.toolCt = me.add({
                xtype: 'container',
                cls: Ext.baseCSSPrefix + 'navigation-tools',
                defaultType: 'tool',
                weighted: true,
                layout: 'hbox',
                // Used to make the box positioned
                bottom: 'auto',
                items: tools
            });
        }
    },
 
    updateNextText: function(text) {
        this.setToolText('navigateNextMonth', text);
    },
 
    updatePrevText: function(text) {
        this.setToolText('navigatePrevMonth', text);
    },
 
    //<debug>
    applyPanes: function(count) {
        if (count < 1) {
            Ext.raise("Cannot configure less than 1 pane for Calendar picker");
        }
 
        return count;
    },
    //</debug>
 
    updatePanes: function(count) {
        var me = this;
 
        me.getLayout().setVisibleChildren(count);
        me.initPanes();
        me.singlePane = count === 1;
        me.toggleCls(Ext.baseCSSPrefix + 'single', me.singlePane);
    },
 
    updateShowFooter: function(showFooter) {
        this.lookup('footer').setHidden(!showFooter);
        this.getShowTodayButton();
    },
 
    updateShowTodayButton: function(showButton) {
        var footerBtn;
 
        this.getButtons();
 
        footerBtn = this.lookup('footerTodayButton');
 
        if (footerBtn) {
            footerBtn.setHidden(!showButton);
        }
    },
 
    applySpecialDates: function(dates) {
        return this.applyDisabledDates(dates);
    },
 
    updateSpecialDates: function(cfg) {
        this.broadcastConfig('specialDates', cfg);
    },
 
    applySpecialDays: function(days) {
        return days ? Ext.Array.toMap(days) : days;
    },
 
    updateSpecialDays: function(daysMap) {
        this.broadcastConfig('specialDays', daysMap);
    },
 
    updateSplitTitle: function(splitTitle) {
        this.getHeader().getTitle().setSplit(splitTitle);
    },
 
    updateStartDay: function(day) {
        this.broadcastConfig('startDay', day);
    },
 
    applyValue: function(date) {
        if (typeof date === 'string') {
            date = Ext.Date.parse(date, this.getFormat());
        }
        // This is to make sure the default value doesn't get stale
        // in long running apps
        else if (!date) {
            date = new Date();
        }
 
        return Ext.isDate(date) ? Ext.Date.clearTime(date, true) : null;
    },
 
    updateValue: function(value, oldValue) {
        var me = this,
            handler = me.getHandler(),
            selectedCls = me.selectedCls,
            cell;
 
        if (oldValue) {
            cell = me.getCellByDate(oldValue);
 
            if (cell) {
                Ext.fly(cell).removeCls(selectedCls);
            }
        }
 
        if (!me.isConfiguring) {
            if (me.hasFocus) {
                me.focusDate(value);
            }
            else {
                me.setFocusableDate(value);
            }
 
            cell = me.getCellByDate(value);
 
            if (cell) {
                Ext.fly(cell).addCls(selectedCls);
            }
 
            me.setTitleByDate(value);
 
            me.fireEvent('change', me, value, oldValue);
 
            if (handler) {
                Ext.callback(handler, me.scope, [me, value, oldValue]);
            }
        }
    },
 
    applyWeekendDays: function(days) {
        return Ext.Array.toMap(days);
    },
 
    updateWeekendDays: function(daysMap) {
        this.broadcastConfig('weekendDays', daysMap);
    },
 
    applyYearPicker: function(yearPicker, oldYearPicker) {
        return Ext.updateWidget(oldYearPicker, yearPicker, this, 'createYearPicker',
                                'yearPickerDefaults');
    },
 
    updateYearPicker: function(yearPicker) {
        if (yearPicker) {
            this.add(yearPicker);
        }
    },
 
    replacePanes: function(increment, animate) {
        var me = this,
            panes, cb, direction, ret;
 
        if (me.destroying || me.destroyed) {
            return;
        }
 
        panes = me.getLayout().getVisibleItems();
 
        cb = function() {
            var pane, offset, j, jlen;
 
            for (= 0, jlen = panes.length; j < jlen; j++) {
                pane = panes[j];
                offset = pane.getMonthOffset();
                pane.setMonthOffset(offset + increment);
            }
        };
 
        if (animate == null) {
            animate = me.getAnimation();
        }
 
        if (animate) {
            direction = increment < 0 ? 'up' : 'down';
            ret = me.animateVertical(me.carouselElement, direction, 0, cb, 'animBody');
        }
        else {
            cb();
            ret = Ext.Deferred.getCachedResolved();
        }
 
        return ret;
    },
 
    initPanes: function() {
        var me = this,
            count = me.getPanes() + this.sidePaneOffset,
            panes = [],
            oldPanes, index, center, i;
 
        index = count - 1;
        center = !index ? index : index % 2 ? Math.floor(index / 2) + 1 : Math.floor(index / 2);
 
        for (= 0; i < count; i++) {
            panes.push(me.getPaneTemplate(- center));
        }
 
        oldPanes = me.getInnerItems();
 
        for (= 0; i < oldPanes.length; i++) {
            me.remove(oldPanes[i], true);
        }
 
        me.add(panes);
        me.getLayout().setFrontItem(center, false);
    },
 
    getPaneByDate: function(date) {
        var me = this,
            panes = me.getInnerItems(),
            month, pane, i, len;
 
        month = Ext.Date.getFirstDateOfMonth(date);
 
        for (= 0, len = panes.length; i < len; i++) {
            pane = panes[i];
 
            if (Ext.Date.isEqual(pane.getMonth(), month)) {
                return pane;
            }
        }
 
        return null;
    },
 
    getCellByDate: function(date) {
        var pane = this.getPaneByDate(date);
 
        return pane ? pane.getCellByDate(date) : null;
    },
 
    updateCellTabIndex: function(date, tabIndex) {
        var cell = date && this.getCellByDate(date);
 
        if (cell) {
            Ext.fly(cell).setTabIndex(tabIndex);
        }
 
        return cell;
    },
 
    canSwitchTo: function(date, offset) {
        var me = this,
            boundary, prevent;
 
        if (offset < 0) {
            boundary = me.getMinDate();
            prevent = !me.getShowBeforeMinDate();
 
            if (boundary && prevent) {
                if (date.getTime() < Ext.Date.getFirstDateOfMonth(boundary).getTime()) {
                    return false;
                }
            }
        }
        else if (offset > 0) {
            boundary = me.getMaxDate();
            prevent = !me.getShowAfterMaxDate();
 
            if (boundary && prevent) {
                if (date.getTime() > Ext.Date.getLastDateOfMonth(boundary).getTime()) {
                    return false;
                }
            }
        }
 
        return true;
    },
 
    setValueWithoutAnim: function(value) {
        this.preventAnim = true;
        this.setValue(value);
        this.preventAnim = false;
    },
 
    navigateTo: function(date, animate) {
        var me = this,
            layout = me.getLayout(),
            month, increment, boundary, prevent;
 
        // Offset is only known beforehand for pointer/touch interaction, where
        // clicking month/year tool switches panes as an action. Keyboard interaction
        // is different; moving focused date might result in not switching panes at all
        // so we have to calculate increment here as a difference between the new date
        // and visible panes.
        // Assignment is intentional
        if (date.getTime() < (month = layout.getFirstVisibleItem().getMonth()).getTime()) {
            boundary = month;
        }
        else if (date.getTime() > (month = layout.getLastVisibleItem().getMonth()).getTime()) {
            boundary = month;
        }
        else {
            boundary = date;
        }
 
        increment = (date.getFullYear() * 12 + date.getMonth()) -
                    (boundary.getFullYear() * 12 + boundary.getMonth());
 
        if (increment < 0) {
            boundary = me.getMinDate();
            prevent = !me.getShowBeforeMinDate();
 
            if (boundary && prevent) {
                if (date.getTime() < Ext.Date.getFirstDateOfMonth(boundary).getTime()) {
                    increment = 0;
                }
            }
        }
        else if (increment > 0) {
            boundary = me.getMaxDate();
            prevent = !me.getShowAfterMaxDate();
 
            if (boundary && prevent) {
                if (date.getTime() > Ext.Date.getLastDateOfMonth(boundary).getTime()) {
                    increment = 0;
                }
            }
        }
 
        return me.navigateByIncrement(increment, animate, 0);
    },
 
    switchPanes: function(increment, animate) {
        var me = this,
            layout = me.getLayout(),
            edgePane, pane;
 
        edgePane = increment < 0 ? layout.getFirstVisibleItem() : layout.getLastVisibleItem();
 
        pane = layout.getEdgeItem(increment);
        pane.setMonthOffset(edgePane.getMonthOffset() + increment);
 
        return layout.move(increment, animate);
    },
 
    onMonthToolClick: function(tool) {
        var me = this,
            D = Ext.Date,
            increment = tool.increment,
            date = D.add(me.getFocusableDate(), D.MONTH, increment),
            hasFocus = me.hasFocus,
            pane = pane = this.getLayout().getFrontItem(),
            month = D.add(pane.getMonth(), D.MONTH, increment);
 
        if (!me.canSwitchTo(month, increment)) {
            return;
        }
 
        me.navIncrement = me.singlePane ? 0 : increment;
 
        if (hasFocus || me.getSelectOnNavigate()) {
            me.setValue(date);
        }
        else {
            me.doFocus = hasFocus;
            me.setFocusableDate(date);
            me.doFocus = false;
        }
 
        me.navIncrement = 0;
    },
 
    onDateClick: function(e) {
        var me = this,
            cell = e.getTarget(me.cellSelector, me.bodyElement),
            date = cell && cell.date,
            focus = true,
            disabled = cell && cell.disabled;
 
        // Click could land on element other than date cell
        if (!date || me.getDisabled()) {
            return;
        }
 
        if (!disabled) {
            me.setValue(date);
 
            if (me.getAutoConfirm()) {
                // Touch events change focus on tap.
                // Prevent this as we are just about to hide.
                // PickerFields revert focus to themselves in a beforehide handler.
                if (e.pointerType === 'touch') {
                    e.preventDefault();
                }
 
                focus = false;
 
                me.fireEvent('select', me, date);
            }
        }
 
        if (focus) {
            // Even though setValue might focus the date, we may
            // either be in a position where the date is disabled
            // or already set.
            me.focusDate(date);
        }
    },
 
    onMouseWheel: function(e) {
        var dy = e.browserEvent.deltaY;
 
        if (dy && !this.pickerVisible) {
            this.onMonthToolClick({
                increment: Ext.Number.sign(dy)
            });
        }
    },
 
    onOkButtonClick: function() {
        this.setValue(this.getFocusableDate());
    },
 
    onCancelButtonClick: function() {
        this.fireEventArgs('tabout', [this]);
    },
 
    onTodayButtonClick: function() {
        var me = this,
            offset;
 
        offset = me.getLayout().getFrontItem().getMonthOffset();
 
        if (offset !== 0) {
            // This looks smoother if switchPane is used
            if (Math.abs(offset) === 1) {
                me.switchPanes(-offset);
            }
            else {
                me.replacePanes(-offset);
            }
        }
 
        me.setValue(Ext.Date.clearTime(new Date()));
    },
 
    getFocusEl: function() {
        if (!this.initialized) {
            return null;
        }
 
        return this.getCellByDate(this.getFocusableDate());
    },
 
    onLeftArrowKey: function(e) {
        this.walkCells(e.target.date, e.ctrlKey ? Ext.Date.MONTH : Ext.Date.DAY, -1);
 
        // We need to prevent default to avoid scrolling the nearest container
        // which in case of a floating Date picker will be the document body.
        // This applies to all navigation keys and Space key.
        e.preventDefault();
    },
 
    onRightArrowKey: function(e) {
        this.walkCells(e.target.date, e.ctrlKey ? Ext.Date.MONTH : Ext.Date.DAY, 1);
 
        e.preventDefault();
    },
 
    onUpArrowKey: function(e) {
        this.walkCells(e.target.date, Ext.Date.DAY, -7);
 
        e.preventDefault();
    },
 
    onDownArrowKey: function(e) {
        this.walkCells(e.target.date, Ext.Date.DAY, 7);
 
        e.preventDefault();
    },
 
    onPageUpKey: function(e) {
        var unit = e.ctrlKey || e.shiftKey ? Ext.Date.YEAR : Ext.Date.MONTH;
 
        this.walkCells(e.target.date, unit, -1);
 
        e.preventDefault();
    },
 
    onPageDownKey: function(e) {
        var unit = e.ctrlKey || e.shiftKey ? Ext.Date.YEAR : Ext.Date.MONTH;
 
        this.walkCells(e.target.date, unit, 1);
 
        e.preventDefault();
    },
 
    onHomeKey: function(e) {
        this.walkCells(Ext.Date.getFirstDateOfMonth(e.target.date));
 
        e.preventDefault();
    },
 
    onEndKey: function(e) {
        this.walkCells(Ext.Date.getLastDateOfMonth(e.target.date));
 
        e.preventDefault();
    },
 
    onBackspaceKey: function(e) {
        this.walkCells(new Date());
 
        e.preventDefault();
    },
 
    onEnterKey: function(e) {
        var me = this,
            target = e.target,
            date = target && target.date;
 
        if (date && !target.disabled) {
            me.setValue(date);
            me.fireEvent('select', me, target.date);
        }
    },
 
    onSpaceKey: function(e) {
        this.onEnterKey(e);
 
        // Space key scrolls as well
        e.preventDefault();
    },
 
    onTabKey: function(e) {
        // When the picker is floating and attached to an input field, its
        // 'select' handler will focus the inputEl so when navigation happens
        // it does so as if the input field was focused all the time.
        // This is the desired behavior and we try not to interfere with it
        // in the picker itself, see below.
        this.handleTabKey(e);
 
        // Allow default behaviour of TAB - it MUST be allowed to navigate.
        return true;
    },
 
    handleTabKey: function(e) {
        var me = this,
            target = e.target,
            picker = me.pickerField;
 
        // We're only setting the value if autoConfirm == true; if it's not then pressing
        // Enter key or clicking OK button is required to confirm date selection
        if (!me.getDisabled() && me.getAutoConfirm() && target && target.date && !target.disabled) {
            me.setValue(target.date);
 
            // If the ownerfield is part of an editor we must preventDefault and let
            // the navigationModel handle the tab event.
            if (picker && picker.isEditorComponent) {
                e.preventDefault();
            }
        }
        // Even if the above condition is not met we have to let the field know
        // that we're tabbing out; that's user action we can do nothing about
        else {
            me.fireEventArgs('tabout', [me]);
        }
    },
 
    walkCells: function(date, unit, increment) {
        var me = this,
            newDate;
 
        if (!me.getDisabled()) {
            date = me.getFocusableDate();
            newDate = unit ? Ext.Date.add(date, unit, increment) : date;
 
            if (me.isDateDisabled(newDate)) {
                me.focusDate(newDate);
            }
            else {
                me.setValue(newDate);
            }
        }
    },
 
    onBodyFocus: function(e) {
        var me = this,
            date = me.getFocusableDate(),
            cell = me.getCellByDate(date);
 
        // Make sure there is a focusable cell in the view
        if (!cell) {
            me.navigateTo(date, false);
        }
 
        cell = me.updateCellTabIndex(date, me.getTabIndex());
        cell.focus();
    },
 
    getTabIndex: function() {
        // We want this method to always return configured tabIndex value
        // instead of trying to read it off the `focusEl`.
        return this.getConfig('tabIndex', true);
    },
 
    getFocusClsEl: function() {
        return this.bodyElement;
    },
 
    onFocusEnter: function(e) {
        if (this.bodyElement.contains(e.target)) {
            this.onFocus(e);
        }
 
        this.callParent([e]);
    },
 
    onFocusLeave: function(e) {
        this.onBlur(e);
        this.callParent([e]);
    },
 
    privates: {
        cellSelector: '.' + Ext.baseCSSPrefix + 'cell',
        clonedCls: Ext.baseCSSPrefix + 'cloned',
        lastNavigate: 0,
        hideFocusCls: Ext.baseCSSPrefix + 'hide-focus',
        selectedCls: Ext.baseCSSPrefix + 'selected',
        toolList: [
            'navigatePrevMonth', 'navigatePrevYear', 'navigateNextYear', 'navigateNextMonth'
        ],
 
        paneWidthMap: {},
        pickerVisible: false,
 
        applyFocusableDate: function(date) {
            var me = this,
                D = Ext.Date,
                boundary;
 
            // Null is a valid value to set onFocusLeave in order to clear the focused cell
            // and allow the value to be set the next time the panel is displayed.
            if (date) {
                // Should check default value (today) as well, it could be that
                // allowed selection is in the past or in the future.
                date = D.clearTime(date || new Date());
 
                if ((boundary = me.getMinDate()) && !me.getShowBeforeMinDate() &&
                    date.getTime() < boundary.getTime()) {
                    date = boundary;
                }
                else if ((boundary = me.getMaxDate()) && !me.getShowAfterMaxDate() &&
                    date.getTime() > boundary.getTime()) {
                    date = boundary;
                }
            }
 
            return date;
        },
 
        updateFocusableDate: function(date, oldDate) {
            var me = this,
                focus = me.doFocus,
                layout = me.getLayout(),
                cls = me.hideFocusCls,
                increment = me.navIncrement,
                visibleItems, toPane, anim, navigate, oldCell, p;
 
            if (me.destroying || me.destroyed) {
                return;
            }
 
            if (oldDate) {
                oldCell = me.getCellByDate(oldDate);
                me.updateCellTabIndex(oldDate, -1);
            }
 
            if (date) {
                toPane = me.getPaneByDate(date);
 
                if (!me.preventAnim) {
                    anim = me.getAnimation();
                }
 
                visibleItems = layout.getVisibleItems();
 
                // New date will be immediately visible, or is in same pane.
                // Simply activate the pane and focus. Do not animate title change.
                me.lastNavigate = navigate = Date.now();
 
                if (!increment && (!anim || visibleItems.indexOf(toPane) > -1)) {
                    me.navigateTo(date, false);
 
                    if (focus) {
                        me.getCellByDate(date).focus();
                    }
                }
                else {
                    // Temporarily remove the focus styling while we're moving away
                    if (oldCell) {
                        Ext.fly(oldCell).addCls(cls);
                    }
 
                    p = increment ? me.navigateByIncrement(increment) : me.navigateTo(date);
 
                    p.then(function() {
                        oldCell = me.getCellByDate(oldDate);
 
                        if (oldCell) {
                            Ext.fly(oldCell).removeCls(cls);
                        }
 
                        // Make sure the frontItem hasn't changed
                        if (focus && me.lastNavigate === navigate) {
                            me.getCellByDate(date).focus();
                        }
                    });
                }
 
                me.updateCellTabIndex(date, me.getTabIndex());
            }
        },
 
        animateVertical: function(el, direction, offset, beforeFn, prop) {
            var me = this,
                clone = el.dom.cloneNode(true),
                ret = new Ext.Deferred();
 
            clone.id = '';
 
            Ext.fly(clone).addCls(me.clonedCls);
 
            el.parent().appendChild(clone);
 
            if (beforeFn) {
                beforeFn();
            }
 
            Ext.destroy(me[prop]);
 
            me[prop] = Ext.Animator.run([{
                offset: offset,
                type: 'slide',
                direction: direction,
                element: el
            }, {
                offset: offset,
                type: 'slideOut',
                direction: direction,
                element: clone,
                callback: function() {
                    Ext.fly(clone).destroy();
                    me[prop] = null;
                    ret.resolve();
                }
            }]);
 
            return ret.promise;
        },
 
        broadcastConfig: function(config, value) {
            if (this.isConfiguring) {
                return;
            }
 
            // eslint-disable-next-line vars-on-top
            var panes = this.getInnerItems(),
                setter, pane, i, len;
 
            setter = Ext.Config.get(config).names.set;
 
            for (= 0, len = panes.length; i < len; i++) {
                pane = panes[i];
 
                if (pane[setter]) {
                    pane[setter](value);
                }
            }
        },
 
        createYearPicker: function(config) {
            return Ext.apply({
                xtype: 'yearpicker',
                hidden: true,
                top: 0,
                right: 0,
                bottom: 0,
                left: 0,
                listeners: {
                    yeartap: 'onYearPickerTap'
                }
            }, config);
        },
 
        getPaneTemplate: function(offset) {
            var me = this;
 
            return {
                xtype: me.paneXtype,
                monthOffset: offset,
                hideOutside: me.getHideOutside(),
                hideCaption: me.getHideCaptions(),
                startDay: me.getStartDay(),
                weekendDays: me.getWeekendDays(),
                specialDates: me.getSpecialDates(),
                specialDays: me.getSpecialDays(),
                format: me.getFormat(),
                captionFormat: me.getCaptionFormat(),
                dateCellFormat: me.getDateCellFormat(),
                headerLength: me.getHeaderLength(),
                transformCellCls: me.transformCellCls
            };
        },
 
        getPositionedItemTarget: function() {
            return this.bodyElement;
        },
 
        isDateDisabled: function(date) {
            var me = this,
                ms = date.getTime(),
                minDate = me.getMinDate(),
                maxDate = me.getMaxDate(),
                disabled, disabledDays, disabledDates, formatted, re;
 
            disabled = (minDate && ms < minDate.getTime()) || (maxDate && ms > maxDate.getTime());
 
            if (!disabled) {
                disabledDays = me.getDisabledDays();
 
                if (disabledDays) {
                    disabled = disabledDays[date.getDay()];
                }
            }
 
            if (!disabled) {
                disabledDates = me.getDisabledDates();
 
                if (disabledDates) {
                    disabled = disabledDates.dates[ms];
                    re = disabledDates.re;
 
                    if (!disabled && re) {
                        formatted = Ext.Date.format(date, me.getFormat());
                        disabled = re.test(formatted);
                    }
                }
            }
 
            return !!disabled;
        },
 
        measurePaneSize: function() {
            var me = this,
                count = me.getPanes(),
                ui = me.getUi() || 'default',
                map = me.paneWidthMap,
                borderWidth;
 
            // Okay this is a hack but will do for now because Carousel layout
            // needs the container to be widthed
            if (!map.hasOwnProperty(ui)) {
                map[ui] = this.getLayout().getFrontItem().measurePaneSize();
            }
 
            borderWidth = me.el.getBorderWidth('lr');
            me.setWidth(borderWidth + count * map[ui]);
        },
 
        navigateByIncrement: function(increment, animate) {
            var ret;
 
            if (Math.abs(increment) === 1) {
                ret = this.switchPanes(increment, animate);
            }
            else {
                if (increment !== 0) {
                    ret = this.replacePanes(increment, animate);
                }
                else if (!animate) {
                    this.getLayout().cancelAnimation();
                    ret = Ext.Deferred.getCachedResolved();
                }
            }
 
            return ret;
        },
 
        onTitleTap: function() {
            var visible;
 
            if (this.getSplitTitle()) {
                visible = false;
            }
            else {
                visible = !this.pickerVisible;
            }
 
            this.toggleYearPicker(visible);
        },
 
        onYearPickerTap: function(picker, year) {
            this.toggleYearPicker(false);
 
            // eslint-disable-next-line vars-on-top
            var d = Ext.Date.clone(this.getFocusableDate());
 
            d.setFullYear(year);
 
            this.setValue(d);
        },
 
        onYearTitleTap: function() {
            this.toggleYearPicker(!this.pickerVisible);
        },
 
        refreshPanes: function() {
            if (this.isConfiguring) {
                return;
            }
 
            // eslint-disable-next-line vars-on-top
            var panes = this.getInnerItems(),
                len = panes.length,
                i;
 
            for (= 0; i < len; ++i) {
                panes[i].refresh();
            }
        },
 
        setTitleByDate: function(date) {
            var me = this,
                prev = me.lastTitleDate,
                anim;
 
            if (prev && prev.getTime() === date.getTime()) {
                anim = false;
            }
            else {
                anim = this.preventAnim ? false : anim;
            }
 
            me.setTitleText(Ext.Date.format(date, me.getHeaderFormat()), date, prev, anim);
 
            me.lastTitleDate = date;
        },
 
        setTitleText: function(text, date, oldDate, animate) {
            var me = this,
                title, direction, titleAnim;
 
            if (me.destroying || me.destroyed) {
                return;
            }
 
            if (animate === undefined) {
                titleAnim = me.getTitleAnimation();
 
                if (titleAnim !== null) {
                    animate = titleAnim;
                }
                else {
                    animate = me.getAnimation();
                }
            }
 
            animate = me.rendered ? animate : false;
 
            title = me.getHeader().getTitle();
 
            if (animate) {
                //<debug>
                if (me.getSplitTitle()) {
                    Ext.raise('Animation is not supported with title split');
                }
                //</debug>
 
                direction = (oldDate || date).getTime() < date.getTime() ? 'bottom' : 'top';
 
                me.animateVertical(title.textElement, direction, '150%', function() {
                    title.setText(text);
                }, 'animTitle');
            }
            else {
                if (me.getSplitTitle()) {
                    title.setYear(date.getFullYear());
                    title.setText(text);
                }
                else {
                    title.setText(text);
                }
            }
        },
 
        setToolText: function(type, text) {
            var tool = this.lookup(type);
 
            if (tool) {
                tool.setTooltip(text);
            }
        },
 
        toggleYearPicker: function(visible) {
            var me = this,
                picker = me.getYearPicker();
 
            if (picker) {
                if (me.getSplitTitle()) {
                    me.getHeader().getTitle().setTitleActive(!visible);
                }
 
                picker.setHidden(!visible);
 
                if (visible) {
                    picker.focusYear(me.getFocusableDate().getFullYear());
                }
 
                me.pickerVisible = visible;
            }
        }
    }
});