    /**
     * Carousels, like [tabs](#!/guide/tabs), are a great way to allow the user to swipe through multiple full-screen pages.
     * A Carousel shows only one of its pages at a time but allows you to swipe through with your finger.
     *
     * Carousels can be oriented either horizontally or vertically and are easy to configure - they just work like any other
     * Container. Here's how to set up a simple horizontal Carousel:
     *
     *     @example
     *     Ext.create('Ext.Carousel', {
     *         fullscreen: true,
     *
     *         defaults: {
     *             styleHtmlContent: true
     *         },
     *
     *         items: [
     *             {
     *                 html : 'Item 1',
     *                 style: 'background-color: #5E99CC'
     *             },
     *             {
     *                 html : 'Item 2',
     *                 style: 'background-color: #759E60'
     *             },
     *             {
     *                 html : 'Item 3'
     *             }
     *         ]
     *     });
     *
     * We can also make Carousels orient themselves vertically:
     *
     *     @example preview
     *     Ext.create('Ext.Carousel', {
     *         fullscreen: true,
     *         direction: 'vertical',
     *
     *         defaults: {
     *             styleHtmlContent: true
     *         },
     *
     *         items: [
     *             {
     *                 html : 'Item 1',
     *                 style: 'background-color: #759E60'
     *             },
     *             {
     *                 html : 'Item 2',
     *                 style: 'background-color: #5E99CC'
     *             }
     *         ]
     *     });
     *
     * ### Common Configurations
     * * {@link #ui} defines the style of the carousel
     * * {@link #direction} defines the direction of the carousel
     * * {@link #indicator} defines if the indicator show be shown
     *
     * ### Useful Methods
     * * {@link #next} moves to the next card
     * * {@link #previous} moves to the previous card
     * * {@link #setActiveItem} moves to the passed card
     *
     * ## Further Reading
     *
     * For more information about Carousels see the [Carousel guide](#!/guide/carousel).
     */
    Ext.define('Ext.carousel.Carousel', {
        extend: 'Ext.Container',

        alternateClassName: 'Ext.Carousel',

        xtype: 'carousel',

        requires: [
            'Ext.fx.Easing',
            'Ext.fx.easing.EaseOut',
            'Ext.carousel.Item',
            'Ext.carousel.Indicator'
        ],

        config: {
            /**
             * @cfg layout
             * Hide layout config in Carousel. It only causes confusion.
             * @accessor
             * @private
             */

            baseCls: 'x-carousel',

            /**
             * @cfg {String} direction
             * The direction of the Carousel, either 'horizontal' or 'vertical'
             * @accessor
             */
            direction: 'horizontal',

            directionLock: false,

            animation: {
                duration: 350,
                easing: 'ease-out'
            },

            /**
             * @cfg {Boolean} indicator
             * Provides an indicator while toggling between child items to let the user
             * know where they are in the card stack.
             * @accessor
             */
            indicator: true,

            /**
             * @cfg {String} ui
             * Style options for Carousel. Default is 'dark'. 'light' is also available.
             * @accessor
             */
            ui: 'dark',

            itemConfig: {},

            bufferSize: 1,

            itemLength: null
        },

        itemLength: 0,

        offset: 0,

        flickStartOffset: 0,

        flickStartTime: 0,

        dragDirection: 0,

        count: 0,

        painted: false,

        activeIndex: -1,

        getItemsCount: function() {
            return this.innerItems.length;
        },

        beforeInitialize: function() {
            this.animationListeners = {
                animationframe: 'onActiveItemAnimationFrame',
                animationend: 'onActiveItemAnimationEnd',
                scope: this
            };

            this.element.on({
                dragstart: 'onDragStart',
                drag: 'onDrag',
                dragend: 'onDragEnd',
                scope: this
            });

            this.on({
                painted: 'onPainted',
                resize: 'onSizeChange'
            });

            this.carouselItems = [];

            this.orderedCarouselItems = [];

            this.inactiveCarouselItems = [];

            this.hiddenTranslation = {
                x: 0,
                y: 0
            }
        },

        updateBufferSize: function(size) {
            var total = size * 2 + 1,
                isRendered = this.isRendered(),
                innerElement = this.innerElement,
                items = this.carouselItems,
                ln = items.length,
                itemConfig = this.getItemConfig(),
                itemLength = this.getItemLength(),
                direction = this.getDirection(),
                setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight',
                i, item;

            for (i = ln; i < total; i++) {
                item = Ext.factory(itemConfig, Ext.carousel.Item);

                if (itemLength) {
                    item[setterName].call(item, itemLength);
                }

                items.push(item);
                innerElement.append(item.renderElement);

                if (isRendered && item.setRendered(true)) {
                    item.fireEvent('renderedchange', this, item, true);
                }
            }
        },

        getActiveCarouselItem: function() {
            return this.orderedCarouselItems[this.getBufferSize()];
        },

        setRendered: function(rendered) {
            var wasRendered = this.rendered;

            if (rendered !== wasRendered) {
                this.rendered = rendered;

                var items = this.items.items,
                    carouselItems = this.carouselItems,
                    i, ln, item;

                for (i = 0,ln = items.length; i < ln; i++) {
                    item = items[i];

                    if (!item.isInnerItem()) {
                        item.setRendered(rendered);
                    }
                }

                for (i = 0,ln = carouselItems.length; i < ln; i++) {
                    carouselItems[i].setRendered(rendered);
                }

                return true;
            }

            return false;
        },

        onPainted: function() {
            this.refresh();
            this.refreshCarouselItems();
        },

        onSizeChange: function() {
            this.refreshSizing();
            this.refreshCarouselItems();
            this.refreshOffset();
        },

        onItemAdd: function(item, index) {
            this.callParent(arguments);

            var innerIndex = this.getInnerItems().indexOf(item),
                indicator = this.getIndicator();

            if (indicator && item.isInnerItem()) {
                indicator.addIndicator();
            }

            if (innerIndex <= this.getActiveIndex()) {
                this.refreshActiveIndex();
            }

            if (this.painted && this.isIndexDirty(innerIndex)) {
                this.refreshActiveItem();
            }
        },

        doItemLayoutAdd: function(item) {
            if (item.isInnerItem()) {
                return;
            }

            this.callParent(arguments);
        },

        onItemRemove: function(item, index) {
            this.callParent(arguments);

            var innerIndex = this.getInnerItems().indexOf(item),
                indicator = this.getIndicator(),
                carouselItems = this.carouselItems,
                i, ln, carouselItem;

            if (item.isInnerItem() && indicator) {
                indicator.removeIndicator();
            }

            if (innerIndex <= this.getActiveIndex()) {
                this.refreshActiveIndex();
            }

            if (this.isIndexDirty(innerIndex)) {
                for (i = 0,ln = carouselItems.length; i < ln; i++) {
                    carouselItem = carouselItems[i];

                    if (carouselItem.getComponent() === item) {
                        carouselItem.setComponent(null);
                    }
                }

                if (this.painted) {
                    this.refreshActiveItem();
                }
            }
        },

        doItemLayoutRemove: function(item) {
            if (item.isInnerItem()) {
                return;
            }

            this.callParent(arguments);
        },

        onItemMove: function(item, toIndex, fromIndex) {
            this.callParent(arguments);

            if (this.painted && (this.isIndexDirty(toIndex) || this.isIndexDirty(fromIndex))) {
                this.refreshActiveItem();
            }
        },

        doItemLayoutMove: function(item) {
            if (item.isInnerItem()) {
                return;
            }

            this.callParent(arguments);
        },

        isIndexDirty: function(index) {
            var activeIndex = this.getActiveIndex();

            return (index >= activeIndex - 1 && index <= activeIndex + 1);
        },

        onDragStart: function(e) {
            var direction = this.getDirection(),
                absDeltaX = e.absDeltaX,
                absDeltaY = e.absDeltaY,
                directionLock = this.getDirectionLock();

            this.isDragging = true;

            if (directionLock) {
                if ((direction === 'horizontal' && absDeltaX > absDeltaY)
                    || (direction === 'vertical' && absDeltaY > absDeltaX)) {
                    e.stopPropagation();
                }
                else {
                    this.isDragging = false;
                    return;
                }
            }

            if (this.isAnimating) {
                this.getActiveCarouselItem().getTranslatable().stopAnimation();
            }

            this.dragStartOffset = this.offset;
            this.dragDirection = 0;
        },

        onDrag: function(e) {
            if (!this.isDragging) {
                return;
            }

            var startOffset = this.dragStartOffset,
                direction = this.getDirection(),
                delta = direction === 'horizontal' ? e.deltaX : e.deltaY,
                lastOffset = this.offset,
                flickStartTime = this.flickStartTime,
                dragDirection = this.dragDirection,
                now = Ext.Date.now(),
                currentActiveIndex = this.getActiveIndex(),
                maxIndex = this.getMaxItemIndex(),
                lastDragDirection = dragDirection,
                offset;

            if ((currentActiveIndex === 0 && delta > 0) || (currentActiveIndex === maxIndex && delta < 0)) {
                delta *= 0.5;
            }

            offset = startOffset + delta;

            if (offset > lastOffset) {
                dragDirection = 1;
            }
            else if (offset < lastOffset) {
                dragDirection = -1;
            }

            if (dragDirection !== lastDragDirection || (now - flickStartTime) > 300) {
                this.flickStartOffset = lastOffset;
                this.flickStartTime = now;
            }

            this.dragDirection = dragDirection;

            this.setOffset(offset);
        },

        onDragEnd: function(e) {
            if (!this.isDragging) {
                return;
            }

            this.onDrag(e);

            this.isDragging = false;

            var now = Ext.Date.now(),
                itemLength = this.itemLength,
                threshold = itemLength / 2,
                offset = this.offset,
                activeIndex = this.getActiveIndex(),
                maxIndex = this.getMaxItemIndex(),
                animationDirection = 0,
                flickDistance = offset - this.flickStartOffset,
                flickDuration = now - this.flickStartTime,
                velocity;

            if (flickDuration > 0 && Math.abs(flickDistance) >= 10) {
                velocity = flickDistance / flickDuration;

                if (Math.abs(velocity) >= 1) {
                    if (velocity < 0 && activeIndex < maxIndex) {
                        animationDirection = -1;
                    }
                    else if (velocity > 0 && activeIndex > 0) {
                        animationDirection = 1;
                    }
                }
            }

            if (animationDirection === 0) {
                if (activeIndex < maxIndex && offset < -threshold) {
                    animationDirection = -1;
                }
                else if (activeIndex > 0 && offset > threshold) {
                    animationDirection = 1;
                }
            }

            this.animationDirection = animationDirection;

            this.setOffsetAnimated(animationDirection * itemLength);
        },

        applyAnimation: function(animation) {
            animation.easing = new Ext.fx.Easing(animation.easing);

            return animation;
        },

        updateDirection: function(direction) {
            var indicator = this.getIndicator();

            this.currentAxis = (direction === 'horizontal') ? 'x' : 'y';

            if (indicator) {
                indicator.setDirection(direction);
            }
        },

        setOffset: function(offset) {
            var orderedCarouselItems = this.orderedCarouselItems,
                bufferSize = this.getBufferSize(),
                activeItem = orderedCarouselItems[bufferSize],
                translation = {},
                itemLength = this.itemLength,
                axis = this.currentAxis,
                nextItem, previousItem, distance, i;

            this.offset = offset;

            offset += this.itemOffset;
            translation[axis] = offset;

            if (activeItem) {
                activeItem.translate(translation);

                for (i = 1,distance = 0; i <= bufferSize; i++) {
                    previousItem = orderedCarouselItems[bufferSize - i];

                    if (previousItem) {
                        distance += itemLength;
                        translation[axis] = offset - distance;
                        previousItem.translate(translation);
                    }
                }

                for (i = 1,distance = 0; i <= bufferSize; i++) {
                    nextItem = orderedCarouselItems[bufferSize + i];

                    if (nextItem) {
                        distance += itemLength;
                        translation[axis] = offset + distance;
                        nextItem.translate(translation);
                    }
                }
            }

            return this;
        },

        setOffsetAnimated: function(offset) {
            var activeCarouselItem = this.orderedCarouselItems[this.getBufferSize()],
                translation = {},
                axis = this.currentAxis;

            this.offset = offset;
            offset += this.itemOffset;

            translation[axis] = offset;

            if (activeCarouselItem) {
                this.isAnimating = true;

                activeCarouselItem.getTranslatable().on(this.animationListeners);
                activeCarouselItem.translate(translation, this.getAnimation());
            }

            return this;
        },

        onActiveItemAnimationFrame: function(translatable, translation) {
            var orderedCarouselItems = this.orderedCarouselItems,
                bufferSize = this.getBufferSize(),
                itemTranslation = {},
                itemLength = this.itemLength,
                axis = this.currentAxis,
                offset = translation[axis],
                previousItem, nextItem, i, distance;

            for (i = 1,distance = 0; i <= bufferSize; i++) {
                previousItem = orderedCarouselItems[bufferSize - i];

                if (previousItem) {
                    distance += itemLength;
                    itemTranslation[axis] = offset - distance;
                    previousItem.translate(itemTranslation);
                }
            }

            for (i = 1,distance = 0; i <= bufferSize; i++) {
                nextItem = orderedCarouselItems[bufferSize + i];

                if (nextItem) {
                    distance += itemLength;
                    itemTranslation[axis] = offset + distance;
                    nextItem.translate(itemTranslation);
                }
            }
        },

        onActiveItemAnimationEnd: function(translatable) {
            var currentActiveIndex = this.getActiveIndex(),
                animationDirection = this.animationDirection,
                axis = this.currentAxis,
                currentOffset = translatable.translation[axis],
                itemLength = this.itemLength,
                offset;

            this.isAnimating = false;

            translatable.un(this.animationListeners);

            if (animationDirection === -1) {
                offset = itemLength + currentOffset;
            }
            else if (animationDirection === 1) {
                offset = currentOffset - itemLength;
            }
            else {
                offset = currentOffset;
            }

            offset -= this.itemOffset;
            this.offset = offset;
            this.setActiveItem(currentActiveIndex - animationDirection);
        },

        refresh: function() {
            this.refreshSizing();
            this.refreshActiveItem();
        },

        refreshSizing: function() {
            var element = this.element,
                itemLength = this.getItemLength(),
                hiddenTranslation = this.hiddenTranslation,
                itemOffset, containerSize;

            if (this.getDirection() === 'horizontal') {
                containerSize = element.getWidth();
                hiddenTranslation.x = -containerSize;
                hiddenTranslation.y = 0;
            }
            else {
                containerSize = element.getHeight();
                hiddenTranslation.y = -containerSize;
                hiddenTranslation.x = 0;
            }

            if (itemLength === null) {
                itemLength = containerSize;
                itemOffset = 0;
            }
            else {
                itemOffset = (containerSize - itemLength) / 2;
            }

            this.itemLength = itemLength;
            this.itemOffset = itemOffset;
        },

        refreshOffset: function() {
            this.setOffset(this.offset);
        },

        refreshActiveItem: function() {
            this.doSetActiveItem(this.getActiveItem());
        },

        /**
         * Returns the index of the currently active card.
         * @return {Number} The index of the currently active card.
         */
        getActiveIndex: function() {
            return this.activeIndex;
        },

        refreshActiveIndex: function() {
            this.activeIndex = this.getInnerItems().indexOf(this.getActiveItem());
        },

        refreshCarouselItems: function() {
            var items = this.carouselItems,
                i, ln, item;

            for (i = 0,ln = items.length; i < ln; i++) {
                item = items[i];
                item.getTranslatable().refresh();
            }

            this.refreshInactiveCarouselItems();
        },

        refreshInactiveCarouselItems: function() {
            var items = this.inactiveCarouselItems,
                hiddenTranslation = this.hiddenTranslation,
                i, ln, item;

            for (i = 0,ln = items.length; i < ln; i++) {
                item = items[i];
                item.translate(hiddenTranslation);
            }
        },

        getMaxItemIndex: function() {
            return this.getItemsCount() - 1;
        },

        doSetActiveItem: function(activeItem) {
            var innerItems = this.getInnerItems(),
                activeIndex = innerItems.indexOf(activeItem),
                maxIndex = this.getMaxItemIndex(),
                indicator = this.getIndicator(),
                bufferSize = this.getBufferSize(),
                carouselItems = this.carouselItems.slice(),
                orderedCarouselItems = this.orderedCarouselItems,
                visibleIndexes = {},
                visibleItems = {},
                visibleItem, component, id, i, index, ln, carouselItem;

            if (carouselItems.length === 0) {
                return;
            }

            this.callParent(arguments);

            orderedCarouselItems.length = 0;

            if (activeItem) {
                id = activeItem.getId();
                visibleItems[id] = activeItem;
                visibleIndexes[id] = bufferSize;

                if (activeIndex > 0) {
                    for (i = 1; i <= bufferSize; i++) {
                        index = activeIndex - i;
                        if (index >= 0) {
                            visibleItem = innerItems[index];
                            id = visibleItem.getId();
                            visibleItems[id] = visibleItem;
                            visibleIndexes[id] = bufferSize - i;
                        }
                        else {
                            break;
                        }
                    }
                }

                if (activeIndex < maxIndex) {
                    for (i = 1; i <= bufferSize; i++) {
                        index = activeIndex + i;
                        if (index <= maxIndex) {
                            visibleItem = innerItems[index];
                            id = visibleItem.getId();
                            visibleItems[id] = visibleItem;
                            visibleIndexes[id] = bufferSize + i;
                        }
                        else {
                            break;
                        }
                    }
                }

                for (i = 0,ln = carouselItems.length; i < ln; i++) {
                    carouselItem = carouselItems[i];
                    component = carouselItem.getComponent();

                    if (component) {
                        id = component.getId();

                        if (visibleIndexes.hasOwnProperty(id)) {
                            carouselItems.splice(i, 1);
                            i--;
                            ln--;
                            delete visibleItems[id];
                            orderedCarouselItems[visibleIndexes[id]] = carouselItem;
                        }
                    }
                }

                for (id in visibleItems) {
                    if (visibleItems.hasOwnProperty(id)) {
                        visibleItem = visibleItems[id];
                        carouselItem = carouselItems.pop();
                        carouselItem.setComponent(visibleItem);
                        orderedCarouselItems[visibleIndexes[id]] = carouselItem;
                    }
                }
            }

            this.inactiveCarouselItems.length = 0;
            this.inactiveCarouselItems = carouselItems;
            this.activeIndex = activeIndex;
            this.refreshOffset();
            this.refreshInactiveCarouselItems();

            if (indicator) {
                indicator.setActiveIndex(activeIndex);
            }
        },

        /**
         * Switches to the next card
         * @return {Ext.carousel.Carousel} this
         */
        next: function() {
            if (this.isAnimating) {
                this.getActiveCarouselItem().getTranslatable().stopAnimation();
                this.setOffset(0);
            }

            if (this.activeIndex === this.getMaxItemIndex()) {
                return this;
            }

            this.animationDirection = -1;
            this.setOffsetAnimated(-this.itemLength);
            return this;
        },

        /**
         * Switches to the previous card
         * @return {Ext.carousel.Carousel} this
         */
        previous: function() {
            if (this.isAnimating) {
                this.getActiveCarouselItem().getTranslatable().stopAnimation();
                this.setOffset(0);
            }

            if (this.activeIndex === 0) {
                return this;
            }

            this.animationDirection = 1;
            this.setOffsetAnimated(this.itemLength);
            return this;
        },

        // @private
        applyIndicator: function(indicator, currentIndicator) {
            return Ext.factory(indicator, Ext.carousel.Indicator, currentIndicator);
        },

        // @private
        updateIndicator: function(indicator) {
            if (indicator) {
                this.insertFirst(indicator);

                indicator.setUi(this.getUi());
                indicator.on({
                    next: 'next',
                    previous: 'previous',
                    scope: this
                });
            }
        },

        destroy: function() {
            var carouselItems = this.carouselItems.slice(),
                indicator = this.getIndicator();

            this.carouselItems.length = 0;

            Ext.destroy(carouselItems);

            if (indicator) {
                indicator.destroy();
            }

            this.callParent();
            delete this.carouselItems;
        }

    }, function() {
        //<deprecated product=touch since=2.0>
        this.override({
            constructor: function(config) {
                if (config && 'activeIndex' in config) {
                    //<debug warn>
                    Ext.Logger.deprecate("'activeIndex' config is deprecated, please use 'activeItem' config instead }");
                    //</debug>

                    config.activeItem = config.activeIndex;
                }

                this.callParent([config]);
            },

            prev: function() {
                console.warn('[Ext.carousel.Carousel] Carousel#prev is deprecated, please use Carousel#previous instead');

                return this.previous();
            },

            isVertical: function() {
                console.warn('[Ext.carousel.Carousel] Carousel#isVertical is deprecated, please use Carousel#getDirection instead');

                return this.getDirection() === 'vertical';
            },

            isHorizontal: function() {
                console.warn('[Ext.carousel.Carousel] Carousel#isHorizontal is deprecated, please use Carousel#getDirection instead');

                return this.getDirection() === 'horizontal';
            }
        });
        //</deprecated>
    });