text: /* global window: false */
  /* global document: false */
  'use strict';
  
  var color = require('chartjs-color');
  var defaults = require('./core.defaults');
  var helpers = require('../helpers/index');
  var scaleService = require('../core/core.scaleService');
  
  module.exports = function() {
  
  	// -- Basic js utility methods
  
  	helpers.configMerge = function(/* objects ... */) {
  		return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), {
  			merger: function(key, target, source, options) {
  				var tval = target[key] || {};
  				var sval = source[key];
  
  				if (key === 'scales') {
  					// scale config merging is complex. Add our own function here for that
  					target[key] = helpers.scaleMerge(tval, sval);
  				} else if (key === 'scale') {
  					// used in polar area & radar charts since there is only one scale
  					target[key] = helpers.merge(tval, [scaleService.getScaleDefaults(sval.type), sval]);
  				} else {
  					helpers._merger(key, target, source, options);
  				}
  			}
  		});
  	};
  
  	helpers.scaleMerge = function(/* objects ... */) {
  		return helpers.merge(helpers.clone(arguments[0]), [].slice.call(arguments, 1), {
  			merger: function(key, target, source, options) {
  				if (key === 'xAxes' || key === 'yAxes') {
  					var slen = source[key].length;
  					var i, type, scale;
  
  					if (!target[key]) {
  						target[key] = [];
  					}
  
  					for (i = 0; i < slen; ++i) {
  						scale = source[key][i];
  						type = helpers.valueOrDefault(scale.type, key === 'xAxes' ? 'category' : 'linear');
  
  						if (i >= target[key].length) {
  							target[key].push({});
  						}
  
  						if (!target[key][i].type || (scale.type && scale.type !== target[key][i].type)) {
  							// new/untyped scale or type changed: let's apply the new defaults
  							// then merge source scale to correctly overwrite the defaults.
  							helpers.merge(target[key][i], [scaleService.getScaleDefaults(type), scale]);
  						} else {
  							// scales type are the same
  							helpers.merge(target[key][i], scale);
  						}
  					}
  				} else {
  					helpers._merger(key, target, source, options);
  				}
  			}
  		});
  	};
  
  	helpers.where = function(collection, filterCallback) {
  		if (helpers.isArray(collection) && Array.prototype.filter) {
  			return collection.filter(filterCallback);
  		}
  		var filtered = [];
  
  		helpers.each(collection, function(item) {
  			if (filterCallback(item)) {
  				filtered.push(item);
  			}
  		});
  
  		return filtered;
  	};
  	helpers.findIndex = Array.prototype.findIndex ?
  		function(array, callback, scope) {
  			return array.findIndex(callback, scope);
  		} :
  		function(array, callback, scope) {
  			scope = scope === undefined ? array : scope;
  			for (var i = 0, ilen = array.length; i < ilen; ++i) {
  				if (callback.call(scope, array[i], i, array)) {
  					return i;
  				}
  			}
  			return -1;
  		};
  	helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) {
  		// Default to start of the array
  		if (helpers.isNullOrUndef(startIndex)) {
  			startIndex = -1;
  		}
  		for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
  			var currentItem = arrayToSearch[i];
  			if (filterCallback(currentItem)) {
  				return currentItem;
  			}
  		}
  	};
  	helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) {
  		// Default to end of the array
  		if (helpers.isNullOrUndef(startIndex)) {
  			startIndex = arrayToSearch.length;
  		}
  		for (var i = startIndex - 1; i >= 0; i--) {
  			var currentItem = arrayToSearch[i];
  			if (filterCallback(currentItem)) {
  				return currentItem;
  			}
  		}
  	};
  
  	// -- Math methods
  	helpers.isNumber = function(n) {
  		return !isNaN(parseFloat(n)) && isFinite(n);
  	};
  	helpers.almostEquals = function(x, y, epsilon) {
  		return Math.abs(x - y) < epsilon;
  	};
  	helpers.almostWhole = function(x, epsilon) {
  		var rounded = Math.round(x);
  		return (((rounded - epsilon) < x) && ((rounded + epsilon) > x));
  	};
  	helpers.max = function(array) {
  		return array.reduce(function(max, value) {
  			if (!isNaN(value)) {
  				return Math.max(max, value);
  			}
  			return max;
  		}, Number.NEGATIVE_INFINITY);
  	};
  	helpers.min = function(array) {
  		return array.reduce(function(min, value) {
  			if (!isNaN(value)) {
  				return Math.min(min, value);
  			}
  			return min;
  		}, Number.POSITIVE_INFINITY);
  	};
  	helpers.sign = Math.sign ?
  		function(x) {
  			return Math.sign(x);
  		} :
  		function(x) {
  			x = +x; // convert to a number
  			if (x === 0 || isNaN(x)) {
  				return x;
  			}
  			return x > 0 ? 1 : -1;
  		};
  	helpers.log10 = Math.log10 ?
  		function(x) {
  			return Math.log10(x);
  		} :
  		function(x) {
  			var exponent = Math.log(x) * Math.LOG10E; // Math.LOG10E = 1 / Math.LN10.
  			// Check for whole powers of 10,
  			// which due to floating point rounding error should be corrected.
  			var powerOf10 = Math.round(exponent);
  			var isPowerOf10 = x === Math.pow(10, powerOf10);
  
  			return isPowerOf10 ? powerOf10 : exponent;
  		};
  	helpers.toRadians = function(degrees) {
  		return degrees * (Math.PI / 180);
  	};
  	helpers.toDegrees = function(radians) {
  		return radians * (180 / Math.PI);
  	};
  	// Gets the angle from vertical upright to the point about a centre.
  	helpers.getAngleFromPoint = function(centrePoint, anglePoint) {
  		var distanceFromXCenter = anglePoint.x - centrePoint.x;
  		var distanceFromYCenter = anglePoint.y - centrePoint.y;
  		var radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
  
  		var angle = Math.atan2(distanceFromYCenter, distanceFromXCenter);
  
  		if (angle < (-0.5 * Math.PI)) {
  			angle += 2.0 * Math.PI; // make sure the returned angle is in the range of (-PI/2, 3PI/2]
  		}
  
  		return {
  			angle: angle,
  			distance: radialDistanceFromCenter
  		};
  	};
  	helpers.distanceBetweenPoints = function(pt1, pt2) {
  		return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
  	};
  	helpers.aliasPixel = function(pixelWidth) {
  		return (pixelWidth % 2 === 0) ? 0 : 0.5;
  	};
  	helpers.splineCurve = function(firstPoint, middlePoint, afterPoint, t) {
  		// Props to Rob Spencer at scaled innovation for his post on splining between points
  		// http://scaledinnovation.com/analytics/splines/aboutSplines.html
  
  		// This function must also respect "skipped" points
  
  		var previous = firstPoint.skip ? middlePoint : firstPoint;
  		var current = middlePoint;
  		var next = afterPoint.skip ? middlePoint : afterPoint;
  
  		var d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2));
  		var d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2));
  
  		var s01 = d01 / (d01 + d12);
  		var s12 = d12 / (d01 + d12);
  
  		// If all points are the same, s01 & s02 will be inf
  		s01 = isNaN(s01) ? 0 : s01;
  		s12 = isNaN(s12) ? 0 : s12;
  
  		var fa = t * s01; // scaling factor for triangle Ta
  		var fb = t * s12;
  
  		return {
  			previous: {
  				x: current.x - fa * (next.x - previous.x),
  				y: current.y - fa * (next.y - previous.y)
  			},
  			next: {
  				x: current.x + fb * (next.x - previous.x),
  				y: current.y + fb * (next.y - previous.y)
  			}
  		};
  	};
  	helpers.EPSILON = Number.EPSILON || 1e-14;
  	helpers.splineCurveMonotone = function(points) {
  		// This function calculates Bézier control points in a similar way than |splineCurve|,
  		// but preserves monotonicity of the provided data and ensures no local extremums are added
  		// between the dataset discrete points due to the interpolation.
  		// See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
  
  		var pointsWithTangents = (points || []).map(function(point) {
  			return {
  				model: point._model,
  				deltaK: 0,
  				mK: 0
  			};
  		});
  
  		// Calculate slopes (deltaK) and initialize tangents (mK)
  		var pointsLen = pointsWithTangents.length;
  		var i, pointBefore, pointCurrent, pointAfter;
  		for (i = 0; i < pointsLen; ++i) {
  			pointCurrent = pointsWithTangents[i];
  			if (pointCurrent.model.skip) {
  				continue;
  			}
  
  			pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
  			pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
  			if (pointAfter && !pointAfter.model.skip) {
  				var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x);
  
  				// In the case of two points that appear at the same x pixel, slopeDeltaX is 0
  				pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0;
  			}
  
  			if (!pointBefore || pointBefore.model.skip) {
  				pointCurrent.mK = pointCurrent.deltaK;
  			} else if (!pointAfter || pointAfter.model.skip) {
  				pointCurrent.mK = pointBefore.deltaK;
  			} else if (this.sign(pointBefore.deltaK) !== this.sign(pointCurrent.deltaK)) {
  				pointCurrent.mK = 0;
  			} else {
  				pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2;
  			}
  		}
  
  		// Adjust tangents to ensure monotonic properties
  		var alphaK, betaK, tauK, squaredMagnitude;
  		for (i = 0; i < pointsLen - 1; ++i) {
  			pointCurrent = pointsWithTangents[i];
  			pointAfter = pointsWithTangents[i + 1];
  			if (pointCurrent.model.skip || pointAfter.model.skip) {
  				continue;
  			}
  
  			if (helpers.almostEquals(pointCurrent.deltaK, 0, this.EPSILON)) {
  				pointCurrent.mK = pointAfter.mK = 0;
  				continue;
  			}
  
  			alphaK = pointCurrent.mK / pointCurrent.deltaK;
  			betaK = pointAfter.mK / pointCurrent.deltaK;
  			squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
  			if (squaredMagnitude <= 9) {
  				continue;
  			}
  
  			tauK = 3 / Math.sqrt(squaredMagnitude);
  			pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK;
  			pointAfter.mK = betaK * tauK * pointCurrent.deltaK;
  		}
  
  		// Compute control points
  		var deltaX;
  		for (i = 0; i < pointsLen; ++i) {
  			pointCurrent = pointsWithTangents[i];
  			if (pointCurrent.model.skip) {
  				continue;
  			}
  
  			pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
  			pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
  			if (pointBefore && !pointBefore.model.skip) {
  				deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3;
  				pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX;
  				pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK;
  			}
  			if (pointAfter && !pointAfter.model.skip) {
  				deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3;
  				pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX;
  				pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK;
  			}
  		}
  	};
  	helpers.nextItem = function(collection, index, loop) {
  		if (loop) {
  			return index >= collection.length - 1 ? collection[0] : collection[index + 1];
  		}
  		return index >= collection.length - 1 ? collection[collection.length - 1] : collection[index + 1];
  	};
  	helpers.previousItem = function(collection, index, loop) {
  		if (loop) {
  			return index <= 0 ? collection[collection.length - 1] : collection[index - 1];
  		}
  		return index <= 0 ? collection[0] : collection[index - 1];
  	};
  	// Implementation of the nice number algorithm used in determining where axis labels will go
  	helpers.niceNum = function(range, round) {
  		var exponent = Math.floor(helpers.log10(range));
  		var fraction = range / Math.pow(10, exponent);
  		var niceFraction;
  
  		if (round) {
  			if (fraction < 1.5) {
  				niceFraction = 1;
  			} else if (fraction < 3) {
  				niceFraction = 2;
  			} else if (fraction < 7) {
  				niceFraction = 5;
  			} else {
  				niceFraction = 10;
  			}
  		} else if (fraction <= 1.0) {
  			niceFraction = 1;
  		} else if (fraction <= 2) {
  			niceFraction = 2;
  		} else if (fraction <= 5) {
  			niceFraction = 5;
  		} else {
  			niceFraction = 10;
  		}
  
  		return niceFraction * Math.pow(10, exponent);
  	};
  	// Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
  	helpers.requestAnimFrame = (function() {
  		if (typeof window === 'undefined') {
  			return function(callback) {
  				callback();
  			};
  		}
  		return window.requestAnimationFrame ||
  			window.webkitRequestAnimationFrame ||
  			window.mozRequestAnimationFrame ||
  			window.oRequestAnimationFrame ||
  			window.msRequestAnimationFrame ||
  			function(callback) {
  				return window.setTimeout(callback, 1000 / 60);
  			};
  	}());
  	// -- DOM methods
  	helpers.getRelativePosition = function(evt, chart) {
  		var mouseX, mouseY;
  		var e = evt.originalEvent || evt;
  		var canvas = evt.target || evt.srcElement;
  		var boundingRect = canvas.getBoundingClientRect();
  
  		var touches = e.touches;
  		if (touches && touches.length > 0) {
  			mouseX = touches[0].clientX;
  			mouseY = touches[0].clientY;
  
  		} else {
  			mouseX = e.clientX;
  			mouseY = e.clientY;
  		}
  
  		// Scale mouse coordinates into canvas coordinates
  		// by following the pattern laid out by 'jerryj' in the comments of
  		// http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
  		var paddingLeft = parseFloat(helpers.getStyle(canvas, 'padding-left'));
  		var paddingTop = parseFloat(helpers.getStyle(canvas, 'padding-top'));
  		var paddingRight = parseFloat(helpers.getStyle(canvas, 'padding-right'));
  		var paddingBottom = parseFloat(helpers.getStyle(canvas, 'padding-bottom'));
  		var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight;
  		var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom;
  
  		// We divide by the current device pixel ratio, because the canvas is scaled up by that amount in each direction. However
  		// the backend model is in unscaled coordinates. Since we are going to deal with our model coordinates, we go back here
  		mouseX = Math.round((mouseX - boundingRect.left - paddingLeft) / (width) * canvas.width / chart.currentDevicePixelRatio);
  		mouseY = Math.round((mouseY - boundingRect.top - paddingTop) / (height) * canvas.height / chart.currentDevicePixelRatio);
  
  		return {
  			x: mouseX,
  			y: mouseY
  		};
  
  	};
  
  	// Private helper function to convert max-width/max-height values that may be percentages into a number
  	function parseMaxStyle(styleValue, node, parentProperty) {
  		var valueInPixels;
  		if (typeof styleValue === 'string') {
  			valueInPixels = parseInt(styleValue, 10);
  
  			if (styleValue.indexOf('%') !== -1) {
  				// percentage * size in dimension
  				valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty];
  			}
  		} else {
  			valueInPixels = styleValue;
  		}
  
  		return valueInPixels;
  	}
  
  	/**
  	 * Returns if the given value contains an effective constraint.
  	 * @private
  	 */
  	function isConstrainedValue(value) {
  		return value !== undefined && value !== null && value !== 'none';
  	}
  
  	// Private helper to get a constraint dimension
  	// @param domNode : the node to check the constraint on
  	// @param maxStyle : the style that defines the maximum for the direction we are using (maxWidth / maxHeight)
  	// @param percentageProperty : property of parent to use when calculating width as a percentage
  	// @see http://www.nathanaeljones.com/blog/2013/reading-max-width-cross-browser
  	function getConstraintDimension(domNode, maxStyle, percentageProperty) {
  		var view = document.defaultView;
  		var parentNode = helpers._getParentNode(domNode);
  		var constrainedNode = view.getComputedStyle(domNode)[maxStyle];
  		var constrainedContainer = view.getComputedStyle(parentNode)[maxStyle];
  		var hasCNode = isConstrainedValue(constrainedNode);
  		var hasCContainer = isConstrainedValue(constrainedContainer);
  		var infinity = Number.POSITIVE_INFINITY;
  
  		if (hasCNode || hasCContainer) {
  			return Math.min(
  				hasCNode ? parseMaxStyle(constrainedNode, domNode, percentageProperty) : infinity,
  				hasCContainer ? parseMaxStyle(constrainedContainer, parentNode, percentageProperty) : infinity);
  		}
  
  		return 'none';
  	}
  	// returns Number or undefined if no constraint
  	helpers.getConstraintWidth = function(domNode) {
  		return getConstraintDimension(domNode, 'max-width', 'clientWidth');
  	};
  	// returns Number or undefined if no constraint
  	helpers.getConstraintHeight = function(domNode) {
  		return getConstraintDimension(domNode, 'max-height', 'clientHeight');
  	};
  	/**
  	 * @private
   	 */
  	helpers._calculatePadding = function(container, padding, parentDimension) {
  		padding = helpers.getStyle(container, padding);
  
  		return padding.indexOf('%') > -1 ? parentDimension / parseInt(padding, 10) : parseInt(padding, 10);
  	};
  	/**
  	 * @private
  	 */
  	helpers._getParentNode = function(domNode) {
  		var parent = domNode.parentNode;
  		if (parent && parent.host) {
  			parent = parent.host;
  		}
  		return parent;
  	};
  	helpers.getMaximumWidth = function(domNode) {
  		var container = helpers._getParentNode(domNode);
  		if (!container) {
  			return domNode.clientWidth;
  		}
  
  		var clientWidth = container.clientWidth;
  		var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth);
  		var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth);
  
  		var w = clientWidth - paddingLeft - paddingRight;
  		var cw = helpers.getConstraintWidth(domNode);
  		return isNaN(cw) ? w : Math.min(w, cw);
  	};
  	helpers.getMaximumHeight = function(domNode) {
  		var container = helpers._getParentNode(domNode);
  		if (!container) {
  			return domNode.clientHeight;
  		}
  
  		var clientHeight = container.clientHeight;
  		var paddingTop = helpers._calculatePadding(container, 'padding-top', clientHeight);
  		var paddingBottom = helpers._calculatePadding(container, 'padding-bottom', clientHeight);
  
  		var h = clientHeight - paddingTop - paddingBottom;
  		var ch = helpers.getConstraintHeight(domNode);
  		return isNaN(ch) ? h : Math.min(h, ch);
  	};
  	helpers.getStyle = function(el, property) {
  		return el.currentStyle ?
  			el.currentStyle[property] :
  			document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
  	};
  	helpers.retinaScale = function(chart, forceRatio) {
  		var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
  		if (pixelRatio === 1) {
  			return;
  		}
  
  		var canvas = chart.canvas;
  		var height = chart.height;
  		var width = chart.width;
  
  		canvas.height = height * pixelRatio;
  		canvas.width = width * pixelRatio;
  		chart.ctx.scale(pixelRatio, pixelRatio);
  
  		// If no style has been set on the canvas, the render size is used as display size,
  		// making the chart visually bigger, so let's enforce it to the "correct" values.
  		// See https://github.com/chartjs/Chart.js/issues/3575
  		if (!canvas.style.height && !canvas.style.width) {
  			canvas.style.height = height + 'px';
  			canvas.style.width = width + 'px';
  		}
  	};
  	// -- Canvas methods
  	helpers.fontString = function(pixelSize, fontStyle, fontFamily) {
  		return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;
  	};
  	helpers.longestText = function(ctx, font, arrayOfThings, cache) {
  		cache = cache || {};
  		var data = cache.data = cache.data || {};
  		var gc = cache.garbageCollect = cache.garbageCollect || [];
  
  		if (cache.font !== font) {
  			data = cache.data = {};
  			gc = cache.garbageCollect = [];
  			cache.font = font;
  		}
  
  		ctx.font = font;
  		var longest = 0;
  		helpers.each(arrayOfThings, function(thing) {
  			// Undefined strings and arrays should not be measured
  			if (thing !== undefined && thing !== null && helpers.isArray(thing) !== true) {
  				longest = helpers.measureText(ctx, data, gc, longest, thing);
  			} else if (helpers.isArray(thing)) {
  				// if it is an array lets measure each element
  				// to do maybe simplify this function a bit so we can do this more recursively?
  				helpers.each(thing, function(nestedThing) {
  					// Undefined strings and arrays should not be measured
  					if (nestedThing !== undefined && nestedThing !== null && !helpers.isArray(nestedThing)) {
  						longest = helpers.measureText(ctx, data, gc, longest, nestedThing);
  					}
  				});
  			}
  		});
  
  		var gcLen = gc.length / 2;
  		if (gcLen > arrayOfThings.length) {
  			for (var i = 0; i < gcLen; i++) {
  				delete data[gc[i]];
  			}
  			gc.splice(0, gcLen);
  		}
  		return longest;
  	};
  	helpers.measureText = function(ctx, data, gc, longest, string) {
  		var textWidth = data[string];
  		if (!textWidth) {
  			textWidth = data[string] = ctx.measureText(string).width;
  			gc.push(string);
  		}
  		if (textWidth > longest) {
  			longest = textWidth;
  		}
  		return longest;
  	};
  	helpers.numberOfLabelLines = function(arrayOfThings) {
  		var numberOfLines = 1;
  		helpers.each(arrayOfThings, function(thing) {
  			if (helpers.isArray(thing)) {
  				if (thing.length > numberOfLines) {
  					numberOfLines = thing.length;
  				}
  			}
  		});
  		return numberOfLines;
  	};
  
  	helpers.color = !color ?
  		function(value) {
  			console.error('Color.js not found!');
  			return value;
  		} :
  		function(value) {
  			/* global CanvasGradient */
  			if (value instanceof CanvasGradient) {
  				value = defaults.global.defaultColor;
  			}
  
  			return color(value);
  		};
  
  	helpers.getHoverColor = function(colorValue) {
  		/* global CanvasPattern */
  		return (colorValue instanceof CanvasPattern) ?
  			colorValue :
  			helpers.color(colorValue).saturate(0.5).darken(0.1).rgbString();
  	};
  };
  