/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ImageCropper.js 616 2013-04-22 23:48:38Z geert $
 */

"use strict";

SUI.control.ImageCropper = SUI.defineClass(
	/** @lends SUI.control.ImageCropper.prototype */{

	/** @ignore */ baseClass: SUI.Box,

	/**
	 * @class
	 * SUI.control.ImageCropper is image cropping tool. With this tool you can
	 * select a rectangular area of a an image. You can send this selection
	 * together with the desired image width (and/or height) to a server that
	 * can do the actual image cropping and resizing. The tool allows you to
	 * set a target width or height, in that case you can make a selection that
	 * is not bound to a ratio. Or to set the target width and height height,
	 * in that case the selection will have a fixed ratio.
	 *
	 * @augments SUI.Box
	 *
	 * @description
	 * Create a SUI.control.ImageCropper.
	 *
	 * @constructs
	 * @param see base class
	 * @param {int} arg.outputHeight The desired height of the image to create
	 *     by the server side procedure.
	 * @param {int} arg.outputWidth The desired width of the image to create
	 *     by the server side procedure.
	 * @param {String} arg.image Location of the image to crop.
	 */
	initializer: function(arg) {

		SUI.control.Date.initializeBase(this, arg);

		// set the output/target dimensions for the image
		this._outputHeight = arg.outputHeight || 0;
		this._outputWidth = arg.outputWidth || 0;

		// show the picture behind the cropper div
		this.el().style.backgroundImage = "url("+arg.image+")";

		// the dragger handles might move a little over the edge, display them
		this.el().style.overflow = "visible";

		// create a structure that holds the four rectangles that gray out the
		// are to crop
		this._crop = {n: null, e: null, s: null, w: null};
		// create the rectangle boxes
		for (var i in this._crop) {
			this._crop[i] = new SUI.Box({parent: this});
			this._crop[i].el().appendChild(document.createElement("SPAN"));
			this._crop[i].addClass("sui-icr-crop");
			this._crop[i].el().style.cursor = "crosshair";
		}

		// create the center 'see-through' box
		this._croparea = new SUI.Box({parent: this});
		// the dragger handles might move a little over the edge, display them
		this._croparea.el().style.overflow = "visible";
		this._croparea.el().style.cursor = "move";
		this._croparea.border(new SUI.Border(1));
		this._croparea.addClass("sui-icr-croparea");

		// add the handler for moving this area
		this._addDragHandler(this._croparea);

		// create a structure that holds the eight dragger handles
		this._handle = {
			n: { cursor:"n-resize", top: "-5", left: "0" },
			nw: { cursor:"nw-resize", top: "-5", left: "-5" },
			w: { cursor:"w-resize", top: "0", left: "-5" },
			sw: { cursor:"sw-resize", bottom:"-5", left: "-5" },
			s: { cursor:"s-resize", bottom:"-5", right: "0" },
			se: { cursor:"se-resize", bottom:"-5", right:"-5" },
			e: { cursor:"e-resize", top: "0", right:"-5" },
			ne: { cursor:"ne-resize", top: "-5", right:"-5" }
		};

		// create the handlers
		for (var i in this._handle) {

			// create handles on the crop area
			var h = new SUI.Box({parent: this._croparea });

			// set the size of the handles
			h.el().style.width = this.DRAGGER_SIZE + "px";
			h.el().style.height = this.DRAGGER_SIZE + "px";
			// correct the position with the offset find in the data structure
			if (this._handle[i].top) {
				h.el().style.top = this._handle[i].top + "px";
			}
			if (this._handle[i].right) {
				h.el().style.right = this._handle[i].right + "px";
			}
			if (this._handle[i].left) {
				h.el().style.left = this._handle[i].left + "px";
			}
			if (this._handle[i].bottom) {
				h.el().style.bottom = this._handle[i].bottom + "px";
			}

			// set the style
			h.el().style.cursor = this._handle[i].cursor;
			h.addClass("sui-icr-handle");

			// some content for the div (what browser bug was that?)
			h.el().appendChild(document.createElement("SPAN"));

			// add the handler for moving this handle
			this._addDragHandler(h);

			// overwrite the entry in the data structure with the handle
			this._handle[i] = h;
		}

		// initialize draw the crop area
		this._rescale();
	},

	/**
	 * Size (width/height) of the dragger handlers.
	 */
	DRAGGER_SIZE: 7,

	/**
	 * Minimum size (width or height) of the crop area
	 */
	MIN_SIZE: 16,

	/**
	 * Get the image cropper data. This is all the data that a server side
	 * procedure needs to cut and resize the image as requested.
	 * @param {Object} Object with the following members:
	 * - {int} originalWidth Width of the image as shown in the cropper
	 * - {int} originalHeight Height of the image as shown in the cropper
	 * - {int} cropWidth Width of the selected region
	 * - {int} cropHeight Height of the selected region
	 * - {int} cropTop Top of the selected region
	 * - {int} cropLeft Left of the selected region
	 * - {int} newWidth Desired width of the picture to create
	 * - {int} newHeight Desired height of the picture to create
	 */
	data: function() {

		var ow = this._outputWidth;
		var oh = this._outputHeight;

		// the desired picture has no width of height
		if (ow==0 && oh==0) {
			return null;
		}

		// if the desired width of the picture was not set calculate it
		if (ow == 0) {
			ow = Math.round(
			 this._croparea.width() * oh / this._croparea.height());
		}
		// if the desired height of the picture was not set calculate it
		if (oh == 0) {
			oh = Math.round(
			 this._croparea.height() * ow / this._croparea.width());
		}

		// fill a data structure with all relevant data and return it
		return {
			originalWidth: this.width(),
			originalHeight: this.height(),
			cropWidth: this._croparea.width(),
			cropHeight: this._croparea.height(),
			cropTop: this._croparea.top(),
			cropLeft: this._croparea.left(),
			newWidth: ow,
			newHeight: oh
		};
	},

	/**
	 * Set the desired output size for the image and redraw the control. If
	 * both are set the crop area will be resized using a fixed ratio of the
	 * given width and height. If the width and not the height or vice verse
	 * were given then you can size the crop area in all directions.
	 * @param {int} w The desired target width of the image
	 * @param {int} h The desired height width of the image
	 */
	targetSize: function(w, h) {

	 // set the output width
		this._outputWidth = parseInt(w, 10);
		if (isNaN(this._outputWidth)) {
			this._outputWidth = 0;
		}

	 // set the output height
		this._outputHeight = parseInt(h, 10);
		if (isNaN(this._outputHeight)) {
			this._outputHeight = 0;
		}

		// and reinitialize and draw the control ...
		this._rescale();
		// ... show only the handles that we need
		this._activateHandles();
	},

	// reference to the drag listener function so we can remove it later
	_ehDrag: null,

	// reference to the end drag listener function so we can remove it later
	_ehEndDrag: null,

	// object with references to the four rectangles around the crop area
	_crop: null,

	// start position of the mouse when starting to drag
	_dragStartPos: null,

	// object with references to all of the resize handlers
	_handle: null,

	// height of the output rectangle
	_outputHeight: 0,

	// width of the output rectangle
	_outputWidth: 0,

	// the width/height ratio to retain while dragging
	_ratio: 0,

	// height of the crop area when dragging starts
	_startHeight: 0,

	// left of the crop area when dragging starts
	_startLeft: 0,

	// top of the crop area when dragging starts
	_startTop: 0,

	// width of the crop area when dragging starts
	_startWidth: 0,

	// when we keep the ratio while dragging, we don't need the handles
	// on the side of the crop area: remove them
	_activateHandles: function() {
	 // should we show ...
	 var d = "block";
		if (this._ratio) {
		 // ... or hide the handles
		 d = "none";
		}
		this._handle.n.el().style.display = d;
		this._handle.w.el().style.display = d;
		this._handle.e.el().style.display = d;
		this._handle.s.el().style.display = d;
	},

	_addDragHandler: function(h) {
	 var that = this;
	 // h and that are the two closure variables
		SUI.browser.addEventListener(h.el(), "mousedown",
		 function(e) {
			 if (!that._startDrag(h, new SUI.Event(this, e))) {
			   SUI.browser.noPropagation(e);
			 }
		 }
		);
	},

	// recalculate the size and position of the crop area while dragging one
	// of the handles
	_drag: function(el, ev) {

	 // the current mouse position
		var p = {x: SUI.browser.getX(ev.event), y: SUI.browser.getY(ev.event)};

		if (el == this._croparea) {

		 // if the crop area was selected, move the crop area
		 this._moveCropArea(p);

		} else if (this._ratio) {

		 // we're dragging the corners and should keep that ration fixed
		 if (el == this._handle.se) {
				this._handle_se(p);
			} else if (el == this._handle.ne) {
				this._handle_ne(p);
			} else if (el == this._handle.nw) {
				this._handle_nw(p);
			} else if (el == this._handle.sw) {
				this._handle_sw(p);
			}

		} else {

		 // we can freely size in any direction, no ratio to worry about
		 if (el == this._handle.se) {
				this._handle_s(p);
				this._handle_e(p);
			} else if (el == this._handle.ne) {
				this._handle_n(p);
				this._handle_e(p);
			} else if (el == this._handle.nw) {
				this._handle_n(p);
				this._handle_w(p);
			} else if (el == this._handle.sw) {
				this._handle_s(p);
				this._handle_w(p);
			} else if (el == this._handle.e) {
				this._handle_e(p);
			} else if (el == this._handle.n) {
				this._handle_n(p);
			} else if (el == this._handle.w) {
				this._handle_w(p);
			} else if (el == this._handle.s) {
				this._handle_s(p);
			}

		}

		// redisplay the new crop area
		this._redraw();
	},

	// The difference between the start position and the current position in
	// horizontal direction while dragging.
	_dx: function(p) {
	 return p.x - this._dragStartPos.x;
	},

	// The difference between the start position and the current position in
	// vertical direction while dragging.
	_dy: function(p) {
	 return p.y - this._dragStartPos.y;
	},

	// remove the listeners used for dragging
	_endDrag: function(e) {
		// Unregister the event handlers ...
		SUI.browser.removeEventListener(document, "mousemove", this._ehDrag);
		SUI.browser.removeEventListener(document, "mouseup", this._ehEndDrag);
	},

	// Handle dragging of the three east draggers (no fixed ratio)
	_handle_e: function(p) {

	 // set the new croparea width
		this._croparea.width(this._startWidth + this._dx(p));

		// if we've gone to far, set croparea width to max/min allowed
		if (this._croparea.width() < this.MIN_SIZE) {
			this._croparea.width(this.MIN_SIZE);
		}
		if (this._croparea.left() + this._croparea.width() > this.width()) {
			this._croparea.width(this.width() - this._croparea.left());
		}
	},

	// Handle dragging of the three north draggers (no fixed _ratio)
	_handle_n: function(p) {

	 // get the bottom first, we might need it later on
	 var b = this._croparea.top() + this._croparea.height();

	 // set the new croparea height and top
		this._croparea.height(this._startHeight - this._dy(p));
		this._croparea.top(this._startTop + this._dy(p));

		// if we've gone to far, set croparea height to max/min allowed
		if (this._croparea.height() < this.MIN_SIZE) {
			this._croparea.height(this.MIN_SIZE);
			this._croparea.top(b - this.MIN_SIZE);
		}
		if (this._croparea.top() < 0) {
			this._croparea.height(b);
			this._croparea.top(0);
		}
	},

	// Handle dragging of the north-east corner while keeping a fixed _ratio
	_handle_ne: function(p) {
	 // get the new size ...
	 var s = this._ratioSize(this._dx(p), -this._dy(p));
	 // ... and check if it's within boundaries ...
		if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
		   && this._croparea.left() + s.w <= this.width()
		   && this._croparea.top() + this._croparea.height() - s.h >= 0) {
		 // ... yes: set top, width and height of crop area
			this._croparea.top(
				 this._croparea.top() + this._croparea.height() - s.h);
			this._croparea.width(s.w);
			this._croparea.height(s.h);
		}
	},

	// Handle dragging of the north-west corner while keeping a fixed _ratio
	_handle_nw: function(p) {
	 // get the new size ...
	 var s = this._ratioSize(-this._dx(p), -this._dy(p));
	 // ... and check if it's within boundaries ...
		if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
		   && this._croparea.left() + this._croparea.width() - s.w >= 0
		   && this._croparea.top() + this._croparea.height() - s.h >= 0) {
		 // ... yes: set top, left, width and height of crop area
			this._croparea.left(
				 this._croparea.left() + this._croparea.width() - s.w);
			this._croparea.top(
				 this._croparea.top() + this._croparea.height() - s.h);
			this._croparea.width(s.w);
			this._croparea.height(s.h);
		}
	},

	// Handle dragging of the three south draggers (no fixed _ratio)
	_handle_s: function(p) {

	 // set the new croparea height
		this._croparea.height(this._startHeight + this._dy(p));

		// if we've gone to far, set croparea height to max/min allowed
		if (this._croparea.height() < this.MIN_SIZE) {
			this._croparea.height(this.MIN_SIZE);
		}
		if (this._croparea.top() + this._croparea.height() > this.height()) {
			this._croparea.height(this.height() - this._croparea.top());
		}
	},

	// Handle dragging of the south-east corner while keeping a fixed _ratio
	_handle_se: function(p) {
	 // get the new size ...
	 var s = this._ratioSize(this._dx(p), this._dy(p));
	 // ... and check if it's within boundaries ...
		if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
		   && this._croparea.left() + s.w <= this.width()
		   && this._croparea.top() + s.h <= this.height()) {
		 // ... yes: set width and height of crop area
			this._croparea.width(s.w);
			this._croparea.height(s.h);
		}
	},

	// Handle dragging of the south-west corner while keeping a fixed _ratio
	_handle_sw: function(p) {
	 // get the new size ...
	 var s = this._ratioSize(-this._dx(p), this._dy(p));
	 // ... and check if it's within boundaries ...
		if (s.w >= this.MIN_SIZE && s.h >= this.MIN_SIZE
		   && this._croparea.left() + this._croparea.width() - s.w >= 0
		   && this._croparea.top() + s.h <= this.height()) {
		 // ... yes: set left, width and height of crop area
			this._croparea.left(
			 this._croparea.left() + this._croparea.width() - s.w);
			this._croparea.width(s.w);
			this._croparea.height(s.h);
		}
	},

	// Handle dragging of the three west draggers (no fixed _ratio)
	_handle_w: function(p) {

	 // get the right first, we might need it later on
		var r = this._croparea.left() + this._croparea.width();

	 // set the new _croparea width and left
		this._croparea.width(this._startWidth - this._dx(p));
		this._croparea.left(this._startLeft + this._dx(p));

		// if we've gone to far, set croparea width to max/min allowed
		if (this._croparea.width() < this.MIN_SIZE) {
			this._croparea.width(this.MIN_SIZE);
			this._croparea.left(r - this.MIN_SIZE);
		}
		if (this._croparea.left() < 0) {
			this._croparea.width(r);
			this._croparea.left(0);
		}
	},

	// move the crop area
	_moveCropArea: function(p) {

	 // set the new top and left
		this._croparea.top(this._startTop + this._dy(p));
		this._croparea.left(this._startLeft + this._dx(p));

		// correct it if we've moved too far
		if (this._croparea.top() < 0) {
			this._croparea.top(0);
		}
		if (this._croparea.left() < 0) {
			this._croparea.left(0);
		}
		if (this._croparea.left()+this._croparea.width() > this.width()) {
			this._croparea.left(this.width()-this._croparea.width());
		}
		if (this._croparea.top()+this._croparea.height() > this.height()) {
			this._croparea.top(this.height()-this._croparea.height());
		}
	},

	// Get the (proposed) new size when resizing with a fixed _ratio.
	_ratioSize: function(dx, dy) {
	 // get the new width an height by adding the delta ...
	 var r = {
			w: this._startWidth + dx,
			h: this._startHeight + dy
	 };
	 // .. and correct the new width and height for the given _ratio
		if (r.w/r.h > this._ratio) {
		 r.h = Math.round(r.w / this._ratio);
		} else {
		 r.w = Math.round(r.h * this._ratio);
		}
		// return the result
	 return r;
	},

	// Redraw the area: set the size of the inner rectangle and the rectangles
	// that darken outer area, set the center positions of the north, south,
	// west and east dragger handles.
	_redraw: function() {

	 // set the CSS size of rectangle on the west side of the croparea
		this._crop.w.left(0);
		this._crop.w.top(this._croparea.top());
		this._crop.w.width(this._croparea.left());
		this._crop.w.height(this._croparea.height());

	 // set the CSS size of rectangle on the east side of the croparea
		this._crop.e.top(this._croparea.top());
		this._crop.e.left(this._croparea.width() + this._croparea.left());
		this._crop.e.width(this.width() - this._croparea.width()
			- this._croparea.left());
		this._crop.e.height(this._croparea.height());

	 // set the CSS size of rectangle on the north side of the croparea
		this._crop.n.top(0);
		this._crop.n.width(this.width());
		this._crop.n.height(this._croparea.top());

	 // set the CSS size of rectangle on the south side of the croparea
		this._crop.s.top(this._croparea.height() + this._croparea.top());
		this._crop.s.width(this.width());
		this._crop.s.height(this.height() - this._croparea.height()
			- this._croparea.top());

	 // set the CSS size of the rectangles
		this._croparea.setDim();
		this._crop.w.setDim();
		this._crop.e.setDim();
		this._crop.n.setDim();
		this._crop.s.setDim();

	 // set the dragger handles on the sides to the center of the sides
		// of the croparea
		this._handle.n.el().style.left =
			Math.round(this._croparea.width()/2) - 5 + "px";
		this._handle.s.el().style.left =
			Math.round(this._croparea.width()/2) - 5 + "px";
		this._handle.e.el().style.top =
			Math.round(this._croparea.height()/2) - 5 + "px";
		this._handle.w.el().style.top =
			Math.round(this._croparea.height()/2) - 5 + "px";
	},

	// the output width and height are changed, reinitialize draw the crop area
	_rescale: function() {

		// is desired output width and height larger than 0 ? ...
		if (this._outputWidth > 0 && this._outputHeight > 0) {

		 // ... we use a fixed _ratio while resizing, calculate the _ratio
			this._ratio = this._outputWidth/this._outputHeight;

			// create a rectangle with the required _ratio of .9 times the
			// height or width of the area, whichever direction fits.
			var w, h;
			if (this.width() / this.height() < this._ratio) {
				w = Math.round(this.width() * .9);
				h = Math.round(w / this._ratio);
			} else {
				h = Math.round(this.height() * .9);
				w = Math.round(h * this._ratio);
			}

			// set the croparea size ...
			this._croparea.width(w);
			this._croparea.height(h);
			// ... and position
			this._croparea.left(Math.round(this.width()-w-4)/2);;
			this._croparea.top(Math.round(this.height()-h-4)/2);

		} else {

			// ... no: then we don't use a _ratio: all sides and corners can
		 // be scaled freely ...
			this._ratio = 0;

			// set the croparea to a size somewhat smaller than the picture
			// size
			this._croparea.width(Math.round(this.width()*.9));
			this._croparea.height(Math.round(this.height()*.9));
			this._croparea.left(Math.round(this.width()*.05));;
			this._croparea.top(Math.round(this.height()*.05));

		}

		// redisplay the new crop area
		this._redraw();
	},

	// start the dragging of a handle or the crop area itself. remember
	// the start position/size of the croparea and mouse click and add the
	// mousemove and mouseup event listeners
	_startDrag: function(el, ev) {

	 // store the start postion and size of the crop area
		this._startWidth = this._croparea.width();
		this._startHeight = this._croparea.height();
		this._startTop = this._croparea.top();
		this._startLeft = this._croparea.left();

		// start the position of the mouse click
		this._dragStartPos = {
			x: SUI.browser.getX(ev.event),
			y: SUI.browser.getY(ev.event)
		};

		// add the event listeners for dragging and stop dragging
		var that = this;
		SUI.browser.addEventListener(document, "mousemove",
		 this._ehDrag = function(e) {
			 if (!that._drag(el, new SUI.Event(this, e))) {
			   SUI.browser.noPropagation(e);
			 }
		 }
		);
		SUI.browser.addEventListener(document, "mouseup",
		 this._ehEndDrag = function(e) {
			 if (!that._endDrag(new SUI.Event(this, e))) {
			   SUI.browser.noPropagation(e);
			 }
		 }
		);
	}

});