text: 'use strict';
  
  import DatasetController from '../core/core.datasetController';
  import defaults from '../core/core.defaults';
  import Rectangle from '../elements/element.rectangle';
  import helpers from '../helpers';
  
  const valueOrDefault = helpers.valueOrDefault;
  
  defaults._set('bar', {
  	hover: {
  		mode: 'index'
  	},
  
  	datasets: {
  		categoryPercentage: 0.8,
  		barPercentage: 0.9,
  		animation: {
  			numbers: {
  				type: 'number',
  				properties: ['x', 'y', 'base', 'width', 'height']
  			}
  		}
  	},
  
  	scales: {
  		x: {
  			type: 'category',
  			offset: true,
  			gridLines: {
  				offsetGridLines: true
  			}
  		},
  		y: {
  			type: 'linear',
  			beginAtZero: true,
  		}
  	}
  });
  
  /**
   * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap.
   * @private
   */
  function computeMinSampleSize(scale, pixels) {
  	var min = scale._length;
  	var prev, curr, i, ilen;
  
  	for (i = 1, ilen = pixels.length; i < ilen; ++i) {
  		min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1]));
  	}
  
  	for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) {
  		curr = scale.getPixelForTick(i);
  		min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min;
  		prev = curr;
  	}
  
  	return min;
  }
  
  /**
   * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null,
   * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This
   * mode currently always generates bars equally sized (until we introduce scriptable options?).
   * @private
   */
  function computeFitCategoryTraits(index, ruler, options) {
  	var thickness = options.barThickness;
  	var count = ruler.stackCount;
  	var curr = ruler.pixels[index];
  	var min = helpers.isNullOrUndef(thickness)
  		? computeMinSampleSize(ruler.scale, ruler.pixels)
  		: -1;
  	var size, ratio;
  
  	if (helpers.isNullOrUndef(thickness)) {
  		size = min * options.categoryPercentage;
  		ratio = options.barPercentage;
  	} else {
  		// When bar thickness is enforced, category and bar percentages are ignored.
  		// Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%')
  		// and deprecate barPercentage since this value is ignored when thickness is absolute.
  		size = thickness * count;
  		ratio = 1;
  	}
  
  	return {
  		chunk: size / count,
  		ratio: ratio,
  		start: curr - (size / 2)
  	};
  }
  
  /**
   * Computes an "optimal" category that globally arranges bars side by side (no gap when
   * percentage options are 1), based on the previous and following categories. This mode
   * generates bars with different widths when data are not evenly spaced.
   * @private
   */
  function computeFlexCategoryTraits(index, ruler, options) {
  	var pixels = ruler.pixels;
  	var curr = pixels[index];
  	var prev = index > 0 ? pixels[index - 1] : null;
  	var next = index < pixels.length - 1 ? pixels[index + 1] : null;
  	var percent = options.categoryPercentage;
  	var start, size;
  
  	if (prev === null) {
  		// first data: its size is double based on the next point or,
  		// if it's also the last data, we use the scale size.
  		prev = curr - (next === null ? ruler.end - ruler.start : next - curr);
  	}
  
  	if (next === null) {
  		// last data: its size is also double based on the previous point.
  		next = curr + curr - prev;
  	}
  
  	start = curr - (curr - Math.min(prev, next)) / 2 * percent;
  	size = Math.abs(next - prev) / 2 * percent;
  
  	return {
  		chunk: size / ruler.stackCount,
  		ratio: options.barPercentage,
  		start: start
  	};
  }
  
  function parseFloatBar(arr, item, vScale, i) {
  	var startValue = vScale._parse(arr[0], i);
  	var endValue = vScale._parse(arr[1], i);
  	var min = Math.min(startValue, endValue);
  	var max = Math.max(startValue, endValue);
  	var barStart = min;
  	var barEnd = max;
  
  	if (Math.abs(min) > Math.abs(max)) {
  		barStart = max;
  		barEnd = min;
  	}
  
  	// Store `barEnd` (furthest away from origin) as parsed value,
  	// to make stacking straight forward
  	item[vScale.axis] = barEnd;
  
  	item._custom = {
  		barStart: barStart,
  		barEnd: barEnd,
  		start: startValue,
  		end: endValue,
  		min: min,
  		max: max
  	};
  }
  
  function parseArrayOrPrimitive(meta, data, start, count) {
  	const iScale = meta.iScale;
  	const vScale = meta.vScale;
  	const labels = iScale._getLabels();
  	const singleScale = iScale === vScale;
  	const parsed = [];
  	let i, ilen, item, entry;
  
  	for (i = start, ilen = start + count; i < ilen; ++i) {
  		entry = data[i];
  		item = {};
  		item[iScale.axis] = singleScale || iScale._parse(labels[i], i);
  
  		if (helpers.isArray(entry)) {
  			parseFloatBar(entry, item, vScale, i);
  		} else {
  			item[vScale.axis] = vScale._parse(entry, i);
  		}
  
  		parsed.push(item);
  	}
  	return parsed;
  }
  
  function isFloatBar(custom) {
  	return custom && custom.barStart !== undefined && custom.barEnd !== undefined;
  }
  
  export default DatasetController.extend({
  
  	dataElementType: Rectangle,
  
  	/**
  	 * @private
  	 */
  	_dataElementOptions: [
  		'backgroundColor',
  		'borderColor',
  		'borderSkipped',
  		'borderWidth',
  		'barPercentage',
  		'barThickness',
  		'categoryPercentage',
  		'maxBarThickness',
  		'minBarLength'
  	],
  
  	/**
  	 * Overriding primitive data parsing since we support mixed primitive/array
  	 * data for float bars
  	 * @private
  	 */
  	_parsePrimitiveData: function() {
  		return parseArrayOrPrimitive.apply(this, arguments);
  	},
  
  	/**
  	 * Overriding array data parsing since we support mixed primitive/array
  	 * data for float bars
  	 * @private
  	 */
  	_parseArrayData: function() {
  		return parseArrayOrPrimitive.apply(this, arguments);
  	},
  
  	/**
  	 * Overriding object data parsing since we support mixed primitive/array
  	 * value-scale data for float bars
  	 * @private
  	 */
  	_parseObjectData: function(meta, data, start, count) {
  		const {iScale, vScale} = meta;
  		const vProp = vScale.axis;
  		const parsed = [];
  		let i, ilen, item, obj, value;
  		for (i = start, ilen = start + count; i < ilen; ++i) {
  			obj = data[i];
  			item = {};
  			item[iScale.axis] = iScale._parseObject(obj, iScale.axis, i);
  			value = obj[vProp];
  			if (helpers.isArray(value)) {
  				parseFloatBar(value, item, vScale, i);
  			} else {
  				item[vScale.axis] = vScale._parseObject(obj, vProp, i);
  			}
  			parsed.push(item);
  		}
  		return parsed;
  	},
  
  	/**
  	 * @private
  	 */
  	_getLabelAndValue: function(index) {
  		const me = this;
  		const meta = me._cachedMeta;
  		const {iScale, vScale} = meta;
  		const parsed = me._getParsed(index);
  		const custom = parsed._custom;
  		const value = isFloatBar(custom)
  			? '[' + custom.start + ', ' + custom.end + ']'
  			: '' + vScale.getLabelForValue(parsed[vScale.axis]);
  
  		return {
  			label: '' + iScale.getLabelForValue(parsed[iScale.axis]),
  			value: value
  		};
  	},
  
  	initialize: function() {
  		var me = this;
  		var meta;
  
  		DatasetController.prototype.initialize.apply(me, arguments);
  
  		meta = me._cachedMeta;
  		meta.stack = me.getDataset().stack;
  		meta.bar = true;
  	},
  
  	update: function(mode) {
  		const me = this;
  		const rects = me._cachedMeta.data;
  
  		me.updateElements(rects, 0, mode);
  	},
  
  	updateElements: function(rectangles, start, mode) {
  		const me = this;
  		const reset = mode === 'reset';
  		const vscale = me._cachedMeta.vScale;
  		const base = vscale.getBasePixel();
  		const horizontal = vscale.isHorizontal();
  		const ruler = me.getRuler();
  		const firstOpts = me._resolveDataElementOptions(start, mode);
  		const sharedOptions = me._getSharedOptions(mode, rectangles[start], firstOpts);
  		const includeOptions = me._includeOptions(mode, sharedOptions);
  
  		let i;
  
  		for (i = 0; i < rectangles.length; i++) {
  			const index = start + i;
  			const options = me._resolveDataElementOptions(index, mode);
  			const vpixels = me.calculateBarValuePixels(index, options);
  			const ipixels = me.calculateBarIndexPixels(index, ruler, options);
  
  			const properties = {
  				horizontal,
  				base: reset ? base : vpixels.base,
  				x: horizontal ? reset ? base : vpixels.head : ipixels.center,
  				y: horizontal ? ipixels.center : reset ? base : vpixels.head,
  				height: horizontal ? ipixels.size : undefined,
  				width: horizontal ? undefined : ipixels.size
  			};
  
  			// all borders are drawn for floating bar
  			/* TODO: float bars border skipping magic
  			if (me._getParsed(i)._custom) {
  				model.borderSkipped = null;
  			}
  			*/
  			if (includeOptions) {
  				properties.options = options;
  			}
  			me._updateElement(rectangles[i], index, properties, mode);
  		}
  
  		me._updateSharedOptions(sharedOptions, mode);
  	},
  
  	/**
  	 * Returns the stacks based on groups and bar visibility.
  	 * @param {number} [last] - The dataset index
  	 * @returns {string[]} The list of stack IDs
  	 * @private
  	 */
  	_getStacks: function(last) {
  		const me = this;
  		const meta = me._cachedMeta;
  		const iScale = meta.iScale;
  		const metasets = iScale._getMatchingVisibleMetas(me._type);
  		const stacked = iScale.options.stacked;
  		const ilen = metasets.length;
  		const stacks = [];
  		let i, item;
  
  		for (i = 0; i < ilen; ++i) {
  			item = metasets[i];
  			// stacked   | meta.stack
  			//           | found | not found | undefined
  			// false     |   x   |     x     |     x
  			// true      |       |     x     |
  			// undefined |       |     x     |     x
  			if (stacked === false || stacks.indexOf(item.stack) === -1 ||
  				(stacked === undefined && item.stack === undefined)) {
  				stacks.push(item.stack);
  			}
  			if (item.index === last) {
  				break;
  			}
  		}
  
  		// No stacks? that means there is no visible data. Let's still initialize an `undefined`
  		// stack where possible invisible bars will be located.
  		// https://github.com/chartjs/Chart.js/issues/6368
  		if (!stacks.length) {
  			stacks.push(undefined);
  		}
  
  		return stacks;
  	},
  
  	/**
  	 * Returns the effective number of stacks based on groups and bar visibility.
  	 * @private
  	 */
  	getStackCount: function() {
  		return this._getStacks().length;
  	},
  
  	/**
  	 * Returns the stack index for the given dataset based on groups and bar visibility.
  	 * @param {number} [datasetIndex] - The dataset index
  	 * @param {string} [name] - The stack name to find
  	 * @returns {number} The stack index
  	 * @private
  	 */
  	getStackIndex: function(datasetIndex, name) {
  		var stacks = this._getStacks(datasetIndex);
  		var index = (name !== undefined)
  			? stacks.indexOf(name)
  			: -1; // indexOf returns -1 if element is not present
  
  		return (index === -1)
  			? stacks.length - 1
  			: index;
  	},
  
  	/**
  	 * @private
  	 */
  	getRuler: function() {
  		const me = this;
  		const meta = me._cachedMeta;
  		const iScale = meta.iScale;
  		const pixels = [];
  		let i, ilen;
  
  		for (i = 0, ilen = meta.data.length; i < ilen; ++i) {
  			pixels.push(iScale.getPixelForValue(me._getParsed(i)[iScale.axis]));
  		}
  
  		return {
  			pixels,
  			start: iScale._startPixel,
  			end: iScale._endPixel,
  			stackCount: me.getStackCount(),
  			scale: iScale
  		};
  	},
  
  	/**
  	 * Note: pixel values are not clamped to the scale area.
  	 * @private
  	 */
  	calculateBarValuePixels: function(index, options) {
  		const me = this;
  		const meta = me._cachedMeta;
  		const vScale = meta.vScale;
  		const minBarLength = options.minBarLength;
  		const parsed = me._getParsed(index);
  		const custom = parsed._custom;
  		let value = parsed[vScale.axis];
  		let start = 0;
  		let length = meta._stacked ? me._applyStack(vScale, parsed) : value;
  		let base, head, size;
  
  		if (length !== value) {
  			start = length - value;
  			length = value;
  		}
  
  		if (isFloatBar(custom)) {
  			value = custom.barStart;
  			length = custom.barEnd - custom.barStart;
  			// bars crossing origin are not stacked
  			if (value !== 0 && helpers.math.sign(value) !== helpers.math.sign(custom.barEnd)) {
  				start = 0;
  			}
  			start += value;
  		}
  
  		// Limit the bar to only extend up to 10 pixels past scale bounds (chartArea)
  		// So we don't try to draw so huge rectangles.
  		// https://github.com/chartjs/Chart.js/issues/5247
  		// TODO: use borderWidth instead (need to move the parsing from rectangle)
  		base = helpers.math._limitValue(vScale.getPixelForValue(start),
  			vScale._startPixel - 10,
  			vScale._endPixel + 10);
  
  		head = vScale.getPixelForValue(start + length);
  		size = head - base;
  
  		if (minBarLength !== undefined && Math.abs(size) < minBarLength) {
  			size = size < 0 ? -minBarLength : minBarLength;
  			head = base + size;
  		}
  
  		return {
  			size: size,
  			base: base,
  			head: head,
  			center: head + size / 2
  		};
  	},
  
  	/**
  	 * @private
  	 */
  	calculateBarIndexPixels: function(index, ruler, options) {
  		var me = this;
  		var range = options.barThickness === 'flex'
  			? computeFlexCategoryTraits(index, ruler, options)
  			: computeFitCategoryTraits(index, ruler, options);
  
  		var stackIndex = me.getStackIndex(me.index, me._cachedMeta.stack);
  		var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2);
  		var size = Math.min(
  			valueOrDefault(options.maxBarThickness, Infinity),
  			range.chunk * range.ratio);
  
  		return {
  			base: center - size / 2,
  			head: center + size / 2,
  			center: center,
  			size: size
  		};
  	},
  
  	draw: function() {
  		const me = this;
  		const chart = me.chart;
  		const meta = me._cachedMeta;
  		const vScale = meta.vScale;
  		const rects = meta.data;
  		const ilen = rects.length;
  		let i = 0;
  
  		helpers.canvas.clipArea(chart.ctx, chart.chartArea);
  
  		for (; i < ilen; ++i) {
  			if (!isNaN(me._getParsed(i)[vScale.axis])) {
  				rects[i].draw(me._ctx);
  			}
  		}
  
  		helpers.canvas.unclipArea(chart.ctx);
  	}
  
  });
  