/**
 * A core util class to bring Draggable behavior to any DOM element,
 * acts as a base class for Scroller and Sortable.
 */
Ext.define('Ext.util.Draggable', {
    isDraggable: true,

    mixins: [
        'Ext.mixin.Observable'
    ],

    config: {
        cls: Ext.baseCSSPrefix + 'draggable',

        draggingCls: Ext.baseCSSPrefix + 'dragging',

        proxyCls: Ext.baseCSSPrefix + 'draggable-proxy',

        element: null,

        constraint: 'container',

        disabled: null,

        /**
         * @cfg {String} direction
         * Possible values: 'vertical', 'horizontal', or 'both'
         * @accessor
         */
        direction: 'both',

        /**
         * @cfg {Number} delay
         * How many milliseconds a user must hold the draggable before starting a
         * drag operation.
         * @accessor
         */
        delay: 0,

        /**
         * @cfg {String} cancelSelector
         * A simple CSS selector that represents elements within the draggable
         * that should NOT initiate a drag.
         * @accessor
         */
        cancelSelector: null,

        /**
         * @cfg {Boolean} revert
         * Whether or not the element or it's proxy will be reverted back (with animation)
         * when it's not dropped and held by a Droppable
         * @accessor
         */
        revert: false,

        /**
         * @cfg {String} group
         * Draggable and Droppable objects can participate in a group which are
         * capable of interacting.
         * @accessor
         */
        group: 'base',

        translateMethod: 'auto',

        initialOffset: 'auto',

        containerConstraint: 'auto',

        autoRefresh: true,

        offset: {
            x: 0,
            y: 0
        }
    },

    CSS_POSITION: 'cssposition',

    CSS_TRANSFORM: 'csstransform',

    DIRECTION_BOTH: 'both',

    DIRECTION_VERTICAL: 'vertical',

    DIRECTION_HORIZONTAL: 'horizontal',

    /**
     * Creates new Draggable.
     * @param {Mixed} el The element you want to make draggable.
     * @param {Object} config The configuration object for this Draggable.
     */
    constructor: function(config) {
        var element;

        this.sizeMonitors = {};

        this.initialConfig = config;

        this.listeners = {
            dragstart: 'onDragStart',
            drag     : 'onDrag',
            dragend  : 'onDragEnd',

            scope: this
        };

        this.isAxisEnabledFlags = { x: false, y: false };

        if (config && config.element) {
            element = config.element;
            delete config.element;

            this.setElement(element);
        }

        return this;
    },

    applyElement: function(element) {
        if (!element) {
            this.container = null;
            return;
        }

        return Ext.get(element);
    },

    updateElement: function(element) {
        if (!this.initialized) {
            this.initialized = true;

            this.initConfig(this.initialConfig);

            element.on(this.listeners);

            if (this.getAutoRefresh()) {
                this.sizeMonitors.element = new Ext.util.SizeMonitor({
                    element: element,
                    callback: this.doRefresh,
                    scope: this
                });
            }

            this.onConfigUpdate('containerConstraint', 'refreshConstraint');
            this.onConfigUpdate('constraint', 'refreshOffset');
        }
        else {
            this.refresh();
        }

        element.addCls(this.getCls());

        return this;
    },

    applyConstraint: function(newConstraint, currentConstraint) {
        var initialOffset = this.getInitialOffset(),
            initialOffsetX, initialOffsetY, constraint, containerConstraint;

        this.givenConstraint = newConstraint;

        constraint = {
            min: { x: -Infinity, y: -Infinity },
            max: { x: Infinity, y: Infinity }
        };

        if (newConstraint === 'container') {
            containerConstraint = this.getContainerConstraint();

            initialOffsetX = initialOffset.x;
            initialOffsetY = initialOffset.y;

            constraint.min.x = containerConstraint.min.x - initialOffsetX;
            constraint.min.y = containerConstraint.min.y - initialOffsetY;
            constraint.max.x = containerConstraint.max.x - initialOffsetX;
            constraint.max.y = containerConstraint.max.y - initialOffsetY;
        }
        else if (Ext.isSimpleObject(newConstraint)) {
            Ext.merge(constraint, currentConstraint || {}, newConstraint);
        }

        return constraint;
    },

    applyContainerConstraint: function(newConstraint, currentConstraint) {
        var constraint, container,
            containerDom, dom, containerStyle, min, max,
            width, height, containerWidth, containerHeight,
            paddingTop, paddingLeft, paddingRight, paddingBottom,
            borderTop, borderLeft, borderRight, borderBottom;

        constraint = {
            min: { x: -Infinity, y: -Infinity },
            max: { x: Infinity, y: Infinity }
        };

        this.givenContainerConstraint = newConstraint;

        if (newConstraint === 'auto' && (container = this.getContainer())) {
            dom = this.getElement().dom;
            containerDom = container.dom;

            containerStyle = window.getComputedStyle(containerDom);

            min = constraint.min;
            max = constraint.max;

            width = dom.offsetWidth;
            height = dom.offsetHeight;

            paddingLeft   = this.getNumberValue(containerStyle.paddingLeft);
            paddingTop    = this.getNumberValue(containerStyle.paddingTop);
            paddingRight  = this.getNumberValue(containerStyle.paddingRight);
            paddingBottom = this.getNumberValue(containerStyle.paddingBottom);

            borderTop     = this.getNumberValue(containerStyle.borderTopWidth);
            borderLeft    = this.getNumberValue(containerStyle.borderLeftWidth);
            borderRight   = this.getNumberValue(containerStyle.borderRightWidth);
            borderBottom  = this.getNumberValue(containerStyle.borderBottomWidth);

            containerWidth = containerDom.offsetWidth;
            containerHeight = containerDom.offsetHeight;

            min.x = paddingLeft;
            min.y = paddingTop;

            max.x = Math.max(min.x, containerWidth - paddingRight - borderLeft - borderRight - width);
            max.y = Math.max(min.y, containerHeight - paddingBottom - borderTop - borderBottom - height);
        }
        else if (Ext.isSimpleObject(newConstraint)) {
            Ext.merge(constraint, currentConstraint || {}, newConstraint);
        }

        return constraint;
    },

    getNumberValue: function(value) {
        value = parseInt(value, 10);

        if (isNaN(value)) {
            value = 0;
        }

        return value;
    },

    applyTranslateMethod: function(method) {
        var cssPosition = this.CSS_POSITION,
            cssTransform = this.CSS_TRANSFORM;

        if (method === 'auto') {
            if (Ext.os.is.Android2) {
                method = cssPosition;
            }
            else {
                method = cssTransform;
            }
        }

        if (method !== cssPosition && method !== cssTransform) {
            method = cssTransform;
        }

        return method;
    },

    updateTranslateMethod: function(newMethod, oldMethod) {
        if (oldMethod) {
            if (oldMethod === this.CSS_TRANSFORM) {
                this.translateWithCssTransform(0, 0);
            }
            else {
                this.translateWithCssPosition(0, 0);
            }
        }

        this.updateOffset(this.getOffset());
    },

    getContainer: function() {
        var container = this.container;

        if (!container) {
            container = this.getElement().getParent();

            if (container) {
                if (this.getAutoRefresh()) {
                    this.sizeMonitors.container = new Ext.util.SizeMonitor({
                        element: container,
                        callback: this.doRefresh,
                        scope: this
                    });
                }

                this.container = container;
            }
        }

        return container;
    },

    applyInitialOffset: function(offset) {
        var dom, style;

        if (offset === 'auto') {
            dom = this.getElement().dom;
            style = window.getComputedStyle(dom);

            offset = {
                x: dom.offsetLeft - this.getNumberValue(style.marginLeft),
                y: dom.offsetTop - this.getNumberValue(style.marginTop)
            };
        }

        return offset;
    },

    detachListeners: function() {
        this.getElement().un(this.listeners);
    },

    updateDirection: function(direction) {
        var isAxisEnabled = this.isAxisEnabledFlags;

        isAxisEnabled.x = (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_HORIZONTAL);
        isAxisEnabled.y = (direction === this.DIRECTION_BOTH || direction === this.DIRECTION_VERTICAL);
    },

    isAxisEnabled: function(axis) {
        this.getDirection();

        return this.isAxisEnabledFlags[axis];
    },

    onDragStart: function(e) {
        if (this.getDisabled()) {
            return false;
        }

        this.fireAction('dragstart', [this, e, this.getOffset()], 'initDragStart');
    },

    initDragStart: function(me, e, startOffset) {
        this.dragStartOffset = startOffset;

        this.isDragging = true;

        this.getElement().addCls(this.getDraggingCls());
    },

    onDrag: function(e) {
        if (!this.isDragging) {
            return;
        }

        var startOffset = this.dragStartOffset;

        this.setOffset({
            x: startOffset.x + e.deltaX,
            y: startOffset.y + e.deltaY
        });

        this.fireEvent('drag', this, e, this.getOffset());
    },

    onDragEnd: function(e) {
        if (!this.isDragging) {
            return;
        }

        this.onDrag(e);

        this.isDragging = false;

        this.fireEvent('dragend', this, e);

        this.getElement().removeCls(this.getDraggingCls());
    },

    applyOffset: function(offset, oldOffset) {
        var x = offset.x,
            y = offset.y,
            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 = oldOffset ? oldOffset.x : 0;
        }

        if (this.isAxisEnabled('y') && typeof y == 'number') {
            y = min(max(y, minOffset.y), maxOffset.y);
        }
        else {
            y = oldOffset ? oldOffset.y : 0;
        }

        return {
            x: x,
            y: y
        };
    },

    updateOffset: function(offset, oldOffset) {
        var x = offset.x,
            y = offset.y;

        if (!oldOffset || x !== oldOffset.x || y !== oldOffset.y) {
            this.moveTo(x, y);
        }
    },

    moveTo: function(x, y) {
        var method = this.getTranslateMethod();

        if (method === this.CSS_POSITION) {
            return this.translateWithCssPosition.apply(this, arguments);
        }
        else {
            return this.translateWithCssTransform.apply(this, arguments);
        }
    },

    translateWithCssPosition: function(x, y) {
        var initialOffset = this.getInitialOffset(),
            domStyle = this.getElement().dom.style;

        domStyle.left = (x + initialOffset.x) + 'px';
        domStyle.top = (y + initialOffset.y) + 'px';
    },

    translateWithCssTransform: function(x, y) {
        var domStyle = this.getElement().dom.style;

        domStyle.webkitTransform = 'translate3d(' + x + 'px, ' + y + 'px, 0px)';
    },

    refreshConstraint: function() {
        this.setConstraint(this.givenConstraint);
    },

    refreshOffset: function() {
        this.setOffset(this.getOffset());
    },

    doRefresh: function() {
        this.setContainerConstraint(this.givenContainerConstraint);
        this.refreshConstraint();
        this.refreshOffset();
    },

    refresh: function() {
        var sizeMonitors = this.sizeMonitors;

        if (sizeMonitors.element) { 
            sizeMonitors.element.refresh();
        }
        
        if (sizeMonitors.container) {
            sizeMonitors.container.refresh();
        }

        this.doRefresh();
    },

    /**
     * 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 sizeMonitors = this.sizeMonitors;

        if (sizeMonitors.element) {
            sizeMonitors.element.destroy();
        }

        if (sizeMonitors.container) {
            sizeMonitors.container.destroy();
        }

        this.getElement().removeCls(this.getCls());
        this.detachListeners();
    }

}, function() {
    //<deprecated product=touch since=2.0>
    this.override({
        constructor: function(config) {
            if (config && config.constrain) {
                //<debug warn>
                Ext.Logger.deprecate("'constrain' config is deprecated, please use 'contraint' instead");
                //</debug>
                config.contraint = config.constrain;
                delete config.constrain;
            }

            return this.callOverridden(arguments);
        }
    });
    //</deprecated>
});