﻿// Constructor function for the Cropper class.
function Cropper() {
}

// Fields and methods for the Cropper class.
Cropper.prototype = {

    // Configuration fields

    grayLayerColor: '#b4e6ff',
    grayLayerOpacity: 0.7,

    moveCursor: 'move',

    resizeHandleWidth: 8,
    resizeHandleHeight: 8,
    resizeHandleColor: '#FA9E18',
    resizeHandleImage: '../images/CropGrabHandleCircle.gif',

    resizeHandleNWCursor: 'nw-resize',
    resizeHandleNECursor: 'ne-resize',
    resizeHandleSWCursor: 'nw-resize',
    resizeHandleSECursor: 'ne-resize',
    
    cropStarted: false,
    borderWidth: 1,
    
    baseObject: null,

    // Public methods

    startCropping: function(imageID, initialWidth, initialHeight, proportionWidth, proportionHeight, minWidth, minHeight, onUpdate)
    {
        this.image = $get(imageID);

        var initialLeft = cropLeft = Math.round((this.image.offsetWidth - initialWidth) / 2);
        var initialTop = Math.round((this.image.offsetHeight - initialHeight) / 2);
        
        this.startCroppingEx(imageID, initialLeft, initialTop, initialWidth, initialHeight, proportionWidth, proportionHeight, minWidth, minHeight, onUpdate);
    },

    startCroppingEx: function(imageID, initialLeft, initialTop, initialWidth, initialHeight, proportionWidth, proportionHeight, minWidth, minHeight, onUpdate)
    {
        if (this.cropStarted)
        {
            this.stopCropping();
        }
    
        this.image = $get(imageID);

        this.cropLeft = initialLeft;
        this.cropTop = initialTop;
        this.cropWidth = initialWidth;
        this.cropHeight = initialHeight;
        this.proportionWidth = proportionWidth;
        this.proportionHeight = proportionHeight;
        this.minWidth = minWidth;
        this.minHeight = minHeight;
        this.onUpdate = onUpdate;

        this.addLayersContainer();
        this.addGrayLayer();
        this.addCropLayer();
        this.addResizeHandles();
        this.addMouseDownHandlers();
        
        this.cropStarted = true;

        this.raiseUpdate();
    },

    stopCropping: function() {
        if (this.grayLayer != null)
        {
            this.grayLayer.parentNode.removeChild(this.grayLayer);
            this.grayLayer = null;
        }
        
        if (this.cropLayer != null)
        {
            this.cropLayer.parentNode.removeChild(this.cropLayer);
            this.cropLayer = null;
        }
        
        if (this.layersContainer != null)
        {
            this.layersContainer.parentNode.removeChild(this.layersContainer);
            this.layersContainer = null;
        }
        
        this.cropStarted = false;
    },

    getCropInfo: function() {
        return {
            left: this.cropLeft,
            top: this.cropTop,
            width: this.cropWidth,
            height: this.cropHeight
        };
    },

    // Private fields

    image: null,

    cropLeft: null,
    cropTop: null,
    cropWidth: null,
    cropHeight: null,

    proportionWidth: null,
    proportionHeight: null,

    minWidth: null,
    minHeight: null,

    onUpdate: null,

    layersContainer: null,
    grayLayer: null,
    cropLayer: null,

    resizeHandleNW: null,
    resizeHandleNE: null,
    resizeHandleSW: null,
    resizeHandleSE: null,

    resizingHandle: null,

    oldGrayLayerCursor: null,
    oldCropLayerCursor: null,

    lastMousePosition: null,

    // Private methods

    addLayersContainer: function() {
        this.layersContainer = document.createElement('div');
        this.layersContainer.style.position = 'absolute';
        this.image.parentNode.insertBefore(this.layersContainer, this.image);
    },

    addGrayLayer: function() {
        this.grayLayer = document.createElement('div');
        this.grayLayer.style.position = 'absolute';
        this.grayLayer.style.width = this.image.offsetWidth + 'px';
        this.grayLayer.style.height = this.image.offsetHeight + 'px';
        this.grayLayer.style.backgroundColor = this.grayLayerColor;
        this.grayLayer.style.opacity = this.grayLayerOpacity;
        this.grayLayer.style.filter = 'alpha(opacity=' + (this.grayLayerOpacity * 100)  + ')';
        this.layersContainer.appendChild(this.grayLayer);
    },

    addCropLayer: function() {
        this.cropLayer = document.createElement('div');
        this.cropLayer.style.position = 'absolute';
        this.cropLayer.style.left = (this.cropLeft - this.borderWidth) + 'px';
        this.cropLayer.style.top = (this.cropTop - this.borderWidth) + 'px';
        this.cropLayer.style.width = this.cropWidth + 'px';
        this.cropLayer.style.height = this.cropHeight + 'px';
        this.cropLayer.style.border = this.borderWidth + 'px dashed white';
        this.cropLayer.style.backgroundImage = 'url(' + this.image.src + ')';
        this.cropLayer.style.backgroundPosition = String.format('-{0}px -{1}px', this.cropLeft, this.cropTop);
        this.cropLayer.style.cursor = this.moveCursor;
        this.layersContainer.appendChild(this.cropLayer);
    },

    addResizeHandles: function() {
        this.resizeHandleNW = this.createResizeHandle(this.resizeHandleNWCursor);
        this.resizeHandleNE = this.createResizeHandle(this.resizeHandleNECursor);
        this.resizeHandleSW = this.createResizeHandle(this.resizeHandleSECursor);
        this.resizeHandleSE = this.createResizeHandle(this.resizeHandleSWCursor);
        this.setResizeHandleLocations();
    },

    createResizeHandle: function(cursor) {
        var handle = document.createElement('div');
        handle.style.position = 'absolute';
        handle.style.width = this.resizeHandleWidth + 'px';
        handle.style.height = this.resizeHandleHeight + 'px';
        handle.style.backgroundColor = ''; //this.resizeHandleColor;
        handle.style.background = 'url(' + this.resizeHandleImage +')';
        handle.style.cursor = cursor;
        handle.style.overflow = 'hidden';
        this.cropLayer.appendChild(handle);
        return handle;
    },

    setResizeHandleLocations: function() {
        var halfWidth = this.resizeHandleWidth / 2;
        var halfHeight = this.resizeHandleHeight / 2;
        this.setElementLocation(this.resizeHandleNW, -this.resizeHandleWidth + halfWidth, -this.resizeHandleHeight + halfHeight);
        this.setElementLocation(this.resizeHandleNE, this.cropWidth - halfWidth, -this.resizeHandleHeight + halfHeight);
        this.setElementLocation(this.resizeHandleSW, -this.resizeHandleWidth + halfWidth, this.cropHeight - halfHeight);
        this.setElementLocation(this.resizeHandleSE, this.cropWidth - halfWidth, this.cropHeight - halfHeight);
    },

    setElementLocation: function(element, left, top) {
        element.style.left = left + 'px';
        element.style.top = top + 'px';
    },

    addMouseDownHandlers: function() {
        this.startMovingHandler = Function.createDelegate(this, this.startMoving);
        $addHandler(this.cropLayer, 'mousedown', this.startMovingHandler);

        this.startResizingNWHandler = Function.createDelegate(this, this.startResizingNW);
        $addHandler(this.resizeHandleNW, 'mousedown', this.startResizingNWHandler);

        this.startResizingNEHandler = Function.createDelegate(this, this.startResizingNE);
        $addHandler(this.resizeHandleNE, 'mousedown', this.startResizingNEHandler);

        this.startResizingSWHandler = Function.createDelegate(this, this.startResizingSW);
        $addHandler(this.resizeHandleSW, 'mousedown', this.startResizingSWHandler);

        this.startResizingSEHandler = Function.createDelegate(this, this.startResizingSE);
        $addHandler(this.resizeHandleSE, 'mousedown', this.startResizingSEHandler);
    },

    startMoving: function(e) {
        e.preventDefault();

        this.removeMoveHandlers();

        this.lastMousePosition = this.getMousePositionFromRawEvent(e.rawEvent);

        this.moveCropHandler = Function.createDelegate(this, this.moveCrop);
        $addHandler(document, 'mousemove', this.moveCropHandler);

        this.stopMovingHandler = Function.createDelegate(this, this.stopMoving);
        $addHandler(document, 'mouseup', this.stopMovingHandler);

        this.isMoving = true;
    },

    moveCrop: function(e) {
        e.preventDefault();

        var currentMousePosition = this.getMousePositionFromRawEvent(e.rawEvent);

        var deltaX = currentMousePosition.x - this.lastMousePosition.x;
        var deltaY = currentMousePosition.y - this.lastMousePosition.y;

        this.cropLeft += deltaX;
        this.cropTop += deltaY;

        if (this.cropLeft < 0) {
            this.cropLeft = 0;
        }

        if ((this.cropLeft + this.cropWidth) > this.image.offsetWidth) {
            this.cropLeft = this.image.offsetWidth - this.cropWidth;
        }

        if (this.cropTop < 0) {
            this.cropTop = 0;
        }

        if ((this.cropTop + this.cropHeight) > this.image.offsetHeight) {
            this.cropTop = this.image.offsetHeight - this.cropHeight;
        }

        this.cropLayer.style.left = this.cropLeft + 'px';
        this.cropLayer.style.top = this.cropTop + 'px';

        this.setBackgroundPosition();

        this.raiseUpdate();

        this.lastMousePosition = currentMousePosition;
    },

    setBackgroundPosition: function() {
        this.cropLayer.style.backgroundPosition = String.format('-{0}px -{1}px', this.cropLeft, this.cropTop);
    },

    stopMoving: function(e) {
        e.preventDefault();
        this.removeMoveHandlers();
    },

    removeMoveHandlers: function() {
        if (this.isMoving) {
            $removeHandler(document, 'mousemove', this.moveCropHandler);
            $removeHandler(document, 'mouseup', this.stopMovingHandler);
            this.isMoving = false;
        }
    },

    startResizingNW: function(e) {
        this.startResizing(e, this.resizeHandleNW);
    },

    startResizingNE: function(e) {
        this.startResizing(e, this.resizeHandleNE);
    },

    startResizingSW: function(e) {
        this.startResizing(e, this.resizeHandleSW);
    },

    startResizingSE: function(e) {
        this.startResizing(e, this.resizeHandleSE);
    },

    startResizing: function(e, resizeHandle) {
        e.preventDefault();

        // Don't let the mousedown event bubble up to cropLayer or it
        // will startMoving will get invoked.
        e.stopPropagation();

        this.removeResizeHandlers();

        this.resizingHandle = resizeHandle;

        this.oldGrayLayerCursor = this.grayLayer.style.cursor;
        this.grayLayer.style.cursor = resizeHandle.style.cursor;

        this.oldCropLayerCursor = this.cropLayer.style.cursor;
        this.cropLayer.style.cursor = resizeHandle.style.cursor;

        this.resizeCropHandler = Function.createDelegate(this, this.resizeCrop);
        $addHandler(document, 'mousemove', this.resizeCropHandler);

        this.stopResizingHandler = Function.createDelegate(this, this.stopResizing);
        $addHandler(document, 'mouseup', this.stopResizingHandler);

        this.isResizing = true;
    },

    resizeCrop: function(e) {
        e.preventDefault();

        var cropPosition = Sys.UI.DomElement.getBounds(this.cropLayer);
        var mousePosition = this.getMousePositionFromRawEvent(e.rawEvent);

        var potentialWidth = 0;
        var potentialHeight = 0;

        // Calculate some the potential size of the crop based on the
        // current mouse position and the handle being used to resize.
        if (this.resizingHandle == this.resizeHandleNW) {
            potentialWidth = cropPosition.x - mousePosition.x + this.cropWidth;
            potentialHeight = cropPosition.y - mousePosition.y + this.cropHeight;
        } else if (this.resizingHandle == this.resizeHandleNE) {
            potentialWidth = mousePosition.x - cropPosition.x;
            potentialHeight = cropPosition.y - mousePosition.y + this.cropHeight;
        } else if (this.resizingHandle == this.resizeHandleSW) {
            potentialWidth = cropPosition.x - mousePosition.x + this.cropWidth;
            potentialHeight = mousePosition.y - cropPosition.y;
        } else {
            potentialWidth = mousePosition.x - cropPosition.x;
            potentialHeight = mousePosition.y - cropPosition.y;
        }

        var newWidth = potentialWidth;
        var newHeight = potentialHeight;

        // If necessary, update the width or height to conform to the
        // required aspect ratio.
        if (this.proportionWidth && this.proportionHeight) {
            if ((potentialWidth / potentialHeight) >= (this.proportionWidth / this.proportionHeight)) {
                newHeight = Math.round(potentialWidth * (this.proportionHeight / this.proportionWidth));
            } else {
                newWidth = Math.round(potentialHeight * (this.proportionWidth / this.proportionHeight));
            }
        }

        var newLeft = this.cropLeft;
        var newTop = this.cropTop;

        // Update left and top, if necessary.
        if (this.resizingHandle == this.resizeHandleNW) {
            newLeft = this.cropLeft + this.cropWidth - newWidth;
            newTop = this.cropTop + this.cropHeight - newHeight;
        } else if (this.resizingHandle == this.resizeHandleNE) {
            newTop = this.cropTop + this.cropHeight - newHeight;
        } else if (this.resizingHandle == this.resizeHandleSW) {
            newLeft = this.cropLeft + this.cropWidth - newWidth;
        } else {
            // Nothing to do for south-east handle.
        }

        if (false) {
            Sys.Debug.trace(String.format('cropLeft: {0}, cropTop: {1}', this.cropLeft, this.cropTop));
            Sys.Debug.trace(String.format('cropWidth: {0}, cropHeight: {1}', this.cropWidth, this.cropHeight));
            Sys.Debug.trace(String.format('deltaX: {0}, deltaY: {1}', deltaX, deltaY));
            Sys.Debug.trace(String.format('potentialWidth: {0}, potentialHeight: {1}', potentialWidth, potentialHeight));
            Sys.Debug.trace(String.format('newWidth: {0}, newHeight: {1}', newWidth, newHeight));
            Sys.Debug.trace(String.format('newLeft: {0}, newTop: {1}', newLeft, newTop));
            Sys.Debug.trace('');
        }

        var allowMinResize = ((this.cropWidth < this.minWidth) && (newWidth > this.cropWidth)) || 
                             ((this.cropHeight < this.minHeight) && (newHeight > this.cropHeight));

        // If the new bounds are valid, update the crop.
        if ((newLeft >= 0) &&
            ((newWidth >= this.minWidth) || allowMinResize) &&
            ((newLeft + newWidth) <= this.image.offsetWidth) &&
            (newTop >= 0) &&
            ((newHeight >= this.minHeight) || allowMinResize) &&
            ((newTop + newHeight) <= this.image.offsetHeight)) {

            this.cropLeft = newLeft;
            this.cropTop = newTop;
            this.cropWidth = newWidth;
            this.cropHeight = newHeight;

            this.cropLayer.style.left = this.cropLeft + 'px';
            this.cropLayer.style.top = this.cropTop + 'px';
            this.cropLayer.style.width = this.cropWidth + 'px';
            this.cropLayer.style.height = this.cropHeight + 'px';

            // Set the position of the background image to match the left
            // and top of the crop
            this.setBackgroundPosition();

            // Update the resize handle positions.
            this.setResizeHandleLocations();

            // This is a workaround for a weird rendering bug in FF that
            // only appears when the width or height changes, but left
            // and top don't. Toggling left like this shouldn't be visible
            // to the user, but doing it forces the browser to re-draw
            // cropLayer correctly.
            this.cropLayer.style.left = (this.cropLayer.offsetLeft + 1) + 'px';
            this.cropLayer.style.left = (this.cropLayer.offsetLeft - 1) + 'px';

            this.raiseUpdate();
        }
    },

    stopResizing: function(e) {
        e.preventDefault();
        this.removeResizeHandlers();
    },

    removeResizeHandlers: function() {
        if (this.isResizing) {
            this.grayLayer.style.cursor = this.oldGrayLayerCursor;
            this.cropLayer.style.cursor = this.oldCropLayerCursor;

            $removeHandler(document, 'mousemove', this.resizeCropHandler);
            $removeHandler(document, 'mouseup', this.stopResizingHandler);

            this.isResizing = false;
        }
    },

    raiseUpdate: function() {
        if (this.onUpdate) {
            this.onUpdate(this.getCropInfo(), this.baseObject);
        }
    },

    getMousePositionFromRawEvent: function(e) {
        var x = 0;
        var y = 0;
        if (e.pageX || e.pageY) {
            x = e.pageX;
            y = e.pageY;
        } else if (e.clientX || e.clientY) {
            x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
            y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        }
        return { x: x, y: y };
    }

};
