var Lang = Y.Lang,
    isBoolean = Lang.isBoolean,
    isNumber = Lang.isNumber,
    isString = Lang.isString,
    capitalize = Y.Resize.capitalize,

    isNode = function(v) {
        return (v instanceof Y.Node);
    },

    toNumber = function(num) {
        return parseFloat(num) || 0;
    },

    BORDER_BOTTOM_WIDTH = 'borderBottomWidth',
    BORDER_LEFT_WIDTH = 'borderLeftWidth',
    BORDER_RIGHT_WIDTH = 'borderRightWidth',
    BORDER_TOP_WIDTH = 'borderTopWidth',
    BORDER = 'border',
    BOTTOM = 'bottom',
    CON = 'con',
    CONSTRAIN = 'constrain',
    HOST = 'host',
    LEFT = 'left',
    MAX_HEIGHT = 'maxHeight',
    MAX_WIDTH = 'maxWidth',
    MIN_HEIGHT = 'minHeight',
    MIN_WIDTH = 'minWidth',
    NODE = 'node',
    OFFSET_HEIGHT = 'offsetHeight',
    OFFSET_WIDTH = 'offsetWidth',
    PRESEVE_RATIO = 'preserveRatio',
    REGION = 'region',
    RESIZE_CONTRAINED = 'resizeConstrained',
    RIGHT = 'right',
    TICK_X = 'tickX',
    TICK_Y = 'tickY',
    TOP = 'top',
    WIDTH = 'width',
    VIEW = 'view',
    VIEWPORT_REGION = 'viewportRegion';

/**
A Resize plugin that will attempt to constrain the resize node to the boundaries.
@module resize
@submodule resize-contrain
@class ResizeConstrained
@param config {Object} Object literal specifying widget configuration properties.
@constructor
@extends Plugin.Base
@namespace Plugin
*/

function ResizeConstrained() {
    ResizeConstrained.superclass.constructor.apply(this, arguments);
}

Y.mix(ResizeConstrained, {
    NAME: RESIZE_CONTRAINED,

    NS: CON,

    ATTRS: {
        /**
        * Will attempt to constrain the resize node to the boundaries. Arguments:<br>
        * 'view': Contrain to Viewport<br>
        * '#selector_string': Constrain to this node<br>
        * '{Region Object}': An Object Literal containing a valid region (top, right, bottom, left) of page positions
        *
        * @attribute constrain
        * @type {String|Object|Node}
        */
        constrain: {
            setter: function(v) {
                if (v && (isNode(v) || isString(v) || v.nodeType)) {
                    v = Y.one(v);
                }

                return v;
            }
        },

        /**
         * The minimum height of the element
         *
         * @attribute minHeight
         * @default 15
         * @type Number
         */
        minHeight: {
            value: 15,
            validator: isNumber
        },

        /**
         * The minimum width of the element
         *
         * @attribute minWidth
         * @default 15
         * @type Number
         */
        minWidth: {
            value: 15,
            validator: isNumber
        },

        /**
         * The maximum height of the element
         *
         * @attribute maxHeight
         * @default Infinity
         * @type Number
         */
        maxHeight: {
            value: Infinity,
            validator: isNumber
        },

        /**
         * The maximum width of the element
         *
         * @attribute maxWidth
         * @default Infinity
         * @type Number
         */
        maxWidth: {
            value: Infinity,
            validator: isNumber
        },

        /**
         * Maintain the element's ratio when resizing.
         *
         * @attribute preserveRatio
         * @default false
         * @type boolean
         */
        preserveRatio: {
            value: false,
            validator: isBoolean
        },

        /**
         * The number of x ticks to span the resize to.
         *
         * @attribute tickX
         * @default false
         * @type Number | false
         */
        tickX: {
            value: false
        },

        /**
         * The number of y ticks to span the resize to.
         *
         * @attribute tickY
         * @default false
         * @type Number | false
         */
        tickY: {
            value: false
        }
    }
});

