/*! http://keith-wood.name/signature.html
	Signature plugin for jQuery UI v1.2.1.
	Requires excanvas.js in IE.
	Written by Keith Wood (wood.keith{at}optusnet.com.au) April 2012.
	Available under the MIT (http://keith-wood.name/licence.html) license. 
	Please attribute the author if you use it. */

/* globals G_vmlCanvasManager */

(function($) { // Hide scope, no $ conflict
	'use strict';

	/** Signature capture and display.
		<p>Depends on <code>jquery.ui.widget</code>, <code>jquery.ui.mouse</code>.</p>
		<p>Expects HTML like:</p>
		<pre>&lt;div>&lt;/div></pre>
		@namespace Signature
		@augments $.Widget
		@example $(selector).signature()
$(selector).signature({color: 'blue', guideline: true}) */
	var signatureOverrides = {

		/** Be notified when a signature changes.
			@callback SignatureChange
			@global
			@this Signature
			@example change: function() {
  console.log('Signature changed');
} */

		/** Global defaults for signature.
			@memberof Signature
			@property {number} [distance=0] The minimum distance to start a drag.
			@property {string} [background='#fff'] The background colour.
			@property {string} [color='#000'] The colour of the signature.
			@property {number} [thickness=2] The thickness of the lines.
			@property {boolean} [guideline=false] <code>true</code> to add a guideline.
			@property {string} [guidelineColor='#a0a0a0'] The guideline colour.
			@property {number} [guidelineOffset=50] The guideline offset (pixels) from the bottom.
			@property {number} [guidelineIndex=10] The guideline indent (pixels) from the edges.
			@property {string} [notAvailable='Your browser doesn\'t support signing']
								The error message to show when no canvas is available.
			@property {number} [scale=1] A scaling factor for rendering the signature (only applies to redraws).
			@property {string|Element|jQuery} [syncField=null] The selector, DOM element, or jQuery object
								for a field to automatically synchronise with a text version of the signature.
			@property {string} [syncFormat='JSON'] The output representation: 'JSON', 'SVG', 'PNG', 'JPEG'.
			@property {boolean} [svgStyles=false] <code>true</code> to use the <code>style</code> attribute in SVG.
			@property {SignatureChange} [change=null] A callback triggered when the signature changes.
			@example $.extend($.kbw.signature.options, {guideline: true}) */
		options: {
			distance: 0,
			background: '#fff',
			color: '#000',
			thickness: 2,
			guideline: false,
			guidelineColor: '#a0a0a0',
			guidelineOffset: 50,
			guidelineIndent: 10,
			notAvailable: 'Your browser doesn\'t support signing',
			scale: 1,
			syncField: null,
			syncFormat: 'JSON',
			svgStyles: false,
			change: null
		},

		/** Initialise a new signature area.
			@memberof Signature
			@private */
		_create: function() {
			this.element.addClass(this.widgetFullName || this.widgetBaseClass);
			try {
				this.canvas = $('<canvas width="' + this.element.width() + '" height="' +
					this.element.height() + '">' + this.options.notAvailable + '</canvas>')[0];
				this.element.append(this.canvas);
			}
			catch (e) {
				$(this.canvas).remove();
				this.resize = true;
				this.canvas = document.createElement('canvas');
				this.canvas.setAttribute('width', this.element.width());
				this.canvas.setAttribute('height', this.element.height());
				this.canvas.innerHTML = this.options.notAvailable;
				this.element.append(this.canvas);
				/* jshint -W106 */
				if (G_vmlCanvasManager) { // Requires excanvas.js
					G_vmlCanvasManager.initElement(this.canvas);
				}
				/* jshint +W106 */
			}
			this.ctx = this.canvas.getContext('2d');
			this._refresh(true);
			this._mouseInit();
		},

		/** Refresh the appearance of the signature area.
			@memberof Signature
			@private
			@param {boolean} init <code>true</code> if initialising. */
		_refresh: function(init) {
			if (this.resize) {
				var parent = $(this.canvas);
				$('div', this.canvas).css({width: parent.width(), height: parent.height()});
			}
			this.ctx.fillStyle = this.options.background;
			this.ctx.strokeStyle = this.options.color;
			this.ctx.lineWidth = this.options.thickness;
			this.ctx.lineCap = 'round';
			this.ctx.lineJoin = 'round';
			this.clear(init);
		},

		/** Clear the signature area.
			@memberof Signature
			@param {boolean} init <code>true</code> if initialising - internal use only.
			@example $(selector).signature('clear') */
		clear: function(init) {
			if (this.options.disabled) {
				return;
			}
			this.ctx.clearRect(0, 0, this.element.width(), this.element.height());
			this.ctx.fillRect(0, 0, this.element.width(), this.element.height());
			if (this.options.guideline) {
				this.ctx.save();
				this.ctx.strokeStyle = this.options.guidelineColor;
				this.ctx.lineWidth = 1;
				this.ctx.beginPath();
				this.ctx.moveTo(this.options.guidelineIndent,
					this.element.height() - this.options.guidelineOffset);
				this.ctx.lineTo(this.element.width() - this.options.guidelineIndent,
					this.element.height() - this.options.guidelineOffset);
				this.ctx.stroke();
				this.ctx.restore();
			}
			this.lines = [];
			if (!init) {
				this._changed();
			}
		},

		/** Synchronise changes and trigger a change event.
			@memberof Signature
			@private
			@param {Event} event The triggering event. */
		_changed: function(event) {
			if (this.options.syncField) {
				var output = '';
				switch (this.options.syncFormat) {
					case 'PNG':
						output = this.toDataURL();
						break;
					case 'JPEG':
						output = this.toDataURL('image/jpeg');
						break;
					case 'SVG':
						output = this.toSVG();
						break;
					default:
						output = this.toJSON();
				}
				$(this.options.syncField).val(output);
			}
			this._trigger('change', event, {});
		},

		/** Refresh the signature when options change.
			@memberof Signature
			@private
			@param {object} options The new option values. */
		_setOptions: function(/* options */) {
			if (this._superApply) {
				this._superApply(arguments); // Base widget handling
			}
			else {
				$.Widget.prototype._setOptions.apply(this, arguments); // Base widget handling
			}
			var count = 0;
			var onlyDisable = true;
			for (var name in arguments[0]) {
				if (arguments[0].hasOwnProperty(name)) {
					count++;
					onlyDisable = onlyDisable && name === 'disabled';
				}
			}
			if (count > 1 || !onlyDisable) {
				this._refresh();
			}
		},

		/** Determine if dragging can start.
			@memberof Signature
			@private
			@param {Event} event The triggering mouse event.
			@return {boolean} <code>true</code> if allowed, <code>false</code> if not */
		_mouseCapture: function(/* event */) {
			return !this.options.disabled;
		},

		/** Start a new line.
			@memberof Signature
			@private
			@param {Event} event The triggering mouse event. */
		_mouseStart: function(event) {
			this.offset = this.element.offset();
			this.offset.left -= document.documentElement.scrollLeft || document.body.scrollLeft;
			this.offset.top -= document.documentElement.scrollTop || document.body.scrollTop;
			this.lastPoint = [this._round(event.clientX - this.offset.left),
				this._round(event.clientY - this.offset.top)];
			this.curLine = [this.lastPoint];
			this.lines.push(this.curLine);
		},

		/** Track the mouse.
			@memberof Signature
			@private
			@param {Event} event The triggering mouse event. */
		_mouseDrag: function(event) {
			var point = [this._round(event.clientX - this.offset.left),
				this._round(event.clientY - this.offset.top)];
			this.curLine.push(point);
			this.ctx.beginPath();
			this.ctx.moveTo(this.lastPoint[0], this.lastPoint[1]);
			this.ctx.lineTo(point[0], point[1]);
			this.ctx.stroke();
			this.lastPoint = point;
		},

		/** End a line.
			@memberof Signature
			@private
			@param {Event} event The triggering mouse event. */
		_mouseStop: function(event) {
			if (this.curLine.length === 1) {
				event.clientY += this.options.thickness;
				this._mouseDrag(event);
			}
			this.lastPoint = null;
			this.curLine = null;
			this._changed(event);
		},

		/** Round to two decimal points.
			@memberof Signature
			@private
			@param {number} value The value to round.
			@return {number} The rounded value. */
		_round: function(value) {
			return Math.round(value * 100) / 100;
		},

		/** Convert the captured lines to JSON text.
			@memberof Signature
			@return {string} The JSON text version of the lines.
			@example var json = $(selector).signature('toJSON') */
		toJSON: function() {
			return '{"lines":[' + $.map(this.lines, function(line) {
				return '[' + $.map(line, function(point) {
						return '[' + point + ']';
					}) + ']';
			}) + ']}';
		},

		/** Convert the captured lines to SVG text.
			@memberof Signature
			@return {string} The SVG text version of the lines.
			@example var svg = $(selector).signature('toSVG') */
		toSVG: function() {
			var attrs1 = (this.options.svgStyles ? 'style="fill: ' + this.options.background + ';"' :
				'fill="' + this.options.background + '"');
			var attrs2 = (this.options.svgStyles ?
				'style="fill: none; stroke: ' + this.options.color + '; stroke-width: ' + this.options.thickness + ';"' :
				'fill="none" stroke="' + this.options.color + '" stroke-width="' + this.options.thickness + '"');
			return '<?xml version="1.0"?>\n<!DOCTYPE svg PUBLIC ' +
				'"-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' +
				'<svg xmlns="http://www.w3.org/2000/svg" width="15cm" height="15cm">\n' +
				'	<g ' + attrs1 + '>\n' +
				'		<rect x="0" y="0" width="' + this.canvas.width + '" height="' + this.canvas.height + '"/>\n' +
				'		<g ' + attrs2 +	'>\n'+
				$.map(this.lines, function(line) {
					return '			<polyline points="' +
						$.map(line, function(point) { return point + ''; }).join(' ') + '"/>\n';
				}).join('') +
				'		</g>\n	</g>\n</svg>\n';
		},
		
		/** Convert the captured lines to an image encoded in a <code>data:</code> URL.
			@memberof Signature
			@param {string} [type='image/png'] The MIME type of the image.
			@param {number} [quality=0.92] The image quality, between 0 and 1.
			@return {string} The signature as a data: URL image.
			@example var data = $(selector).signature('toDataURL', 'image/jpeg') */
		toDataURL: function(type, quality) {
			return this.canvas.toDataURL(type, quality);
		},

		/** Draw a signature from its JSON or SVG description or <code>data:</code> URL.
			<p>Note that drawing a <code>data:</code> URL does not reconstruct the internal representation!</p>
			@memberof Signature
			@param {object|string} sig An object with attribute <code>lines</code> being an array of arrays of points
							or the text version of the JSON or SVG or a <code>data:</code> URL containing an image.
			@example $(selector).signature('draw', sigAsJSON) */
		draw: function(sig) {
			if (this.options.disabled) {
				return;
			}
			this.clear(true);
			if (typeof sig === 'string' && sig.indexOf('data:') === 0) { // Data URL
				this._drawDataURL(sig, this.options.scale);
			} else if (typeof sig === 'string' && sig.indexOf('<svg') > -1) { // SVG
				this._drawSVG(sig, this.options.scale);
			} else {
				this._drawJSON(sig, this.options.scale);
			}
			this._changed();
		},

		/** Draw a signature from its JSON description.
			@memberof Signature
			@private
			@param {object|string} sig An object with attribute <code>lines</code> being an array of arrays of points
							or the text version of the JSON.
			@param {number} scale A scaling factor. */
		_drawJSON: function(sig, scale) {
			if (typeof sig === 'string') {
				sig = $.parseJSON(sig);
			}
			this.lines = sig.lines || [];
			var ctx = this.ctx;
			$.each(this.lines, function() {
				ctx.beginPath();
				$.each(this, function(i) {
					ctx[i === 0 ? 'moveTo' : 'lineTo'](this[0] * scale, this[1] * scale);
				});
				ctx.stroke();
			});
		},

		/** Draw a signature from its SVG description.
			@memberof Signature
			@private
			@param {string} sig The text version of the SVG.
			@param {number} scale A scaling factor. */
		_drawSVG: function(sig, scale) {
			var lines = this.lines = [];
			$(sig).find('polyline').each(function() {
				var line = [];
				$.each($(this).attr('points').split(' '), function(i, point) {
					var xy = point.split(',');
					line.push([parseFloat(xy[0]), parseFloat(xy[1])]);
				});
				lines.push(line);
			});
			var ctx = this.ctx;
			$.each(this.lines, function() {
				ctx.beginPath();
				$.each(this, function(i) {
					ctx[i === 0 ? 'moveTo' : 'lineTo'](this[0] * scale, this[1] * scale);
				});
				ctx.stroke();
			});
		},

		/** Draw a signature from its <code>data:</code> URL.
			<p>Note that this does not reconstruct the internal representation!</p>
			@memberof Signature
			@private
			@param {string} sig The <code>data:</code> URL containing an image.
			@param {number} scale A scaling factor. */
		_drawDataURL: function(sig, scale) {
			var image = new Image();
			var context = this.ctx;
			image.onload = function() {
				context.drawImage(this, 0, 0, image.width * scale, image.height * scale);
			};
			image.src = sig;
		},

		/** Determine whether or not any drawing has occurred.
			@memberof Signature
			@return {boolean} <code>true</code> if not signed, <code>false</code> if signed.
			@example if ($(selector).signature('isEmpty')) ... */
		isEmpty: function() {
			return this.lines.length === 0;
		},

		/** Remove the signature functionality.
			@memberof Signature
			@private */
		_destroy: function() {
			this.element.removeClass(this.widgetFullName || this.widgetBaseClass);
			$(this.canvas).remove();
			this.canvas = this.ctx = this.lines = null;
			this._mouseDestroy();
		}
	};

	if (!$.Widget.prototype._destroy) {
		$.extend(signatureOverrides, {
			/* Remove the signature functionality. */
			destroy: function() {
				this._destroy();
				$.Widget.prototype.destroy.call(this); // Base widget handling
			}
		});
	}

	if ($.Widget.prototype._getCreateOptions === $.noop) {
		$.extend(signatureOverrides, {
			/* Restore the metadata functionality. */
			_getCreateOptions: function() {
				return $.metadata && $.metadata.get(this.element[0])[this.widgetName];
			}
		});
	}

	$.widget('kbw.signature', $.ui.mouse, signatureOverrides);

	// Make some things more accessible
	$.kbw.signature.options = $.kbw.signature.prototype.options;

})(jQuery);
