/**
 * Carousels, like 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,
 *
 *         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
 *     Ext.create('Ext.Carousel', {
 *         fullscreen: true,
 *         direction: 'vertical',
 *
 *         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
 */
Ext.define('Ext.carousel.Carousel', {
    extend: 'Ext.Container',
 
    alternateClassName: 'Ext.Carousel',
 
    xtype: 'carousel',
 
    requires: [
        'Ext.fx.easing.EaseOut',
        'Ext.carousel.Item',
        'Ext.Indicator',
        'Ext.util.TranslatableGroup'
    ],
 
    config: {
        /**
         * @cfg layout
         * Hide layout config in Carousel. It only causes confusion.
         * @accessor
         * @private
         */
 
        /**
         * @cfg {String} direction
         * The direction of the Carousel, either 'horizontal' or 'vertical'.
         * @accessor
         */
        direction: 'horizontal',
 
        animation: {
            duration: 250,
            easing: {
                type: 'ease-out'
            }
        },
 
        /**
         * @cfg draggable
         * @hide
         */
 
        /**
         * @cfg {Boolean/Ext.carousel.Indicator} 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: {
            translatable: {
                type: 'csstransform'
            }
        },
 
        bufferSize: 1,
 
        itemLength: null
    },
 
    baseCls: Ext.baseCSSPrefix + 'carousel',
 
    itemLength: 0,
 
    offset: 0,
 
    flickStartOffset: 0,
 
    flickStartTime: 0,
 
    dragDirection: 0,
 
    count: 0,
 
    painted: false,
 
    activeIndex: -1,
 
    beforeInitialize: function() {
        var me = this;
 
        me.element.on({
            resize: 'onSizeChange',
            dragstart: 'onDragStart',
            drag: 'onDrag',
            dragend: 'onDragEnd',
            dragcancel: 'onDragEnd',
            scope: me
        });
 
        me.carouselItems = [];
 
        me.orderedCarouselItems = [];
 
        me.inactiveCarouselItems = [];
 
        me.hiddenTranslation = 0;
    },
 
    updateBufferSize: function(size) {
        var ItemClass = Ext.carousel.Item,
            total = size * 2 + 1,
            isRendered = this.isRendered(),
            bodyElement = this.bodyElement,
            items = this.carouselItems,
            ln = items.length,
            itemConfig = Ext.apply({
                ownerCmp: this
            }, this.getItemConfig()),
            itemLength = this.getItemLength(),
            direction = this.getDirection(),
            setterName = direction === 'horizontal' ? 'setWidth' : 'setHeight',
            i, item;
 
        for (= ln; i < total; i++) {
            item = Ext.factory(itemConfig, ItemClass);
 
            if (itemLength) {
                item[setterName].call(item, itemLength);
            }
 
            items.push(item);
            bodyElement.append(item.renderElement);
 
            if (isRendered && item.setRendered(true)) {
                item.fireEvent('renderedchange', this, item, true);
            }
        }
 
        this.getTranslatable().setActiveIndex(size);
    },
 
    onSizeChange: function() {
        this.refreshSizing();
        this.refreshCarouselItems();
        this.refreshActiveItem();
    },
 
    onItemAdd: function(item, index) {
        var innerIndex, indicator;
 
        this.callParent([item, index]);
 
        innerIndex = this.getInnerItems().indexOf(item);
        indicator = this.getIndicator();
 
        if (indicator && item.isInnerItem()) {
            indicator.add();
        }
 
        if (innerIndex <= this.getActiveIndex()) {
            this.refreshActiveIndex();
        }
 
        if (this.isIndexDirty(innerIndex) && !this.isItemsInitializing) {
            this.refreshActiveItem();
        }
    },
 
    doItemLayoutAdd: function(item, index, destroying) {
        if (item.isInnerItem()) {
            return;
        }
 
        this.callParent(arguments);
    },
 
    onItemRemove: function(item, index, destroying) {
        var innerIndex, indicator, carouselItems,
            i, ln, carouselItem;
 
        this.callParent(arguments);
 
        innerIndex = this.getInnerItems().indexOf(item);
        indicator = this.getIndicator();
        carouselItems = this.carouselItems;
 
        if (item.isInnerItem() && indicator) {
            indicator.remove();
        }
 
        if (innerIndex <= this.getActiveIndex()) {
            this.refreshActiveIndex();
        }
 
        if (this.isIndexDirty(innerIndex)) {
            for (= 0, ln = carouselItems.length; i < ln; i++) {
                carouselItem = carouselItems[i];
 
                if (carouselItem.getComponent() === item) {
                    carouselItem.setComponent(null);
                    break;
                }
            }
 
            this.refreshActiveItem();
        }
    },
 
    doItemLayoutRemove: function(item) {
        if (item.isInnerItem()) {
            return;
        }
 
        this.callParent(arguments);
    },
 
    onInnerItemMove: function(item, toIndex, fromIndex) {
        if ((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(),
            bufferSize = this.getBufferSize();
 
        return (index >= activeIndex - bufferSize && index <= activeIndex + bufferSize);
    },
 
    getTranslatable: function() {
        var me = this,
            translatable = me.translatable;
 
        if (!translatable) {
            me.translatable = translatable = new Ext.util.TranslatableGroup();
            translatable.setItems(me.orderedCarouselItems);
            translatable.on('animationend', 'onAnimationEnd', me);
        }
 
        return translatable;
    },
 
    onDragStart: function(e) {
        var direction = this.getDirection(),
            absDeltaX = e.absDeltaX,
            absDeltaY = e.absDeltaY;
 
        this.isDragging = true;
 
        if ((direction === 'horizontal' && absDeltaX > absDeltaY) ||
            (direction === 'vertical' && absDeltaY > absDeltaX)) {
            e.stopPropagation();
        }
        else {
            this.isDragging = false;
 
            return;
        }
 
        this.getTranslatable().stopAnimation();
 
        this.dragStartOffset = this.offset;
        this.dragDirection = 0;
    },
 
    onDrag: function(e) {
        var startOffset, direction, delta, lastOffset, flickStartTime,
            dragDirection, now, currentActiveIndex, maxIndex,
            lastDragDirection, offset;
 
        if (!this.isDragging) {
            return;
        }
 
        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;
 
        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) {
        var now, itemLength, threshold, offset, activeIndex, maxIndex, animationDirection,
            flickDistance, flickDuration, indicator, velocity;
 
        if (!this.isDragging) {
            return;
        }
 
        this.onDrag(e);
 
        this.isDragging = false;
 
        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;
        indicator = this.getIndicator();
 
        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;
            }
        }
 
        if (indicator) {
            indicator.setActiveIndex(activeIndex - animationDirection);
        }
 
        this.animationDirection = animationDirection;
 
        this.setOffsetAnimated(animationDirection * itemLength);
    },
 
    onRender: function() {
        this.callParent();
        this.refresh();
    },
 
    applyAnimation: function(animation) {
        animation.easing = Ext.factory(animation.easing, Ext.fx.easing.EaseOut);
 
        return animation;
    },
 
    updateDirection: function(direction) {
        var indicator = this.getIndicator(),
            vertical = (direction === 'vertical');
 
        this.currentAxis = vertical ? 'y' : 'x';
 
        this.setTouchAction(vertical ? { panY: false } : { panX: false });
 
        if (indicator) {
            indicator.setDirection(direction);
        }
    },
 
    /**
     * @private
     * @chainable
     */
    setOffset: function(offset) {
        this.offset = offset;
 
        if (Ext.isNumber(this.itemOffset)) {
            this.getTranslatable().translateAxis(this.currentAxis, offset + this.itemOffset);
        }
 
        return this;
    },
 
    /**
     * @private
     * @return {Ext.carousel.Carousel} this
     * @chainable
     */
    setOffsetAnimated: function(offset) {
        var indicator = this.getIndicator(),
            offsetSum;
 
        if (indicator) {
            indicator.setActiveIndex(this.getActiveIndex() - this.animationDirection);
        }
 
        this.offset = offset;
 
        offsetSum = offset + this.itemOffset;
 
        this.getTranslatable().translateAxis(this.currentAxis, offsetSum, this.getAnimation());
 
        return this;
    },
 
    onAnimationEnd: function(translatable) {
        var currentActiveIndex, animationDirection, axis, currentOffset,
            itemLength, offset;
 
        if (this.destroyed) {
            return;
        }
 
        currentActiveIndex = this.getActiveIndex();
        animationDirection = this.animationDirection;
        axis = this.currentAxis;
        currentOffset = translatable[axis];
        itemLength = this.itemLength;
 
        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(),
            translatableItemLength = {
                x: 0,
                y: 0
            },
            itemOffset, containerSize;
 
        if (this.getDirection() === 'horizontal') {
            containerSize = element.getWidth();
        }
        else {
            containerSize = element.getHeight();
        }
 
        this.hiddenTranslation = -containerSize;
 
        if (itemLength === null) {
            itemLength = containerSize;
            itemOffset = 0;
        }
        else {
            itemOffset = (containerSize - itemLength) / 2;
        }
 
        this.itemLength = itemLength;
        this.itemOffset = itemOffset;
        translatableItemLength[this.currentAxis] = itemLength;
        this.getTranslatable().setItemLength(translatableItemLength);
    },
 
    refreshOffset: function() {
        this.setOffset(this.offset);
    },
 
    refreshActiveItem: function() {
        this.updateActiveItem(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.getInnerItemIndex(this.getActiveItem());
    },
 
    refreshCarouselItems: function() {
        var items = this.carouselItems,
            i, ln, item;
 
        for (= 0, ln = items.length; i < ln; i++) {
            item = items[i];
            item.getTranslatable().refresh();
        }
 
        this.refreshInactiveCarouselItems();
    },
 
    refreshInactiveCarouselItems: function() {
        var items = this.inactiveCarouselItems,
            hiddenTranslation = this.hiddenTranslation,
            axis = this.currentAxis,
            i, ln, item;
 
        for (= 0, ln = items.length; i < ln; i++) {
            item = items[i];
            item.translateAxis(axis, hiddenTranslation);
        }
    },
 
    /**
     * @private
     * @return {Number} 
     */
    getMaxItemIndex: function() {
        return this.innerItems.length - 1;
    },
 
    /**
     * @private
     * @return {Number} 
     */
    getInnerItemIndex: function(item) {
        return this.innerItems.indexOf(item);
    },
 
    /**
     * @private
     * @return {Object} 
     */
    getInnerItemAt: function(index) {
        return this.innerItems[index];
    },
 
    /**
     * @private
     * @return {Object} 
     */
    applyActiveItem: function(activeItem, oldActiveItem) {
        var activeIndex;
 
        activeItem = this.callParent([activeItem, oldActiveItem]);
 
        if (activeItem) {
            activeIndex = this.getInnerItemIndex(activeItem);
 
            if (activeIndex !== -1) {
                this.activeIndex = activeIndex;
 
                return activeItem;
            }
        }
    },
 
    updateActiveItem: function(activeItem, oldActiveItem) {
        var me = this,
            activeIndex = me.getActiveIndex(),
            maxIndex = me.getMaxItemIndex(),
            indicator = me.getIndicator(),
            bufferSize = me.getBufferSize(),
            carouselItems = me.carouselItems.slice(),
            orderedCarouselItems = this.orderedCarouselItems,
            visibleIndexes = {},
            visibleItems = {},
            visibleItem, component, id, i, index, ln, carouselItem;
 
        if (carouselItems.length === 0) {
            return;
        }
 
        me.callParent([activeItem, oldActiveItem]);
 
        orderedCarouselItems.length = 0;
 
        if (activeItem) {
            id = activeItem.getId();
            visibleItems[id] = activeItem;
            visibleIndexes[id] = bufferSize;
 
            if (activeIndex > 0) {
                for (= 1; i <= bufferSize; i++) {
                    index = activeIndex - i;
 
                    if (index >= 0) {
                        visibleItem = me.getInnerItemAt(index);
                        id = visibleItem.getId();
                        visibleItems[id] = visibleItem;
                        visibleIndexes[id] = bufferSize - i;
                    }
                    else {
                        break;
                    }
                }
            }
 
            if (activeIndex < maxIndex) {
                for (= 1; i <= bufferSize; i++) {
                    index = activeIndex + i;
 
                    if (index <= maxIndex) {
                        visibleItem = me.getInnerItemAt(index);
                        id = visibleItem.getId();
                        visibleItems[id] = visibleItem;
                        visibleIndexes[id] = bufferSize + i;
                    }
                    else {
                        break;
                    }
                }
            }
 
            for (= 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;
                }
            }
        }
 
        me.inactiveCarouselItems.length = 0;
        me.inactiveCarouselItems = carouselItems;
        me.refreshOffset();
        me.refreshInactiveCarouselItems();
 
        if (indicator && !indicator.isDestroying && activeIndex !== -1) {
            indicator.sync(me.getInnerItems().length, activeIndex);
        }
    },
 
    /**
     * Switches to the next card.
     * @return {Ext.carousel.Carousel} this
     * @chainable
     */
    next: function() {
        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
     * @chainable
     */
    previous: function() {
        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.Indicator, currentIndicator);
    },
 
    /**
     * @private
     */
    updateIndicator: function(indicator) {
        var me = this,
            bottom, right;
 
        if (indicator) {
            if (me.getDirection() === 'horizontal') {
                bottom = 0;
                right = null;
            }
            else {
                bottom = null;
                right = 0;
            }
 
            indicator
                // force the indicator to be floating
                .setRight(right)
                .setBottom(bottom)
                .setUi(me.getUi())
                .on({
                    indicatortap: 'onIndicatorTap',
                    next: 'next',
                    previous: 'previous',
                    scope: me
                });
 
            me.insertFirst(indicator);
        }
    },
 
    onIndicatorTap: function(indicator, index) {
        this.setActiveItem(index);
    },
 
    doDestroy: function() {
        var me = this,
            carouselItems = me.carouselItems.slice();
 
        me.carouselItems.length = 0;
 
        Ext.destroy(carouselItems, me.getIndicator(), me.translatable);
 
        me.callParent();
    }
});