/**
 * 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'
    ],

    requires: [
        'Ext.util.Translatable'
    ],

    /**
     * @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: {}
    },

    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);

        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, currentInstance);
        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.refresh();
    },

    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.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.detachListeners();

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

        me.callParent();
    }
});

