/**
 * Provides constraining behavior for a {@link Ext.drag.Source}.
 */
Ext.define('Ext.drag.Constraint', {
    alias: 'drag.constraint.base',
    mixins: [
        'Ext.mixin.Factoryable'
    ],
 
    factoryConfig: {
        defaultType: 'base',
        type: 'drag.constraint'
    },
 
    config: {
        /**
         * @cfg {Boolean/String/HTMLElement/Ext.dom.Element} element
         *
         * The element to constrain to:
         * - `true` to constrain to the parent of the {@link Ext.drag.Source#element}.
         * - The id, DOM element or Ext.dom.Element to constrain to.
         */
        element: null,
 
        /**
         * @cfg {Boolean} horizontal
         * `true` to limit dragging to the horizontal axis.
         */
        horizontal: null,
 
        /**
         * @cfg {Ext.util.Region} region
         *
         * The region to constrain to.
         */
        region: null,
 
        /**
         * @cfg {Number/Object} snap
         * The interval to move this drag target during a drag in both dimensions.
         * - `{x: 30}`, snap only x
         * - `{y: 30}`, snap only y
         * - `{x: 30, y: 40}`, snap both
         * - `40`, snap both to `40`.
         *
         * The snap may also be a function to calculate the snap value on each tick.
         *
         *      snap: {
         *          x: function(info, x) {
         *              return x < 300 ? 150 : 500;
         *          }
         *      }
         */
        snap: null,
 
        /**
         * @cfg {Ext.drag.Source} source
         * The {@link Ext.drag.Source source} for the constraint. This will be
         * set automatically when constructed via the source.
         */
        source: null,
 
        /**
         * @cfg {Boolean} vertical
         * `true` to limit dragging to the vertical axis.
         */
        vertical: null,
 
        /**
         * @cfg {Number[]} x
         * The minimum and maximum x position. Use `null` to
         * not set a constraint:
         * - `[100, null]`, constrain only the minimum
         * - `[null, 100]`, constrain only the maximum
         * - `[200, 200]`, constrain both.
         */
        x: null,
 
        /**
         * @cfg {Number[]} y
         * The minimum and maximum y position. Use `null` to
         * not set a constraint:
         * - `[100, null]`, constrain only the minimum
         * - `[null, 100]`, constrain only the maximum
         * - `[200, 200]`, constrain both.
         */
        y: null
    },
 
    constructor: function(config) {
        this.initConfig(config);
    },
 
    applyElement: function(element) {
        if (element && typeof element !== 'boolean') {
            element = Ext.get(element);
        }
 
        return element || null;
    },
 
    applySnap: function(snap) {
        if (typeof snap === 'number') {
            snap = {
                x: snap,
                y: snap
            };
        }
 
        return snap;
    },
 
    /**
     * Constrain the position of the drag proxy using the configured rules.
     *
     * @param {Number[]} xy The position.
     * @param {Ext.drag.Info} info The drag information.
     *
     * @return {Number[]} The xy position.
     */
    constrain: function(xy, info) {
        var me = this,
            x = xy[0],
            y = xy[1],
            constrainInfo = me.constrainInfo,
            initial = constrainInfo.initial,
            constrainX = constrainInfo.x,
            constrainY = constrainInfo.y,
            snap = constrainInfo.snap,
            min, max;
 
        if (!constrainInfo.vertical) {
            if (snap && snap.x) {
                if (snap.xFn) {
                    x = snap.x.call(me, info, x);
                }
                else {
                    x = me.doSnap(x, initial.x, snap.x);
                }
            }
 
            if (constrainX) {
                min = constrainX[0];
                max = constrainX[1];
 
                if (min !== null && x < min) {
                    x = min;
                }
 
                if (max !== null && x > max) {
                    x = max;
                }
            }
        }
        else {
            x = initial.x;
        }
 
        if (!constrainInfo.horizontal) {
            if (snap && snap.y) {
                if (snap.yFn) {
                    y = snap.y.call(me, info, y);
                }
                else {
                    y = me.doSnap(y, initial.y, snap.y);
                }
            }
 
            if (constrainY) {
                min = constrainY[0];
                max = constrainY[1];
 
                if (min !== null && y < min) {
                    y = min;
                }
 
                if (max !== null && y > max) {
                    y = max;
                }
            }
        }
        else {
            y = initial.y;
        }
 
        return [x, y];
    },
 
    destroy: function() {
        this.setSource(null);
        this.setElement(null);
        this.callParent();
    },
 
    privates: {
        /**
         * Constrains 2 values, while taking into
         * account nulls.
         * @param {Number} a The first value.
         * @param {Number} b The second value.
         * @param {Function} resolver The function to resolve the value if
         * both are non null.
         * @return {Number} If both values are `null`, `null`. If `a` is null, `b`.
         * If `b` is null, `a`, otherwise the result of the resolver, passing a & b.
         *
         * @private
         */
        constrainValue: function(a, b, resolver) {
            var val = null,
                aNull = a === null,
                bNull = b === null;
 
            if (!(aNull && bNull)) {
                if (aNull) {
                    val = b;
                }
                else if (bNull) {
                    val = a;
                }
                else {
                    val = resolver(a, b);
                }
            }
 
            return val;
        },
 
        /**
         * Calculates the position to move the proxy element
         * to when using snapping.
         *
         * @param {Number} position The current mouse position.
         * @param {Number} initial The start position.
         * @param {Number} snap The snap position.
         * @return {Number} The snapped position.
         *
         * @private
         */
        doSnap: function(position, initial, snap) {
            if (!snap) {
                return position;
            }
 
            /* eslint-disable-next-line vars-on-top */
            var ratio = (position - initial) / snap,
                floor = Math.floor(ratio);
 
            // Check whether we need to snap less than current, or
            // greater than current position.
            if (ratio - floor <= 0.5) {
                ratio = floor;
            }
            else {
                ratio = floor + 1;
            }
 
            return initial + (snap * ratio);
        },
 
        /**
         * Setup data that is needed during a drag for monitoring constraints.
         * Attempt to merge min/max values with any constrain region.
         *
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        onDragStart: function(info) {
            var me = this,
                snap = me.getSnap(),
                vertical = !!me.getVertical(),
                horizontal = !!me.getHorizontal(),
                element = me.getElement(),
                region = me.getRegion(),
                proxy = info.proxy,
                proxyEl = proxy.element,
                x = me.getX(),
                y = me.getY(),
                minX = null,
                maxX = null,
                minY = null,
                maxY = null,
                rminX = null,
                rmaxX = null,
                rminY = null,
                rmaxY = null,
                pos, size;
 
            if (element) {
                if (typeof element === 'boolean') {
                    element = me.getSource().getElement().parent();
                }
 
                if (info.local) {
                    pos = element.getStyle('position');
 
                    if (pos === 'relative' || pos === 'absolute') {
                        size = element.getSize();
                        region = new Ext.util.Region(0, size.width, size.height, 0);
                    }
                    else {
                        region = element.getRegion(true, true);
                    }
                }
                else {
                    region = element.getRegion(true);
                }
            }
 
            if (region) {
                if (!vertical) {
                    rminX = region.left;
                    rmaxX = region.right - (proxyEl ? proxy.width : 0);
                }
 
                if (!horizontal) {
                    rminY = region.top;
                    rmaxY = region.bottom - (proxyEl ? proxy.height : 0);
                }
            }
 
            // The following piece sets up the numeric values for our constraint.
            // If there is an axis constraint, don't bother calculating the values since
            // it is already explicitly constrained so we can shortcut that portion.
            //
            // Attempt to merge the appropriate min/max values (if needed). With:
            // a) A region and a minimum, the larger value is needed (stricter constraint)
            // b) A region and a maximum, the smaller value is needed (stricter constraint)
 
            if (!vertical && (region || x)) {
                if (x) {
                    minX = x[0];
                    maxX = x[1];
                }
 
                if (minX !== null || maxX !== null || rminX !== null || rmaxX !== null) {
                    minX = me.constrainValue(minX, rminX, Math.max);
                    maxX = me.constrainValue(maxX, rmaxX, Math.min);
                    x = [minX, maxX];
                }
            }
 
            if (!horizontal && (region || y)) {
                if (y) {
                    minY = y[0];
                    maxY = y[1];
                }
 
                if (minY !== null || maxY !== null || rminY !== null || rmaxY !== null) {
                    minY = me.constrainValue(minY, rminY, Math.max);
                    maxY = me.constrainValue(maxY, rmaxY, Math.min);
                    y = [minY, maxY];
                }
            }
 
            if (snap) {
                snap = {
                    x: snap.x,
                    xFn: typeof snap.x === 'function',
                    y: snap.y,
                    yFn: typeof snap.y === 'function'
                };
            }
 
            me.constrainInfo = {
                initial: info.element.initial,
                vertical: vertical,
                horizontal: horizontal,
                x: x,
                y: y,
                snap: snap
            };
        }
    }
});