Ext.define('Ext.dataview.listswiper.Accordion', {
    extend: 'Ext.dataview.listswiper.Item',
    xtype: 'listswiperaccordion',
 
    classCls: Ext.baseCSSPrefix + 'listswiperaccordion',
 
    cls: Ext.baseCSSPrefix + 'item-no-tap',
 
    config: {
        bodyOffset: null,
 
        actionDefaults: {
            cls: Ext.baseCSSPrefix + 'listswiperaction',
            xtype: 'button',
            iconAlign: 'top'
        },
 
        actionUI: 'square',
 
        singleActionDefaults: {},
 
        multiActionDefaults: {},
 
        undo: {
            cls: Ext.baseCSSPrefix + 'listswiperundoable',
            ui: 'undo',
            docked: 'right',
            ignoreDefaults: true
        },
 
        thresholds: null,
 
        /**
         * @cfg {Boolean} scaleDrag
         * Determines if the delta of a drag should be scaled depending on 
         * where the drag is started.
         * This causes drags that start in the middle of an item to move the items faster.
         * This means shorter drag distances when dragging from the middle or far sides
         */
        scaleDrag: true,
 
        /**
         * @cfg {Boolean} swipeToCommit
         * Determines if a full swipe should trigger the default action
         * If false a full swipe will result in the accordion being left in the open state
         */
        swipeToCommit: true,
 
        /**
         * @cfg {String} state
         * 'dragpeek','dragopen', 'dragcommit', 'open', 'undo'
         */
 
        /**
         * Current side that is revealed
         * @private
         */
        side: null
    },
 
    layout: {
        type: 'hbox',
        align: 'stretch'
    },
 
    template: [{
        reference: 'bodyElement',
        cls: Ext.baseCSSPrefix + 'body-el',
        uiCls: 'body-el',
 
        children: [{
            reference: 'leftElement',
            cls: Ext.baseCSSPrefix +
                'listswiperaccordion-wrapper ' +
                Ext.baseCSSPrefix +
                'listswiperaccordion-wrapper-left'
        }, {
            reference: 'rightElement',
            cls: Ext.baseCSSPrefix +
                'listswiperaccordion-wrapper ' +
                Ext.baseCSSPrefix +
                'listswiperaccordion-wrapper-right'
        }]
    }],
 
    scrollDock: null,
 
    constructor: function(config) {
        var me = this;
 
        me.left = {
            name: 'left',
            isLeft: true,
            items: []
        };
 
        me.right = {
            name: 'right',
            isLeft: false,
            items: []
        };
 
        me.callParent([config]);
    },
 
    initialize: function() {
        var me = this,
            target = me.getTranslationTarget();
 
        me.callParent();
        target.on({
            scope: me,
            tap: 'onDismissTap'
        });
    },
 
    destroy: function() {
        var me = this,
            target = me.getTranslationTarget();
 
        if (me.$undoTimer) {
            Ext.unraf(me.$undoTimer);
        }
 
        //<debug>
        if (me.thresholdEl) {
            me.thresholdEl.destroy();
        }
        //</debug>
 
        target.un({
            scope: me,
            tap: 'onDismissTap'
        });
 
        me.callParent();
    },
 
    applyLeftActions: function(items) {
        this.addActions('left', items);
    },
 
    applyRightActions: function(items) {
        this.addActions('right', items);
    },
 
    applySide: function(side) {
        this.side = side && this[side];
 
        return side;
    },
 
    getButtonBackgroundColor: function(button) {
        var action = button.$action,
            backgroundColorEl = button[action.backgroundColorEl || 'innerElement'];
 
        return backgroundColorEl.getStyle('backgroundColor');
    },
 
    addActions: function(side, items) {
        var me = this,
            i, config, action, button;
 
        side = me[side];
        side.el = side.isLeft ? me.leftElement : me.rightElement;
        side.multiple = items.length > 1;
        config = side.multiple ? me.getMultiActionDefaults() : me.getSingleActionDefaults();
        side.el.toggleCls(me.baseCls + '-multiple', side.multiple);
        side.el.toggleCls(me.baseCls + '-single', !side.multiple);
 
        for (= 0; i < items.length; i++) {
            action = me.createActionItem(Ext.apply({}, items[i], config));
            action.$side = side;
            button = me.add(action);
            button.addUi(this.getActionUI());
            button.$action = action;
            button.$originalHandler = button.getHandler();
            button.setHandler(me.onActionTap.bind(me, action));
 
            side.items.push(button);
        }
    },
 
    createActionItem: function(config) {
        return Ext.apply({}, config, this.getActionDefaults());
    },
 
    getSwipeRange: function() {
        var me = this,
            side = me.side,
            plugin = me.owner,
            swipeMax = plugin.getSwipeMax();
 
        return me.itemWidth * (swipeMax[side.multiple ? 'multiple' : 'single'] / 100);
    },
 
    onActionTap: function(action, button, e) {
        var me = this,
            state = me.getState();
 
        if (state !== 'dragpeek') {
            e.stopPropagation();
            this.commit(e, action, button);
        }
    },
 
    onDismissTap: function() {
        var me = this,
            plugin = me.owner,
            dimissOnTap = plugin.getDismissOnTap();
 
        if (dimissOnTap) {
            me.dismiss();
        }
    },
 
    onRender: function() {
        var me = this,
            item = me.ownerCmp;
 
        me.itemWidth = item.el.measure('w');
        me.syncSides();
    },
 
    updateSide: function(side, oldSide) {
        var me = this,
            layout = me.getLayout();
 
        me.el.replaceCls(oldSide, side, me.baseCls + '-side');
 
        if (side === 'left') {
            layout.setPack('start');
        }
        else {
            layout.setPack('end');
        }
    },
 
    updateState: function(state, oldState) {
        var me = this,
            side = me.side,
            defaultButton = me.getDefaultButton();
 
        me.callParent([state, oldState]);
 
        if (side.multiple) {
            me.el.toggleCls(me.baseCls + '-collapsed', state === 'dragcommit');
 
            if (state === 'dragcommit') {
                defaultButton.el.setStyle({ 'flex-basis': side.maxActionWidth + 'px' });
            }
            else {
                defaultButton.setStyle({ 'flex-basis': null });
            }
 
            if (oldState === 'dragcommit' && me.isDragging) {
                me.el.addCls(me.baseCls + '-was-collapsed');
            }
            else {
                me.el.removeCls(me.baseCls + '-was-collapsed');
            }
        }
    },
 
    privates: {
        destroyItem: function() {
            var me = this,
                plugin = me.owner,
                item = me.ownerCmp;
 
            if (!me.destroyed) {
                me.animating = false;
                me.el.removeCls(me.baseCls + '-was-collapsed');
                plugin.destroyItem(item);
            }
        },
        animateItem: function(offset, config) {
            var me = this,
                side, target, duration, completeFn;
 
            config = config || {};
 
            side = me.side;
            target = this.getTranslationTarget();
            duration = config.duration || 150;
 
            return new Ext.Promise(function(resolve) {
                me.animating = true;
                me.offset = side.isLeft ? offset : -offset;
 
                completeFn = function() {
                    if (!me.destroyed) {
                        me.animating = false;
                        me.el.removeCls(me.baseCls + '-was-collapsed');
                    }
 
                    resolve();
                };
 
                if (target.dom) {
                    if (side.el.dom) {
                        side.el.animate({
                            preserveEndState: true,
                            duration: duration,
                            to: {
                                width: offset
                            }
                        });
                    }
 
                    target.animate({
                        preserveEndState: true,
                        duration: duration,
                        to: {
                            transform: {
                                translateX: me.offset
                            }
                        },
 
                        callback: completeFn
                    });
                }
                else {
                    completeFn();
                }
            });
        },
 
        commit: function(e, action, button) {
            var me = this,
                plugin = me.owner,
                undoable, handler,
                delay, precommitResult, undo, backgroundColor;
 
            action = action || me.getDefaultAction();
            button = button || me.getDefaultButton();
            undoable = action.undoable;
            handler = button.$originalHandler;
 
            me.setAction(action);
            me.$precommitResult = precommitResult = me.invokeAction(action, 'precommit');
 
            if (handler) {
                me.snapback().then(function() {
                    Ext.callback(handler, button.getScope(), [action, e], 0, button);
                }).then(function() {
                    me.destroyItem();
                });
            }
            else {
                if (!undoable) {
                    me.dismiss();
                }
                else {
                    undo = me.add(me.getUndo());
                    undo.addUi(button.getUi());
 
                    me.bodyElement.on({
                        scope: me,
                        tap: 'onDismissTap'
                    });
 
                    if (me.$undoTimer) {
                        Ext.unraf(me.$undoTimer);
                    }
 
                    me.$undoTimer = Ext.raf(function() {
                        me.$undoTimer = null;
                        me.setState('undo');
                        backgroundColor = me.getButtonBackgroundColor(button);
 
                        if (backgroundColor) {
                            me.el.setStyle('backgroundColor', backgroundColor);
                        }
 
                        undo.setHandler(me.onUndoTap.bind(me));
 
                        delay = plugin.getCommitDelay();
 
                        if (delay) {
                            if (precommitResult && precommitResult.then) {
                                precommitResult.then(function() {
                                    plugin.dismissAllTask.delay(delay);
                                });
                            }
                            else {
                                plugin.dismissAllTask.delay(delay);
                            }
                        }
                    });
                }
            }
        },
 
        onUndoTap: function() {
            this.undo();
        },
 
        undo: function() {
            var me = this,
                action = me.getAction(),
                precommitResult = me.$precommitResult;
 
            me.setState('open');
 
            if (precommitResult && precommitResult.then) {
                precommitResult.then(function() {
                    me.$precommitResult = null;
                    me.undo();
                });
 
                return;
            }
 
            me.snapback().then(function() {
                me.invokeAction(action, 'revert');
            }).then(function() {
                me.destroyItem();
            });
        },
 
        //<debug>
        createThresholds: function() {
            var me = this,
                item = me.ownerCmp,
                plugin = this.owner,
                side = me.side,
                swipeRange = me.getSwipeRange(),
                commitThreshold = side.commitThreshold,
                openThreshold = side.openThreshold,
                width;
 
            if (plugin.showThresholds && !me.thresholdEl) {
                width = (swipeRange - (commitThreshold - openThreshold) - openThreshold);
                me.thresholdEl = item.el.append({
                    style: {
                        display: 'flex',
                        flexDirection: 'row',
                        justifyContent: 'flex-end',
                        position: 'absolute',
                        height: 'auto',
                        top: 0,
                        left: 0,
                        right: 0
                    },
                    children: [
                        {
                            style: {
                                width: width + 'px',
                                height: '6px',
                                opacity: 0.8,
                                backgroundColor: '#EF5350'
                            }
                        },
                        {
                            style: {
                                width: (commitThreshold - openThreshold) + 'px',
                                height: '6px',
                                opacity: 0.8,
                                backgroundColor: '#FFEE58'
                            }
                        },
                        {
                            style: {
                                width: openThreshold + 'px',
                                height: '6px',
                                opacity: 0.8,
                                backgroundColor: '#66BB6A'
                            }
                        }
                    ]
                });
            }
 
            if (me.thresholdEl) {
                me.thresholdEl.setStyle({
                    flexDirection: !side.isLeft ? 'row' : 'row-reverse'
                });
            }
        },
        //</debug>
 
        dismiss: function() {
            var me = this,
                action = me.getAction(),
                precommitResult = me.$precommitResult;
 
            if (precommitResult && precommitResult.then) {
                precommitResult.then(function() {
                    me.$precommitResult = null;
                    me.dismiss();
                });
 
                return;
            }
 
            if (action) {
                me.snapback().then(function() {
                    me.invokeAction(action, 'commit');
                }).then(function() {
                    me.destroyItem();
                });
            }
            else {
                me.snapback(true);
            }
        },
 
        onDragStart: function(e) {
            var me = this,
                state = me.getState();
 
            if (me.animating || state === 'undo') {
                return;
            }
 
            e.claimGesture();
            me.initialOffset = me.offset || 0;
            me.startX = e.getX() - me.el.getX() - me.initialOffset;
            me.isDragging = true;
            me.syncState(e.deltaX);
        },
 
        onDragMove: function(e) {
            // Additonal check for undo state so that it will not create multiple undo buttons 
            if ((this.getState()) === 'undo') {
                return;
            }
 
            e.preventDefault();
            this.syncState(e.deltaX);
        },
 
        onDragEnd: function(e) {
            var me = this,
                state = me.getState();
 
            e.preventDefault();
            me.isDragging = false;
 
            if (state === 'dragcommit') {
                me.commit(e);
            }
            else if (state === 'dragopen') {
                me.open();
            }
            else if (state === 'undo') {
                // avoid calling snapback in case it is already in undo state
                return;
            }
            else {
                me.snapback(true);
            }
        },
 
        getDefaultButton: function(side) {
            var items;
 
            side = side || this.side;
 
            items = side.items;
 
            return items[side.isLeft ? 0 : items.length - 1];
        },
 
        getDefaultAction: function(side) {
            var button = this.getDefaultButton(side);
 
            return button && button.$action;
        },
 
        getRenderTarget: function(item) {
            var side = item && item.$side;
 
            if (side) {
                return side.el;
            }
 
            return this.callParent(arguments);
        },
 
        open: function() {
            return this.animateItem(this.side.naturalWidth);
        },
 
        snapback: function(destroy) {
            var me = this,
                anim = me.animateItem(0);
 
            return destroy
                ? anim.then(function() {
                    me.destroyItem();
                })
                : anim;
        },
 
        syncSides: function() {
            var me = this;
 
            me.syncSide('left');
            me.syncSide('right');
        },
 
        syncSide: function(side) {
            var me = this,
                thresholds = me.getThresholds(),
                itemWidth = me.itemWidth,
                element = side === 'left' ? me.leftElement : me.rightElement,
                children = element.dom.childNodes,
                maxActionWidth = 0,
                backgroundColor, defaultButton, childWidth, i, child, naturalWidth;
 
            side = me[side];
            defaultButton = this.getDefaultButton(side);
 
            element.addCls(me.baseCls + '-measure');
 
            for (= 0; i < children.length; i++) {
                child = Ext.get(children[i]);
                childWidth = child.measure('w');
 
                if (childWidth > maxActionWidth) {
                    maxActionWidth = childWidth;
                }
            }
 
            naturalWidth = side.naturalWidth = maxActionWidth * children.length;
            side.maxActionWidth = maxActionWidth;
 
            if (thresholds && thresholds.open) {
                side.openThreshold = (thresholds.open / 100) * itemWidth;
            }
            else {
                side.openThreshold = maxActionWidth;
            }
 
            if (thresholds && thresholds.commit) {
                side.commitThreshold = (thresholds.commit / 100) * itemWidth;
            }
            else {
                side.commitThreshold = Math.min(0.95 * itemWidth, naturalWidth * 1.4);
            }
 
            element.removeCls(me.baseCls + '-measure');
 
            if (side.multiple) {
                backgroundColor = me.getButtonBackgroundColor(defaultButton);
 
                if (backgroundColor) {
                    side.el.setStyle('backgroundColor', backgroundColor);
                }
            }
        },
 
        syncState: function(deltaX) {
            var me = this,
                plugin = me.owner,
                itemWidth = me.itemWidth,
                swipeToCommit = me.getSwipeToCommit(),
                scaleDrag = me.getScaleDrag(),
                testOffset = me.initialOffset + deltaX,
                side = this[(testOffset < 0 ? 'right' : 'left')],
                // Determines the scale (1-3) in which to multiple the delta by depending 
                // on where you start the drag.
                // Drags started in the middle will scale faster allowing for items to be
                // seen without extremely long drags.
                scaler = scaleDrag
                    ? Math.max(1, Math.min(3, Math.abs(
                        (side.isLeft ? 0 : 1) - (me.startX / itemWidth)) * 3)
                    )
                    : 1,
                offset = me.offset = me.initialOffset + (deltaX * scaler),
                currentSide = me.side,
                directionLock = plugin.getDirectionLock(),
                positiveOffset, swipeRange, openThreshold, commitThreshold;
 
            if (this.left.items.length === 0 || this.right.items.length === 0) {
                directionLock = false;
            }
 
            // Empty Items or Direction locked will friction drag
            if (
                side.items.length === 0 ||
                (currentSide && (side.name !== currentSide.name && directionLock))
            ) {
                offset = me.offset = me.initialOffset + (deltaX * 0.1);
 
                // Possible first drag was on an empty side, we need to set the side
                if (side.items.length === 0) {
                    me.setSide(side.name);
                }
 
                me.setState('draglocked');
            }
            else {
                me.setSide(side.name);
 
                // Depends on side being set, must be done after.
                swipeRange = me.getSwipeRange();
                openThreshold = side.openThreshold;
                commitThreshold = side.commitThreshold;
 
                positiveOffset = (side.isLeft
                    ? Math.abs(Math.max(0, offset))
                    : Math.abs(Math.min(0, offset))
                );
 
                //<debug>
                me.createThresholds();
                //</debug>
 
                if (positiveOffset <= openThreshold) {
                    me.setState('dragpeek');
                }
                else if (positiveOffset <= commitThreshold || !swipeToCommit) {
                    me.setState('dragopen');
                }
                else {
                    me.setState('dragcommit');
                }
 
                if (side.isLeft) {
                    offset = Math.min(offset, swipeRange);
                }
                else {
                    offset = Math.max(offset, -swipeRange);
                }
            }
 
            me.setBodyOffset(offset);
        },
 
        updateBodyOffset: function(offset) {
            var me = this,
                side = me.side,
                target = me.getTranslationTarget();
 
            target.setStyle('transform', 'translateX(' + offset + 'px)');
            side.el.setWidth(Math.abs(offset));
        }
    }
});