/**
 * jquery-circle-progress - jQuery Plugin to draw animated circular progress bars:
 * {@link http://kottenator.github.io/jquery-circle-progress/}
 *
 * @author Rostyslav Bryzgunov <kottenator@gmail.com>
 * @version 1.2.2
 * @licence MIT
 * @preserve
 */
// UMD factory - https://github.com/umdjs/umd/blob/d31bb6ee7098715e019f52bdfe27b3e4bfd2b97e/templates/jqueryPlugin.js
// Uses AMD, CommonJS or browser globals to create a jQuery plugin.
(function(factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD - register as an anonymous module
		define(['jquery'], factory);
	} else if (typeof module === 'object' && module.exports) {
		// Node/CommonJS
		var $ = require('jquery');
		factory($);
		module.exports = $;
	} else {
		// Browser globals
		factory(jQuery);
	}
})(function($) {
	/**
	 * Inner implementation of the circle progress bar.
	 * The class is not exposed _yet_ but you can create an instance through jQuery method call.
	 *
	 * @param {object} config - You can customize any class member (property or method).
	 * @class
	 * @alias CircleProgress
	 */
	function CircleProgress(config) {
		this.init(config);
	}

	CircleProgress.prototype = {
		//--------------------------------------- public options ---------------------------------------
		/**
		 * This is the only required option. It should be from `0.0` to `1.0`.
		 * @type {number}
		 * @default 0.0
		 */
		value: 0.0,

		/**
		 * Size of the canvas in pixels.
		 * It's a square so we need only one dimension.
		 * @type {number}
		 * @default 100.0
		 */
		size: 100.0,

		/**
		 * Initial angle for `0.0` value in radians.
		 * @type {number}
		 * @default -Math.PI
		 */
		startAngle: -Math.PI,

		/**
		 * Width of the arc in pixels.
		 * If it's `'auto'` - the value is calculated as `[this.size]{@link CircleProgress#size} / 14`.
		 * @type {number|string}
		 * @default 'auto'
		 */
		thickness: 'auto',

		/**
		 * Fill of the arc. You may set it to:
		 *
		 *   - solid color:
		 *     - `'#3aeabb'`
		 *     - `{ color: '#3aeabb' }`
		 *     - `{ color: 'rgba(255, 255, 255, .3)' }`
		 *   - linear gradient _(left to right)_:
		 *     - `{ gradient: ['#3aeabb', '#fdd250'], gradientAngle: Math.PI / 4 }`
		 *     - `{ gradient: ['red', 'green', 'blue'], gradientDirection: [x0, y0, x1, y1] }`
		 *     - `{ gradient: [["red", .2], ["green", .3], ["blue", .8]] }`
		 *   - image:
		 *     - `{ image: 'http://i.imgur.com/pT0i89v.png' }`
		 *     - `{ image: imageObject }`
		 *     - `{ color: 'lime', image: 'http://i.imgur.com/pT0i89v.png' }` -
		 *       color displayed until the image is loaded
		 *
		 * @default {gradient: ['#3aeabb', '#fdd250']}
		 */
		fill: {
			gradient: ['#3aeabb', '#fdd250']
		},

		/**
		 * Color of the "empty" arc. Only a color fill supported by now.
		 * @type {string}
		 * @default 'rgba(0, 0, 0, .1)'
		 */
		emptyFill: 'rgba(0, 0, 0, .1)',

		/**
		 * jQuery Animation config.
		 * You can pass `false` to disable the animation.
		 * @see http://api.jquery.com/animate/
		 * @type {object|boolean}
		 * @default {duration: 1200, easing: 'circleProgressEasing'}
		 */
		animation: {
			duration: 1200,
			easing: 'circleProgressEasing'
		},

		/**
		 * Default animation starts at `0.0` and ends at specified `value`. Let's call this _direct animation_.
		 * If you want to make _reversed animation_ - set `animationStartValue: 1.0`.
		 * Also you may specify any other value from `0.0` to `1.0`.
		 * @type {number}
		 * @default 0.0
		 */
		animationStartValue: 0.0,

		/**
		 * Reverse animation and arc draw.
		 * By default, the arc is filled from `0.0` to `value`, _clockwise_.
		 * With `reverse: true` the arc is filled from `1.0` to `value`, _counter-clockwise_.
		 * @type {boolean}
		 * @default false
		 */
		reverse: false,

		/**
		 * Arc line cap: `'butt'`, `'round'` or `'square'` -
		 * [read more]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.lineCap}.
		 * @type {string}
		 * @default 'butt'
		 */
		lineCap: 'butt',

		/**
		 * Canvas insertion mode: append or prepend it into the parent element?
		 * @type {string}
		 * @default 'prepend'
		 */
		insertMode: 'prepend',

		//------------------------------ protected properties and methods ------------------------------
		/**
		 * Link to {@link CircleProgress} constructor.
		 * @protected
		 */
		constructor: CircleProgress,

		/**
		 * Container element. Should be passed into constructor config.
		 * @protected
		 * @type {jQuery}
		 */
		el: null,

		/**
		 * Canvas element. Automatically generated and prepended to [this.el]{@link CircleProgress#el}.
		 * @protected
		 * @type {HTMLCanvasElement}
		 */
		canvas: null,

		/**
		 * 2D-context of [this.canvas]{@link CircleProgress#canvas}.
		 * @protected
		 * @type {CanvasRenderingContext2D}
		 */
		ctx: null,

		/**
		 * Radius of the outer circle. Automatically calculated as `[this.size]{@link CircleProgress#size} / 2`.
		 * @protected
		 * @type {number}
		 */
		radius: 0.0,

		/**
		 * Fill of the main arc. Automatically calculated, depending on [this.fill]{@link CircleProgress#fill} option.
		 * @protected
		 * @type {string|CanvasGradient|CanvasPattern}
		 */
		arcFill: null,

		/**
		 * Last rendered frame value.
		 * @protected
		 * @type {number}
		 */
		lastFrameValue: 0.0,

		/**
		 * Init/re-init the widget.
		 *
		 * Throws a jQuery event:
		 *
		 * - `circle-inited(jqEvent)`
		 *
		 * @param {object} config - You can customize any class member (property or method).
		 */
		init: function(config) {
			$.extend(this, config);
			this.radius = this.size / 2;
			this.initWidget();
			this.initFill();
			this.draw();
			this.el.trigger('circle-inited');
		},

		/**
		 * Initialize `<canvas>`.
		 * @protected
		 */
		initWidget: function() {
			if (!this.canvas)
				this.canvas = $('<canvas>')[this.insertMode == 'prepend' ? 'prependTo' : 'appendTo'](this.el)[0];

			var canvas = this.canvas;
			canvas.width = this.size;
			canvas.height = this.size;
			this.ctx = canvas.getContext('2d');

			if (window.devicePixelRatio > 1) {
				var scaleBy = window.devicePixelRatio;
				canvas.style.width = canvas.style.height = this.size + 'px';
				canvas.width = canvas.height = this.size * scaleBy;
				this.ctx.scale(scaleBy, scaleBy);
			}
		},

		/**
		 * This method sets [this.arcFill]{@link CircleProgress#arcFill}.
		 * It could do this async (on image load).
		 * @protected
		 */
		initFill: function() {
			var self = this,
				fill = this.fill,
				ctx = this.ctx,
				size = this.size;

			if (!fill)
				throw Error("The fill is not specified!");

			if (typeof fill == 'string')
				fill = {color: fill};

			if (fill.color)
				this.arcFill = fill.color;

			if (fill.gradient) {
				var gr = fill.gradient;

				if (gr.length == 1) {
					this.arcFill = gr[0];
				} else if (gr.length > 1) {
					var ga = fill.gradientAngle || 0, // gradient direction angle; 0 by default
						gd = fill.gradientDirection || [
							size / 2 * (1 - Math.cos(ga)), // x0
							size / 2 * (1 + Math.sin(ga)), // y0
							size / 2 * (1 + Math.cos(ga)), // x1
							size / 2 * (1 - Math.sin(ga))  // y1
						];

					var lg = ctx.createLinearGradient.apply(ctx, gd);

					for (var i = 0; i < gr.length; i++) {
						var color = gr[i],
							pos = i / (gr.length - 1);

						if ($.isArray(color)) {
							pos = color[1];
							color = color[0];
						}

						lg.addColorStop(pos, color);
					}

					this.arcFill = lg;
				}
			}

			if (fill.image) {
				var img;

				if (fill.image instanceof Image) {
					img = fill.image;
				} else {
					img = new Image();
					img.src = fill.image;
				}

				if (img.complete)
					setImageFill();
				else
					img.onload = setImageFill;
			}

			function setImageFill() {
				var bg = $('<canvas>')[0];
				bg.width = self.size;
				bg.height = self.size;
				bg.getContext('2d').drawImage(img, 0, 0, size, size);
				self.arcFill = self.ctx.createPattern(bg, 'no-repeat');
				self.drawFrame(self.lastFrameValue);
			}
		},

		/**
		 * Draw the circle.
		 * @protected
		 */
		draw: function() {
			if (this.animation)
				this.drawAnimated(this.value);
			else
				this.drawFrame(this.value);
		},

		/**
		 * Draw a single animation frame.
		 * @protected
		 * @param {number} v - Frame value.
		 */
		drawFrame: function(v) {
			this.lastFrameValue = v;
			this.ctx.clearRect(0, 0, this.size, this.size);
			this.drawEmptyArc(v);
			this.drawArc(v);
		},

		/**
		 * Draw the arc (part of the circle).
		 * @protected
		 * @param {number} v - Frame value.
		 */
		drawArc: function(v) {
			if (v === 0)
				return;

			var ctx = this.ctx,
				r = this.radius,
				t = this.getThickness(),
				a = this.startAngle;

			ctx.save();
			ctx.beginPath();

			if (!this.reverse) {
				ctx.arc(r, r, r - t / 2, a, a + Math.PI * 2 * v);
			} else {
				ctx.arc(r, r, r - t / 2, a - Math.PI * 2 * v, a);
			}

			ctx.lineWidth = t;
			ctx.lineCap = this.lineCap;
			ctx.strokeStyle = this.arcFill;
			ctx.stroke();
			ctx.restore();
		},

		/**
		 * Draw the _empty (background)_ arc (part of the circle).
		 * @protected
		 * @param {number} v - Frame value.
		 */
		drawEmptyArc: function(v) {
			var ctx = this.ctx,
				r = this.radius,
				t = this.getThickness(),
				a = this.startAngle;

			if (v < 1) {
				ctx.save();
				ctx.beginPath();

				if (v <= 0) {
					ctx.arc(r, r, r - t / 2, 0, Math.PI * 2);
				} else {
					if (!this.reverse) {
						ctx.arc(r, r, r - t / 2, a + Math.PI * 2 * v, a);
					} else {
						ctx.arc(r, r, r - t / 2, a, a - Math.PI * 2 * v);
					}
				}

				ctx.lineWidth = t;
				ctx.strokeStyle = this.emptyFill;
				ctx.stroke();
				ctx.restore();
			}
		},

		/**
		 * Animate the progress bar.
		 *
		 * Throws 3 jQuery events:
		 *
		 * - `circle-animation-start(jqEvent)`
		 * - `circle-animation-progress(jqEvent, animationProgress, stepValue)` - multiple event
		 *   animationProgress: from `0.0` to `1.0`; stepValue: from `0.0` to `value`
		 * - `circle-animation-end(jqEvent)`
		 *
		 * @protected
		 * @param {number} v - Final value.
		 */
		drawAnimated: function(v) {
			var self = this,
				el = this.el,
				canvas = $(this.canvas);

			// stop previous animation before new "start" event is triggered
			canvas.stop(true, false);
			el.trigger('circle-animation-start');

			canvas
				.css({animationProgress: 0})
				.animate({animationProgress: 1}, $.extend({}, this.animation, {
					step: function(animationProgress) {
						var stepValue = self.animationStartValue * (1 - animationProgress) + v * animationProgress;
						self.drawFrame(stepValue);
						el.trigger('circle-animation-progress', [animationProgress, stepValue]);
					}
				}))
				.promise()
				.always(function() {
					// trigger on both successful & failure animation end
					el.trigger('circle-animation-end');
				});
		},

		/**
		 * Get the circle thickness.
		 * @see CircleProgress#thickness
		 * @protected
		 * @returns {number}
		 */
		getThickness: function() {
			return $.isNumeric(this.thickness) ? this.thickness : this.size / 14;
		},

		/**
		 * Get current value.
		 * @protected
		 * @return {number}
		 */
		getValue: function() {
			return this.value;
		},

		/**
		 * Set current value (with smooth animation transition).
		 * @protected
		 * @param {number} newValue
		 */
		setValue: function(newValue) {
			if (this.animation)
				this.animationStartValue = this.lastFrameValue;
			this.value = newValue;
			this.draw();
		}
	};

	//----------------------------------- Initiating jQuery plugin -----------------------------------
	$.circleProgress = {
		// Default options (you may override them)
		defaults: CircleProgress.prototype
	};

	// ease-in-out-cubic
	$.easing.circleProgressEasing = function(x) {
		if (x < 0.5) {
			x = 2 * x;
			return 0.5 * x * x * x;
		} else {
			x = 2 - 2 * x;
			return 1 - 0.5 * x * x * x;
		}
	};

	/**
	 * Creates an instance of {@link CircleProgress}.
	 * Produces [init event]{@link CircleProgress#init} and [animation events]{@link CircleProgress#drawAnimated}.
	 *
	 * @param {object} [configOrCommand] - Config object or command name.
	 *
	 * Config example (you can specify any {@link CircleProgress} property):
	 *
	 * ```js
	 * { value: 0.75, size: 50, animation: false }
	 * ```
	 *
	 * Commands:
	 *
	 * ```js
	 * el.circleProgress('widget'); // get the <canvas>
	 * el.circleProgress('value'); // get the value
	 * el.circleProgress('value', newValue); // update the value
	 * el.circleProgress('redraw'); // redraw the circle
	 * el.circleProgress(); // the same as 'redraw'
	 * ```
	 *
	 * @param {string} [commandArgument] - Some commands (like `'value'`) may require an argument.
	 * @see CircleProgress
	 * @alias "$(...).circleProgress"
	 */
	$.fn.circleProgress = function(configOrCommand, commandArgument) {
		var dataName = 'circle-progress',
			firstInstance = this.data(dataName);

		if (configOrCommand == 'widget') {
			if (!firstInstance)
				throw Error('Calling "widget" method on not initialized instance is forbidden');
			return firstInstance.canvas;
		}

		if (configOrCommand == 'value') {
			if (!firstInstance)
				throw Error('Calling "value" method on not initialized instance is forbidden');
			if (typeof commandArgument == 'undefined') {
				return firstInstance.getValue();
			} else {
				var newValue = arguments[1];
				return this.each(function() {
					$(this).data(dataName).setValue(newValue);
				});
			}
		}

		return this.each(function() {
			var el = $(this),
				instance = el.data(dataName),
				config = $.isPlainObject(configOrCommand) ? configOrCommand : {};

			if (instance) {
				instance.init(config);
			} else {
				var initialConfig = $.extend({}, el.data());
				if (typeof initialConfig.fill == 'string')
					initialConfig.fill = JSON.parse(initialConfig.fill);
				if (typeof initialConfig.animation == 'string')
					initialConfig.animation = JSON.parse(initialConfig.animation);
				config = $.extend(initialConfig, config);
				config.el = el;
				instance = new CircleProgress(config);
				el.data(dataName, instance);
			}
		});
	};
});