Y.extend(ResizeConstrained, Y.Plugin.Base, {
    /**
     * Stores the <code>constrain</code>
     * surrounding information retrieved from
     * <a href="Resize.html#method__getBoxSurroundingInfo">_getBoxSurroundingInfo</a>.
     *
     * @property constrainSurrounding
     * @type Object
     * @default null
     */
    constrainSurrounding: null,

    initializer: function() {
        var instance = this,
            host = instance.get(HOST);

        host.delegate.dd.plug(
            Y.Plugin.DDConstrained,
            {
                tickX: instance.get(TICK_X),
                tickY: instance.get(TICK_Y)
            }
        );

        host.after('resize:align', Y.bind(instance._handleResizeAlignEvent, instance));
        host.on('resize:start', Y.bind(instance._handleResizeStartEvent, instance));
    },

    /**
     * Helper method to update the current values on
     * <a href="Resize.html#property_info">info</a> to respect the
     * constrain node.
     *
     * @method _checkConstrain
     * @param {String} axis 'top' or 'left'
     * @param {String} axisConstrain 'bottom' or 'right'
     * @param {String} offset 'offsetHeight' or 'offsetWidth'
     * @protected
     */
    _checkConstrain: function(axis, axisConstrain, offset) {
        var instance = this,
            point1,
            point1Constrain,
            point2,
            point2Constrain,
            host = instance.get(HOST),
            info = host.info,
            constrainBorders = instance.constrainSurrounding.border,
            region = instance._getConstrainRegion();

        if (region) {
            point1 = info[axis] + info[offset];
            point1Constrain = region[axisConstrain] - toNumber(constrainBorders[capitalize(BORDER, axisConstrain, WIDTH)]);

            if (point1 >= point1Constrain) {
                info[offset] -= (point1 - point1Constrain);
            }

            point2 = info[axis];
            point2Constrain = region[axis] + toNumber(constrainBorders[capitalize(BORDER, axis, WIDTH)]);

            if (point2 <= point2Constrain) {
                info[axis] += (point2Constrain - point2);
                info[offset] -= (point2Constrain - point2);
            }
        }
    },

    /**
     * Update the current values on <a href="Resize.html#property_info">info</a>
     * to respect the maxHeight and minHeight.
     *
     * @method _checkHeight
     * @protected
     */
    _checkHeight: function() {
        var instance = this,
            host = instance.get(HOST),
            info = host.info,
            maxHeight = (instance.get(MAX_HEIGHT) + host.totalVSurrounding),
            minHeight = (instance.get(MIN_HEIGHT) + host.totalVSurrounding);

        instance._checkConstrain(TOP, BOTTOM, OFFSET_HEIGHT);

        if (info.offsetHeight > maxHeight) {
            host._checkSize(OFFSET_HEIGHT, maxHeight);
        }

        if (info.offsetHeight < minHeight) {
            host._checkSize(OFFSET_HEIGHT, minHeight);
        }
    },

    /**
     * Update the current values on <a href="Resize.html#property_info">info</a>
     * calculating the correct ratio for the other values.
     *
     * @method _checkRatio
     * @protected
     */
    _checkRatio: function() {
        var instance = this,
            host = instance.get(HOST),
            info = host.info,
            originalInfo = host.originalInfo,
            oWidth = originalInfo.offsetWidth,
            oHeight = originalInfo.offsetHeight,
            oTop = originalInfo.top,
            oLeft = originalInfo.left,
            oBottom = originalInfo.bottom,
            oRight = originalInfo.right,
            // wRatio/hRatio functions keep the ratio information always synced with the current info information
            // RETURN: percentage how much width/height has changed from the original width/height
            wRatio = function() {
                return (info.offsetWidth/oWidth);
            },
            hRatio = function() {
                return (info.offsetHeight/oHeight);
            },
            isClosestToHeight = host.changeHeightHandles,
            bottomDiff,
            constrainBorders,
            constrainRegion,
            leftDiff,
            rightDiff,
            topDiff;

        // check whether the resizable node is closest to height or not
        if (instance.get(CONSTRAIN) && host.changeHeightHandles && host.changeWidthHandles) {
            constrainRegion = instance._getConstrainRegion();
            constrainBorders = instance.constrainSurrounding.border;
            bottomDiff = (constrainRegion.bottom - toNumber(constrainBorders[BORDER_BOTTOM_WIDTH])) - oBottom;
            leftDiff = oLeft - (constrainRegion.left + toNumber(constrainBorders[BORDER_LEFT_WIDTH]));
            rightDiff = (constrainRegion.right - toNumber(constrainBorders[BORDER_RIGHT_WIDTH])) - oRight;
            topDiff = oTop - (constrainRegion.top + toNumber(constrainBorders[BORDER_TOP_WIDTH]));

            if (host.changeLeftHandles && host.changeTopHandles) {
                isClosestToHeight = (topDiff < leftDiff);
            }
            else if (host.changeLeftHandles) {
                isClosestToHeight = (bottomDiff < leftDiff);
            }
            else if (host.changeTopHandles) {
                isClosestToHeight = (topDiff < rightDiff);
            }
            else {
                isClosestToHeight = (bottomDiff < rightDiff);
            }
        }

        // when the height of the resizable element touch the border of the constrain first
        // force the offsetWidth to be calculated based on the height ratio
        if (isClosestToHeight) {
            info.offsetWidth = oWidth*hRatio();
            instance._checkWidth();
            info.offsetHeight = oHeight*wRatio();
        }
        else {
            info.offsetHeight = oHeight*wRatio();
            instance._checkHeight();
            info.offsetWidth = oWidth*hRatio();
        }

        // fixing the top on handles which are able to change top
        // the idea here is change the top based on how much the height has changed instead of follow the dy
        if (host.changeTopHandles) {
            info.top = oTop + (oHeight - info.offsetHeight);
        }

        // fixing the left on handles which are able to change left
        // the idea here is change the left based on how much the width has changed instead of follow the dx
        if (host.changeLeftHandles) {
            info.left = oLeft + (oWidth - info.offsetWidth);
        }

        // rounding values to avoid pixel jumpings
        Y.each(info, function(value, key) {
            if (isNumber(value)) {
                info[key] = Math.round(value);
            }
        });
    },

    /**
     * Check whether the resizable node is inside the constrain region.
     *
     * @method _checkRegion
     * @protected
     * @return {boolean}
     */
    _checkRegion: function() {
        var instance = this,
            host = instance.get(HOST),
            region = instance._getConstrainRegion();

        return Y.DOM.inRegion(null, region, true, host.info);
    },

    /**
     * Update the current values on <a href="Resize.html#property_info">info</a>
     * to respect the maxWidth and minWidth.
     *
     * @method _checkWidth
     * @protected
     */
    _checkWidth: function() {
        var instance = this,
            host = instance.get(HOST),
            info = host.info,
            maxWidth = (instance.get(MAX_WIDTH) + host.totalHSurrounding),
            minWidth = (instance.get(MIN_WIDTH) + host.totalHSurrounding);

        instance._checkConstrain(LEFT, RIGHT, OFFSET_WIDTH);

        if (info.offsetWidth < minWidth) {
            host._checkSize(OFFSET_WIDTH, minWidth);
        }

        if (info.offsetWidth > maxWidth) {
            host._checkSize(OFFSET_WIDTH, maxWidth);
        }
    },

    /**
     * Get the constrain region based on the <code>constrain</code>
     * attribute.
     *
     * @method _getConstrainRegion
     * @protected
     * @return {Object Region}
     */
    _getConstrainRegion: function() {
        var instance = this,
            host = instance.get(HOST),
            node = host.get(NODE),
            constrain = instance.get(CONSTRAIN),
            region = null;

        if (constrain) {
            if (constrain === VIEW) {
                region = node.get(VIEWPORT_REGION);
            }
            else if (isNode(constrain)) {
                region = constrain.get(REGION);
            }
            else {
                region = constrain;
            }
        }

        return region;
    },

    _handleResizeAlignEvent: function() {
        var instance = this,
            host = instance.get(HOST);

        // check the max/min height and locking top when these values are reach
        instance._checkHeight();

        // check the max/min width and locking left when these values are reach
        instance._checkWidth();

        // calculating the ratio, for proportionally resizing
        if (instance.get(PRESEVE_RATIO)) {
            instance._checkRatio();
        }

        if (instance.get(CONSTRAIN) && !instance._checkRegion()) {
            host.info = host.lastInfo;
        }
    },

    _handleResizeStartEvent: function() {
        var instance = this,
            constrain = instance.get(CONSTRAIN),
            host = instance.get(HOST);

        instance.constrainSurrounding = host._getBoxSurroundingInfo(constrain);
    }
});

Y.namespace('Plugin');
Y.Plugin.ResizeConstrained = ResizeConstrained;
