/**
 * A core util class to bring Draggable behavior to a Component. This class is specifically designed only for
 * absolutely positioned elements starting from top: 0, left: 0. The initialOffset can then be set via configuration
 * to have the elements in a different position.
 */
Ext.define('Ext.util.Draggable', {
    isDraggable: true,
 
    mixins: [
        'Ext.mixin.Observable'
    ],
 
    /**
     * @event dragstart
     * @preventable
     * Fires whenever the component starts to be dragged
     * @param {Ext.util.Draggable} this
     * @param {Ext.event.Event} e the event object
     * @param {Number} offsetX The current offset value on the x axis
     * @param {Number} offsetY The current offset value on the y axis
     */
 
    /**
     * @event drag
     * Fires whenever the component is dragged
     * @param {Ext.util.Draggable} this
     * @param {Ext.event.Event} e the event object
     * @param {Number} offsetX The new offset value on the x axis
     * @param {Number} offsetY The new offset value on the y axis
     */
 
    /**
     * @event dragend
     * Fires whenever the component is dragged
     * @param {Ext.util.Draggable} this
     * @param {Ext.event.Event} e the event object
     * @param {Number} offsetX The current offset value on the x axis
     * @param {Number} offsetY The current offset value on the y axis
     */
 
    config: {
        cls: Ext.baseCSSPrefix + 'draggable',
 
        draggingCls: Ext.baseCSSPrefix + 'dragging',
 
        element: null,
 
        constraint: 'container',
 
        disabled: null,
 
        /**
         * @cfg {String} direction 
         * Possible values: 'vertical', 'horizontal', or 'both'
         * @accessor
         */
        direction: 'both',
 
        /**
         * @cfg {Object/Number} initialOffset
         * The initial draggable offset.  When specified as Number,
         * both x and y will be set to that value.
         */
        initialOffset: {
            x: 0,
            y: 0
        },
 
        translatable: {},
 
        /**
         * @cfg {Ext.Component} The component being dragged.
         */
        component: null
    },
 
    DIRECTION_BOTH: 'both',
 
    DIRECTION_VERTICAL: 'vertical',
 
    DIRECTION_HORIZONTAL: 'horizontal',
 
    defaultConstraint: {
        min: { x: -Infinity, y: -Infinity },
        max: { x: Infinity, y: Infinity }
    },
 
    containerWidth: 0,
 
    containerHeight: 0,
 
    width: 0,
 
    height: 0,
 
    /**
     * Creates new Draggable.
     * @param {Object} config The configuration object for this Draggable.
     */
    constructor: function(config) {
        var element;
 
        this.extraConstraint = {};
 
        this.initialConfig = config;
 
        this.offset = {
            x: 0,
            y: 0
        };
 
        this.elementListeners = {
            dragstart: 'onDragStart',
            drag     : 'onDrag',
            dragend  : 'onDragEnd',
            resize   : 'onElementResize',
            touchstart : 'onPress',
            touchend   : 'onRelease',
            // high priority ensures that these listeners run before user listeners 
            // so that draggable state is correct in user handlers 
            priority: 2000,
            scope: this
        };
 
        if (config && config.element) {
            element = config.element;
            delete config.element;
 
            this.setElement(element);
        }
 
        return this;
    },
 
    applyElement: function(element) {
        if (!element) {
            return;
        }
 
        return Ext.get(element);
    },
 
    updateElement: function(element) {
        element.on(this.elementListeners);
        element.setTouchAction({
            panX: false,
            panY: false
        });
 
        this.mixins.observable.constructor.call(this, this.initialConfig);
    },
 
    updateInitialOffset: function(initialOffset) {
        if (typeof initialOffset === 'number') {
            initialOffset = {
                x: initialOffset,
                y: initialOffset
            };
        }
 
        var offset = this.offset,
            x, y;
 
        offset.x = x = initialOffset.x;
        offset.y = y = initialOffset.y;
 
        this.getTranslatable().translate(x, y);
    },
 
    updateCls: function(cls) {
        this.getElement().addCls(cls);
    },
 
    applyTranslatable: function(translatable, currentInstance) {
        translatable = Ext.factory(translatable, Ext.util.translatable.CssTransform, currentInstance, 'translatable');
        if (translatable) {
            translatable.setElement(this.getElement());
        }
 
        return translatable;
    },
 
    setExtraConstraint: function(constraint) {
        this.extraConstraint = constraint || {};
 
        this.refreshConstraint();
 
        return this;
    },
 
    addExtraConstraint: function(constraint) {
        Ext.merge(this.extraConstraint, constraint);
 
        this.refreshConstraint();
 
        return this;
    },
 
    applyConstraint: function(newConstraint) {
        this.currentConstraint = newConstraint;
 
        if (!newConstraint) {
            newConstraint = this.defaultConstraint;
        }
 
        if (newConstraint === 'container') {
            return Ext.merge(this.getContainerConstraint(), this.extraConstraint);
        }
 
        return Ext.merge({}, this.extraConstraint, newConstraint);
    },
 
    updateConstraint: function() {
        this.refreshOffset();
    },
 
    getContainerConstraint: function() {
        var container = this.getContainer(),
            element = this.getElement(),
            borders;
 
        if (!container || !element.dom) {
            return this.defaultConstraint;
        }
 
        borders = container.getBorders();
        return {
            min: { x: 0, y: 0 },
            max: { x: this.containerWidth - this.width - borders.beforeX - borders.afterX, y: this.containerHeight - this.height - borders.beforeY - borders.afterY }
        };
    },
 
    getContainer: function() {
        var container = this.container;
 
        if (!container) {
            container = this.getElement().getParent();
 
            if (container) {
                this.container = container;
 
                container.on({
                    resize: 'onContainerResize',
                    destroy: 'onContainerDestroy',
                    scope: this,
                    // The resize listener must have a high priority, so that the draggable 
                    // instance is refreshed prior to other parties who may be listening 
                    // for resize on the same element.  For example, slider listens to 
                    // resize on its element and expects that the draggable thumbs have 
                    // already had their draggable instances refreshed. 
                    priority: 2000
                });
            }
        }
 
        return container;
    },
 
    onElementResize: function(element, info) {
        this.width = info.width;
        this.height = info.height;
        this.refreshContainerSize();
    },
 
    onContainerResize: function(container, info) {
        this.containerWidth = info.contentWidth;
        this.containerHeight = info.contentHeight;
 
        this.refresh();
    },
 
    refreshContainerSize: function() {
        // refreshes container size from dom.  Useful when the draggable element did not 
        // have a parentNode at the time the draggable was initialized.  Invoke this 
        // as soon as the element is appended to its parent to ensure correct constraining 
        var me = this,
            container = me.getContainer();
 
        me.containerWidth = container.getWidth();
        me.containerHeight = container.getHeight();
 
        this.refresh();
 
        return me;
    },
 
    onContainerDestroy: function() {
        delete this.container;
        delete this.containerSizeMonitor;
    },
 
    detachListeners: function() {
        this.getElement().un(this.elementListeners);
    },
 
    isAxisEnabled: function(axis) {
        var direction = this.getDirection();
 
        if (axis === 'x') {
            return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL);
        }
 
        return (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL);
    },
 
    onPress: function(e) {
        this.fireEvent('touchstart', this, e);
    },
 
    onRelease: function(e) {
        this.fireEvent('touchend', this, e);
    },
 
    onDragStart: function(e) {
        var me = this,
            offset = me.offset;
 
        if (me.getDisabled()) {
            return false;
        }
 
        me.fireEventedAction('dragstart', [me, e, offset.x, offset.y], me.initDragStart, me);
    },
 
    initDragStart: function(me, e, offsetX, offsetY) {
        this.dragStartOffset = {
            x: offsetX,
            y: offsetY
        };
 
        this.isDragging = true;
 
        this.getElement().addCls(this.getDraggingCls());
    },
 
    onDrag: function(e) {
        if (!this.isDragging) {
            return;
        }
 
        var startOffset = this.dragStartOffset;
 
        this.fireAction('drag', [this, e, startOffset.x + e.deltaX, startOffset.y + e.deltaY], this.doDrag);
    },
 
    doDrag: function(me, e, offsetX, offsetY) {
        me.setOffset(offsetX, offsetY);
    },
 
    onDragEnd: function(e) {
        if (!this.isDragging) {
            return;
        }
 
        this.onDrag(e);
 
        this.isDragging = false;
 
        this.getElement().removeCls(this.getDraggingCls());
 
        this.fireEvent('dragend', this, e, this.offset.x, this.offset.y);
    },
 
    setOffset: function(x, y, animation) {
        var currentOffset = this.offset,
            constraint = this.getConstraint(),
            minOffset = constraint.min,
            maxOffset = constraint.max,
            min = Math.min,
            max = Math.max;
 
        if (this.isAxisEnabled('x') && typeof x === 'number') {
            x = min(max(x, minOffset.x), maxOffset.x);
        }
        else {
            x = currentOffset.x;
        }
 
        if (this.isAxisEnabled('y') && typeof y === 'number') {
            y = min(max(y, minOffset.y), maxOffset.y);
        }
        else {
            y = currentOffset.y;
        }
 
        currentOffset.x = x;
        currentOffset.y = y;
 
        this.getTranslatable().translate(x, y, animation);
    },
 
    getOffset: function() {
        return this.offset;
    },
 
    refreshConstraint: function() {
        this.setOffset.apply(this, this.getTranslatable().syncPosition());
        this.setConstraint(this.currentConstraint);
    },
 
    refreshOffset: function() {
        var offset = this.offset;
 
        this.setOffset(offset.x, offset.y);
    },
 
    refresh: function() {
        this.refreshConstraint();
        this.getTranslatable().refresh();
        this.refreshOffset();
    },
 
    /**
     * Enable the Draggable.
     * @return {Ext.util.Draggable} This Draggable instance
     */
    enable: function() {
        return this.setDisabled(false);
    },
 
    /**
     * Disable the Draggable.
     * @return {Ext.util.Draggable} This Draggable instance
     */
    disable: function() {
        return this.setDisabled(true);
    },
 
    destroy: function() {
        var me = this,
            translatable = me.getTranslatable();
 
        var element = me.getElement();
        if (element && !element.destroyed) {
            element.removeCls(me.getCls());
        }
        me.setComponent(null);
 
        me.detachListeners();
 
        if (translatable) {
            translatable.destroy();
        }
 
        me.callParent();
    }
});