/**
 * A mixin which allows a data component to be sorted
 * @ignore
 */
Ext.define('Ext.Sortable', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },
 
    requires: ['Ext.util.Draggable'],
 
    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'sortable',
 
        /**
         * @cfg {Number} delay 
         * How many milliseconds a user must hold the draggable before starting a
         * drag operation.
         * @private
         * @accessor
         */
        delay: 0
 
        },
 
    /**
     * @cfg {String} direction 
     * Possible values: 'vertical', 'horizontal'.
     */
    direction: 'vertical',
 
    /**
     * @cfg {String} cancelSelector 
     * A simple CSS selector that represents elements within the draggable
     * that should NOT initiate a drag.
     */
    cancelSelector: null,
 
    // not yet implemented 
    //indicator: true, 
    //proxy: true, 
    //tolerance: null, 
 
    /**
     * @cfg {HTMLElement/Boolean} constrain
     * An Element to constrain the Sortable dragging to.
     * If `true` is specified, the dragging will be constrained to the element
     * of the sortable.
     */
    constrain: window,
    /**
     * @cfg {String} group 
     * Draggable and Droppable objects can participate in a group which are
     * capable of interacting.
     */
    group: 'base',
 
    /**
     * @cfg {Boolean} revert 
     * This should NOT be changed.
     * @private
     */
    revert: true,
 
    /**
     * @cfg {String} itemSelector 
     * A simple CSS selector that represents individual items within the Sortable.
     */
    itemSelector: null,
 
    /**
     * @cfg {String} handleSelector 
     * A simple CSS selector to indicate what is the handle to drag the Sortable.
     */
    handleSelector: null,
 
    /**
     * @cfg {Boolean} disabled 
     * Passing in `true` will disable this Sortable.
     */
    disabled: false,
 
    // Properties 
 
    /**
     * Read-only property that indicates whether a Sortable is currently sorting.
     * @type Boolean
     * @private
     * @readonly
     */
    sorting: false,
 
    /**
     * Read-only value representing whether the Draggable can be moved vertically.
     * This is automatically calculated by Draggable by the direction configuration.
     * @type Boolean
     * @private
     * @readonly
     */
    vertical: false,
 
    /**
     * Creates new Sortable.
     * @param {Mixed} el 
     * @param {Object} config 
     */
    constructor : function(el, config) {
        config = config || {};
        Ext.apply(this, config);
 
        this.addEvents(
            /**
             * @event sortstart
             * @param {Ext.Sortable} this
             * @param {Ext.event.Event} e
             */
            'sortstart',
            /**
             * @event sortend
             * @param {Ext.Sortable} this
             * @param {Ext.event.Event} e
             */
            'sortend',
            /**
             * @event sortchange
             * @param {Ext.Sortable} this
             * @param {Ext.Element} el The Element being dragged.
             * @param {Number} index The index of the element after the sort change.
             */
            'sortchange'
 
            // not yet implemented. 
            // 'sortupdate', 
            // 'sortreceive', 
            // 'sortremove', 
            // 'sortenter', 
            // 'sortleave', 
            // 'sortactivate', 
            // 'sortdeactivate' 
        );
 
        this.el = Ext.get(el);
        this.callParent();
 
        this.mixins.observable.constructor.call(this);
 
        if (this.direction == 'horizontal') {
            this.horizontal = true;
        }
        else if (this.direction == 'vertical') {
            this.vertical = true;
        }
        else {
            this.horizontal = this.vertical = true;
        }
 
        this.el.addCls(this.baseCls);
        this.startEventName = (this.getDelay() > 0) ? 'taphold' : 'tapstart';
        if (!this.disabled) {
            this.enable();
        }
    },
 
    /**
     * @private
     */
    onStart : function(e, t) {
        if (this.cancelSelector && e.getTarget(this.cancelSelector)) {
            return;
        }
        if (this.handleSelector && !e.getTarget(this.handleSelector)) {
            return;
        }
 
        if (!this.sorting) {
            this.onSortStart(e, t);
        }
    },
 
    /**
     * @private
     */
    onSortStart : function(e, t) {
        this.sorting = true;
        var draggable = Ext.create('Ext.util.Draggable', t, {
            threshold: 0,
            revert: this.revert,
            direction: this.direction,
            constrain: this.constrain === true ? this.el : this.constrain,
            animationDuration: 100
        });
        draggable.on({
            drag: this.onDrag,
            dragend: this.onDragEnd,
            scope: this
        });
 
        this.dragEl = t;
        this.calculateBoxes();
 
        if (!draggable.dragging) {
            draggable.onStart(e);
        }
 
        this.fireEvent('sortstart', this, e);
    },
 
    /**
     * @private
     */
    calculateBoxes : function() {
        this.items = [];
        var els = this.el.select(this.itemSelector, false),
            ln = els.length, i, item, el, box;
 
        for (= 0; i < ln; i++) {
            el = els[i];
            if (el != this.dragEl) {
                item = Ext.fly(el).getRegion();
                item.el = el;
                this.items.push(item);
            }
        }
    },
 
    /**
     * @private
     */
    onDrag : function(draggable, e) {
        var items = this.items,
            ln = items.length,
            region = draggable.region,
            sortChange = false,
            i, intersect, overlap, item;
 
        for (= 0; i < ln; i++) {
            item = items[i];
            intersect = region.intersect(item);
            if (intersect) {
                if (this.vertical && Math.abs(intersect.top - intersect.bottom) > (region.bottom - region.top) / 2) {
                    if (region.bottom > item.top && item.top > region.top) {
                        draggable.el.insertAfter(item.el);
                    }
                    else {
                        draggable.el.insertBefore(item.el);
                    }
                    sortChange = true;
                }
                else if (this.horizontal && Math.abs(intersect.left - intersect.right) > (region.right - region.left) / 2) {
                    if (region.right > item.left && item.left > region.left) {
                        draggable.el.insertAfter(item.el);
                    }
                    else {
                        draggable.el.insertBefore(item.el);
                    }
                    sortChange = true;
                }
 
                if (sortChange) {
                    // We reset the draggable (initializes all the new start values) 
                    draggable.reset();
 
                    // Move the draggable to its current location (since the transform is now 
                    // different) 
                    draggable.moveTo(region.left, region.top);
 
                    // Finally lets recalculate all the items boxes 
                    this.calculateBoxes();
                    this.fireEvent('sortchange', this, draggable.el, this.el.select(this.itemSelector, false).indexOf(draggable.el.dom));
                    return;
                }
            }
        }
    },
 
    /**
     * @private
     */
    onDragEnd : function(draggable, e) {
        draggable.destroy();
        this.sorting = false;
        this.fireEvent('sortend', this, draggable, e);
    },
 
    /**
     * Enables sorting for this Sortable.
     * This method is invoked immediately after construction of a Sortable unless
     * the disabled configuration is set to `true`.
     */
    enable : function() {
        this.el.on(this.startEventName, this.onStart, this, {delegate: this.itemSelector, holdThreshold: this.getDelay()});
        this.disabled = false;
    },
 
    /**
     * Disables sorting for this Sortable.
     */
    disable : function() {
        this.el.un(this.startEventName, this.onStart, this);
        this.disabled = true;
    },
 
    /**
     * Method to determine whether this Sortable is currently disabled.
     * @return {Boolean} The disabled state of this Sortable.
     */
    isDisabled: function() {
        return this.disabled;
    },
 
    /**
     * Method to determine whether this Sortable is currently sorting.
     * @return {Boolean} The sorting state of this Sortable.
     */
    isSorting : function() {
        return this.sorting;
    },
 
    /**
     * Method to determine whether this Sortable is currently disabled.
     * @return {Boolean} The disabled state of this Sortable.
     */
    isVertical : function() {
        return this.vertical;
    },
 
    /**
     * Method to determine whether this Sortable is currently sorting.
     * @return {Boolean} The sorting state of this Sortable.
     */
    isHorizontal : function() {
        return this.horizontal;
    }
});