text: 'use strict';
  
  var helpers = require('../helpers/index');
  var Scale = require('../core/core.scale');
  var scaleService = require('../core/core.scaleService');
  var Ticks = require('../core/core.ticks');
  
  /**
   * Generate a set of logarithmic ticks
   * @param generationOptions the options used to generate the ticks
   * @param dataRange the range of the data
   * @returns {Array<Number>} array of tick values
   */
  function generateTicks(generationOptions, dataRange) {
  	var ticks = [];
  	var valueOrDefault = helpers.valueOrDefault;
  
  	// Figure out what the max number of ticks we can support it is based on the size of
  	// the axis area. For now, we say that the minimum tick spacing in pixels must be 50
  	// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
  	// the graph
  	var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min))));
  
  	var endExp = Math.floor(helpers.log10(dataRange.max));
  	var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp));
  	var exp, significand;
  
  	if (tickVal === 0) {
  		exp = Math.floor(helpers.log10(dataRange.minNotZero));
  		significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp));
  
  		ticks.push(tickVal);
  		tickVal = significand * Math.pow(10, exp);
  	} else {
  		exp = Math.floor(helpers.log10(tickVal));
  		significand = Math.floor(tickVal / Math.pow(10, exp));
  	}
  	var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;
  
  	do {
  		ticks.push(tickVal);
  
  		++significand;
  		if (significand === 10) {
  			significand = 1;
  			++exp;
  			precision = exp >= 0 ? 1 : precision;
  		}
  
  		tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision;
  	} while (exp < endExp || (exp === endExp && significand < endSignificand));
  
  	var lastTick = valueOrDefault(generationOptions.max, tickVal);
  	ticks.push(lastTick);
  
  	return ticks;
  }
  
  
  module.exports = function(Chart) {
  
  	var defaultConfig = {
  		position: 'left',
  
  		// label settings
  		ticks: {
  			callback: Ticks.formatters.logarithmic
  		}
  	};
  
  	var LogarithmicScale = Scale.extend({
  		determineDataLimits: function() {
  			var me = this;
  			var opts = me.options;
  			var chart = me.chart;
  			var data = chart.data;
  			var datasets = data.datasets;
  			var isHorizontal = me.isHorizontal();
  			function IDMatches(meta) {
  				return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id;
  			}
  
  			// Calculate Range
  			me.min = null;
  			me.max = null;
  			me.minNotZero = null;
  
  			var hasStacks = opts.stacked;
  			if (hasStacks === undefined) {
  				helpers.each(datasets, function(dataset, datasetIndex) {
  					if (hasStacks) {
  						return;
  					}
  
  					var meta = chart.getDatasetMeta(datasetIndex);
  					if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) &&
  						meta.stack !== undefined) {
  						hasStacks = true;
  					}
  				});
  			}
  
  			if (opts.stacked || hasStacks) {
  				var valuesPerStack = {};
  
  				helpers.each(datasets, function(dataset, datasetIndex) {
  					var meta = chart.getDatasetMeta(datasetIndex);
  					var key = [
  						meta.type,
  						// we have a separate stack for stack=undefined datasets when the opts.stacked is undefined
  						((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''),
  						meta.stack
  					].join('.');
  
  					if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) {
  						if (valuesPerStack[key] === undefined) {
  							valuesPerStack[key] = [];
  						}
  
  						helpers.each(dataset.data, function(rawValue, index) {
  							var values = valuesPerStack[key];
  							var value = +me.getRightValue(rawValue);
  							// invalid, hidden and negative values are ignored
  							if (isNaN(value) || meta.data[index].hidden || value < 0) {
  								return;
  							}
  							values[index] = values[index] || 0;
  							values[index] += value;
  						});
  					}
  				});
  
  				helpers.each(valuesPerStack, function(valuesForType) {
  					if (valuesForType.length > 0) {
  						var minVal = helpers.min(valuesForType);
  						var maxVal = helpers.max(valuesForType);
  						me.min = me.min === null ? minVal : Math.min(me.min, minVal);
  						me.max = me.max === null ? maxVal : Math.max(me.max, maxVal);
  					}
  				});
  
  			} else {
  				helpers.each(datasets, function(dataset, datasetIndex) {
  					var meta = chart.getDatasetMeta(datasetIndex);
  					if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) {
  						helpers.each(dataset.data, function(rawValue, index) {
  							var value = +me.getRightValue(rawValue);
  							// invalid, hidden and negative values are ignored
  							if (isNaN(value) || meta.data[index].hidden || value < 0) {
  								return;
  							}
  
  							if (me.min === null) {
  								me.min = value;
  							} else if (value < me.min) {
  								me.min = value;
  							}
  
  							if (me.max === null) {
  								me.max = value;
  							} else if (value > me.max) {
  								me.max = value;
  							}
  
  							if (value !== 0 && (me.minNotZero === null || value < me.minNotZero)) {
  								me.minNotZero = value;
  							}
  						});
  					}
  				});
  			}
  
  			// Common base implementation to handle ticks.min, ticks.max
  			this.handleTickRangeOptions();
  		},
  		handleTickRangeOptions: function() {
  			var me = this;
  			var opts = me.options;
  			var tickOpts = opts.ticks;
  			var valueOrDefault = helpers.valueOrDefault;
  			var DEFAULT_MIN = 1;
  			var DEFAULT_MAX = 10;
  
  			me.min = valueOrDefault(tickOpts.min, me.min);
  			me.max = valueOrDefault(tickOpts.max, me.max);
  
  			if (me.min === me.max) {
  				if (me.min !== 0 && me.min !== null) {
  					me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1);
  					me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1);
  				} else {
  					me.min = DEFAULT_MIN;
  					me.max = DEFAULT_MAX;
  				}
  			}
  			if (me.min === null) {
  				me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1);
  			}
  			if (me.max === null) {
  				me.max = me.min !== 0
  					? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1)
  					: DEFAULT_MAX;
  			}
  			if (me.minNotZero === null) {
  				if (me.min > 0) {
  					me.minNotZero = me.min;
  				} else if (me.max < 1) {
  					me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max)));
  				} else {
  					me.minNotZero = DEFAULT_MIN;
  				}
  			}
  		},
  		buildTicks: function() {
  			var me = this;
  			var opts = me.options;
  			var tickOpts = opts.ticks;
  			var reverse = !me.isHorizontal();
  
  			var generationOptions = {
  				min: tickOpts.min,
  				max: tickOpts.max
  			};
  			var ticks = me.ticks = generateTicks(generationOptions, me);
  
  			// At this point, we need to update our max and min given the tick values since we have expanded the
  			// range of the scale
  			me.max = helpers.max(ticks);
  			me.min = helpers.min(ticks);
  
  			if (tickOpts.reverse) {
  				reverse = !reverse;
  				me.start = me.max;
  				me.end = me.min;
  			} else {
  				me.start = me.min;
  				me.end = me.max;
  			}
  			if (reverse) {
  				ticks.reverse();
  			}
  		},
  		convertTicksToLabels: function() {
  			this.tickValues = this.ticks.slice();
  
  			Scale.prototype.convertTicksToLabels.call(this);
  		},
  		// Get the correct tooltip label
  		getLabelForIndex: function(index, datasetIndex) {
  			return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]);
  		},
  		getPixelForTick: function(index) {
  			return this.getPixelForValue(this.tickValues[index]);
  		},
  		/**
  		 * Returns the value of the first tick.
  		 * @param {Number} value - The minimum not zero value.
  		 * @return {Number} The first tick value.
  		 * @private
  		 */
  		_getFirstTickValue: function(value) {
  			var exp = Math.floor(helpers.log10(value));
  			var significand = Math.floor(value / Math.pow(10, exp));
  
  			return significand * Math.pow(10, exp);
  		},
  		getPixelForValue: function(value) {
  			var me = this;
  			var reverse = me.options.ticks.reverse;
  			var log10 = helpers.log10;
  			var firstTickValue = me._getFirstTickValue(me.minNotZero);
  			var offset = 0;
  			var innerDimension, pixel, start, end, sign;
  
  			value = +me.getRightValue(value);
  			if (reverse) {
  				start = me.end;
  				end = me.start;
  				sign = -1;
  			} else {
  				start = me.start;
  				end = me.end;
  				sign = 1;
  			}
  			if (me.isHorizontal()) {
  				innerDimension = me.width;
  				pixel = reverse ? me.right : me.left;
  			} else {
  				innerDimension = me.height;
  				sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0)
  				pixel = reverse ? me.top : me.bottom;
  			}
  			if (value !== start) {
  				if (start === 0) { // include zero tick
  					offset = helpers.getValueOrDefault(
  						me.options.ticks.fontSize,
  						Chart.defaults.global.defaultFontSize
  					);
  					innerDimension -= offset;
  					start = firstTickValue;
  				}
  				if (value !== 0) {
  					offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start));
  				}
  				pixel += sign * offset;
  			}
  			return pixel;
  		},
  		getValueForPixel: function(pixel) {
  			var me = this;
  			var reverse = me.options.ticks.reverse;
  			var log10 = helpers.log10;
  			var firstTickValue = me._getFirstTickValue(me.minNotZero);
  			var innerDimension, start, end, value;
  
  			if (reverse) {
  				start = me.end;
  				end = me.start;
  			} else {
  				start = me.start;
  				end = me.end;
  			}
  			if (me.isHorizontal()) {
  				innerDimension = me.width;
  				value = reverse ? me.right - pixel : pixel - me.left;
  			} else {
  				innerDimension = me.height;
  				value = reverse ? pixel - me.top : me.bottom - pixel;
  			}
  			if (value !== start) {
  				if (start === 0) { // include zero tick
  					var offset = helpers.getValueOrDefault(
  						me.options.ticks.fontSize,
  						Chart.defaults.global.defaultFontSize
  					);
  					value -= offset;
  					innerDimension -= offset;
  					start = firstTickValue;
  				}
  				value *= log10(end) - log10(start);
  				value /= innerDimension;
  				value = Math.pow(10, log10(start) + value);
  			}
  			return value;
  		}
  	});
  
  	scaleService.registerScaleType('logarithmic', LogarithmicScale, defaultConfig);
  };
  