text: 'use strict';
  
  var defaults = require('./core.defaults');
  var helpers = require('../helpers/index');
  
  var extend = helpers.extend;
  
  function filterByPosition(array, position) {
  	return helpers.where(array, function(v) {
  		return v.pos === position;
  	});
  }
  
  function sortByWeight(array, reverse) {
  	return array.sort(function(a, b) {
  		var v0 = reverse ? b : a;
  		var v1 = reverse ? a : b;
  		return v0.weight === v1.weight ?
  			v0.index - v1.index :
  			v0.weight - v1.weight;
  	});
  }
  
  function wrapBoxes(boxes) {
  	var layoutBoxes = [];
  	var i, ilen, box;
  
  	for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) {
  		box = boxes[i];
  		layoutBoxes.push({
  			index: i,
  			box: box,
  			pos: box.position,
  			horizontal: box.isHorizontal(),
  			weight: box.weight
  		});
  	}
  	return layoutBoxes;
  }
  
  function setLayoutDims(layouts, params) {
  	var i, ilen, layout;
  	for (i = 0, ilen = layouts.length; i < ilen; ++i) {
  		layout = layouts[i];
  		// store width used instead of chartArea.w in fitBoxes
  		layout.width = layout.horizontal
  			? layout.box.fullWidth && params.availableWidth
  			: params.vBoxMaxWidth;
  		// store height used instead of chartArea.h in fitBoxes
  		layout.height = layout.horizontal && params.hBoxMaxHeight;
  	}
  }
  
  function buildLayoutBoxes(boxes) {
  	var layoutBoxes = wrapBoxes(boxes);
  	var left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);
  	var right = sortByWeight(filterByPosition(layoutBoxes, 'right'));
  	var top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);
  	var bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));
  
  	return {
  		leftAndTop: left.concat(top),
  		rightAndBottom: right.concat(bottom),
  		chartArea: filterByPosition(layoutBoxes, 'chartArea'),
  		vertical: left.concat(right),
  		horizontal: top.concat(bottom)
  	};
  }
  
  function getCombinedMax(maxPadding, chartArea, a, b) {
  	return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]);
  }
  
  function updateDims(chartArea, params, layout) {
  	var box = layout.box;
  	var maxPadding = chartArea.maxPadding;
  	var newWidth, newHeight;
  
  	if (layout.size) {
  		// this layout was already counted for, lets first reduce old size
  		chartArea[layout.pos] -= layout.size;
  	}
  	layout.size = layout.horizontal ? box.height : box.width;
  	chartArea[layout.pos] += layout.size;
  
  	if (box.getPadding) {
  		var boxPadding = box.getPadding();
  		maxPadding.top = Math.max(maxPadding.top, boxPadding.top);
  		maxPadding.left = Math.max(maxPadding.left, boxPadding.left);
  		maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom);
  		maxPadding.right = Math.max(maxPadding.right, boxPadding.right);
  	}
  
  	newWidth = params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right');
  	newHeight = params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom');
  
  	if (newWidth !== chartArea.w || newHeight !== chartArea.h) {
  		chartArea.w = newWidth;
  		chartArea.h = newHeight;
  
  		// return true if chart area changed in layout's direction
  		var sizes = layout.horizontal ? [newWidth, chartArea.w] : [newHeight, chartArea.h];
  		return sizes[0] !== sizes[1] && (!isNaN(sizes[0]) || !isNaN(sizes[1]));
  	}
  }
  
  function handleMaxPadding(chartArea) {
  	var maxPadding = chartArea.maxPadding;
  
  	function updatePos(pos) {
  		var change = Math.max(maxPadding[pos] - chartArea[pos], 0);
  		chartArea[pos] += change;
  		return change;
  	}
  	chartArea.y += updatePos('top');
  	chartArea.x += updatePos('left');
  	updatePos('right');
  	updatePos('bottom');
  }
  
  function getMargins(horizontal, chartArea) {
  	var maxPadding = chartArea.maxPadding;
  
  	function marginForPositions(positions) {
  		var margin = {left: 0, top: 0, right: 0, bottom: 0};
  		positions.forEach(function(pos) {
  			margin[pos] = Math.max(chartArea[pos], maxPadding[pos]);
  		});
  		return margin;
  	}
  
  	return horizontal
  		? marginForPositions(['left', 'right'])
  		: marginForPositions(['top', 'bottom']);
  }
  
  function fitBoxes(boxes, chartArea, params) {
  	var refitBoxes = [];
  	var i, ilen, layout, box, refit, changed;
  
  	for (i = 0, ilen = boxes.length; i < ilen; ++i) {
  		layout = boxes[i];
  		box = layout.box;
  
  		box.update(
  			layout.width || chartArea.w,
  			layout.height || chartArea.h,
  			getMargins(layout.horizontal, chartArea)
  		);
  		if (updateDims(chartArea, params, layout)) {
  			changed = true;
  			if (refitBoxes.length) {
  				// Dimensions changed and there were non full width boxes before this
  				// -> we have to refit those
  				refit = true;
  			}
  		}
  		if (!box.fullWidth) { // fullWidth boxes don't need to be re-fitted in any case
  			refitBoxes.push(layout);
  		}
  	}
  
  	return refit ? fitBoxes(refitBoxes, chartArea, params) || changed : changed;
  }
  
  function placeBoxes(boxes, chartArea, params) {
  	var userPadding = params.padding;
  	var x = chartArea.x;
  	var y = chartArea.y;
  	var i, ilen, layout, box;
  
  	for (i = 0, ilen = boxes.length; i < ilen; ++i) {
  		layout = boxes[i];
  		box = layout.box;
  		if (layout.horizontal) {
  			box.left = box.fullWidth ? userPadding.left : chartArea.left;
  			box.right = box.fullWidth ? params.outerWidth - userPadding.right : chartArea.left + chartArea.w;
  			box.top = y;
  			box.bottom = y + box.height;
  			box.width = box.right - box.left;
  			y = box.bottom;
  		} else {
  			box.left = x;
  			box.right = x + box.width;
  			box.top = chartArea.top;
  			box.bottom = chartArea.top + chartArea.h;
  			box.height = box.bottom - box.top;
  			x = box.right;
  		}
  	}
  
  	chartArea.x = x;
  	chartArea.y = y;
  }
  
  defaults._set('global', {
  	layout: {
  		padding: {
  			top: 0,
  			right: 0,
  			bottom: 0,
  			left: 0
  		}
  	}
  });
  
  /**
   * @interface ILayoutItem
   * @prop {string} position - The position of the item in the chart layout. Possible values are
   * 'left', 'top', 'right', 'bottom', and 'chartArea'
   * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area
   * @prop {boolean} fullWidth - if true, and the item is horizontal, then push vertical boxes down
   * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom)
   * @prop {function} update - Takes two parameters: width and height. Returns size of item
   * @prop {function} getPadding -  Returns an object with padding on the edges
   * @prop {number} width - Width of item. Must be valid after update()
   * @prop {number} height - Height of item. Must be valid after update()
   * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update
   * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update
   * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update
   * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update
   */
  
  // The layout service is very self explanatory.  It's responsible for the layout within a chart.
  // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need
  // It is this service's responsibility of carrying out that layout.
  module.exports = {
  	defaults: {},
  
  	/**
  	 * Register a box to a chart.
  	 * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title.
  	 * @param {Chart} chart - the chart to use
  	 * @param {ILayoutItem} item - the item to add to be layed out
  	 */
  	addBox: function(chart, item) {
  		if (!chart.boxes) {
  			chart.boxes = [];
  		}
  
  		// initialize item with default values
  		item.fullWidth = item.fullWidth || false;
  		item.position = item.position || 'top';
  		item.weight = item.weight || 0;
  		item._layers = item._layers || function() {
  			return [{
  				z: 0,
  				draw: function() {
  					item.draw.apply(item, arguments);
  				}
  			}];
  		};
  
  		chart.boxes.push(item);
  	},
  
  	/**
  	 * Remove a layoutItem from a chart
  	 * @param {Chart} chart - the chart to remove the box from
  	 * @param {ILayoutItem} layoutItem - the item to remove from the layout
  	 */
  	removeBox: function(chart, layoutItem) {
  		var index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1;
  		if (index !== -1) {
  			chart.boxes.splice(index, 1);
  		}
  	},
  
  	/**
  	 * Sets (or updates) options on the given `item`.
  	 * @param {Chart} chart - the chart in which the item lives (or will be added to)
  	 * @param {ILayoutItem} item - the item to configure with the given options
  	 * @param {object} options - the new item options.
  	 */
  	configure: function(chart, item, options) {
  		var props = ['fullWidth', 'position', 'weight'];
  		var ilen = props.length;
  		var i = 0;
  		var prop;
  
  		for (; i < ilen; ++i) {
  			prop = props[i];
  			if (Object.prototype.hasOwnProperty.call(options, prop)) {
  				item[prop] = options[prop];
  			}
  		}
  	},
  
  	/**
  	 * Fits boxes of the given chart into the given size by having each box measure itself
  	 * then running a fitting algorithm
  	 * @param {Chart} chart - the chart
  	 * @param {number} width - the width to fit into
  	 * @param {number} height - the height to fit into
  	 */
  	update: function(chart, width, height) {
  		if (!chart) {
  			return;
  		}
  
  		var layoutOptions = chart.options.layout || {};
  		var padding = helpers.options.toPadding(layoutOptions.padding);
  
  		var availableWidth = width - padding.width;
  		var availableHeight = height - padding.height;
  		var boxes = buildLayoutBoxes(chart.boxes);
  		var verticalBoxes = boxes.vertical;
  		var horizontalBoxes = boxes.horizontal;
  
  		// Essentially we now have any number of boxes on each of the 4 sides.
  		// Our canvas looks like the following.
  		// The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and
  		// B1 is the bottom axis
  		// There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays
  		// These locations are single-box locations only, when trying to register a chartArea location that is already taken,
  		// an error will be thrown.
  		//
  		// |----------------------------------------------------|
  		// |                  T1 (Full Width)                   |
  		// |----------------------------------------------------|
  		// |    |    |                 T2                  |    |
  		// |    |----|-------------------------------------|----|
  		// |    |    | C1 |                           | C2 |    |
  		// |    |    |----|                           |----|    |
  		// |    |    |                                     |    |
  		// | L1 | L2 |           ChartArea (C0)            | R1 |
  		// |    |    |                                     |    |
  		// |    |    |----|                           |----|    |
  		// |    |    | C3 |                           | C4 |    |
  		// |    |----|-------------------------------------|----|
  		// |    |    |                 B1                  |    |
  		// |----------------------------------------------------|
  		// |                  B2 (Full Width)                   |
  		// |----------------------------------------------------|
  		//
  
  		var params = Object.freeze({
  			outerWidth: width,
  			outerHeight: height,
  			padding: padding,
  			availableWidth: availableWidth,
  			vBoxMaxWidth: availableWidth / 2 / verticalBoxes.length,
  			hBoxMaxHeight: availableHeight / 2
  		});
  		var chartArea = extend({
  			maxPadding: extend({}, padding),
  			w: availableWidth,
  			h: availableHeight,
  			x: padding.left,
  			y: padding.top
  		}, padding);
  
  		setLayoutDims(verticalBoxes.concat(horizontalBoxes), params);
  
  		// First fit vertical boxes
  		fitBoxes(verticalBoxes, chartArea, params);
  
  		// Then fit horizontal boxes
  		if (fitBoxes(horizontalBoxes, chartArea, params)) {
  			// if the area changed, re-fit vertical boxes
  			fitBoxes(verticalBoxes, chartArea, params);
  		}
  
  		handleMaxPadding(chartArea, params);
  
  		// Finally place the boxes to correct coordinates
  		placeBoxes(boxes.leftAndTop, chartArea, params);
  
  		// Move to opposite side of chart
  		chartArea.x += chartArea.w;
  		chartArea.y += chartArea.h;
  
  		placeBoxes(boxes.rightAndBottom, chartArea, params);
  
  		chart.chartArea = {
  			left: chartArea.left,
  			top: chartArea.top,
  			right: chartArea.left + chartArea.w,
  			bottom: chartArea.top + chartArea.h
  		};
  
  		// Finally update boxes in chartArea (radial scale for example)
  		helpers.each(boxes.chartArea, function(layout) {
  			var box = layout.box;
  			extend(box, chart.chartArea);
  			box.update(chartArea.w, chartArea.h);
  		});
  	}
  };
  